root/sys/dev/usb/uslhcom.c
/*      $OpenBSD: uslhcom.c,v 1.10 2024/05/23 03:21:09 jsg Exp $        */

/*
 * 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/malloc.h>
#include <sys/tty.h>
#include <sys/device.h>

#include <dev/usb/usb.h>
#include <dev/usb/usbdi.h>
#include <dev/usb/usbdevs.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 *);

const 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 (!UHIDEV_CLAIM_MULTIPLE_REPORTID(uha))
                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);
}