/* $OpenBSD: uslhcom.c,v 1.7 2021/03/08 14:35:57 jcs Exp $ */ /* * Copyright (c) 2015 SASANO Takayoshi * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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_MULTIPLE_REPORTID) 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_hdev.sc_intr = uslhcom_intr; sc->sc_hdev.sc_parent = uha->parent; sc->sc_hdev.sc_report_id = uha->reportid; 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"); sc->sc_udev = dev; 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 */ bzero(&uca, sizeof uca); 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_isize, 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, sc->sc_hdev.sc_isize); 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); }