Hi, I am trying to write a driver for Silicon Labs CP2110 USB HID based UART. Here is work-in-progress code, and it seems to set uca.uhidev properly. (I wrote code/tested on 5.6-release and ported to -current.)
Exar's XR21B1421 uses similar protocol so I named the driver uhidcom(4), but currently it is not supported --- too expensive to buy evaluation board. -- SASANO Takayoshi <[email protected]> Index: arch/i386/conf/GENERIC =================================================================== RCS file: /cvs/src/sys/arch/i386/conf/GENERIC,v retrieving revision 1.792 diff -u -p -u -p -r1.792 GENERIC --- arch/i386/conf/GENERIC 11 Dec 2014 19:44:17 -0000 1.792 +++ arch/i386/conf/GENERIC 17 Dec 2014 09:57:54 -0000 @@ -274,6 +274,8 @@ ukbd* at uhidev? # USB keyboard wskbd* at ukbd? mux 1 ucycom* at uhidev? # Cypress serial ucom* at ucycom? +uhidcom* at uhidev? # Silicon Labs CP2110 USB HID UART +ucom* at uhidcom? uticom* at uhub? # TI serial ucom* at uticom? uhid* at uhidev? # USB generic HID support Index: dev/usb/files.usb =================================================================== RCS file: /cvs/src/sys/dev/usb/files.usb,v retrieving revision 1.120 diff -u -p -u -p -r1.120 files.usb --- dev/usb/files.usb 11 Dec 2014 19:44:17 -0000 1.120 +++ dev/usb/files.usb 17 Dec 2014 09:58:00 -0000 @@ -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 uhidcom: hid, ucombus +attach uhidcom at uhidbus +file dev/usb/uhidcom.c uhidcom needs-flag + # Printers device ulpt: firmload attach ulpt at uhub Index: dev/usb/uhidcom.c =================================================================== RCS file: dev/usb/uhidcom.c diff -N dev/usb/uhidcom.c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ dev/usb/uhidcom.c 17 Dec 2014 09:58:01 -0000 @@ -0,0 +1,489 @@ +/* $OpenBSD: */ + +/* + * Copyright (c) 2014 SASANO Takayoshi <[email protected]> + * + * 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/uhidcomreg.h> + +#define UHIDCOM_DEBUG +#ifdef UHIDCOM_DEBUG +#define DPRINTFN(n, x) do { if(uhidcomdebug > (n)) printf x; } while (0) +int uhidcomdebug = 10; +#else +#define DPRINTFN(n, x) +#endif +#define DPRINTF(x) DPRINTFN(0, x) + +struct uhidcom_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 uhidcom_get_status(void *, int, u_char *, u_char *); +void uhidcom_set(void *, int, int, int); +int uhidcom_param(void *, int, struct termios *); +int uhidcom_open(void *, int); +void uhidcom_close(void *, int); +void uhidcom_write(void *, int, u_char *, u_char *, u_int32_t *); +void uhidcom_read(void *, int, u_char **, u_int32_t *); +void uhidcom_intr(struct uhidev *, void *, u_int); + +int uhidcom_match(struct device *, void *, void *); +void uhidcom_attach(struct device *, struct device *, void *); +int uhidcom_detach(struct device *, int); + +usbd_status uhidcom_uart_endis(struct uhidcom_softc *, int); +usbd_status uhidcom_clear_fifo(struct uhidcom_softc *, int); +usbd_status uhidcom_get_version(struct uhidcom_softc *, struct uhidcom_version_info *); +usbd_status uhidcom_get_uart_status(struct uhidcom_softc *, struct uhidcom_uart_status *); +usbd_status uhidcom_set_break(struct uhidcom_softc *, int); +usbd_status uhidcom_set_config(struct uhidcom_softc *, struct uhidcom_uart_config *); +void uhidcom_set_baud_rate(struct uhidcom_uart_config *, u_int32_t); +int uhidcom_create_config(struct uhidcom_uart_config *, struct termios *); +int uhidcom_setup(struct uhidcom_softc *, struct uhidcom_uart_config *); + +struct ucom_methods uhidcom_methods = { + uhidcom_get_status, + uhidcom_set, + uhidcom_param, + NULL, + uhidcom_open, + uhidcom_close, + uhidcom_read, + uhidcom_write, +}; + +static const struct usb_devno uhidcom_devs[] = { + { USB_VENDOR_SILABS, USB_PRODUCT_SILABS_CP2110 }, +}; + +struct cfdriver uhidcom_cd = { + NULL, "uhidcom", DV_DULL +}; + +const struct cfattach uhidcom_ca = { + sizeof(struct uhidcom_softc), + uhidcom_match, uhidcom_attach, uhidcom_detach +}; + +/* ---------------------------------------------------------------------- + * driver entry points + */ + +int +uhidcom_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(uhidcom_devs, + uha->uaa->vendor, uha->uaa->product) != NULL ? + UMATCH_VENDOR_PRODUCT : UMATCH_NONE); +} + +void +uhidcom_attach(struct device *parent, struct device *self, void *aux) +{ + struct uhidcom_softc *sc = (struct uhidcom_softc *)self; + struct uhidev_attach_arg *uha = aux; + struct usbd_device *dev = uha->parent->sc_udev; + struct ucom_attach_args uca; + struct uhidcom_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 = uhidcom_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(("uhidcom_attach: uhidev_open %d\n", err)); + return; + } + + DPRINTF(("uhidcom_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(("uhidcom_attach: isize %d osize %d fsize %d\n", + sc->sc_hdev.sc_isize, sc->sc_hdev.sc_osize, + sc->sc_hdev.sc_fsize)); + + uhidcom_uart_endis(sc, UART_DISABLE); + uhidcom_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 = UHIDCOM_TX_HEADER_SIZE; + uca.uhidev = sc->sc_hdev.sc_parent; + uca.device = uha->uaa->device; + uca.iface = uha->uaa->iface; + uca.methods = &uhidcom_methods; + uca.arg = sc; + uca.info = NULL; + + sc->sc_subdev = config_found_sm(self, &uca, ucomprint, ucomsubmatch); +} + +int +uhidcom_detach(struct device *self, int flags) +{ + struct uhidcom_softc *sc = (struct uhidcom_softc *)self; + + DPRINTF(("uhidcom_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 + */ + +usbd_status +uhidcom_uart_endis(struct uhidcom_softc *sc, int enable) +{ + u_char val; + + val = enable; + return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT, + GET_SET_UART_ENABLE, &val, sizeof(val)); +} + +usbd_status +uhidcom_clear_fifo(struct uhidcom_softc *sc, int fifo) +{ + u_char val; + + val = fifo; + return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT, + SET_CLEAR_FIFOS, &val, sizeof(val)); +} + +usbd_status +uhidcom_get_version(struct uhidcom_softc *sc, struct uhidcom_version_info *version) +{ + return uhidev_get_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT, + GET_VERSION, version, sizeof(*version)); +} + +usbd_status +uhidcom_get_uart_status(struct uhidcom_softc *sc, struct uhidcom_uart_status *status) +{ + return uhidev_get_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT, + GET_UART_STATUS, &status, sizeof(*status)); +} + +usbd_status +uhidcom_set_break(struct uhidcom_softc *sc, int onoff) +{ + int reportid; + u_char 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, sizeof(val)); +} + +usbd_status +uhidcom_set_config(struct uhidcom_softc *sc, struct uhidcom_uart_config *config) +{ + return uhidev_set_report(sc->sc_hdev.sc_parent, UHID_FEATURE_REPORT, + GET_SET_UART_CONFIG, &config->baud_rate, + sizeof(struct uhidcom_uart_config) - + sizeof(config->report_id)); +} + +void +uhidcom_set_baud_rate(struct uhidcom_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 +uhidcom_create_config(struct uhidcom_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; + + uhidcom_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 +uhidcom_setup(struct uhidcom_softc *sc, struct uhidcom_uart_config *config) +{ + struct uhidcom_uart_status status; + + if (uhidcom_uart_endis(sc, UART_DISABLE)) + return EIO; + + if (uhidcom_set_config(sc, config)) + return EIO; + + if (uhidcom_clear_fifo(sc, CLEAR_TX_FIFO | CLEAR_RX_FIFO)) + return EIO; + + if (uhidcom_get_uart_status(sc, &status)) + return EIO; + + if (uhidcom_uart_endis(sc, UART_ENABLE)) + return EIO; + + return 0; +} + +/* ---------------------------------------------------------------------- + * methods for ucom + */ + +void +uhidcom_get_status(void *arg, int portno, u_char *rlsr, u_char *rmsr) +{ + struct uhidcom_softc *sc = arg; + + if (usbd_is_dying(sc->sc_udev)) + return; + + *rlsr = sc->sc_lsr; + *rmsr = sc->sc_msr; +} + +void +uhidcom_set(void *arg, int portno, int reg, int onoff) +{ + struct uhidcom_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: + uhidcom_set_break(sc, onoff); + break; + } +} + +int +uhidcom_param(void *arg, int portno, struct termios *t) +{ + struct uhidcom_softc *sc = arg; + struct uhidcom_uart_config config; + int ret; + + if (usbd_is_dying(sc->sc_udev)) + return 0; + + ret = uhidcom_create_config(&config, t); + if (ret) + return ret; + + ret = uhidcom_setup(sc, &config); + if (ret) + return ret; + + return 0; +} + +int +uhidcom_open(void *arg, int portno) +{ + struct uhidcom_softc *sc = arg; + struct uhidcom_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); + + uhidcom_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 = uhidcom_set_config(sc, &config); + if (ret) + return ret; + + return 0; +} + +void +uhidcom_close(void *arg, int portno) +{ + struct uhidcom_softc *sc = arg; + int s; + + if (usbd_is_dying(sc->sc_udev)) + return; + + uhidcom_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 +uhidcom_read(void *arg, int portno, u_char **ptr, u_int32_t *cnt) +{ + struct uhidcom_softc *sc = arg; + + *ptr = sc->sc_ibuf; + *cnt = sc->sc_icnt; +} + +void +uhidcom_write(void *arg, int portno, u_char *to, u_char *data, u_int32_t *cnt) +{ + bcopy(data, &to[UHIDCOM_TX_HEADER_SIZE], *cnt); + to[0] = *cnt; /* add Report ID (= transmit length) */ + *cnt += UHIDCOM_TX_HEADER_SIZE; +} + +void +uhidcom_intr(struct uhidev *addr, void *ibuf, u_int len) +{ + extern void ucomreadcb(struct usbd_xfer *, void *, usbd_status); + struct uhidcom_softc *sc = (struct uhidcom_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); +} Index: dev/usb/uhidcomreg.h =================================================================== RCS file: dev/usb/uhidcomreg.h diff -N dev/usb/uhidcomreg.h --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ dev/usb/uhidcomreg.h 17 Dec 2014 09:58:01 -0000 @@ -0,0 +1,106 @@ +/* $OpenBSD: */ + +/* + * Copyright (c) 2014 SASANO Takayoshi <[email protected]> + * + * 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 UHIDCOM_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 uhidcom_uart_status { + u_char report_id; /* GET_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 uhidcom_version_info { + u_char report_id; /* GET_VERSION */ + u_char product_id; + u_char product_revision; +} __packed; + +/* GET_SET_UART_CONFIG */ +struct uhidcom_uart_config { + u_char report_id; /* GET_SET_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
