| /* | |
| * 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; | |
| } | |
| } | |
| 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_DEVICE(qdev_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 const Property serial_properties[] = { | |
| DEFINE_PROP_CHR("chardev", USBSerialState, cs), | |
| DEFINE_PROP_BOOL("always-plugged", USBSerialState, always_plugged, false), | |
| }; | |
| static void usb_serial_dev_class_init(ObjectClass *klass, const 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, const 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 const Property braille_properties[] = { | |
| DEFINE_PROP_CHR("chardev", USBSerialState, cs), | |
| }; | |
| static void usb_braille_class_initfn(ObjectClass *klass, const 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) |