(new) device driver for Silicon Labs CP2110 USB HID based UART.
authoruaa <uaa@openbsd.org>
Sat, 3 Jan 2015 20:22:56 +0000 (20:22 +0000)
committeruaa <uaa@openbsd.org>
Sat, 3 Jan 2015 20:22:56 +0000 (20:22 +0000)
ok by deraadt@

sys/dev/usb/files.usb
sys/dev/usb/uslhcom.c [new file with mode: 0644]
sys/dev/usb/uslhcomreg.h [new file with mode: 0644]

index abe78d1..89c67ba 100644 (file)
@@ -1,4 +1,4 @@
-#      $OpenBSD: files.usb,v 1.120 2014/12/11 19:44:17 tedu Exp $
+#      $OpenBSD: files.usb,v 1.121 2015/01/03 20:22:56 uaa Exp $
 #      $NetBSD: files.usb,v 1.16 2000/02/14 20:29:54 augustss Exp $
 #
 # Config file and device description for machine-independent USB code.
@@ -110,6 +110,11 @@ device     ucycom: hid, ucombus
 attach ucycom at uhidbus
 file   dev/usb/ucycom.c                ucycom                  needs-flag
 
+# Silicon Labs USB HID based UART controller
+device uslhcom: hid, ucombus
+attach uslhcom at uhidbus
+file   dev/usb/uslhcom.c               uslhcom                 needs-flag
+
 # Printers
 device ulpt: firmload
 attach ulpt at uhub
diff --git a/sys/dev/usb/uslhcom.c b/sys/dev/usb/uslhcom.c
new file mode 100644 (file)
index 0000000..2478680
--- /dev/null
@@ -0,0 +1,504 @@
+/*     $OpenBSD: */
+
+/*
+ * Copyright (c) 2015 SASANO Takayoshi <uaa@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Device driver for Silicon Labs CP2110 USB HID-UART bridge.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/kernel.h>
+#include <sys/malloc.h>
+#include <sys/conf.h>
+#include <sys/tty.h>
+#include <sys/device.h>
+
+#include <dev/usb/usb.h>
+#include <dev/usb/usbdi.h>
+#include <dev/usb/usbdi_util.h>
+#include <dev/usb/usbdevs.h>
+
+#include <dev/usb/hid.h>
+#include <dev/usb/usbhid.h>
+#include <dev/usb/uhidev.h>
+
+#include <dev/usb/ucomvar.h>
+#include <dev/usb/uslhcomreg.h>
+
+#ifdef USLHCOM_DEBUG
+#define        DPRINTF(x)      if (uslhcomdebug) printf x
+#else
+#define        DPRINTF(x)
+#endif
+
+struct uslhcom_softc {
+       struct uhidev            sc_hdev;
+       struct usbd_device      *sc_udev;
+
+       u_char                  *sc_ibuf;
+       u_int                    sc_icnt;
+
+       u_char                   sc_lsr;
+       u_char                   sc_msr;
+
+       struct device           *sc_subdev;
+};
+
+void           uslhcom_get_status(void *, int, u_char *, u_char *);
+void           uslhcom_set(void *, int, int, int);
+int            uslhcom_param(void *, int, struct termios *);
+int            uslhcom_open(void *, int);
+void           uslhcom_close(void *, int);
+void           uslhcom_write(void *, int, u_char *, u_char *, u_int32_t *);
+void           uslhcom_read(void *, int, u_char **, u_int32_t *);
+void           uslhcom_intr(struct uhidev *, void *, u_int);
+
+int            uslhcom_match(struct device *, void *, void *);
+void           uslhcom_attach(struct device *, struct device *, void *);
+int            uslhcom_detach(struct device *, int);
+
+int            uslhcom_uart_endis(struct uslhcom_softc *, int);
+int            uslhcom_clear_fifo(struct uslhcom_softc *, int);
+int            uslhcom_get_version(struct uslhcom_softc *, struct uslhcom_version_info *);
+int            uslhcom_get_uart_status(struct uslhcom_softc *, struct uslhcom_uart_status *);
+int            uslhcom_set_break(struct uslhcom_softc *, int);
+int            uslhcom_set_config(struct uslhcom_softc *, struct uslhcom_uart_config *);
+void           uslhcom_set_baud_rate(struct uslhcom_uart_config *, u_int32_t);
+int            uslhcom_create_config(struct uslhcom_uart_config *, struct termios *);
+int            uslhcom_setup(struct uslhcom_softc *, struct uslhcom_uart_config *);
+
+struct ucom_methods uslhcom_methods = {
+       uslhcom_get_status,
+       uslhcom_set,
+       uslhcom_param,
+       NULL,
+       uslhcom_open,
+       uslhcom_close,
+       uslhcom_read,
+       uslhcom_write,
+};
+
+static const struct usb_devno uslhcom_devs[] = {
+       { USB_VENDOR_SILABS, USB_PRODUCT_SILABS_CP2110 },
+};
+
+struct cfdriver uslhcom_cd = {
+       NULL, "uslhcom", DV_DULL
+};
+
+const struct cfattach uslhcom_ca = {
+       sizeof(struct uslhcom_softc),
+       uslhcom_match, uslhcom_attach, uslhcom_detach
+};
+
+/* ----------------------------------------------------------------------
+ * driver entry points
+ */
+
+int
+uslhcom_match(struct device *parent, void *match, void *aux)
+{
+       struct uhidev_attach_arg *uha = aux;
+
+       /* use all report IDs */
+       if (uha->reportid != UHIDEV_CLAIM_ALLREPORTID)
+               return UMATCH_NONE;
+
+       return (usb_lookup(uslhcom_devs,
+                          uha->uaa->vendor, uha->uaa->product) != NULL ?
+               UMATCH_VENDOR_PRODUCT : UMATCH_NONE);
+}
+
+void
+uslhcom_attach(struct device *parent, struct device *self, void *aux)
+{
+       struct uslhcom_softc *sc = (struct uslhcom_softc *)self;
+       struct uhidev_attach_arg *uha = aux;
+       struct usbd_device *dev = uha->parent->sc_udev;
+       struct ucom_attach_args uca;
+       struct uslhcom_version_info version;
+       int err, repid, size, rsize;
+       void *desc;
+
+       sc->sc_udev = dev;
+       sc->sc_lsr = sc->sc_msr = 0;
+       sc->sc_hdev.sc_intr = uslhcom_intr;
+       sc->sc_hdev.sc_parent = uha->parent;
+       sc->sc_hdev.sc_report_id = uha->reportid;
+       sc->sc_hdev.sc_isize = sc->sc_hdev.sc_osize = sc->sc_hdev.sc_fsize = 0;
+
+       uhidev_get_report_desc(uha->parent, &desc, &size);
+       for (repid = 0; repid < uha->parent->sc_nrepid; repid++) {
+               rsize = hid_report_size(desc, size, hid_input, repid);
+               if (sc->sc_hdev.sc_isize < rsize) sc->sc_hdev.sc_isize = rsize;
+               rsize = hid_report_size(desc, size, hid_output, repid);
+               if (sc->sc_hdev.sc_osize < rsize) sc->sc_hdev.sc_osize = rsize;
+               rsize = hid_report_size(desc, size, hid_feature, repid);
+               if (sc->sc_hdev.sc_fsize < rsize) sc->sc_hdev.sc_fsize = rsize;
+       }
+
+       printf("\n");
+
+       err = uhidev_open(&sc->sc_hdev);
+       if (err) {
+               DPRINTF(("uslhcom_attach: uhidev_open %d\n", err));
+               return;
+       }
+
+       DPRINTF(("uslhcom_attach: sc %p opipe %p ipipe %p report_id %d\n",
+                sc, sc->sc_hdev.sc_parent->sc_opipe,
+                sc->sc_hdev.sc_parent->sc_ipipe, uha->reportid));
+       DPRINTF(("uslhcom_attach: isize %d osize %d fsize %d\n",
+                sc->sc_hdev.sc_isize, sc->sc_hdev.sc_osize,
+                sc->sc_hdev.sc_fsize));
+
+       uslhcom_uart_endis(sc, UART_DISABLE);
+       uslhcom_get_version(sc, &version);
+       printf("%s: pid %#x rev %#x\n", sc->sc_hdev.sc_dev.dv_xname,
+              version.product_id, version.product_revision);
+
+       /* setup ucom layer */
+       uca.portno = UCOM_UNK_PORTNO;
+       uca.bulkin = uca.bulkout = -1;
+       uca.ibufsize = uca.ibufsizepad = 0;
+       uca.obufsize = sc->sc_hdev.sc_osize;
+       uca.opkthdrlen = USLHCOM_TX_HEADER_SIZE;
+       uca.uhidev = sc->sc_hdev.sc_parent;
+       uca.device = uha->uaa->device;
+       uca.iface = uha->uaa->iface;
+       uca.methods = &uslhcom_methods;
+       uca.arg = sc;
+       uca.info = NULL;
+
+       sc->sc_subdev = config_found_sm(self, &uca, ucomprint, ucomsubmatch);
+}
+
+int
+uslhcom_detach(struct device *self, int flags)
+{
+       struct uslhcom_softc *sc = (struct uslhcom_softc *)self;
+
+       DPRINTF(("uslhcom_detach: sc=%p flags=%d\n", sc, flags));
+       if (sc->sc_subdev != NULL) {
+               config_detach(sc->sc_subdev, flags);
+               sc->sc_subdev = NULL;
+       }
+
+       if (sc->sc_hdev.sc_state & UHIDEV_OPEN)
+               uhidev_close(&sc->sc_hdev);
+
+       return 0;
+}
+
+/* ----------------------------------------------------------------------
+ * low level I/O
+ */
+
+int
+uslhcom_uart_endis(struct uslhcom_softc *sc, int enable)
+{
+       int len;
+       u_char val;
+
+       len = sizeof(val);
+       val = enable;
+
+       return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                GET_SET_UART_ENABLE, &val, len) != len;
+}
+
+int
+uslhcom_clear_fifo(struct uslhcom_softc *sc, int fifo)
+{
+       int len;
+       u_char val;
+
+       len = sizeof(val);
+       val = fifo;
+
+       return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                SET_CLEAR_FIFOS, &val, len) != len;
+}
+
+int
+uslhcom_get_version(struct uslhcom_softc *sc, struct uslhcom_version_info *version)
+{
+       int len;
+
+       len = sizeof(*version);
+
+       return uhidev_get_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                GET_VERSION, version, len) < len;
+}
+
+int
+uslhcom_get_uart_status(struct uslhcom_softc *sc, struct uslhcom_uart_status *status)
+{
+       int len;
+
+       len = sizeof(*status);
+
+       return uhidev_get_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                GET_UART_STATUS, status, len) < len;
+}
+
+int
+uslhcom_set_break(struct uslhcom_softc *sc, int onoff)
+{
+       int len, reportid;
+       u_char val;
+
+       len = sizeof(val);
+
+       if (onoff) {
+               val = 0; /* send break until SET_STOP_LINE_BREAK */
+               reportid = SET_TRANSMIT_LINE_BREAK;
+       } else {
+               val = 0; /* any value can be accepted */
+               reportid = SET_STOP_LINE_BREAK;
+       }
+
+       return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                reportid, &val, len) != len;
+}
+
+int
+uslhcom_set_config(struct uslhcom_softc *sc, struct uslhcom_uart_config *config)
+{
+       int len;
+
+       len = sizeof(*config);
+
+       return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT,
+                                GET_SET_UART_CONFIG, config, len) != len;
+}
+
+void
+uslhcom_set_baud_rate(struct uslhcom_uart_config *config, u_int32_t baud_rate)
+{
+       config->baud_rate[0] = baud_rate >> 24;
+       config->baud_rate[1] = baud_rate >> 16;
+       config->baud_rate[2] = baud_rate >> 8;
+       config->baud_rate[3] = baud_rate >> 0;
+}
+
+int
+uslhcom_create_config(struct uslhcom_uart_config *config, struct termios *t)
+{
+       if (t->c_ospeed < UART_CONFIG_BAUD_RATE_MIN ||
+           t->c_ospeed > UART_CONFIG_BAUD_RATE_MAX)
+               return EINVAL;
+
+       uslhcom_set_baud_rate(config, t->c_ospeed);
+
+       if (ISSET(t->c_cflag, PARENB)) {
+               if (ISSET(t->c_cflag, PARODD))
+                       config->parity = UART_CONFIG_PARITY_ODD;
+               else
+                       config->parity = UART_CONFIG_PARITY_EVEN;
+       } else
+               config->parity = UART_CONFIG_PARITY_NONE;
+
+       if (ISSET(t->c_cflag, CRTSCTS))
+               config->data_control = UART_CONFIG_DATA_CONTROL_HARD;
+       else
+               config->data_control = UART_CONFIG_DATA_CONTROL_NONE;
+
+       switch (ISSET(t->c_cflag, CSIZE)) {
+       case CS5:
+               config->data_bits = UART_CONFIG_DATA_BITS_5;
+               break;
+       case CS6:
+               config->data_bits = UART_CONFIG_DATA_BITS_6;
+               break;
+       case CS7:
+               config->data_bits = UART_CONFIG_DATA_BITS_7;
+               break;
+       case CS8:
+               config->data_bits = UART_CONFIG_DATA_BITS_8;
+               break;
+       default:
+               return EINVAL;
+       }
+
+       if (ISSET(t->c_cflag, CSTOPB))
+               config->stop_bits = UART_CONFIG_STOP_BITS_2;
+       else
+               config->stop_bits = UART_CONFIG_STOP_BITS_1;
+
+       return 0;
+}
+
+int
+uslhcom_setup(struct uslhcom_softc *sc, struct uslhcom_uart_config *config)
+{
+       struct uslhcom_uart_status status;
+
+       if (uslhcom_uart_endis(sc, UART_DISABLE))
+               return EIO;
+
+       if (uslhcom_set_config(sc, config))
+               return EIO;
+
+       if (uslhcom_clear_fifo(sc, CLEAR_TX_FIFO | CLEAR_RX_FIFO))
+               return EIO;
+
+       if (uslhcom_get_uart_status(sc, &status))
+               return EIO;
+
+       if (uslhcom_uart_endis(sc, UART_ENABLE))
+               return EIO;
+
+       return 0;
+}
+
+/* ----------------------------------------------------------------------
+ * methods for ucom
+ */
+
+void
+uslhcom_get_status(void *arg, int portno, u_char *rlsr, u_char *rmsr)
+{
+       struct uslhcom_softc *sc = arg;
+
+       if (usbd_is_dying(sc->sc_udev))
+               return;
+
+       *rlsr = sc->sc_lsr;
+       *rmsr = sc->sc_msr;
+}
+
+void
+uslhcom_set(void *arg, int portno, int reg, int onoff)
+{
+       struct uslhcom_softc *sc = arg;
+
+       if (usbd_is_dying(sc->sc_udev))
+               return;
+
+       switch (reg) {
+       case UCOM_SET_DTR:
+       case UCOM_SET_RTS:
+               /* no support, do nothing */
+               break;
+       case UCOM_SET_BREAK:
+               uslhcom_set_break(sc, onoff);
+               break;
+       }
+}
+
+int
+uslhcom_param(void *arg, int portno, struct termios *t)
+{
+       struct uslhcom_softc *sc = arg;
+       struct uslhcom_uart_config config;
+       int ret;
+
+       if (usbd_is_dying(sc->sc_udev))
+               return 0;
+
+       ret = uslhcom_create_config(&config, t);
+       if (ret)
+               return ret;
+
+       ret = uslhcom_setup(sc, &config);
+       if (ret)
+               return ret;
+
+       return 0;
+}
+
+int
+uslhcom_open(void *arg, int portno)
+{
+       struct uslhcom_softc *sc = arg;
+       struct uslhcom_uart_config config;
+       int ret;
+
+       if (usbd_is_dying(sc->sc_udev))
+               return EIO;
+
+       sc->sc_ibuf = malloc(sc->sc_hdev.sc_osize + sizeof(u_char),
+                            M_USBDEV, M_WAITOK);
+
+       uslhcom_set_baud_rate(&config, 9600);
+       config.parity = UART_CONFIG_PARITY_NONE;
+       config.data_control = UART_CONFIG_DATA_CONTROL_NONE;
+       config.data_bits = UART_CONFIG_DATA_BITS_8;
+       config.stop_bits = UART_CONFIG_STOP_BITS_1;
+
+       ret = uslhcom_set_config(sc, &config);
+       if (ret)
+               return ret;
+
+       return 0;
+}
+
+void
+uslhcom_close(void *arg, int portno)
+{
+       struct uslhcom_softc *sc = arg;
+       int s;
+
+       if (usbd_is_dying(sc->sc_udev))
+               return;
+
+       uslhcom_uart_endis(sc, UART_DISABLE);
+
+       s = splusb();
+       if (sc->sc_ibuf != NULL) {
+               free(sc->sc_ibuf, M_USBDEV, 0);
+               sc->sc_ibuf = NULL;
+       }
+       splx(s);
+}
+
+void
+uslhcom_read(void *arg, int portno, u_char **ptr, u_int32_t *cnt)
+{
+       struct uslhcom_softc *sc = arg;
+
+       *ptr = sc->sc_ibuf;
+       *cnt = sc->sc_icnt;
+}
+
+void
+uslhcom_write(void *arg, int portno, u_char *to, u_char *data, u_int32_t *cnt)
+{
+       bcopy(data, &to[USLHCOM_TX_HEADER_SIZE], *cnt);
+       to[0] = *cnt;   /* add Report ID (= transmit length) */
+       *cnt += USLHCOM_TX_HEADER_SIZE;
+}
+
+void
+uslhcom_intr(struct uhidev *addr, void *ibuf, u_int len)
+{
+       extern void ucomreadcb(struct usbd_xfer *, void *, usbd_status);
+       struct uslhcom_softc *sc = (struct uslhcom_softc *)addr;
+       int s;
+
+       if (sc->sc_ibuf == NULL)
+               return;
+
+       s = spltty();
+       sc->sc_icnt = len;      /* Report ID is already stripped */
+       bcopy(ibuf, sc->sc_ibuf, len);
+       ucomreadcb(addr->sc_parent->sc_ixfer, sc->sc_subdev,
+                  USBD_NORMAL_COMPLETION);
+       splx(s);
+}
diff --git a/sys/dev/usb/uslhcomreg.h b/sys/dev/usb/uslhcomreg.h
new file mode 100644 (file)
index 0000000..286cde2
--- /dev/null
@@ -0,0 +1,103 @@
+/*     $OpenBSD: */
+
+/*
+ * Copyright (c) 2015 SASANO Takayoshi <uaa@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define        USLHCOM_TX_HEADER_SIZE          sizeof(u_char)
+
+#define        SET_TRANSMIT_DATA(x)            (x)
+#define        GET_RECEIVE_DATA(x)             (x)
+
+#define        SET_DEVICE_RESET                0x40
+#define        GET_SET_UART_ENABLE             0x41
+#define        GET_UART_STATUS                 0x42
+#define        SET_CLEAR_FIFOS                 0x43
+#define        GET_GPIO_STATE                  0x44
+#define        SET_GPIO_STATE                  0x45
+#define        GET_VERSION                     0x46
+#define        GET_SET_OTP_LOCK_BYTE           0x47
+
+#define        GET_SET_UART_CONFIG             0x50
+#define        SET_TRANSMIT_LINE_BREAK         0x51
+#define        SET_STOP_LINE_BREAK             0x52
+
+
+/* SET_DEVICE_RESET */
+#define        DEVICE_RESET_VALUE              0x00
+
+/* GET_SET_UART_ENABLE */
+#define        UART_DISABLE                    0x00
+#define        UART_ENABLE                     0x01
+
+/* GET_UART_STATUS */
+struct uslhcom_uart_status {
+       u_char  tx_fifo[2];     /* (big endian) */
+       u_char  rx_fifo[2];     /* (big endian) */
+       u_char  error_status;
+       u_char  break_status;
+} __packed;
+
+#define        ERROR_STATUS_PARITY             0x01
+#define        ERROR_STATUS_OVERRUN            0x02
+#define        BREAK_STATUS                    0x01
+
+/* SET_CLEAR_FIFO */
+#define        CLEAR_TX_FIFO                   0x01
+#define        CLEAR_RX_FIFO                   0x02
+
+/* GET_VERSION */
+struct uslhcom_version_info {
+       u_char  product_id;
+       u_char  product_revision;
+} __packed;
+
+/* GET_SET_UART_CONFIG */
+struct uslhcom_uart_config {
+       u_char  baud_rate[4];   /* (big endian) */
+       u_char  parity;
+       u_char  data_control;
+       u_char  data_bits;
+       u_char  stop_bits;
+} __packed;
+
+/*
+ * Silicon Labs CP2110/4 Application Note (AN434) Rev 0.4 says that
+ * valid baud rate is 300bps to 500,000bps.
+ * But HidUartSample of CP2110 SDK accepts 50bps to 2,000,000bps.
+ */
+#define        UART_CONFIG_BAUD_RATE_MIN       50
+#define        UART_CONFIG_BAUD_RATE_MAX       2000000
+
+#define        UART_CONFIG_PARITY_NONE         0x00
+#define        UART_CONFIG_PARITY_EVEN         0x01
+#define        UART_CONFIG_PARITY_ODD          0x02
+#define        UART_CONFIG_PARITY_MARK         0x03
+#define        UART_CONFIG_PARITY_SPACE        0x04
+
+#define        UART_CONFIG_DATA_CONTROL_NONE   0x00
+#define        UART_CONFIG_DATA_CONTROL_HARD   0x01
+
+/*
+ * AN434 Rev 0.4 describes setting 0x05 ... 0x08 to configure data bits.
+ * But actually it requires different values.
+ */
+#define        UART_CONFIG_DATA_BITS_5         0x00
+#define        UART_CONFIG_DATA_BITS_6         0x01
+#define        UART_CONFIG_DATA_BITS_7         0x02
+#define        UART_CONFIG_DATA_BITS_8         0x03
+
+#define        UART_CONFIG_STOP_BITS_1         0x00
+#define        UART_CONFIG_STOP_BITS_2         0x01