aboutsummaryrefslogtreecommitdiffstats
path: root/hw/usb/dev-serial.c
diff options
context:
space:
mode:
authorTimos Ampelikiotis <t.ampelikiotis@virtualopensystems.com>2023-10-10 11:40:56 +0000
committerTimos Ampelikiotis <t.ampelikiotis@virtualopensystems.com>2023-10-10 11:40:56 +0000
commite02cda008591317b1625707ff8e115a4841aa889 (patch)
treeaee302e3cf8b59ec2d32ec481be3d1afddfc8968 /hw/usb/dev-serial.c
parentcc668e6b7e0ffd8c9d130513d12053cf5eda1d3b (diff)
Introduce Virtio-loopback epsilon release:
Epsilon release introduces a new compatibility layer which make virtio-loopback design to work with QEMU and rust-vmm vhost-user backend without require any changes. Signed-off-by: Timos Ampelikiotis <t.ampelikiotis@virtualopensystems.com> Change-Id: I52e57563e08a7d0bdc002f8e928ee61ba0c53dd9
Diffstat (limited to 'hw/usb/dev-serial.c')
-rw-r--r--hw/usb/dev-serial.c709
1 files changed, 709 insertions, 0 deletions
diff --git a/hw/usb/dev-serial.c b/hw/usb/dev-serial.c
new file mode 100644
index 000000000..63047d79c
--- /dev/null
+++ b/hw/usb/dev-serial.c
@@ -0,0 +1,709 @@
+/*
+ * FTDI FT232BM Device emulation
+ *
+ * Copyright (c) 2006 CodeSourcery.
+ * Copyright (c) 2008 Samuel Thibault <samuel.thibault@ens-lyon.org>
+ * Written by Paul Brook, reused for FTDI by Samuel Thibault
+ *
+ * This code is licensed under the LGPL.
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qemu/cutils.h"
+#include "qemu/error-report.h"
+#include "qemu/module.h"
+#include "hw/qdev-properties.h"
+#include "hw/qdev-properties-system.h"
+#include "hw/usb.h"
+#include "migration/vmstate.h"
+#include "desc.h"
+#include "chardev/char-serial.h"
+#include "chardev/char-fe.h"
+#include "qom/object.h"
+#include "trace.h"
+
+
+#define RECV_BUF (512 - (2 * 8))
+
+/* Commands */
+#define FTDI_RESET 0
+#define FTDI_SET_MDM_CTRL 1
+#define FTDI_SET_FLOW_CTRL 2
+#define FTDI_SET_BAUD 3
+#define FTDI_SET_DATA 4
+#define FTDI_GET_MDM_ST 5
+#define FTDI_SET_EVENT_CHR 6
+#define FTDI_SET_ERROR_CHR 7
+#define FTDI_SET_LATENCY 9
+#define FTDI_GET_LATENCY 10
+
+/* RESET */
+
+#define FTDI_RESET_SIO 0
+#define FTDI_RESET_RX 1
+#define FTDI_RESET_TX 2
+
+/* SET_MDM_CTRL */
+
+#define FTDI_DTR 1
+#define FTDI_SET_DTR (FTDI_DTR << 8)
+#define FTDI_RTS 2
+#define FTDI_SET_RTS (FTDI_RTS << 8)
+
+/* SET_FLOW_CTRL */
+
+#define FTDI_NO_HS 0
+#define FTDI_RTS_CTS_HS 1
+#define FTDI_DTR_DSR_HS 2
+#define FTDI_XON_XOFF_HS 4
+
+/* SET_DATA */
+
+#define FTDI_PARITY (0x7 << 8)
+#define FTDI_ODD (0x1 << 8)
+#define FTDI_EVEN (0x2 << 8)
+#define FTDI_MARK (0x3 << 8)
+#define FTDI_SPACE (0x4 << 8)
+
+#define FTDI_STOP (0x3 << 11)
+#define FTDI_STOP1 (0x0 << 11)
+#define FTDI_STOP15 (0x1 << 11)
+#define FTDI_STOP2 (0x2 << 11)
+
+/* GET_MDM_ST */
+/* TODO: should be sent every 40ms */
+#define FTDI_CTS (1 << 4) /* CTS line status */
+#define FTDI_DSR (1 << 5) /* DSR line status */
+#define FTDI_RI (1 << 6) /* RI line status */
+#define FTDI_RLSD (1 << 7) /* Receive Line Signal Detect */
+
+/* Status */
+
+#define FTDI_DR (1 << 0) /* Data Ready */
+#define FTDI_OE (1 << 1) /* Overrun Err */
+#define FTDI_PE (1 << 2) /* Parity Err */
+#define FTDI_FE (1 << 3) /* Framing Err */
+#define FTDI_BI (1 << 4) /* Break Interrupt */
+#define FTDI_THRE (1 << 5) /* Transmitter Holding Register */
+#define FTDI_TEMT (1 << 6) /* Transmitter Empty */
+#define FTDI_FIFO (1 << 7) /* Error in FIFO */
+
+struct USBSerialState {
+ USBDevice dev;
+
+ USBEndpoint *intr;
+ uint8_t recv_buf[RECV_BUF];
+ uint16_t recv_ptr;
+ uint16_t recv_used;
+ uint8_t event_chr;
+ uint8_t error_chr;
+ uint8_t event_trigger;
+ bool always_plugged;
+ uint8_t flow_control;
+ uint8_t xon;
+ uint8_t xoff;
+ QEMUSerialSetParams params;
+ int latency; /* ms */
+ CharBackend cs;
+};
+
+#define TYPE_USB_SERIAL "usb-serial-dev"
+OBJECT_DECLARE_SIMPLE_TYPE(USBSerialState, USB_SERIAL)
+
+enum {
+ STR_MANUFACTURER = 1,
+ STR_PRODUCT_SERIAL,
+ STR_PRODUCT_BRAILLE,
+ STR_SERIALNUMBER,
+};
+
+static const USBDescStrings desc_strings = {
+ [STR_MANUFACTURER] = "QEMU",
+ [STR_PRODUCT_SERIAL] = "QEMU USB SERIAL",
+ [STR_PRODUCT_BRAILLE] = "QEMU USB BAUM BRAILLE",
+ [STR_SERIALNUMBER] = "1",
+};
+
+static const USBDescIface desc_iface0 = {
+ .bInterfaceNumber = 0,
+ .bNumEndpoints = 2,
+ .bInterfaceClass = 0xff,
+ .bInterfaceSubClass = 0xff,
+ .bInterfaceProtocol = 0xff,
+ .eps = (USBDescEndpoint[]) {
+ {
+ .bEndpointAddress = USB_DIR_IN | 0x01,
+ .bmAttributes = USB_ENDPOINT_XFER_BULK,
+ .wMaxPacketSize = 64,
+ },{
+ .bEndpointAddress = USB_DIR_OUT | 0x02,
+ .bmAttributes = USB_ENDPOINT_XFER_BULK,
+ .wMaxPacketSize = 64,
+ },
+ }
+};
+
+static const USBDescDevice desc_device = {
+ .bcdUSB = 0x0200,
+ .bMaxPacketSize0 = 8,
+ .bNumConfigurations = 1,
+ .confs = (USBDescConfig[]) {
+ {
+ .bNumInterfaces = 1,
+ .bConfigurationValue = 1,
+ .bmAttributes = USB_CFG_ATT_ONE | USB_CFG_ATT_WAKEUP,
+ .bMaxPower = 50,
+ .nif = 1,
+ .ifs = &desc_iface0,
+ },
+ },
+};
+
+static const USBDesc desc_serial = {
+ .id = {
+ .idVendor = 0x0403,
+ .idProduct = 0x6001,
+ .bcdDevice = 0x0400,
+ .iManufacturer = STR_MANUFACTURER,
+ .iProduct = STR_PRODUCT_SERIAL,
+ .iSerialNumber = STR_SERIALNUMBER,
+ },
+ .full = &desc_device,
+ .str = desc_strings,
+};
+
+static const USBDesc desc_braille = {
+ .id = {
+ .idVendor = 0x0403,
+ .idProduct = 0xfe72,
+ .bcdDevice = 0x0400,
+ .iManufacturer = STR_MANUFACTURER,
+ .iProduct = STR_PRODUCT_BRAILLE,
+ .iSerialNumber = STR_SERIALNUMBER,
+ },
+ .full = &desc_device,
+ .str = desc_strings,
+};
+
+static void usb_serial_set_flow_control(USBSerialState *s,
+ uint8_t flow_control)
+{
+ USBDevice *dev = USB_DEVICE(s);
+ USBBus *bus = usb_bus_from_device(dev);
+
+ /* TODO: ioctl */
+ s->flow_control = flow_control;
+ trace_usb_serial_set_flow_control(bus->busnr, dev->addr, flow_control);
+}
+
+static void usb_serial_set_xonxoff(USBSerialState *s, int xonxoff)
+{
+ USBDevice *dev = USB_DEVICE(s);
+ USBBus *bus = usb_bus_from_device(dev);
+
+ s->xon = xonxoff & 0xff;
+ s->xoff = (xonxoff >> 8) & 0xff;
+
+ trace_usb_serial_set_xonxoff(bus->busnr, dev->addr, s->xon, s->xoff);
+}
+
+static void usb_serial_reset(USBSerialState *s)
+{
+ s->event_chr = 0x0d;
+ s->event_trigger = 0;
+ s->recv_ptr = 0;
+ s->recv_used = 0;
+ /* TODO: purge in char driver */
+ usb_serial_set_flow_control(s, FTDI_NO_HS);
+}
+
+static void usb_serial_handle_reset(USBDevice *dev)
+{
+ USBSerialState *s = USB_SERIAL(dev);
+ USBBus *bus = usb_bus_from_device(dev);
+
+ trace_usb_serial_reset(bus->busnr, dev->addr);
+
+ usb_serial_reset(s);
+ /* TODO: Reset char device, send BREAK? */
+}
+
+static uint8_t usb_get_modem_lines(USBSerialState *s)
+{
+ int flags;
+ uint8_t ret;
+
+ if (qemu_chr_fe_ioctl(&s->cs,
+ CHR_IOCTL_SERIAL_GET_TIOCM, &flags) == -ENOTSUP) {
+ return FTDI_CTS | FTDI_DSR | FTDI_RLSD;
+ }
+
+ ret = 0;
+ if (flags & CHR_TIOCM_CTS) {
+ ret |= FTDI_CTS;
+ }
+ if (flags & CHR_TIOCM_DSR) {
+ ret |= FTDI_DSR;
+ }
+ if (flags & CHR_TIOCM_RI) {
+ ret |= FTDI_RI;
+ }
+ if (flags & CHR_TIOCM_CAR) {
+ ret |= FTDI_RLSD;
+ }
+
+ return ret;
+}
+
+static void usb_serial_handle_control(USBDevice *dev, USBPacket *p,
+ int request, int value, int index,
+ int length, uint8_t *data)
+{
+ USBSerialState *s = USB_SERIAL(dev);
+ USBBus *bus = usb_bus_from_device(dev);
+ int ret;
+
+ trace_usb_serial_handle_control(bus->busnr, dev->addr, request, value);
+
+ ret = usb_desc_handle_control(dev, p, request, value, index, length, data);
+ if (ret >= 0) {
+ return;
+ }
+
+ switch (request) {
+ case EndpointOutRequest | USB_REQ_CLEAR_FEATURE:
+ break;
+
+ /* Class specific requests. */
+ case VendorDeviceOutRequest | FTDI_RESET:
+ switch (value) {
+ case FTDI_RESET_SIO:
+ usb_serial_reset(s);
+ break;
+ case FTDI_RESET_RX:
+ s->recv_ptr = 0;
+ s->recv_used = 0;
+ /* TODO: purge from char device */
+ break;
+ case FTDI_RESET_TX:
+ /* TODO: purge from char device */
+ break;
+ }
+ break;
+ case VendorDeviceOutRequest | FTDI_SET_MDM_CTRL:
+ {
+ static int flags;
+ qemu_chr_fe_ioctl(&s->cs, CHR_IOCTL_SERIAL_GET_TIOCM, &flags);
+ if (value & FTDI_SET_RTS) {
+ if (value & FTDI_RTS) {
+ flags |= CHR_TIOCM_RTS;
+ } else {
+ flags &= ~CHR_TIOCM_RTS;
+ }
+ }
+ if (value & FTDI_SET_DTR) {
+ if (value & FTDI_DTR) {
+ flags |= CHR_TIOCM_DTR;
+ } else {
+ flags &= ~CHR_TIOCM_DTR;
+ }
+ }
+ qemu_chr_fe_ioctl(&s->cs, CHR_IOCTL_SERIAL_SET_TIOCM, &flags);
+ break;
+ }
+ case VendorDeviceOutRequest | FTDI_SET_FLOW_CTRL: {
+ uint8_t flow_control = index >> 8;
+
+ usb_serial_set_flow_control(s, flow_control);
+ if (flow_control & FTDI_XON_XOFF_HS) {
+ usb_serial_set_xonxoff(s, value);
+ }
+ break;
+ }
+ case VendorDeviceOutRequest | FTDI_SET_BAUD: {
+ static const int subdivisors8[8] = { 0, 4, 2, 1, 3, 5, 6, 7 };
+ int subdivisor8 = subdivisors8[((value & 0xc000) >> 14)
+ | ((index & 1) << 2)];
+ int divisor = value & 0x3fff;
+
+ /* chip special cases */
+ if (divisor == 1 && subdivisor8 == 0) {
+ subdivisor8 = 4;
+ }
+ if (divisor == 0 && subdivisor8 == 0) {
+ divisor = 1;
+ }
+
+ s->params.speed = (48000000 / 2) / (8 * divisor + subdivisor8);
+ trace_usb_serial_set_baud(bus->busnr, dev->addr, s->params.speed);
+ qemu_chr_fe_ioctl(&s->cs, CHR_IOCTL_SERIAL_SET_PARAMS, &s->params);
+ break;
+ }
+ case VendorDeviceOutRequest | FTDI_SET_DATA:
+ switch (value & 0xff) {
+ case 7:
+ s->params.data_bits = 7;
+ break;
+ case 8:
+ s->params.data_bits = 8;
+ break;
+ default:
+ /*
+ * According to a comment in Linux's ftdi_sio.c original FTDI
+ * chips fall back to 8 data bits for unsupported data_bits
+ */
+ trace_usb_serial_unsupported_data_bits(bus->busnr, dev->addr,
+ value & 0xff);
+ s->params.data_bits = 8;
+ }
+
+ switch (value & FTDI_PARITY) {
+ case 0:
+ s->params.parity = 'N';
+ break;
+ case FTDI_ODD:
+ s->params.parity = 'O';
+ break;
+ case FTDI_EVEN:
+ s->params.parity = 'E';
+ break;
+ default:
+ trace_usb_serial_unsupported_parity(bus->busnr, dev->addr,
+ value & FTDI_PARITY);
+ goto fail;
+ }
+
+ switch (value & FTDI_STOP) {
+ case FTDI_STOP1:
+ s->params.stop_bits = 1;
+ break;
+ case FTDI_STOP2:
+ s->params.stop_bits = 2;
+ break;
+ default:
+ trace_usb_serial_unsupported_stopbits(bus->busnr, dev->addr,
+ value & FTDI_STOP);
+ goto fail;
+ }
+
+ trace_usb_serial_set_data(bus->busnr, dev->addr, s->params.parity,
+ s->params.data_bits, s->params.stop_bits);
+ qemu_chr_fe_ioctl(&s->cs, CHR_IOCTL_SERIAL_SET_PARAMS, &s->params);
+ /* TODO: TX ON/OFF */
+ break;
+ case VendorDeviceRequest | FTDI_GET_MDM_ST:
+ data[0] = usb_get_modem_lines(s) | 1;
+ data[1] = FTDI_THRE | FTDI_TEMT;
+ p->actual_length = 2;
+ break;
+ case VendorDeviceOutRequest | FTDI_SET_EVENT_CHR:
+ /* TODO: handle it */
+ s->event_chr = value;
+ break;
+ case VendorDeviceOutRequest | FTDI_SET_ERROR_CHR:
+ /* TODO: handle it */
+ s->error_chr = value;
+ break;
+ case VendorDeviceOutRequest | FTDI_SET_LATENCY:
+ s->latency = value;
+ break;
+ case VendorDeviceRequest | FTDI_GET_LATENCY:
+ data[0] = s->latency;
+ p->actual_length = 1;
+ break;
+ default:
+ fail:
+ trace_usb_serial_unsupported_control(bus->busnr, dev->addr, request,
+ value);
+ p->status = USB_RET_STALL;
+ break;
+ }
+}
+
+static void usb_serial_token_in(USBSerialState *s, USBPacket *p)
+{
+ const int max_packet_size = desc_iface0.eps[0].wMaxPacketSize;
+ int packet_len;
+ uint8_t header[2];
+
+ packet_len = p->iov.size;
+ if (packet_len <= 2) {
+ p->status = USB_RET_NAK;
+ return;
+ }
+
+ header[0] = usb_get_modem_lines(s) | 1;
+ /* We do not have the uart details */
+ /* handle serial break */
+ if (s->event_trigger && s->event_trigger & FTDI_BI) {
+ s->event_trigger &= ~FTDI_BI;
+ header[1] = FTDI_BI;
+ usb_packet_copy(p, header, 2);
+ return;
+ } else {
+ header[1] = 0;
+ }
+
+ if (!s->recv_used) {
+ p->status = USB_RET_NAK;
+ return;
+ }
+
+ while (s->recv_used && packet_len > 2) {
+ int first_len, len;
+
+ len = MIN(packet_len, max_packet_size);
+ len -= 2;
+ if (len > s->recv_used) {
+ len = s->recv_used;
+ }
+
+ first_len = RECV_BUF - s->recv_ptr;
+ if (first_len > len) {
+ first_len = len;
+ }
+ usb_packet_copy(p, header, 2);
+ usb_packet_copy(p, s->recv_buf + s->recv_ptr, first_len);
+ if (len > first_len) {
+ usb_packet_copy(p, s->recv_buf, len - first_len);
+ }
+ s->recv_used -= len;
+ s->recv_ptr = (s->recv_ptr + len) % RECV_BUF;
+ packet_len -= len + 2;
+ }
+
+ return;
+}
+
+static void usb_serial_handle_data(USBDevice *dev, USBPacket *p)
+{
+ USBSerialState *s = USB_SERIAL(dev);
+ USBBus *bus = usb_bus_from_device(dev);
+ uint8_t devep = p->ep->nr;
+ struct iovec *iov;
+ int i;
+
+ switch (p->pid) {
+ case USB_TOKEN_OUT:
+ if (devep != 2) {
+ goto fail;
+ }
+ for (i = 0; i < p->iov.niov; i++) {
+ iov = p->iov.iov + i;
+ /*
+ * XXX this blocks entire thread. Rewrite to use
+ * qemu_chr_fe_write and background I/O callbacks
+ */
+ qemu_chr_fe_write_all(&s->cs, iov->iov_base, iov->iov_len);
+ }
+ p->actual_length = p->iov.size;
+ break;
+
+ case USB_TOKEN_IN:
+ if (devep != 1) {
+ goto fail;
+ }
+ usb_serial_token_in(s, p);
+ break;
+
+ default:
+ trace_usb_serial_bad_token(bus->busnr, dev->addr);
+ fail:
+ p->status = USB_RET_STALL;
+ break;
+ }
+}
+
+static int usb_serial_can_read(void *opaque)
+{
+ USBSerialState *s = opaque;
+
+ if (!s->dev.attached) {
+ return 0;
+ }
+ return RECV_BUF - s->recv_used;
+}
+
+static void usb_serial_read(void *opaque, const uint8_t *buf, int size)
+{
+ USBSerialState *s = opaque;
+ int first_size, start;
+
+ /* room in the buffer? */
+ if (size > (RECV_BUF - s->recv_used)) {
+ size = RECV_BUF - s->recv_used;
+ }
+
+ start = s->recv_ptr + s->recv_used;
+ if (start < RECV_BUF) {
+ /* copy data to end of buffer */
+ first_size = RECV_BUF - start;
+ if (first_size > size) {
+ first_size = size;
+ }
+
+ memcpy(s->recv_buf + start, buf, first_size);
+
+ /* wrap around to front if needed */
+ if (size > first_size) {
+ memcpy(s->recv_buf, buf + first_size, size - first_size);
+ }
+ } else {
+ start -= RECV_BUF;
+ memcpy(s->recv_buf + start, buf, size);
+ }
+ s->recv_used += size;
+
+ usb_wakeup(s->intr, 0);
+}
+
+static void usb_serial_event(void *opaque, QEMUChrEvent event)
+{
+ USBSerialState *s = opaque;
+
+ switch (event) {
+ case CHR_EVENT_BREAK:
+ s->event_trigger |= FTDI_BI;
+ break;
+ case CHR_EVENT_OPENED:
+ if (!s->always_plugged && !s->dev.attached) {
+ usb_device_attach(&s->dev, &error_abort);
+ }
+ break;
+ case CHR_EVENT_CLOSED:
+ if (!s->always_plugged && s->dev.attached) {
+ usb_device_detach(&s->dev);
+ }
+ break;
+ case CHR_EVENT_MUX_IN:
+ case CHR_EVENT_MUX_OUT:
+ /* Ignore */
+ break;
+ }
+}
+
+static void usb_serial_realize(USBDevice *dev, Error **errp)
+{
+ USBSerialState *s = USB_SERIAL(dev);
+ Error *local_err = NULL;
+
+ usb_desc_create_serial(dev);
+ usb_desc_init(dev);
+ dev->auto_attach = 0;
+
+ if (!qemu_chr_fe_backend_connected(&s->cs)) {
+ error_setg(errp, "Property chardev is required");
+ return;
+ }
+
+ usb_check_attach(dev, &local_err);
+ if (local_err) {
+ error_propagate(errp, local_err);
+ return;
+ }
+
+ qemu_chr_fe_set_handlers(&s->cs, usb_serial_can_read, usb_serial_read,
+ usb_serial_event, NULL, s, NULL, true);
+ usb_serial_handle_reset(dev);
+
+ if ((s->always_plugged || qemu_chr_fe_backend_open(&s->cs)) &&
+ !dev->attached) {
+ usb_device_attach(dev, &error_abort);
+ }
+ s->intr = usb_ep_get(dev, USB_TOKEN_IN, 1);
+}
+
+static USBDevice *usb_braille_init(void)
+{
+ USBDevice *dev;
+ Chardev *cdrv;
+
+ cdrv = qemu_chr_new("braille", "braille", NULL);
+ if (!cdrv) {
+ return NULL;
+ }
+
+ dev = usb_new("usb-braille");
+ qdev_prop_set_chr(&dev->qdev, "chardev", cdrv);
+ return dev;
+}
+
+static const VMStateDescription vmstate_usb_serial = {
+ .name = "usb-serial",
+ .unmigratable = 1,
+};
+
+static Property serial_properties[] = {
+ DEFINE_PROP_CHR("chardev", USBSerialState, cs),
+ DEFINE_PROP_BOOL("always-plugged", USBSerialState, always_plugged, false),
+ DEFINE_PROP_END_OF_LIST(),
+};
+
+static void usb_serial_dev_class_init(ObjectClass *klass, void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+ uc->realize = usb_serial_realize;
+ uc->handle_reset = usb_serial_handle_reset;
+ uc->handle_control = usb_serial_handle_control;
+ uc->handle_data = usb_serial_handle_data;
+ dc->vmsd = &vmstate_usb_serial;
+ set_bit(DEVICE_CATEGORY_INPUT, dc->categories);
+}
+
+static const TypeInfo usb_serial_dev_type_info = {
+ .name = TYPE_USB_SERIAL,
+ .parent = TYPE_USB_DEVICE,
+ .instance_size = sizeof(USBSerialState),
+ .abstract = true,
+ .class_init = usb_serial_dev_class_init,
+};
+
+static void usb_serial_class_initfn(ObjectClass *klass, void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+ uc->product_desc = "QEMU USB Serial";
+ uc->usb_desc = &desc_serial;
+ device_class_set_props(dc, serial_properties);
+}
+
+static const TypeInfo serial_info = {
+ .name = "usb-serial",
+ .parent = TYPE_USB_SERIAL,
+ .class_init = usb_serial_class_initfn,
+};
+
+static Property braille_properties[] = {
+ DEFINE_PROP_CHR("chardev", USBSerialState, cs),
+ DEFINE_PROP_END_OF_LIST(),
+};
+
+static void usb_braille_class_initfn(ObjectClass *klass, void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ USBDeviceClass *uc = USB_DEVICE_CLASS(klass);
+
+ uc->product_desc = "QEMU USB Braille";
+ uc->usb_desc = &desc_braille;
+ device_class_set_props(dc, braille_properties);
+}
+
+static const TypeInfo braille_info = {
+ .name = "usb-braille",
+ .parent = TYPE_USB_SERIAL,
+ .class_init = usb_braille_class_initfn,
+};
+
+static void usb_serial_register_types(void)
+{
+ type_register_static(&usb_serial_dev_type_info);
+ type_register_static(&serial_info);
+ type_register_static(&braille_info);
+ usb_legacy_register("usb-braille", "braille", usb_braille_init);
+}
+
+type_init(usb_serial_register_types)