diff --git a/lib/pbio/drv/usb/stm32_usbd/usbd_desc.c b/lib/pbio/drv/usb/stm32_usbd/usbd_desc.c index e3355d9aa..969b3082a 100644 --- a/lib/pbio/drv/usb/stm32_usbd/usbd_desc.c +++ b/lib/pbio/drv/usb/stm32_usbd/usbd_desc.c @@ -82,10 +82,10 @@ pbdrv_usb_dev_desc_union_t USBD_DeviceDesc = { .s = { .bLength = sizeof(pbdrv_usb_dev_desc_t), .bDescriptorType = DESC_TYPE_DEVICE, - .bcdUSB = 0x0210, /* 2.1.0 (for BOS support) */ - .bDeviceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bDeviceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bDeviceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bcdUSB = 0x0200, /* 2.0.0 */ + .bDeviceClass = USB_CLASS_MISC, + .bDeviceSubClass = USB_MISC_SUBCLASS_COMMON, + .bDeviceProtocol = USB_MISC_PROTOCOL_IAD, .bMaxPacketSize0 = USB_MAX_EP0_SIZE, .idVendor = PBDRV_CONFIG_USB_VID, .idProduct = PBDRV_CONFIG_USB_PID, @@ -221,21 +221,13 @@ static uint8_t *USBD_Pybricks_SerialStrDescriptor(USBD_SpeedTypeDef speed, uint1 return (uint8_t *)USBD_StringSerial; } -static uint8_t *USBD_Pybricks_BOSDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { - /* Prevent unused argument(s) compilation warning */ - UNUSED(speed); - - *length = sizeof(pbdrv_usb_bos_desc_set.s); - return (uint8_t *)&pbdrv_usb_bos_desc_set; -} - USBD_DescriptorsTypeDef USBD_Pybricks_Desc = { .GetDeviceDescriptor = USBD_Pybricks_DeviceDescriptor, .GetLangIDStrDescriptor = USBD_Pybricks_LangIDStrDescriptor, .GetManufacturerStrDescriptor = USBD_Pybricks_ManufacturerStrDescriptor, .GetProductStrDescriptor = USBD_Pybricks_ProductStrDescriptor, .GetSerialStrDescriptor = USBD_Pybricks_SerialStrDescriptor, - .GetBOSDescriptor = USBD_Pybricks_BOSDescriptor, + .GetBOSDescriptor = NULL, }; void USBD_Pybricks_Desc_Init(void) { diff --git a/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.c b/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.c index f550db0ec..c3c1e5d86 100644 --- a/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.c +++ b/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.c @@ -36,14 +36,10 @@ */ /* Includes ------------------------------------------------------------------*/ -#include -#include - #include "usbd_ctlreq.h" #include "usbd_pybricks.h" #include "../usb_ch9.h" -#include "../usb_common_desc.h" /** @addtogroup STM32_USB_DEVICE_LIBRARY @@ -116,7 +112,14 @@ USBD_ClassTypeDef USBD_Pybricks_ClassDriver = /* USB Pybricks device Configuration Descriptor */ typedef struct PBDRV_PACKED { pbdrv_usb_conf_desc_t conf_desc; - pbdrv_usb_iface_desc_t iface_desc; + pbdrv_usb_iad_desc_t iad; + pbdrv_usb_iface_desc_t comm_iface; + pbdrv_usb_cdc_header_desc_t cdc_header; + pbdrv_usb_cdc_call_mgmt_desc_t cdc_call_mgmt; + pbdrv_usb_cdc_acm_desc_t cdc_acm; + pbdrv_usb_cdc_union_desc_t cdc_union; + pbdrv_usb_ep_desc_t cmd_ep; + pbdrv_usb_iface_desc_t data_iface; pbdrv_usb_ep_desc_t ep_out; pbdrv_usb_ep_desc_t ep_in; } pbdrv_usb_stm32_conf_t; @@ -128,21 +131,80 @@ static pbdrv_usb_stm32_conf_union_t USBD_Pybricks_CfgDesc = { .bLength = sizeof(pbdrv_usb_conf_desc_t), .bDescriptorType = DESC_TYPE_CONFIGURATION, .wTotalLength = sizeof(pbdrv_usb_stm32_conf_t), - .bNumInterfaces = 1, + .bNumInterfaces = 2, .bConfigurationValue = 1, .iConfiguration = 0, .bmAttributes = USB_CONF_DESC_BM_ATTR_MUST_BE_SET, .bMaxPower = 250, /* 500mA (number of 2mA units) */ }, - .iface_desc = { + /* Interface Association: groups the comm and data interfaces into one + * CDC ACM function. */ + .iad = { + .bLength = sizeof(pbdrv_usb_iad_desc_t), + .bDescriptorType = DESC_TYPE_INTERFACE_ASSOCIATION, + .bFirstInterface = 0, + .bInterfaceCount = 2, + .bFunctionClass = USB_CLASS_CDC, + .bFunctionSubClass = USB_CDC_SUBCLASS_ACM, + .bFunctionProtocol = USB_CDC_PROTOCOL_AT, + .iFunction = 0, + }, + /* Communication interface */ + .comm_iface = { .bLength = sizeof(pbdrv_usb_iface_desc_t), .bDescriptorType = DESC_TYPE_INTERFACE, .bInterfaceNumber = 0, .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = USB_CLASS_CDC, + .bInterfaceSubClass = USB_CDC_SUBCLASS_ACM, + .bInterfaceProtocol = USB_CDC_PROTOCOL_AT, + .iInterface = 0, + }, + .cdc_header = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_header_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_HEADER, + .bcdCDC = 0x0110, + }, + .cdc_call_mgmt = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_call_mgmt_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_CALL_MGMT, + .bmCapabilities = 0x00, + .bDataInterface = 1, + }, + .cdc_acm = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_acm_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_ACM, + .bmCapabilities = 0x02, + }, + .cdc_union = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_union_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_UNION, + .bControlInterface = 0, + .bSubordinateInterface0 = 1, + }, + .cmd_ep = { + .bLength = sizeof(pbdrv_usb_ep_desc_t), + .bDescriptorType = DESC_TYPE_ENDPOINT, + .bEndpointAddress = USBD_PYBRICKS_CMD_EP, + .bmAttributes = PBDRV_USB_EP_TYPE_INTR, + .wMaxPacketSize = USBD_PYBRICKS_CMD_PACKET_SIZE, + .bInterval = 16, + }, + /* Data interface */ + .data_iface = { + .bLength = sizeof(pbdrv_usb_iface_desc_t), + .bDescriptorType = DESC_TYPE_INTERFACE, + .bInterfaceNumber = 1, + .bAlternateSetting = 0, .bNumEndpoints = 2, - .bInterfaceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bInterfaceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bInterfaceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bInterfaceClass = USB_CLASS_CDC_DATA, + .bInterfaceSubClass = 0, + .bInterfaceProtocol = 0, .iInterface = 0, }, .ep_out = { @@ -185,12 +247,28 @@ static USBD_StatusTypeDef USBD_Pybricks_Init(USBD_HandleTypeDef *pdev, uint8_t c pdev->pClassData = &hPybricks; + /* Default line coding reported to the host: 115200 baud, 1 stop bit, no + * parity, 8 data bits. This is inert: USB does not transfer data at this + * rate (it is not a real UART). CDC ACM just requires valid line coding to + * be stored and echoed back on GET_LINE_CODING. */ + hPybricks.LineCoding[0] = 0x00; + hPybricks.LineCoding[1] = 0xC2; + hPybricks.LineCoding[2] = 0x01; + hPybricks.LineCoding[3] = 0x00; + hPybricks.LineCoding[4] = 0x00; + hPybricks.LineCoding[5] = 0x00; + hPybricks.LineCoding[6] = 0x08; + hPybricks.CmdOpCode = 0xFFU; + (void)USBD_LL_OpenEP(pdev, USBD_PYBRICKS_IN_EP, USBD_EP_TYPE_BULK, USBD_PYBRICKS_MAX_PACKET_SIZE); pdev->ep_in[USBD_PYBRICKS_IN_EP & 0xFU].is_used = 1U; (void)USBD_LL_OpenEP(pdev, USBD_PYBRICKS_OUT_EP, USBD_EP_TYPE_BULK, USBD_PYBRICKS_MAX_PACKET_SIZE); pdev->ep_out[USBD_PYBRICKS_OUT_EP & 0xFU].is_used = 1U; + (void)USBD_LL_OpenEP(pdev, USBD_PYBRICKS_CMD_EP, USBD_EP_TYPE_INTR, USBD_PYBRICKS_CMD_PACKET_SIZE); + pdev->ep_in[USBD_PYBRICKS_CMD_EP & 0xFU].is_used = 1U; + /* Init physical Interface components */ ((USBD_Pybricks_ItfTypeDef *)pdev->pUserData[pdev->classId])->Init(); @@ -217,6 +295,10 @@ static USBD_StatusTypeDef USBD_Pybricks_DeInit(USBD_HandleTypeDef *pdev, uint8_t (void)USBD_LL_CloseEP(pdev, USBD_PYBRICKS_OUT_EP); pdev->ep_out[USBD_PYBRICKS_OUT_EP & 0xFU].is_used = 0U; + /* Close command EP */ + (void)USBD_LL_CloseEP(pdev, USBD_PYBRICKS_CMD_EP); + pdev->ep_in[USBD_PYBRICKS_CMD_EP & 0xFU].is_used = 0U; + /* DeInit physical Interface components */ if (pdev->pClassData != NULL) { ((USBD_Pybricks_ItfTypeDef *)pdev->pUserData[pdev->classId])->DeInit(); @@ -235,6 +317,7 @@ static USBD_StatusTypeDef USBD_Pybricks_DeInit(USBD_HandleTypeDef *pdev, uint8_t */ static USBD_StatusTypeDef USBD_Pybricks_Setup(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) { + USBD_Pybricks_HandleTypeDef *hPybricks = pdev->pClassData; uint8_t ifalt = 0U; uint16_t status_info = 0U; USBD_StatusTypeDef ret = USBD_OK; @@ -242,24 +325,37 @@ static USBD_StatusTypeDef USBD_Pybricks_Setup(USBD_HandleTypeDef *pdev, switch (req->bmRequest & USB_REQ_TYPE_MASK) { case USB_REQ_TYPE_CLASS: - ret = ((USBD_Pybricks_ItfTypeDef *)pdev->pUserData[pdev->classId])->ReadCharacteristic(pdev, req); - break; - - case USB_REQ_TYPE_VENDOR: + if (hPybricks == NULL) { + USBD_CtlError(pdev, req); + ret = USBD_FAIL; + break; + } switch (req->bRequest) { - case PBDRV_USB_VENDOR_REQ_MS_20: - (void)USBD_CtlSendData(pdev, - (uint8_t *)&pbdrv_usb_ms_20_desc_set, - MIN(sizeof(pbdrv_usb_ms_20_desc_set.s), req->wLength)); + case USB_CDC_REQ_SET_LINE_CODING: + /* Receive the line coding into the handle. The value is + * accepted but not acted on (the link is not a real UART). */ + if (req->wLength == USB_CDC_LINE_CODING_SIZE) { + hPybricks->CmdOpCode = (uint8_t)req->bRequest; + hPybricks->CmdLength = USB_CDC_LINE_CODING_SIZE; + (void)USBD_CtlPrepareRx(pdev, hPybricks->LineCoding, USB_CDC_LINE_CODING_SIZE); + } else { + USBD_CtlError(pdev, req); + ret = USBD_FAIL; + } break; - case PBDRV_USB_VENDOR_REQ_WEBUSB: - if ((req->wValue == PBDRV_USB_WEBUSB_LANDING_PAGE_URL_IDX) && (req->wIndex == WEBUSB_REQ_GET_URL)) { - (void)USBD_CtlSendData(pdev, - (uint8_t *)&pbdrv_usb_webusb_landing_page, - MIN(pbdrv_usb_webusb_landing_page.s.bLength, req->wLength)); - } + case USB_CDC_REQ_GET_LINE_CODING: + (void)USBD_CtlSendData(pdev, hPybricks->LineCoding, + MIN(USB_CDC_LINE_CODING_SIZE, req->wLength)); + break; + + case USB_CDC_REQ_SET_CONTROL_LINE_STATE: + /* The DTR bit indicates whether a host application has + * opened the port. This is the USB analog of a BLE host + * subscribing to notifications. */ + ((USBD_Pybricks_ItfTypeDef *)pdev->pUserData[pdev->classId])->SetControlLineState( + (req->wValue & USB_CDC_CONTROL_LINE_STATE_DTR) != 0U); break; default: @@ -388,6 +484,14 @@ static USBD_StatusTypeDef USBD_Pybricks_DataOut(USBD_HandleTypeDef *pdev, uint8_ * @retval status */ static USBD_StatusTypeDef USBD_Pybricks_EP0_RxReady(USBD_HandleTypeDef *pdev) { + USBD_Pybricks_HandleTypeDef *hPybricks = pdev->pClassData; + + if (hPybricks != NULL && hPybricks->CmdOpCode != 0xFFU) { + /* SET_LINE_CODING data has been received into hPybricks->LineCoding. + * Nothing to do; the value is stored for GET_LINE_CODING. */ + hPybricks->CmdOpCode = 0xFFU; + } + return USBD_OK; } diff --git a/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.h b/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.h index c040d9813..2939c9231 100644 --- a/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.h +++ b/lib/pbio/drv/usb/stm32_usbd/usbd_pybricks.h @@ -25,8 +25,12 @@ extern "C" { #endif /* Includes ------------------------------------------------------------------*/ +#include + #include "usbd_ioreq.h" +#include "../usb_ch9.h" + /** @addtogroup STM32_USB_DEVICE_LIBRARY * @{ */ @@ -38,8 +42,10 @@ extern "C" { #define USBD_PYBRICKS_IN_EP 0x81U /* EP1 for data IN */ #define USBD_PYBRICKS_OUT_EP 0x01U /* EP1 for data OUT */ +#define USBD_PYBRICKS_CMD_EP 0x82U /* EP2 for CDC notifications */ #define USBD_PYBRICKS_MAX_PACKET_SIZE 64U +#define USBD_PYBRICKS_CMD_PACKET_SIZE 8U /** * @} @@ -59,7 +65,7 @@ typedef struct USBD_StatusTypeDef (*DeInit)(void); USBD_StatusTypeDef (*Receive)(uint8_t *Buf, uint32_t Len); USBD_StatusTypeDef (*TransmitCplt)(uint8_t *Buf, uint32_t Len, uint8_t epnum); - USBD_StatusTypeDef (*ReadCharacteristic)(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req); + USBD_StatusTypeDef (*SetControlLineState)(bool dtr); } USBD_Pybricks_ItfTypeDef; @@ -69,6 +75,9 @@ typedef struct uint8_t *TxBuffer; uint32_t RxLength; uint32_t TxLength; + uint8_t CmdOpCode; + uint8_t CmdLength; + uint8_t LineCoding[USB_CDC_LINE_CODING_SIZE]; } USBD_Pybricks_HandleTypeDef; diff --git a/lib/pbio/drv/usb/usb.c b/lib/pbio/drv/usb/usb.c index fdc0807fa..8bb70cdbe 100644 --- a/lib/pbio/drv/usb/usb.c +++ b/lib/pbio/drv/usb/usb.c @@ -23,25 +23,109 @@ #include +#include + +#include + +#include + +#include +#include + static pbio_util_void_callback_t pbdrv_usb_host_connection_changed_callback; void pbdrv_usb_set_host_connection_changed_callback(pbio_util_void_callback_t callback) { pbdrv_usb_host_connection_changed_callback = callback; } + /** - * Host is subscribed to our outgoing event messages. + * Whether the host has opened the serial port (DTR asserted). This is the USB + * analog of a BLE host subscribing to notifications. */ -static bool pbdrv_usb_events_subscribed; +static bool pbdrv_usb_dtr; -static void pbdrv_usb_events_subscribed_set(bool subscribed) { - pbdrv_usb_events_subscribed = subscribed; - if (pbdrv_usb_host_connection_changed_callback) { - pbdrv_usb_host_connection_changed_callback(); +bool pbdrv_usb_connection_is_active(void) { + return pbdrv_usb_is_ready() && pbdrv_usb_dtr; +} + +// +// COBS (Consistent Overhead Byte Stuffing) framing. +// +// The host to hub and hub to host directions are raw byte streams (CDC ACM / +// Web Serial), so messages are delimited with a zero byte and COBS-encoded to +// guarantee the payload itself never contains a zero. This is self +// synchronizing: a corrupt or oversized frame is discarded at the next +// delimiter without losing frame alignment. +// +// The delimiter, size macros and the encode/decode helpers live in usb.h so +// the mock simulation drivers can reuse them. +// + +/** + * COBS-encodes @p len bytes from @p src into @p dst and appends the frame + * delimiter. @p dst must have room for at least ::PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE bytes + * when @p len is at most ::PBDRV_USB_MAX_DECODED_MESSAGE_SIZE. + * + * @return Number of bytes written to @p dst, including the trailing delimiter. + */ +uint32_t pbdrv_usb_cobs_encode(const uint8_t *src, uint32_t len, uint8_t *dst) { + uint32_t read_idx = 0; + uint32_t write_idx = 1; + uint32_t code_idx = 0; + uint8_t code = 1; + + while (read_idx < len) { + if (src[read_idx] == 0) { + dst[code_idx] = code; + code = 1; + code_idx = write_idx++; + read_idx++; + } else { + dst[write_idx++] = src[read_idx++]; + if (++code == 0xFF) { + dst[code_idx] = code; + code = 1; + code_idx = write_idx++; + } + } } + + dst[code_idx] = code; + dst[write_idx++] = PBDRV_USB_COBS_DELIMITER; + + return write_idx; } -bool pbdrv_usb_connection_is_active(void) { - return pbdrv_usb_events_subscribed && pbdrv_usb_is_ready(); +/** + * COBS-decodes @p len bytes from @p src (a single frame with the delimiter + * already stripped) into @p dst. + * + * @return Number of decoded bytes, or 0 if the frame was empty or malformed. + */ +uint32_t pbdrv_usb_cobs_decode(const uint8_t *src, uint32_t len, uint8_t *dst, uint32_t dst_max) { + uint32_t read_idx = 0; + uint32_t write_idx = 0; + + while (read_idx < len) { + uint8_t code = src[read_idx++]; + + for (uint8_t i = 1; i < code; i++) { + if (read_idx >= len || write_idx >= dst_max) { + // Malformed frame: ran out of input or output space. + return 0; + } + dst[write_idx++] = src[read_idx++]; + } + + if (code < 0xFF && read_idx < len) { + if (write_idx >= dst_max) { + return 0; + } + dst[write_idx++] = 0; + } + } + + return write_idx; } /** @@ -53,15 +137,66 @@ void pbdrv_usb_set_receive_handler(pbdrv_usb_receive_handler_t handler) { pbdrv_usb_receive_handler = handler; } +/** Maximum number of value bytes in a USB read reply message. */ +#define PBDRV_USB_READ_MAX_PAYLOAD (PBDRV_USB_MAX_DECODED_MESSAGE_SIZE - 4) + +static uint32_t pbdrv_usb_copy_str(uint8_t *buf, const char *str) { + uint32_t size = strlen(str); + if (size > PBDRV_USB_READ_MAX_PAYLOAD) { + size = PBDRV_USB_READ_MAX_PAYLOAD; + } + memcpy(buf, str, size); + return size; +} + +/** + * Provides characteristic values when a USB host requests a read. This is the + * USB analog of a BLE host reading the device info or Pybricks characteristics. + * + * REVISIT: this reaches up into pbsys for the value sources, much like each BLE + * driver still does today. We should invert this so pbsys registers a single read + * handler shared by USB and all BLE drivers. + */ +static uint32_t pbdrv_usb_read_characteristic(uint8_t service, uint16_t char_id, uint8_t *buf) { + switch (service) { + case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT: + switch (char_id) { + case 0x2A00: // Device Name + return pbdrv_usb_copy_str(buf, pbdrv_bluetooth_get_hub_name()); + case 0x2A26: // Firmware Revision + return pbdrv_usb_copy_str(buf, PBIO_VERSION_STR); + case 0x2A28: // Software Revision + return pbdrv_usb_copy_str(buf, PBIO_PROTOCOL_VERSION_STR); + default: + return 0; + } + case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_PYBRICKS: + switch (char_id) { + case 0x0003: // Hub capabilities + pbio_pybricks_hub_capabilities(buf, + PBDRV_USB_MAX_DECODED_MESSAGE_SIZE - 1, + PBSYS_CONFIG_APP_FEATURE_FLAGS, + pbsys_storage_get_maximum_program_size(), + PBSYS_CONFIG_HMI_NUM_SLOTS); + return PBIO_PYBRICKS_HUB_CAPABILITIES_VALUE_SIZE; + default: + return 0; + } + default: + return 0; + } +} + // // Functions related to sending value notifications (stdout, status, app data). // /** - * Each event has a buffer of maximum packet size. They are prioritized by - * ascending event number, so status gets sent first, then stdout, etc. + * Each event has a buffer sized to the maximum host notification. They are + * prioritized by ascending event number, so status gets sent first, then + * stdout, etc. */ -static uint8_t pbdrv_usb_noti_buf[PBIO_PYBRICKS_EVENT_NUM_EVENTS][PBDRV_CONFIG_USB_MAX_PACKET_SIZE] __attribute__((aligned(4))); +static uint8_t pbdrv_usb_noti_buf[PBIO_PYBRICKS_EVENT_NUM_EVENTS][PBSYS_CONFIG_HOST_NOTIFICATION_SIZE] __attribute__((aligned(4))); static uint32_t pbdrv_usb_noti_size[PBIO_PYBRICKS_EVENT_NUM_EVENTS]; /** @@ -70,6 +205,41 @@ static uint32_t pbdrv_usb_noti_size[PBIO_PYBRICKS_EVENT_NUM_EVENTS]; static uint8_t pbdrv_usb_status_data[PBIO_PYBRICKS_EVENT_STATUS_REPORT_SIZE]; static bool pbdrv_usb_status_data_pending; +/** + * Incoming COBS frame assembly buffer (encoded bytes, delimiter excluded). + */ +static uint8_t pbdrv_usb_rx_frame[PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE]; +static uint32_t pbdrv_usb_rx_frame_len; +static bool pbdrv_usb_rx_overflow; + +void pbdrv_usb_on_dtr_changed(bool dtr) { + if (dtr == pbdrv_usb_dtr) { + return; + } + + pbdrv_usb_dtr = dtr; + + // Drop any partially-assembled incoming frame. Otherwise stray bytes from a + // previous port opener (e.g. an OS modem probe like ModemManager, which + // writes AT strings with no frame delimiter) would prepend to and corrupt + // the first real frame of this new connection. + pbdrv_usb_rx_frame_len = 0; + pbdrv_usb_rx_overflow = false; + + if (dtr) { + // Host just opened the port. Send the current status right away, like + // the first notification after a BLE host subscribes. Device info is + // not pushed; the host reads it on demand via read requests. + pbdrv_usb_status_data_pending = true; + } + + if (pbdrv_usb_host_connection_changed_callback) { + pbdrv_usb_host_connection_changed_callback(); + } + + pbio_os_request_poll(); +} + void pbdrv_usb_schedule_status_update(const uint8_t *status_msg) { // Ignore if message identical to last. if (!memcmp(pbdrv_usb_status_data, status_msg, sizeof(pbdrv_usb_status_data))) { @@ -211,53 +381,105 @@ static bool update_and_get_event_buffer(uint8_t **buf, uint32_t **len) { return false; } +/** + * Pending command response to the most recently received command. + * + * The host keeps a single command outstanding at a time (like a BLE write with + * response), so a single command response slot is sufficient. + */ +static uint8_t pbdrv_usb_command_response_buf[sizeof(uint32_t) + 1] = { + [0] = PBIO_PYBRICKS_IN_EP_MSG_RESPONSE, +}; +static bool pbdrv_usb_command_response_pending; +/** + * Pending reply to the most recently received read request. + * + * Like responses, the host keeps a single read outstanding at a time, so a + * single reply slot is sufficient. The buffer holds the full message + * `[READ_REPLY, service, char_id_lo, char_id_hi, value...]`. + */ +static uint8_t pbdrv_usb_read_reply_buf[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; +static uint32_t pbdrv_usb_read_reply_len; +static bool pbdrv_usb_read_reply_pending; -static bool pbdrv_usb_respond_soon; -static pbio_pybricks_error_t pbdrv_usb_respond_result; +/** Number of header bytes before the value in a read reply. */ +#define PBDRV_USB_READ_REPLY_HEADER_SIZE 4 /** - * Non-blocking poll handler to process pending incoming messages. + * Non-blocking poll handler to process incoming bytes. + * + * Bytes are assembled into COBS frames. Each completed frame is decoded into a + * host-to-hub message whose first byte selects the type: a command is handled + * synchronously and its result queued as a response, while a read request is + * answered synchronously and queued as a read reply. This never depends on the + * transmit state, so it cannot deadlock. */ static void pbdrv_usb_handle_data_in(void) { - // Ignore incoming data if we haven't sent our previous response yet. - if (pbdrv_usb_respond_soon) { - return; - } - - // Data is copied here so the driver can immediately clear it and queue - // the next receive. - uint8_t data_in[PBDRV_CONFIG_USB_MAX_PACKET_SIZE]; + // Bytes are copied here so the driver can immediately queue the next + // receive. Only the single USB process thread runs this, so a static + // scratch buffer is safe and keeps the worst-case packet off the stack. + static uint8_t data_in[PBDRV_USB_RX_PACKET_MAX_SIZE]; uint32_t size = pbdrv_usb_get_data_and_start_receive(data_in); - // Expecting at least EP_MSG and payload. - if (size < 2) { - return; - } + for (uint32_t i = 0; i < size; i++) { + uint8_t byte = data_in[i]; + + if (byte != PBDRV_USB_COBS_DELIMITER) { + if (pbdrv_usb_rx_frame_len < sizeof(pbdrv_usb_rx_frame)) { + pbdrv_usb_rx_frame[pbdrv_usb_rx_frame_len++] = byte; + } else { + // Frame too big. Discard until the next delimiter resyncs us. + pbdrv_usb_rx_overflow = true; + } + continue; + } - switch (data_in[0]) { - case PBIO_PYBRICKS_OUT_EP_MSG_SUBSCRIBE: - pbdrv_usb_events_subscribed_set(data_in[1]); - pbdrv_usb_respond_result = PBIO_PYBRICKS_ERROR_OK; - pbdrv_usb_respond_soon = true; - - // Schedule sending current status immediately after subscribing. - pbdrv_usb_status_data_pending = true; - break; - case PBIO_PYBRICKS_OUT_EP_MSG_COMMAND: - if (pbdrv_usb_receive_handler) { - pbdrv_usb_respond_result = pbdrv_usb_receive_handler(data_in + 1, size - 1); - pbdrv_usb_respond_soon = true; + // Delimiter reached: end of frame. + if (!pbdrv_usb_rx_overflow && pbdrv_usb_rx_frame_len > 0) { + uint8_t msg[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; + uint32_t msg_size = pbdrv_usb_cobs_decode( + pbdrv_usb_rx_frame, pbdrv_usb_rx_frame_len, msg, sizeof(msg)); + + // The first byte is the host-to-hub message type and the rest is + // its payload. + if (msg_size >= 2 && msg[0] == PBIO_PYBRICKS_OUT_EP_MSG_COMMAND && pbdrv_usb_receive_handler) { + // Compute the response synchronously and queue it immediately. + pbio_set_uint32_le(&pbdrv_usb_command_response_buf[1], + pbdrv_usb_receive_handler(&msg[1], msg_size - 1)); + pbdrv_usb_command_response_pending = true; + pbio_os_request_poll(); + } else if (msg_size >= 4 && msg[0] == PBIO_PYBRICKS_OUT_EP_MSG_READ) { + // A read request is [type, service, char_id_lo, char_id_hi]. + // Read the value synchronously and queue the reply, echoing the + // selector so the host can correlate it. + uint8_t service = msg[1]; + uint16_t char_id = pbio_get_uint16_le(&msg[2]); + pbdrv_usb_read_reply_buf[0] = PBIO_PYBRICKS_IN_EP_MSG_READ_REPLY; + pbdrv_usb_read_reply_buf[1] = service; + pbdrv_usb_read_reply_buf[2] = msg[2]; + pbdrv_usb_read_reply_buf[3] = msg[3]; + uint32_t value_size = pbdrv_usb_read_characteristic( + service, char_id, &pbdrv_usb_read_reply_buf[PBDRV_USB_READ_REPLY_HEADER_SIZE]); + pbdrv_usb_read_reply_len = PBDRV_USB_READ_REPLY_HEADER_SIZE + value_size; + pbdrv_usb_read_reply_pending = true; + pbio_os_request_poll(); } - break; + } + + pbdrv_usb_rx_frame_len = 0; + pbdrv_usb_rx_overflow = false; } } static void pbdrv_usb_reset_state(void) { - pbdrv_usb_events_subscribed_set(false); - pbdrv_usb_respond_soon = false; + pbdrv_usb_on_dtr_changed(false); + pbdrv_usb_command_response_pending = false; + pbdrv_usb_read_reply_pending = false; pbdrv_usb_status_data_pending = false; + pbdrv_usb_rx_frame_len = 0; + pbdrv_usb_rx_overflow = false; lwrb_reset(&pbdrv_usb_stdout_ring_buf); memset(pbdrv_usb_noti_size, 0, sizeof(pbdrv_usb_noti_size)); } @@ -271,6 +493,10 @@ static pbio_error_t pbdrv_usb_process_thread(pbio_os_state_t *state, void *conte static uint32_t *noti_size; static uint8_t *noti_buf; + // COBS-encoded frame scratch buffer, populated just before each transmit. + static uint8_t tx_frame[PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE]; + static uint32_t tx_frame_len; + pbio_error_t err; // Runs every time. If there is no connection, there just won't be data. @@ -289,23 +515,34 @@ static pbio_error_t pbdrv_usb_process_thread(pbio_os_state_t *state, void *conte while (pbdrv_usb_process.request != PBIO_OS_PROCESS_REQUEST_TYPE_CANCEL && pbdrv_usb_is_ready()) { - // Find out what we should send, if anything, priotizing response, then - // status, then stdout, then other events. - if (pbdrv_usb_respond_soon) { - // Double buffer response so we're ready for another. - static pbio_pybricks_error_t error_code; - error_code = pbdrv_usb_respond_result; - pbdrv_usb_respond_soon = false; + // Find out what we should send, if anything, prioritizing replies + // to host requests (command response, then read reply), then status, then + // stdout, then other events. Unlike events, replies do not require + // an active connection, since they answer a request that was just + // received. + if (pbdrv_usb_command_response_pending) { + tx_frame_len = pbdrv_usb_cobs_encode(pbdrv_usb_command_response_buf, sizeof(pbdrv_usb_command_response_buf), tx_frame); + + PBIO_OS_AWAIT(state, &sub, err = pbdrv_usb_tx_chunk(&sub, tx_frame, tx_frame_len)); + pbdrv_usb_command_response_pending = false; + if (err != PBIO_SUCCESS) { + pbdrv_usb_reset_state(); + PBIO_OS_AWAIT(state, &sub, pbdrv_usb_tx_reset(&sub)); + } + } else if (pbdrv_usb_read_reply_pending) { + // Reply to a characteristic read request. + tx_frame_len = pbdrv_usb_cobs_encode(pbdrv_usb_read_reply_buf, pbdrv_usb_read_reply_len, tx_frame); - // Send the response. - PBIO_OS_AWAIT(state, &sub, err = pbdrv_usb_tx_response(&sub, error_code)); + PBIO_OS_AWAIT(state, &sub, err = pbdrv_usb_tx_chunk(&sub, tx_frame, tx_frame_len)); + pbdrv_usb_read_reply_pending = false; if (err != PBIO_SUCCESS) { pbdrv_usb_reset_state(); PBIO_OS_AWAIT(state, &sub, pbdrv_usb_tx_reset(&sub)); } } else if (pbdrv_usb_connection_is_active() && update_and_get_event_buffer(¬i_buf, ¬i_size)) { - // Send out pending event if any. - PBIO_OS_AWAIT(state, &sub, err = pbdrv_usb_tx_event(&sub, noti_buf, *noti_size)); + tx_frame_len = pbdrv_usb_cobs_encode(noti_buf, *noti_size, tx_frame); + + PBIO_OS_AWAIT(state, &sub, err = pbdrv_usb_tx_chunk(&sub, tx_frame, tx_frame_len)); *noti_size = 0; if (err != PBIO_SUCCESS) { pbdrv_usb_reset_state(); @@ -336,7 +573,8 @@ void pbdrv_usb_init(void) { pbdrv_usb_noti_buf[i][1] = i; // event type } - static uint8_t stdout_buf[PBDRV_CONFIG_USB_MAX_PACKET_SIZE * PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS]; + // Hardware agnostic buffer before user print waits. + static uint8_t stdout_buf[512]; lwrb_init(&pbdrv_usb_stdout_ring_buf, stdout_buf, PBIO_ARRAY_SIZE(stdout_buf)); pbio_os_process_start(&pbdrv_usb_process, pbdrv_usb_process_thread, NULL); diff --git a/lib/pbio/drv/usb/usb.h b/lib/pbio/drv/usb/usb.h index 2864675eb..03bbd0a5d 100644 --- a/lib/pbio/drv/usb/usb.h +++ b/lib/pbio/drv/usb/usb.h @@ -18,6 +18,52 @@ #define PBDRV_USB_TRANSMIT_TIMEOUT (500) +/** Frame delimiter byte. */ +#define PBDRV_USB_COBS_DELIMITER 0x00 + +/** + * Maximum size of a host notification, and of any other host-facing message. + * + * This is deliberately decoupled from the USB hardware packet size: it bounds + * how large a single message can be, independent of how the transport splits + * the resulting byte stream into hardware packets. + * + * REVISIT: this is a system-level concept that should eventually live in pbsys + * config and be shared with the BLE transport. + */ +#define PBSYS_CONFIG_HOST_NOTIFICATION_SIZE (62) + +/** Maximum size of a decoded message (message type byte plus payload). */ +#define PBDRV_USB_MAX_DECODED_MESSAGE_SIZE PBSYS_CONFIG_HOST_NOTIFICATION_SIZE + +/** Maximum size of a COBS-encoded frame including the trailing delimiter. */ +#define PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE (PBDRV_USB_MAX_DECODED_MESSAGE_SIZE + PBDRV_USB_MAX_DECODED_MESSAGE_SIZE / 254 + 2) + +/** + * Largest single USB packet any platform delivers on the data OUT endpoint + * (EV3 high-speed bulk). The common driver provides a receive scratch buffer of + * this size; the framing layer then reassembles messages from the raw byte + * stream, which may pack several small frames into one hardware packet. + */ +#define PBDRV_USB_RX_PACKET_MAX_SIZE (512) + +/** + * COBS-encodes @p len bytes from @p src into @p dst and appends the frame + * delimiter. @p dst must have room for at least ::PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE bytes + * when @p len is at most ::PBDRV_USB_MAX_DECODED_MESSAGE_SIZE. + * + * @return Number of bytes written to @p dst, including the trailing delimiter. + */ +uint32_t pbdrv_usb_cobs_encode(const uint8_t *src, uint32_t len, uint8_t *dst); + +/** + * COBS-decodes @p len bytes from @p src (a single frame with the delimiter + * already stripped) into @p dst. + * + * @return Number of decoded bytes, or 0 if the frame was empty or malformed. + */ +uint32_t pbdrv_usb_cobs_decode(const uint8_t *src, uint32_t len, uint8_t *dst, uint32_t dst_max); + /** * Initializes the USB driver on boot. */ @@ -39,52 +85,48 @@ void pbdrv_usb_deinit(void); void pbdrv_usb_deinit_device(void); /** - * Gets most recent incoming message and copies it to provided buffer. + * Gets bytes most recently received on the data OUT endpoint and copies them + * to the provided buffer. * - * The message is then cleared and the driver prepares to read a new message. + * The driver's receive buffer is then cleared and prepared to receive again. * - * @param [in] data Buffer to copy the message to. + * The host to hub direction is a raw byte stream (the message framing is + * handled by the common driver), so the returned bytes are an arbitrary slice + * of that stream, not necessarily a whole message. + * + * @param [in] data Buffer to copy the bytes to. Must be at least + * ::PBDRV_USB_RX_PACKET_MAX_SIZE bytes. * @return Number of bytes copied. Zero means nothing was available. */ uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data); /** - * Sends and awaits event message from hub to host via the Pybricks USB interface OUT endpoint. + * Sends and awaits a raw chunk of bytes on the data IN endpoint. * * Driver-specific implementation. Must return within ::PBDRV_USB_TRANSMIT_TIMEOUT. * * The USB process ensures that only one call is made at once. * - * Data must include the endpoint type and event code, so size is at least 2. - * * @param [in] state Protothread state. * @param [in] data Data to send. * @param [in] size Data size. * @return ::PBIO_SUCCESS on completion. - * ::PBIO_ERROR_INVALID_OP if there is no connection. * ::PBIO_ERROR_AGAIN while awaiting. * ::PBIO_ERROR_BUSY if this operation is already ongoing. - * ::PBIO_ERROR_INVALID_ARG if @p size is too large. * ::PBIO_ERROR_TIMEDOUT if the operation was started but could not complete. */ -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size); +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size); /** - * Sends and awaits response to an earlier incoming message. - * - * Driver-specific implementation. Must return within ::PBDRV_USB_TRANSMIT_TIMEOUT. + * Notifies the common driver that the host's serial control line state (DTR) + * changed. Called by the platform driver, typically from interrupt context. * - * The USB process ensures that only one call is made at once. + * DTR asserted means a host application has opened the serial port and is the + * USB analog of a BLE host subscribing to notifications. * - * @param [in] state Protothread state. - * @param [in] code Error code to send. - * @return ::PBIO_SUCCESS on completion. - * ::PBIO_ERROR_INVALID_OP if there is no connection. - * ::PBIO_ERROR_AGAIN while awaiting. - * ::PBIO_ERROR_BUSY if this operation is already ongoing. - * ::PBIO_ERROR_TIMEDOUT if the operation was started but could not complete. + * @param [in] dtr True if DTR is asserted (port open), otherwise false. */ -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code); +void pbdrv_usb_on_dtr_changed(bool dtr); /** * Resets the driver transmission state. diff --git a/lib/pbio/drv/usb/usb_ch9.h b/lib/pbio/drv/usb/usb_ch9.h index dcc50f8de..395790763 100644 --- a/lib/pbio/drv/usb/usb_ch9.h +++ b/lib/pbio/drv/usb/usb_ch9.h @@ -80,12 +80,6 @@ PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_setup_packet) #define DESC_TYPE_DEVICE_QUALIFIER 6 #define DESC_TYPE_OTHER_SPEED_CONFIGURATION 7 #define DESC_TYPE_INTERFACE_POWER 8 -// Binary Object Store, originally from the Wireless USB / USB 3.0 spec -// These descriptors have been backported to USB 2 and are available to -// devices which indicate a bcdDevice of at least 2.1. -// They are used for WebUSB and Microsoft WinUSB driver installation. -#define DESC_TYPE_BOS 15 -#define DESC_TYPE_DEVICE_CAPABILITY 16 // Device descriptor typedef struct PBDRV_PACKED { @@ -170,125 +164,87 @@ typedef struct PBDRV_PACKED { } pbdrv_usb_dev_qualifier_desc_t; PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_dev_qualifier_desc); -// Binary Object Store -typedef struct PBDRV_PACKED { - uint8_t bLength; - uint8_t bDescriptorType; - uint16_t wTotalLength; - uint8_t bNumDeviceCaps; -} pbdrv_usb_bos_desc_t; - -typedef struct PBDRV_PACKED { - uint32_t u0; - uint16_t u1; - uint16_t u2; - uint8_t u3[8]; -} pbdrv_usb_uuid_t; +// +// USB Communications Device Class (CDC) and Interface Association Descriptor +// (IAD) definitions, used for the CDC-ACM (virtual serial port) function. +// -// Platform-specific capabilities -#define USB_DEVICE_CAPABILITY_TYPE_PLATFORM 5 +// Interface Association Descriptor +#define DESC_TYPE_INTERFACE_ASSOCIATION 11 typedef struct PBDRV_PACKED { uint8_t bLength; uint8_t bDescriptorType; - uint8_t bDevCapabilityType; - uint8_t bReserved; - pbdrv_usb_uuid_t uuid; -} pbdrv_usb_platform_caps_common_t; - + uint8_t bFirstInterface; + uint8_t bInterfaceCount; + uint8_t bFunctionClass; + uint8_t bFunctionSubClass; + uint8_t bFunctionProtocol; + uint8_t iFunction; +} pbdrv_usb_iad_desc_t; + +// Device/function class codes +#define USB_CLASS_MISC 0xEF +#define USB_MISC_SUBCLASS_COMMON 0x02 +#define USB_MISC_PROTOCOL_IAD 0x01 + +#define USB_CLASS_CDC 0x02 +#define USB_CDC_SUBCLASS_ACM 0x02 +#define USB_CDC_PROTOCOL_AT 0x01 +#define USB_CLASS_CDC_DATA 0x0A + +// CDC functional descriptor types and subtypes +#define USB_CDC_CS_INTERFACE 0x24 +#define USB_CDC_CS_ENDPOINT 0x25 +#define USB_CDC_FUNC_SUBTYPE_HEADER 0x00 +#define USB_CDC_FUNC_SUBTYPE_CALL_MGMT 0x01 +#define USB_CDC_FUNC_SUBTYPE_ACM 0x02 +#define USB_CDC_FUNC_SUBTYPE_UNION 0x06 + +// CDC class-specific requests +#define USB_CDC_REQ_SET_LINE_CODING 0x20 +#define USB_CDC_REQ_GET_LINE_CODING 0x21 +#define USB_CDC_REQ_SET_CONTROL_LINE_STATE 0x22 + +// SET_CONTROL_LINE_STATE wValue bits +#define USB_CDC_CONTROL_LINE_STATE_DTR 0x01 +#define USB_CDC_CONTROL_LINE_STATE_RTS 0x02 + +// Number of bytes in a CDC line coding structure +#define USB_CDC_LINE_CODING_SIZE 7 + +// CDC Header functional descriptor typedef struct PBDRV_PACKED { - pbdrv_usb_platform_caps_common_t hdr; - uint16_t bcdVersion; - uint8_t bVendorCode; - uint8_t iLandingPage; -} pbdrv_usb_webusb_capability_t; - -// {3408b638-09a9-47a0-8bfd-a0768815b665} -#define USB_PLATFORM_CAP_GUID_WEBUSB { \ - .u0 = 0x3408b638, \ - .u1 = 0x09a9, \ - .u2 = 0x47a0, \ - .u3 = {0x8b, 0xfd, 0xa0, 0x76, 0x88, 0x15, 0xb6, 0x65}, \ -} + uint8_t bFunctionLength; + uint8_t bDescriptorType; // CS_INTERFACE + uint8_t bDescriptorSubtype; // HEADER + uint16_t bcdCDC; +} pbdrv_usb_cdc_header_desc_t; +// CDC Call Management functional descriptor typedef struct PBDRV_PACKED { - pbdrv_usb_platform_caps_common_t hdr; - uint32_t dwWindowsVersion; - uint16_t wMSOSDescriptorSetTotalLength; - uint8_t bMS_VendorCode; - uint8_t bAltEnumCode; -} pbdrv_usb_microsoft_20_capability_t; - -// {D8DD60DF-4589-4CC7-9CD2-659D9E648A9F} -#define USB_PLATFORM_CAP_GUID_MS_20 { \ - .u0 = 0xD8DD60DF, \ - .u1 = 0x4589, \ - .u2 = 0x4CC7, \ - .u3 = {0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F}, \ -} - -// These are not technically part of USB Chapter 9, but -// we need them and they logically fit with the rest of the structs. - -#define WEBUSB_REQ_GET_URL 2 -#define WEBUSB_DESC_TYPE_URL 3 -#define WEBUSB_URL_SCHEME_HTTP 0 -#define WEBUSB_URL_SCHEME_HTTPS 1 - + uint8_t bFunctionLength; + uint8_t bDescriptorType; // CS_INTERFACE + uint8_t bDescriptorSubtype; // CALL_MGMT + uint8_t bmCapabilities; + uint8_t bDataInterface; +} pbdrv_usb_cdc_call_mgmt_desc_t; + +// CDC Abstract Control Management functional descriptor typedef struct PBDRV_PACKED { - uint8_t bLength; - uint8_t bDescriptorType; - uint8_t bScheme; - uint8_t url[]; -} pbdrv_usb_webusb_url_desc_t; -PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_webusb_url_desc); - -// Microsoft wIndex values -#define MS_OS_20_DESCRIPTOR_INDEX 7 -#define MS_OS_20_SET_ALT_ENUMERATION 8 - -// Microsoft descriptor types -#define MS_OS_20_SET_HEADER_DESCRIPTOR 0 -#define MS_OS_20_SUBSET_HEADER_CONFIGURATION 1 -#define MS_OS_20_SUBSET_HEADER_FUNCTION 2 -#define MS_OS_20_FEATURE_COMPATBLE_ID 3 -#define MS_OS_20_FEATURE_REG_PROPERTY 4 -#define MS_OS_20_FEATURE_MIN_RESUME_TIME 5 -#define MS_OS_20_FEATURE_MODEL_ID 6 -#define MS_OS_20_FEATURE_CCGP_DEVICE 7 -#define MS_OS_20_FEATURE_VENDOR_REVISION 8 - -// We only need (and thus define) the following descriptor types - -typedef struct PBDRV_PACKED { - uint16_t wLength; - uint16_t wDescriptorType; - uint32_t dwWindowsVersion; - uint16_t wTotalLength; -} pbdrv_usb_ms_20_desc_set_header_t; + uint8_t bFunctionLength; + uint8_t bDescriptorType; // CS_INTERFACE + uint8_t bDescriptorSubtype; // ACM + uint8_t bmCapabilities; +} pbdrv_usb_cdc_acm_desc_t; +// CDC Union functional descriptor typedef struct PBDRV_PACKED { - uint16_t wLength; - uint16_t wDescriptorType; - uint8_t CompatibleID[8]; - uint8_t SubCompatibleID[8]; -} pbdrv_usb_ms_20_compatible_t; - -typedef struct PBDRV_PACKED { - uint16_t wLength; - uint16_t wDescriptorType; - uint16_t wPropertyDataType; -} pbdrv_usb_ms_20_reg_prop_hdr_t; - -#define MS_OS_20_REG_PROP_TYPE_REG_SZ 1 -#define MS_OS_20_REG_PROP_TYPE_REG_EXPAND_SZ 2 -#define MS_OS_20_REG_PROP_TYPE_REG_BINARY 3 -#define MS_OS_20_REG_PROP_TYPE_REG_DWORD_LE 4 -#define MS_OS_20_REG_PROP_TYPE_REG_DWORD_BE 5 -#define MS_OS_20_REG_PROP_TYPE_REG_LINK 6 -#define MS_OS_20_REG_PROP_TYPE_REG_MULTI_SZ 7 - -// This is the minimum Windows version needed to support these descriptors -#define MS_WINDOWS_VERSION_81 0x06030000 + uint8_t bFunctionLength; + uint8_t bDescriptorType; // CS_INTERFACE + uint8_t bDescriptorSubtype; // UNION + uint8_t bControlInterface; + uint8_t bSubordinateInterface0; +} pbdrv_usb_cdc_union_desc_t; #endif // _INTERNAL_PBDRV_USB_CH9_H_ diff --git a/lib/pbio/drv/usb/usb_common_desc.c b/lib/pbio/drv/usb/usb_common_desc.c index edc3e2802..45300078c 100644 --- a/lib/pbio/drv/usb/usb_common_desc.c +++ b/lib/pbio/drv/usb/usb_common_desc.c @@ -2,99 +2,13 @@ // Copyright (c) 2025 The Pybricks Authors // Descriptors which are shared across devices -// (i.e. WebUSB, Microsoft OS descriptors) #include #if PBDRV_CONFIG_USB -#include - #include "usb_common_desc.h" -#if PBIO_VERSION_LEVEL_HEX == 0xA -#define PYBRICKS_CODE_URL "labs.pybricks.com" -#elif PBIO_VERSION_LEVEL_HEX == 0xB -#define PYBRICKS_CODE_URL "beta.pybricks.com" -#else -#define PYBRICKS_CODE_URL "code.pybricks.com" -#endif - -const pbdrv_usb_webusb_url_desc_union_t pbdrv_usb_webusb_landing_page = { - .s = { - .bLength = sizeof(pbdrv_usb_webusb_url_desc_t) + sizeof(PYBRICKS_CODE_URL) - 1, - .bDescriptorType = WEBUSB_DESC_TYPE_URL, - .bScheme = WEBUSB_URL_SCHEME_HTTPS, - .url = PYBRICKS_CODE_URL, - }, -}; - -#define MS_20_REGISTRY_DATA_EXTRA_SZ \ - 2 + sizeof(PBDRV_USB_MS_20_REG_PROPERTY_NAME) + \ - 2 + sizeof(PBDRV_USB_MS_20_REG_PROPERTY_VAL) - -const pbdrv_usb_ms_20_desc_set_union_t pbdrv_usb_ms_20_desc_set = { - .s = { - .desc_set_hdr = { - .wLength = sizeof(pbdrv_usb_ms_20_desc_set_header_t), - .wDescriptorType = MS_OS_20_SET_HEADER_DESCRIPTOR, - .dwWindowsVersion = MS_WINDOWS_VERSION_81, - .wTotalLength = sizeof(pbdrv_usb_ms_20_desc_set_t), - }, - .compatible = { - .wLength = sizeof(pbdrv_usb_ms_20_compatible_t), - .wDescriptorType = MS_OS_20_FEATURE_COMPATBLE_ID, - .CompatibleID = "WINUSB", - .SubCompatibleID = "", - }, - .reg_prop_hdr = { - .wLength = sizeof(pbdrv_usb_ms_20_reg_prop_hdr_t) + MS_20_REGISTRY_DATA_EXTRA_SZ, - .wDescriptorType = MS_OS_20_FEATURE_REG_PROPERTY, - .wPropertyDataType = MS_OS_20_REG_PROP_TYPE_REG_MULTI_SZ, - }, - .property_name_len = sizeof(PBDRV_USB_MS_20_REG_PROPERTY_NAME), - .device_interface_guids = PBDRV_USB_MS_20_REG_PROPERTY_NAME, - .property_val_len = sizeof(PBDRV_USB_MS_20_REG_PROPERTY_VAL), - .device_interface_guid_val = PBDRV_USB_MS_20_REG_PROPERTY_VAL, - } -}; - -const pbdrv_usb_bos_desc_set_union_t pbdrv_usb_bos_desc_set = { - .s = { - .bos = { - .bLength = sizeof(pbdrv_usb_bos_desc_t), - .bDescriptorType = DESC_TYPE_BOS, - .wTotalLength = sizeof(pbdrv_usb_bos_desc_set_t), - .bNumDeviceCaps = 2, - }, - .webusb = { - .hdr = { - .bLength = sizeof(pbdrv_usb_webusb_capability_t), - .bDescriptorType = DESC_TYPE_DEVICE_CAPABILITY, - .bDevCapabilityType = USB_DEVICE_CAPABILITY_TYPE_PLATFORM, - .bReserved = 0, - .uuid = USB_PLATFORM_CAP_GUID_WEBUSB, - }, - .bcdVersion = 0x0100, - .bVendorCode = PBDRV_USB_VENDOR_REQ_WEBUSB, - .iLandingPage = PBDRV_USB_WEBUSB_LANDING_PAGE_URL_IDX, - }, - .ms_20 = { - .hdr = { - .bLength = sizeof(pbdrv_usb_microsoft_20_capability_t), - .bDescriptorType = DESC_TYPE_DEVICE_CAPABILITY, - .bDevCapabilityType = USB_DEVICE_CAPABILITY_TYPE_PLATFORM, - .bReserved = 0, - .uuid = USB_PLATFORM_CAP_GUID_MS_20, - }, - .dwWindowsVersion = MS_WINDOWS_VERSION_81, - .wMSOSDescriptorSetTotalLength = sizeof(pbdrv_usb_ms_20_desc_set_t), - .bMS_VendorCode = PBDRV_USB_VENDOR_REQ_MS_20, - .bAltEnumCode = 0, - } - } -}; - const pbdrv_usb_langid_union_t pbdrv_usb_str_desc_langid = { .s = { .bLength = 4, diff --git a/lib/pbio/drv/usb/usb_common_desc.h b/lib/pbio/drv/usb/usb_common_desc.h index ff77c17e2..25eabd65f 100644 --- a/lib/pbio/drv/usb/usb_common_desc.h +++ b/lib/pbio/drv/usb/usb_common_desc.h @@ -2,7 +2,6 @@ // Copyright (c) 2025 The Pybricks Authors // Descriptors which are shared across devices -// (i.e. WebUSB, Microsoft OS descriptors) #ifndef _INTERNAL_PBDRV_USB_COMMON_DESC_H_ #define _INTERNAL_PBDRV_USB_COMMON_DESC_H_ @@ -14,51 +13,6 @@ #include "usb_ch9.h" -/** - * USB vendor requests which will be used to retrieve WebUSB and Microsoft descriptors - */ -enum { - PBDRV_USB_VENDOR_REQ_WEBUSB, - PBDRV_USB_VENDOR_REQ_MS_20, -}; - -// The descriptor index for the WebUSB landing page -#define PBDRV_USB_WEBUSB_LANDING_PAGE_URL_IDX 1 - -#define PBDRV_USB_MS_20_REG_PROPERTY_NAME u"DeviceInterfaceGUIDs" -// This UUID has been generated by Pybricks and identifies specifically -// our devices which use the Pybricks protocol. Windows APIs can match on this. -// An extra null terminator is required to terminate a REG_MULTI_SZ -#define PBDRV_USB_MS_20_REG_PROPERTY_VAL u"{A5C44A4C-53D4-4389-9821-AE95051908A1}\x00" - -// Complete set of Microsoft USB descriptors -typedef struct PBDRV_PACKED { - pbdrv_usb_ms_20_desc_set_header_t desc_set_hdr; - pbdrv_usb_ms_20_compatible_t compatible; - pbdrv_usb_ms_20_reg_prop_hdr_t reg_prop_hdr; - uint16_t property_name_len; - uint16_t device_interface_guids[PBIO_ARRAY_SIZE(PBDRV_USB_MS_20_REG_PROPERTY_NAME)]; - uint16_t property_val_len; - uint16_t device_interface_guid_val[PBIO_ARRAY_SIZE(PBDRV_USB_MS_20_REG_PROPERTY_VAL)]; -} pbdrv_usb_ms_20_desc_set_t; -PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_ms_20_desc_set); - -extern const pbdrv_usb_ms_20_desc_set_union_t pbdrv_usb_ms_20_desc_set; - -// WebUSB descriptor -extern const pbdrv_usb_webusb_url_desc_union_t pbdrv_usb_webusb_landing_page; - -// Complete set of USB BOS descriptors -typedef struct PBDRV_PACKED { - pbdrv_usb_bos_desc_t bos; - // WebUSB must occur before Microsoft OS descriptors - pbdrv_usb_webusb_capability_t webusb; - pbdrv_usb_microsoft_20_capability_t ms_20; -} pbdrv_usb_bos_desc_set_t; -PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_bos_desc_set); - -extern const pbdrv_usb_bos_desc_set_union_t pbdrv_usb_bos_desc_set; - // (Human-readable) string descriptors typedef struct PBDRV_PACKED { uint8_t bLength; diff --git a/lib/pbio/drv/usb/usb_ev3.c b/lib/pbio/drv/usb/usb_ev3.c index a0ace57aa..69cb79c65 100644 --- a/lib/pbio/drv/usb/usb_ev3.c +++ b/lib/pbio/drv/usb/usb_ev3.c @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2025 The Pybricks Authors -// EV3 / TI AM1808 / Mentor Graphics MUSBMHDRC driver -// implementing a bespoke USB stack for Pybricks USB protocol +// EV3 / TI AM1808 / Mentor Graphics MUSBMHDRC driver implementing a bespoke +// USB stack for a USB CDC-ACM (virtual serial port) device. #include @@ -12,23 +12,13 @@ #include #include -#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include #include "usb.h" -#include - #include #include #include @@ -58,13 +48,10 @@ #define EP0_BUF_SZ 64 #define PYBRICKS_EP_PKT_SZ_FS 64 #define PYBRICKS_EP_PKT_SZ_HS 512 +// Packet size of the CDC notification (interrupt IN) endpoint. We never send +// notifications, so this only needs to be large enough to be valid. +#define NOTIF_EP_PKT_SZ 8 -// All buffers must allow the highest possible packet size. When writing -// to them, pbdrv_usb_max_package_size() gets the actual limit based on -// active speed mode. -#if PBDRV_CONFIG_USB_MAX_PACKET_SIZE != PYBRICKS_EP_PKT_SZ_HS -#error Inconsistent USB packet size -#endif /** * Indices for string descriptors @@ -82,12 +69,12 @@ static const pbdrv_usb_dev_desc_union_t dev_desc = { .s = { .bLength = sizeof(pbdrv_usb_dev_desc_t), .bDescriptorType = DESC_TYPE_DEVICE, - // A BOS descriptor is needed for Windows driver auto-installation, - // so this must be at least 2.1 - .bcdUSB = 0x0210, - .bDeviceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bDeviceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bDeviceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bcdUSB = 0x0200, + // Use the Interface Association Descriptor device class so that the + // CDC comm and data interfaces are grouped into one function. + .bDeviceClass = USB_CLASS_MISC, + .bDeviceSubClass = USB_MISC_SUBCLASS_COMMON, + .bDeviceProtocol = USB_MISC_PROTOCOL_IAD, .bMaxPacketSize0 = EP0_BUF_SZ, .idVendor = PBDRV_CONFIG_USB_VID, .idProduct = PBDRV_CONFIG_USB_PID, @@ -102,10 +89,10 @@ static const pbdrv_usb_dev_qualifier_desc_union_t dev_qualifier_desc = { .s = { .bLength = sizeof(pbdrv_usb_dev_qualifier_desc_t), .bDescriptorType = DESC_TYPE_DEVICE_QUALIFIER, - .bcdUSB = 0x0210, - .bDeviceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bDeviceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bDeviceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bcdUSB = 0x0200, + .bDeviceClass = USB_CLASS_MISC, + .bDeviceSubClass = USB_MISC_SUBCLASS_COMMON, + .bDeviceProtocol = USB_MISC_PROTOCOL_IAD, .bMaxPacketSize0 = EP0_BUF_SZ, .bNumConfigurations = 1, .bReserved = 0, @@ -114,105 +101,151 @@ static const pbdrv_usb_dev_qualifier_desc_union_t dev_qualifier_desc = { typedef struct PBDRV_PACKED { pbdrv_usb_conf_desc_t conf_desc; - pbdrv_usb_iface_desc_t iface_desc; + pbdrv_usb_iad_desc_t iad; + pbdrv_usb_iface_desc_t comm_iface; + pbdrv_usb_cdc_header_desc_t cdc_header; + pbdrv_usb_cdc_call_mgmt_desc_t cdc_call_mgmt; + pbdrv_usb_cdc_acm_desc_t cdc_acm; + pbdrv_usb_cdc_union_desc_t cdc_union; + pbdrv_usb_ep_desc_t notif_ep; + pbdrv_usb_iface_desc_t data_iface; pbdrv_usb_ep_desc_t ep_1_out; pbdrv_usb_ep_desc_t ep_1_in; } pbdrv_usb_ev3_conf_1_t; PBDRV_USB_TYPE_PUNNING_HELPER(pbdrv_usb_ev3_conf_1); +// The high-speed and full-speed configuration descriptors are identical apart +// from the bulk endpoint max packet size, so a macro fills in the common parts. +#define PBDRV_USB_EV3_CONF_1_COMMON(bulk_pkt_size) \ + .conf_desc = { \ + .bLength = sizeof(pbdrv_usb_conf_desc_t), \ + .bDescriptorType = DESC_TYPE_CONFIGURATION, \ + .wTotalLength = sizeof(pbdrv_usb_ev3_conf_1_t), \ + .bNumInterfaces = 2, \ + .bConfigurationValue = 1, \ + .iConfiguration = 0, \ + .bmAttributes = USB_CONF_DESC_BM_ATTR_MUST_BE_SET | USB_CONF_DESC_BM_ATTR_SELF_POWERED, \ + .bMaxPower = 0, \ + }, \ + /* Interface Association: groups the comm and data interfaces into one \ + * CDC ACM function. */ \ + .iad = { \ + .bLength = sizeof(pbdrv_usb_iad_desc_t), \ + .bDescriptorType = DESC_TYPE_INTERFACE_ASSOCIATION, \ + .bFirstInterface = 0, \ + .bInterfaceCount = 2, \ + .bFunctionClass = USB_CLASS_CDC, \ + .bFunctionSubClass = USB_CDC_SUBCLASS_ACM, \ + .bFunctionProtocol = USB_CDC_PROTOCOL_AT, \ + .iFunction = 0, \ + }, \ + /* Communication interface */ \ + .comm_iface = { \ + .bLength = sizeof(pbdrv_usb_iface_desc_t), \ + .bDescriptorType = DESC_TYPE_INTERFACE, \ + .bInterfaceNumber = 0, \ + .bAlternateSetting = 0, \ + .bNumEndpoints = 1, \ + .bInterfaceClass = USB_CLASS_CDC, \ + .bInterfaceSubClass = USB_CDC_SUBCLASS_ACM, \ + .bInterfaceProtocol = USB_CDC_PROTOCOL_AT, \ + .iInterface = 0, \ + }, \ + .cdc_header = { \ + .bFunctionLength = sizeof(pbdrv_usb_cdc_header_desc_t), \ + .bDescriptorType = USB_CDC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_HEADER, \ + .bcdCDC = 0x0110, \ + }, \ + .cdc_call_mgmt = { \ + .bFunctionLength = sizeof(pbdrv_usb_cdc_call_mgmt_desc_t), \ + .bDescriptorType = USB_CDC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_CALL_MGMT, \ + .bmCapabilities = 0x00, \ + .bDataInterface = 1, \ + }, \ + .cdc_acm = { \ + .bFunctionLength = sizeof(pbdrv_usb_cdc_acm_desc_t), \ + .bDescriptorType = USB_CDC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_ACM, \ + .bmCapabilities = 0x02, \ + }, \ + .cdc_union = { \ + .bFunctionLength = sizeof(pbdrv_usb_cdc_union_desc_t), \ + .bDescriptorType = USB_CDC_CS_INTERFACE, \ + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_UNION, \ + .bControlInterface = 0, \ + .bSubordinateInterface0 = 1, \ + }, \ + .notif_ep = { \ + .bLength = sizeof(pbdrv_usb_ep_desc_t), \ + .bDescriptorType = DESC_TYPE_ENDPOINT, \ + .bEndpointAddress = 0x82, \ + .bmAttributes = PBDRV_USB_EP_TYPE_INTR, \ + .wMaxPacketSize = NOTIF_EP_PKT_SZ, \ + .bInterval = 16, \ + }, \ + /* Data interface */ \ + .data_iface = { \ + .bLength = sizeof(pbdrv_usb_iface_desc_t), \ + .bDescriptorType = DESC_TYPE_INTERFACE, \ + .bInterfaceNumber = 1, \ + .bAlternateSetting = 0, \ + .bNumEndpoints = 2, \ + .bInterfaceClass = USB_CLASS_CDC_DATA, \ + .bInterfaceSubClass = 0, \ + .bInterfaceProtocol = 0, \ + .iInterface = 0, \ + }, \ + .ep_1_out = { \ + .bLength = sizeof(pbdrv_usb_ep_desc_t), \ + .bDescriptorType = DESC_TYPE_ENDPOINT, \ + .bEndpointAddress = 0x01, \ + .bmAttributes = PBDRV_USB_EP_TYPE_BULK, \ + .wMaxPacketSize = (bulk_pkt_size), \ + .bInterval = 0, \ + }, \ + .ep_1_in = { \ + .bLength = sizeof(pbdrv_usb_ep_desc_t), \ + .bDescriptorType = DESC_TYPE_ENDPOINT, \ + .bEndpointAddress = 0x81, \ + .bmAttributes = PBDRV_USB_EP_TYPE_BULK, \ + .wMaxPacketSize = (bulk_pkt_size), \ + .bInterval = 0, \ + } + static const pbdrv_usb_ev3_conf_1_union_t configuration_1_desc_hs = { .s = { - .conf_desc = { - .bLength = sizeof(pbdrv_usb_conf_desc_t), - .bDescriptorType = DESC_TYPE_CONFIGURATION, - .wTotalLength = sizeof(pbdrv_usb_ev3_conf_1_t), - .bNumInterfaces = 1, - .bConfigurationValue = 1, - .iConfiguration = 0, - .bmAttributes = USB_CONF_DESC_BM_ATTR_MUST_BE_SET | USB_CONF_DESC_BM_ATTR_SELF_POWERED, - .bMaxPower = 0, - }, - .iface_desc = { - .bLength = sizeof(pbdrv_usb_iface_desc_t), - .bDescriptorType = DESC_TYPE_INTERFACE, - .bInterfaceNumber = 0, - .bAlternateSetting = 0, - .bNumEndpoints = 2, - .bInterfaceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bInterfaceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bInterfaceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, - .iInterface = 0, - }, - .ep_1_out = { - .bLength = sizeof(pbdrv_usb_ep_desc_t), - .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x01, - .bmAttributes = PBDRV_USB_EP_TYPE_BULK, - .wMaxPacketSize = PYBRICKS_EP_PKT_SZ_HS, - .bInterval = 0, - }, - .ep_1_in = { - .bLength = sizeof(pbdrv_usb_ep_desc_t), - .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x81, - .bmAttributes = PBDRV_USB_EP_TYPE_BULK, - .wMaxPacketSize = PYBRICKS_EP_PKT_SZ_HS, - .bInterval = 0, - }, + PBDRV_USB_EV3_CONF_1_COMMON(PYBRICKS_EP_PKT_SZ_HS), } }; static const pbdrv_usb_ev3_conf_1_union_t configuration_1_desc_fs = { .s = { - .conf_desc = { - .bLength = sizeof(pbdrv_usb_conf_desc_t), - .bDescriptorType = DESC_TYPE_CONFIGURATION, - .wTotalLength = sizeof(pbdrv_usb_ev3_conf_1_t), - .bNumInterfaces = 1, - .bConfigurationValue = 1, - .iConfiguration = 0, - .bmAttributes = USB_CONF_DESC_BM_ATTR_MUST_BE_SET | USB_CONF_DESC_BM_ATTR_SELF_POWERED, - .bMaxPower = 0, - }, - .iface_desc = { - .bLength = sizeof(pbdrv_usb_iface_desc_t), - .bDescriptorType = DESC_TYPE_INTERFACE, - .bInterfaceNumber = 0, - .bAlternateSetting = 0, - .bNumEndpoints = 2, - .bInterfaceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bInterfaceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bInterfaceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, - .iInterface = 0, - }, - .ep_1_out = { - .bLength = sizeof(pbdrv_usb_ep_desc_t), - .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x01, - .bmAttributes = PBDRV_USB_EP_TYPE_BULK, - .wMaxPacketSize = PYBRICKS_EP_PKT_SZ_FS, - .bInterval = 0, - }, - .ep_1_in = { - .bLength = sizeof(pbdrv_usb_ep_desc_t), - .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x81, - .bmAttributes = PBDRV_USB_EP_TYPE_BULK, - .wMaxPacketSize = PYBRICKS_EP_PKT_SZ_FS, - .bInterval = 0, - }, + PBDRV_USB_EV3_CONF_1_COMMON(PYBRICKS_EP_PKT_SZ_FS), } }; // This dynamic buffer is needed in order to have an aligned, // global-lifetime buffer for sending dynamic data in response -// to control transfers. This is used for the serial number string -// and for Pybricks protocol requests. +// to control transfers. This is used for the serial number string. static union { uint8_t b[EP0_BUF_SZ]; uint32_t u[EP0_BUF_SZ / sizeof(uint32_t)]; } pbdrv_usb_ev3_ep0_buffer; -_Static_assert(PBIO_PYBRICKS_HUB_CAPABILITIES_VALUE_SIZE <= EP0_BUF_SZ); + +// CDC line coding (baud rate, stop bits, parity, data bits). The host can set +// and get it, but we ignore the actual values since this is a virtual port. +// Defaults to 115200 8N1. +static union { + uint8_t b[USB_CDC_LINE_CODING_SIZE]; + uint32_t u[(USB_CDC_LINE_CODING_SIZE + sizeof(uint32_t) - 1) / sizeof(uint32_t)]; +} pbdrv_usb_ev3_line_coding = { + .b = { 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08 }, +}; + +// Set while EP0 is waiting for the host to send the SET_LINE_CODING data stage. +static bool pbdrv_usb_ep0_expect_line_coding; // Defined in pbio/platform/ev3/platform.c extern uint8_t pbdrv_ev3_bluetooth_mac_address[6]; @@ -269,8 +302,7 @@ _Static_assert(sizeof(usb_cppi_hpd_t) <= CPPI_DESCRIPTOR_ALIGN); // rather than dynamically allocating them as needed enum { CPPI_DESC_RX, - CPPI_DESC_TX_RESPONSE, - CPPI_DESC_TX_PYBRICKS_EVENT, + CPPI_DESC_TX, // the minimum number of descriptors we can allocate is 32, // even though we do not use nearly all of them CPPI_DESC_COUNT = 32, @@ -315,8 +347,8 @@ static void usb_setup_rx_dma_desc(void) { // Fill in the CPPI DMA descriptor to send a packet -static void usb_setup_tx_dma_desc(int tx_type, void *buf, uint32_t buf_len) { - PBDRV_UNCACHED(cppi_descriptors[tx_type]) = (usb_cppi_hpd_t) { +static void usb_setup_tx_dma_desc(void *buf, uint32_t buf_len) { + PBDRV_UNCACHED(cppi_descriptors[CPPI_DESC_TX]) = (usb_cppi_hpd_t) { .word0 = { .hostPktType = CPPI_HOST_PACKET_DESCRIPTOR_TYPE, .pktLength = buf_len, @@ -338,7 +370,7 @@ static void usb_setup_tx_dma_desc(int tx_type, void *buf, uint32_t buf_len) { pbdrv_compiler_memory_barrier(); HWREG(USB_0_OTGBASE + CPDMA_QUEUE_REGISTER_D + TX_SUBMITQ1 * 16) = - (uint32_t)(&cppi_descriptors[tx_type]) | CPPI_DESCRIPTOR_SIZE_BITS; + (uint32_t)(&cppi_descriptors[CPPI_DESC_TX]) | CPPI_DESCRIPTOR_SIZE_BITS; } // Helper function to set up CPPI DMA upon USB reset @@ -363,6 +395,20 @@ static void usb_reset_cppi_dma(void) { HWREGH(USB0_BASE + USB_O_TXFIFOADD) = EP0_BUF_SZ / 8; HWREGH(USB0_BASE + USB_O_RXFIFOADD) = (EP0_BUF_SZ + PYBRICKS_EP_PKT_SZ_HS) / 8; + // Set up the CDC notification endpoint (EP2 IN, interrupt). The host + // (e.g. Linux cdc_acm) requires this endpoint to exist to bind the driver, + // but we never send notifications, so it just NAKs forever. Its FIFO is + // placed after the EP1 TX and RX FIFOs. + HWREGB(USB0_BASE + USB_O_EPIDX) = 2; + HWREGB(USB0_BASE + USB_O_TXFIFOSZ) = USB_TXFIFOSZ_SIZE_8; + HWREGH(USB0_BASE + USB_O_TXFIFOADD) = + (EP0_BUF_SZ + PYBRICKS_EP_PKT_SZ_HS + PYBRICKS_EP_PKT_SZ_HS) / 8; + HWREGH(USB0_BASE + USB_O_TXMAXP2) = NOTIF_EP_PKT_SZ; + // Select TX direction for the shared endpoint FIFO. + HWREGB(USB0_BASE + USB_O_TXCSRH2) = USB_TXCSRH2_MODE; + // Reset data toggle and flush the FIFO. + HWREGB(USB0_BASE + USB_O_TXCSRL2) = USB_TXCSRL2_CLRDT | USB_TXCSRL2_FLUSH; + // Set up the TX fifo for DMA and a stall condition HWREGH(USB0_BASE + USB_O_TXCSRL1) = ((USB_TXCSRH1_AUTOSET | USB_TXCSRH1_MODE | USB_TXCSRH1_DMAEN | USB_TXCSRH1_DMAMOD) << 8) | USB_TXCSRL1_STALL; // Set up the RX fifo for DMA and a stall condition @@ -491,11 +537,6 @@ static bool usb_get_descriptor(uint16_t wValue) { return true; } break; - - case DESC_TYPE_BOS: - pbdrv_usb_setup_data_to_send = pbdrv_usb_bos_desc_set.u; - pbdrv_usb_setup_data_to_send_sz = sizeof(pbdrv_usb_bos_desc_set.s); - return true; } return false; @@ -545,6 +586,7 @@ static void usb_device_intr(void) { HWREGH(USB0_BASE + USB_O_CSRL0) = 0; pbdrv_usb_setup_data_to_send = 0; pbdrv_usb_addr_needs_setting = false; + pbdrv_usb_ep0_expect_line_coding = false; } if (peri_csr & USB_CSRL0_SETEND) { @@ -552,6 +594,7 @@ static void usb_device_intr(void) { HWREGH(USB0_BASE + USB_O_CSRL0) = USB_CSRL0_SETENDC; pbdrv_usb_setup_data_to_send = 0; pbdrv_usb_addr_needs_setting = false; + pbdrv_usb_ep0_expect_line_coding = false; } // If we got here (and didn't wipe out this flag), @@ -562,7 +605,21 @@ static void usb_device_intr(void) { pbdrv_usb_addr_needs_setting = false; } - if (peri_csr & USB_CSRL0_RXRDY) { + if ((peri_csr & USB_CSRL0_RXRDY) && pbdrv_usb_ep0_expect_line_coding) { + // OUT data stage of a CDC SET_LINE_CODING request. Read the line + // coding bytes out of the FIFO. The values are ignored since this + // is a virtual serial port, but we store them so GET_LINE_CODING + // returns something consistent. + pbdrv_usb_ep0_expect_line_coding = false; + uint32_t count = HWREGB(USB0_BASE + USB_O_COUNT0); + for (uint32_t i = 0; i < count; i++) { + uint8_t byte = HWREGB(USB0_BASE + USB_O_FIFO0); + if (i < sizeof(pbdrv_usb_ev3_line_coding.b)) { + pbdrv_usb_ev3_line_coding.b[i] = byte; + } + } + HWREGH(USB0_BASE + USB_O_CSRL0) = USB_CSRL0_RXRDYC | USB_CSRL0_DATAEND; + } else if (peri_csr & USB_CSRL0_RXRDY) { // Got a new setup packet pbdrv_usb_setup_packet_union_t setup_pkt; bool handled = false; @@ -592,6 +649,8 @@ static void usb_device_intr(void) { // Reset data toggle, clear stall, flush fifo HWREGB(USB0_BASE + USB_O_TXCSRL1) = USB_TXCSRL1_CLRDT | USB_TXCSRL1_FLUSH; HWREGB(USB0_BASE + USB_O_RXCSRL1) = USB_RXCSRL1_CLRDT | USB_RXCSRL1_FLUSH; + // Reset the notification endpoint too. + HWREGB(USB0_BASE + USB_O_TXCSRL2) = USB_TXCSRL2_CLRDT | USB_TXCSRL2_FLUSH; } else { // deconfiguring @@ -690,82 +749,32 @@ static void usb_device_intr(void) { } break; - case BM_REQ_TYPE_VENDOR: - switch (setup_pkt.s.bRequest) { - case PBDRV_USB_VENDOR_REQ_WEBUSB: - if (setup_pkt.s.wIndex == WEBUSB_REQ_GET_URL && setup_pkt.s.wValue == PBDRV_USB_WEBUSB_LANDING_PAGE_URL_IDX) { - pbdrv_usb_setup_data_to_send = pbdrv_usb_webusb_landing_page.u; - pbdrv_usb_setup_data_to_send_sz = pbdrv_usb_webusb_landing_page.s.bLength; - handled = true; - } - break; - - case PBDRV_USB_VENDOR_REQ_MS_20: - if (setup_pkt.s.wIndex == MS_OS_20_DESCRIPTOR_INDEX) { - pbdrv_usb_setup_data_to_send = pbdrv_usb_ms_20_desc_set.u; - pbdrv_usb_setup_data_to_send_sz = sizeof(pbdrv_usb_ms_20_desc_set.s); - handled = true; - } - break; - } - break; - case BM_REQ_TYPE_CLASS: if ((setup_pkt.s.bmRequestType & BM_REQ_RECIP_MASK) != BM_REQ_RECIP_IF) { - // Pybricks class requests must be directed at the interface + // CDC class requests are directed at the comm interface. break; } switch (setup_pkt.s.bRequest) { - const char *s; - - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT: - // Standard GATT characteristic - switch (setup_pkt.s.wValue) { - case 0x2A00: - // GATT Device Name characteristic - s = pbdrv_bluetooth_get_hub_name(); - pbdrv_usb_setup_data_to_send_sz = strlen(s); - memcpy(pbdrv_usb_ev3_ep0_buffer.b, s, pbdrv_usb_setup_data_to_send_sz); - pbdrv_usb_setup_data_to_send = pbdrv_usb_ev3_ep0_buffer.u; - handled = true; - break; - - case 0x2A26: - // GATT Firmware Revision characteristic - s = PBIO_VERSION_STR; - pbdrv_usb_setup_data_to_send_sz = strlen(s); - memcpy(pbdrv_usb_ev3_ep0_buffer.b, s, pbdrv_usb_setup_data_to_send_sz); - pbdrv_usb_setup_data_to_send = pbdrv_usb_ev3_ep0_buffer.u; - handled = true; - break; + case USB_CDC_REQ_SET_LINE_CODING: + // The 7-byte line coding follows in an OUT data + // stage, which we read on the next RXRDY interrupt. + pbdrv_usb_ep0_expect_line_coding = true; + handled = true; + break; - case 0x2A28: - // GATT Software Revision characteristic - s = PBIO_PROTOCOL_VERSION_STR; - pbdrv_usb_setup_data_to_send_sz = strlen(s); - memcpy(pbdrv_usb_ev3_ep0_buffer.b, s, pbdrv_usb_setup_data_to_send_sz); - pbdrv_usb_setup_data_to_send = pbdrv_usb_ev3_ep0_buffer.u; - handled = true; - break; - } + case USB_CDC_REQ_GET_LINE_CODING: + pbdrv_usb_setup_data_to_send = pbdrv_usb_ev3_line_coding.u; + pbdrv_usb_setup_data_to_send_sz = sizeof(pbdrv_usb_ev3_line_coding.b); + handled = true; break; - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_PYBRICKS: - // Pybricks characteristic - switch (setup_pkt.s.wValue) { - case 0x0003: - pbio_pybricks_hub_capabilities( - pbdrv_usb_ev3_ep0_buffer.b, - (pbdrv_usb_is_usb_hs ? PYBRICKS_EP_PKT_SZ_HS : PYBRICKS_EP_PKT_SZ_FS) - 1, - PBSYS_CONFIG_APP_FEATURE_FLAGS, - pbsys_storage_get_maximum_program_size(), - PBSYS_CONFIG_HMI_NUM_SLOTS); - pbdrv_usb_setup_data_to_send = pbdrv_usb_ev3_ep0_buffer.u; - pbdrv_usb_setup_data_to_send_sz = PBIO_PYBRICKS_HUB_CAPABILITIES_VALUE_SIZE; - handled = true; - break; - } + case USB_CDC_REQ_SET_CONTROL_LINE_STATE: + // DTR asserted means a host app opened the serial + // port (analogous to a BLE host subscribing). + pbdrv_usb_on_dtr_changed( + (setup_pkt.s.wValue & USB_CDC_CONTROL_LINE_STATE_DTR) != 0); + handled = true; break; } break; @@ -779,8 +788,9 @@ static void usb_device_intr(void) { // We implement something similar but not identical. In general, we wait until // we have completely processed the request and decided what we're going to do // before we indicate that we are ready to progress to the next phase. - // We do not support any requests that require receiving data from the host, - // only zero-data requests or those that require sending data to the host. + // The only request that requires receiving data from the host is the CDC + // SET_LINE_CODING request, which is acknowledged here and whose data stage + // is read on the following EP0 RXRDY interrupt. // For errors or zero-data requests, we set USB_CSRL0_RXRDYC and USB_CSRL0_DATAEND // at the same time so that we don't get spurious SETUPEND errors // (which we treat as a command failure). For requests that require sending data, @@ -789,6 +799,10 @@ static void usb_device_intr(void) { if (!handled) { // Indicate we read the packet, but also send stall HWREGH(USB0_BASE + USB_O_CSRL0) = USB_CSRL0_RXRDYC | USB_CSRL0_STALL; + } else if (pbdrv_usb_ep0_expect_line_coding) { + // Acknowledge the SETUP only; the host to device data stage is + // received on the next EP0 RXRDY interrupt. + HWREGH(USB0_BASE + USB_O_CSRL0) = USB_CSRL0_RXRDYC; } else { if (pbdrv_usb_setup_data_to_send) { // Indicate we read the packet @@ -867,10 +881,7 @@ static void usb_device_intr(void) { // Pop the descriptor from the queue uint32_t qctrld = HWREG(USB_0_OTGBASE + CPDMA_QUEUE_REGISTER_D + TX_COMPQ1 * 16) & ~CPDMA_QUEUE_REGISTER_DESC_SIZE_MASK; - if (qctrld == (uint32_t)(&cppi_descriptors[CPPI_DESC_TX_RESPONSE])) { - transmitting = false; - pbio_os_request_poll(); - } else if (qctrld == (uint32_t)(&cppi_descriptors[CPPI_DESC_TX_PYBRICKS_EVENT])) { + if (qctrld == (uint32_t)(&cppi_descriptors[CPPI_DESC_TX])) { transmitting = false; pbio_os_request_poll(); } @@ -1032,33 +1043,33 @@ pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { PBIO_OS_ASYNC_END(PBIO_SUCCESS); } -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { static pbio_os_timer_t timer; PBIO_OS_ASYNC_BEGIN(state); if (transmitting) { - DEBUG_PRINT("Cannot transmit USB event, busy.\n"); + DEBUG_PRINT("Cannot transmit USB chunk, busy.\n"); return PBIO_ERROR_BUSY; } transmitting = true; pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - // Transmit event. + // Transmit the raw bytes. Framing is handled by the common driver. pbdrv_cache_prepare_before_dma(data, size); - usb_setup_tx_dma_desc(CPPI_DESC_TX_PYBRICKS_EVENT, (uint8_t *)data, size); + usb_setup_tx_dma_desc((uint8_t *)data, size); PBIO_OS_AWAIT_UNTIL(state, !transmitting || pbio_os_timer_is_expired(&timer)); if (pbio_os_timer_is_expired(&timer)) { // Transmission has taken too long, so reset the state to allow // new transmissions. This can happen if the host stops reading - // data for some reason. This need some time to complete, so delegate + // data for some reason. This needs some time to complete, so delegate // the reset back to the process. - DEBUG_PRINT("USB event timed out\n"); - return PBIO_SUCCESS; + DEBUG_PRINT("USB chunk timed out\n"); + return PBIO_ERROR_TIMEDOUT; } #if DEBUG uint32_t elapsed_ms = pbdrv_clock_get_ms() - timer.start; @@ -1069,37 +1080,4 @@ pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uin PBIO_OS_ASYNC_END(PBIO_SUCCESS); } -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code) { - - static pbio_os_timer_t timer; - - static uint8_t ep1_tx_response_buf[1 + sizeof(uint32_t)] __aligned(4) = { PBIO_PYBRICKS_IN_EP_MSG_RESPONSE }; - - PBIO_OS_ASYNC_BEGIN(state); - - if (transmitting) { - DEBUG_PRINT("Cannot transmit USB response, busy.\n"); - return PBIO_ERROR_BUSY; - } - - transmitting = true; - pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - - // Response is just the error code. - pbio_set_uint32_le(&ep1_tx_response_buf[1], code); - - // Transmit response. - pbdrv_cache_prepare_before_dma(ep1_tx_response_buf, sizeof(ep1_tx_response_buf)); - usb_setup_tx_dma_desc(CPPI_DESC_TX_RESPONSE, ep1_tx_response_buf, sizeof(ep1_tx_response_buf)); - - // Wait until complete or trigger reset on timeout. - PBIO_OS_AWAIT_UNTIL(state, !transmitting || pbio_os_timer_is_expired(&timer)); - if (pbio_os_timer_is_expired(&timer)) { - DEBUG_PRINT("USB response timed out\n"); - return PBIO_ERROR_TIMEDOUT; - } - - PBIO_OS_ASYNC_END(PBIO_SUCCESS); -} - #endif // PBDRV_CONFIG_USB_EV3 diff --git a/lib/pbio/drv/usb/usb_nxt.c b/lib/pbio/drv/usb/usb_nxt.c index b5c45e905..ac2649a3f 100644 --- a/lib/pbio/drv/usb/usb_nxt.c +++ b/lib/pbio/drv/usb/usb_nxt.c @@ -5,6 +5,11 @@ * See lib/pbio/platform/nxt/nxos/AUTHORS for a full list of the developers. */ +// NXT / Atmel AT91SAM7S256 UDP driver implementing a USB CDC-ACM (virtual +// serial port) device. The byte stream is framed (COBS) by the common USB +// driver in usb.c, and the host's serial port open/close (DTR) is used to +// detect connection state, just like the STM32 and EV3 drivers. + #include #if PBDRV_CONFIG_USB_NXT @@ -13,18 +18,14 @@ #include #include -#include #include -#include -#include -#include -#include +#include +#include #include #include "nxos/interrupts.h" -#include "nxos/assert.h" #include "nxos/drivers/systick.h" #include "nxos/drivers/aic.h" #include "nxos/util.h" @@ -39,58 +40,19 @@ /* The USB controller supports up to 4 endpoints. */ #define PBDRV_USB_NXT_N_ENDPOINTS 4 -/* Maximum data packet sizes. Endpoint 0 is a special case (control - * endpoint). - * - * TODO: Discuss the need/use for separating recv/send. - */ +/* Physical endpoint numbers used by this driver. */ +#define EP_CONTROL 0 /* Control endpoint. */ +#define EP_BULK_OUT 1 /* CDC data, host to hub. */ +#define EP_BULK_IN 2 /* CDC data, hub to host. */ +#define EP_NOTIF 3 /* CDC notification (interrupt IN), never used. */ + +/* Maximum data packet sizes. Endpoint 0 is a special case (control endpoint). */ #define MAX_EP0_SIZE 8 #define MAX_RCV_SIZE 64 #define MAX_SND_SIZE 64 - -/* Various constants for the setup packets. - * - * TODO: clean up these. Most are unused. - */ -#define USB_BMREQUEST_DIR 0x80 -#define USB_BMREQUEST_H_TO_D 0x00 -#define USB_BMREQUEST_D_TO_H 0x80 -#define USB_BMREQUEST_TYPE 0x60 -#define USB_BMREQUEST_TYPE_STD 0x00 -#define USB_BMREQUEST_TYPE_CLASS 0x20 -#define USB_BMREQUEST_TYPE_VENDOR 0x40 -#define USB_BMREQUEST_RCPT 0x1F -#define USB_BMREQUEST_RCPT_DEV 0x00 /* device */ -#define USB_BMREQUEST_RCPT_INT 0x01 /* interface */ -#define USB_BMREQUEST_RCPT_EPT 0x02 /* endpoint */ -#define USB_BMREQUEST_RCPT_OTH 0x03 /* other */ - -// Standard requests -#define USB_BREQUEST_GET_STATUS 0x0 -#define USB_BREQUEST_CLEAR_FEATURE 0x1 -#define USB_BREQUEST_SET_FEATURE 0x3 -#define USB_BREQUEST_SET_ADDRESS 0x5 -#define USB_BREQUEST_GET_DESCRIPTOR 0x6 -#define USB_BREQUEST_SET_DESCRIPTOR 0x7 -#define USB_BREQUEST_GET_CONFIG 0x8 -#define USB_BREQUEST_SET_CONFIG 0x9 -#define USB_BREQUEST_GET_INTERFACE 0xA -#define USB_BREQUEST_SET_INTERFACE 0xB - -#define USB_WVALUE_TYPE (0xFF << 8) -#define USB_DESC_TYPE_DEVICE 1 -#define USB_DESC_TYPE_CONFIG 2 -#define USB_DESC_TYPE_STR 3 -#define USB_DESC_TYPE_INT 4 -#define USB_DESC_TYPE_ENDPT 5 -#define USB_DESC_TYPE_DEVICE_QUALIFIER 6 -#define USB_DESC_TYPE_BOS 15 - -// BOS descriptor related defines -#define USB_DEVICE_CAPABILITY_TYPE 0x10 -#define USB_DEV_CAP_TYPE_PLATFORM 5 - -#define USB_WVALUE_INDEX 0xFF +/* Packet size of the CDC notification (interrupt IN) endpoint. We never send + * notifications, so this only needs to be large enough to be valid. */ +#define NOTIF_EP_PKT_SZ 8 /** * Indices for string descriptors @@ -102,30 +64,20 @@ enum { STRING_DESC_SERIAL, }; -/* The following definitions are 'raw' USB setup packets. They are all - * standard responses to various setup requests by the USB host. These - * packets are all constant, and mostly boilerplate. Don't be too - * bothered if you skip over these to real code. - * - * If you want to understand the full meaning of every bit of these - * packets, you should refer to the USB 2.0 specifications. - * - * One point of interest: the USB device space is partitionned by - * vendor and product ID. As we are lacking money and real need, we - * don't have a vendor ID to use. Therefore, we are currently - * piggybacking on Lego's device space, using an unused product ID. - */ +// Device descriptor. The device class uses the Interface Association +// Descriptor so that the CDC comm and data interfaces are grouped into one +// function. static const pbdrv_usb_dev_desc_t pbdrv_usb_nxt_device_descriptor = { .bLength = sizeof(pbdrv_usb_dev_desc_t), .bDescriptorType = DESC_TYPE_DEVICE, - .bcdUSB = 0x0210, /* This packet is USB 2.1 (needed for BOS descriptors). */ - .bDeviceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bDeviceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bDeviceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bcdUSB = 0x0200, + .bDeviceClass = USB_CLASS_MISC, + .bDeviceSubClass = USB_MISC_SUBCLASS_COMMON, + .bDeviceProtocol = USB_MISC_PROTOCOL_IAD, .bMaxPacketSize0 = MAX_EP0_SIZE, - .idVendor = 0x0694, /* Vendor ID : LEGO */ - .idProduct = 0x0002, /* Product ID : NXT */ - .bcdDevice = 0x0200, /* Product revision: 2.0.0. */ + .idVendor = PBDRV_CONFIG_USB_VID, + .idProduct = PBDRV_CONFIG_USB_PID, + .bcdDevice = 0x0200, .iManufacturer = STRING_DESC_MFG, .iProduct = STRING_DESC_PRODUCT, .iSerialNumber = STRING_DESC_SERIAL, @@ -134,7 +86,14 @@ static const pbdrv_usb_dev_desc_t pbdrv_usb_nxt_device_descriptor = { typedef struct PBDRV_PACKED { pbdrv_usb_conf_desc_t conf_desc; - pbdrv_usb_iface_desc_t iface_desc; + pbdrv_usb_iad_desc_t iad; + pbdrv_usb_iface_desc_t comm_iface; + pbdrv_usb_cdc_header_desc_t cdc_header; + pbdrv_usb_cdc_call_mgmt_desc_t cdc_call_mgmt; + pbdrv_usb_cdc_acm_desc_t cdc_acm; + pbdrv_usb_cdc_union_desc_t cdc_union; + pbdrv_usb_ep_desc_t notif_ep; + pbdrv_usb_iface_desc_t data_iface; pbdrv_usb_ep_desc_t ep_out; pbdrv_usb_ep_desc_t ep_in; } pbdrv_usb_nxt_conf_t; @@ -144,7 +103,7 @@ static const pbdrv_usb_nxt_conf_t pbdrv_usb_nxt_full_config = { .bLength = sizeof(pbdrv_usb_conf_desc_t), .bDescriptorType = DESC_TYPE_CONFIGURATION, .wTotalLength = sizeof(pbdrv_usb_nxt_conf_t), - .bNumInterfaces = 1, + .bNumInterfaces = 2, .bConfigurationValue = 1, .iConfiguration = 0, /* Configuration attributes bitmap. Bit 7 (MSB) must be 1, bit 6 is @@ -154,35 +113,93 @@ static const pbdrv_usb_nxt_conf_t pbdrv_usb_nxt_full_config = { .bmAttributes = USB_CONF_DESC_BM_ATTR_MUST_BE_SET | USB_CONF_DESC_BM_ATTR_SELF_POWERED, .bMaxPower = 0, }, - .iface_desc = { + /* Interface Association: groups the comm and data interfaces into one + * CDC ACM function. */ + .iad = { + .bLength = sizeof(pbdrv_usb_iad_desc_t), + .bDescriptorType = DESC_TYPE_INTERFACE_ASSOCIATION, + .bFirstInterface = 0, + .bInterfaceCount = 2, + .bFunctionClass = USB_CLASS_CDC, + .bFunctionSubClass = USB_CDC_SUBCLASS_ACM, + .bFunctionProtocol = USB_CDC_PROTOCOL_AT, + .iFunction = 0, + }, + /* Communication interface. */ + .comm_iface = { .bLength = sizeof(pbdrv_usb_iface_desc_t), .bDescriptorType = DESC_TYPE_INTERFACE, .bInterfaceNumber = 0, .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = USB_CLASS_CDC, + .bInterfaceSubClass = USB_CDC_SUBCLASS_ACM, + .bInterfaceProtocol = USB_CDC_PROTOCOL_AT, + .iInterface = 0, + }, + .cdc_header = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_header_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_HEADER, + .bcdCDC = 0x0110, + }, + .cdc_call_mgmt = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_call_mgmt_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_CALL_MGMT, + .bmCapabilities = 0x00, + .bDataInterface = 1, + }, + .cdc_acm = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_acm_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_ACM, + .bmCapabilities = 0x02, + }, + .cdc_union = { + .bFunctionLength = sizeof(pbdrv_usb_cdc_union_desc_t), + .bDescriptorType = USB_CDC_CS_INTERFACE, + .bDescriptorSubtype = USB_CDC_FUNC_SUBTYPE_UNION, + .bControlInterface = 0, + .bSubordinateInterface0 = 1, + }, + /* Notification endpoint (EP3, interrupt IN). The host (e.g. Linux cdc_acm) + * requires this endpoint to exist to bind the driver, but we never send + * notifications, so it just NAKs forever. */ + .notif_ep = { + .bLength = sizeof(pbdrv_usb_ep_desc_t), + .bDescriptorType = DESC_TYPE_ENDPOINT, + .bEndpointAddress = 0x80 | EP_NOTIF, + .bmAttributes = PBDRV_USB_EP_TYPE_INTR, + .wMaxPacketSize = NOTIF_EP_PKT_SZ, + .bInterval = 16, + }, + /* Data interface. */ + .data_iface = { + .bLength = sizeof(pbdrv_usb_iface_desc_t), + .bDescriptorType = DESC_TYPE_INTERFACE, + .bInterfaceNumber = 1, + .bAlternateSetting = 0, .bNumEndpoints = 2, - .bInterfaceClass = PBIO_PYBRICKS_USB_DEVICE_CLASS, - .bInterfaceSubClass = PBIO_PYBRICKS_USB_DEVICE_SUBCLASS, - .bInterfaceProtocol = PBIO_PYBRICKS_USB_DEVICE_PROTOCOL, + .bInterfaceClass = USB_CLASS_CDC_DATA, + .bInterfaceSubClass = 0, + .bInterfaceProtocol = 0, .iInterface = 0, }, - /* - * Descriptor for EP1. - */ + /* Bulk OUT endpoint (EP1, host to hub). */ .ep_out = { .bLength = sizeof(pbdrv_usb_ep_desc_t), .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x01, /* Endpoint number. MSB is zero, meaning this is an OUT EP. */ + .bEndpointAddress = EP_BULK_OUT, .bmAttributes = PBDRV_USB_EP_TYPE_BULK, .wMaxPacketSize = MAX_RCV_SIZE, .bInterval = 0, }, - /* - * Descriptor for EP2. - */ + /* Bulk IN endpoint (EP2, hub to host). */ .ep_in = { .bLength = sizeof(pbdrv_usb_ep_desc_t), .bDescriptorType = DESC_TYPE_ENDPOINT, - .bEndpointAddress = 0x82, /* Endpoint number. MSB is one, meaning this is an IN EP. */ + .bEndpointAddress = 0x80 | EP_BULK_IN, .bmAttributes = PBDRV_USB_EP_TYPE_BULK, .wMaxPacketSize = MAX_SND_SIZE, .bInterval = 0, @@ -198,42 +215,40 @@ typedef struct PBDRV_PACKED { static pbdrv_usb_serial_number_desc_t pbdrv_usb_str_desc_serial; -typedef enum { - USB_UNINITIALIZED, - USB_READY, - USB_BUSY, - USB_SUSPENDED, -} pbdrv_usb_nxt_status_t; +// CDC line coding (baud rate, stop bits, parity, data bits). The host can set +// and get it, but we ignore the actual values since this is a virtual port. +// Defaults to 115200 8N1. +static uint8_t pbdrv_usb_nxt_line_coding[USB_CDC_LINE_CODING_SIZE] = { + 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08, +}; -/* - * The USB device state. Contains the current USB state (selected - * configuration, etc.) and transitory state for data transfers. - */ -static volatile struct { - /* The current state of the device. */ - pbdrv_usb_nxt_status_t status; +// Set while EP0 is waiting for the host to send the SET_LINE_CODING data stage. +static bool pbdrv_usb_nxt_expect_line_coding; - /* Holds the status the bus was in before entering suspend. */ - pbdrv_usb_nxt_status_t pre_suspend_status; +/* True once the host has selected a configuration (SET_CONFIGURATION). This is + * the USB analog of being enumerated and ready for data transfers. */ +static volatile bool pbdrv_usb_nxt_configured; - /* When the host gives us an address, we must send a null ACK packet - * back before actually changing addresses. This field stores the - * address that should be set once the ACK is sent. - */ - uint32_t new_device_address; +/* True while a transmission on the data IN endpoint (EP2) is in progress. */ +static volatile bool pbdrv_usb_nxt_transmitting; - /* The currently selected USB configuration. */ - uint8_t current_config; +/* When the host gives us an address, we must send a null ACK packet back + * before actually changing addresses. This field stores the address that + * should be set once the ACK is sent. */ +static uint32_t pbdrv_usb_nxt_new_device_address; - /* Holds the state of the data transmissions on both EP0 and - * EP2. This only gets used if the transmission needed to be split - * into several USB packets. - * 0 = EP0 - * 1 = EP2 - */ - uint8_t *tx_data[2]; - uint32_t tx_len[2]; -} pbdrv_usb_nxt_state; +/* The currently selected USB configuration. */ +static uint8_t pbdrv_usb_nxt_current_config; + +/* Holds the state of split (multi-packet) transmissions, indexed by the + * physical endpoint number (the same numbering used for the CSR/FDR + * registers). Only EP0 (control) and EP2 (bulk IN) ever transmit, so only + * those slots are used, but sizing the arrays to the full endpoint count keeps + * the indexing uniform with the hardware registers and avoids the previous + * endpoint/2 aliasing. + */ +static uint8_t *pbdrv_usb_nxt_tx_data[PBDRV_USB_NXT_N_ENDPOINTS]; +static uint32_t pbdrv_usb_nxt_tx_len[PBDRV_USB_NXT_N_ENDPOINTS]; /* The flags in the UDP_CSR register are a little strange: writing to * them does not instantly change their value. Their value will change @@ -265,18 +280,12 @@ static void pbdrv_usb_nxt_csr_set_flag(uint8_t endpoint, uint32_t flags) { static void pbdrv_usb_nxt_write_data(int endpoint, const void *ptr_, uint32_t length) { const uint8_t *ptr = ptr_; uint32_t packet_size; - int tx; - if (endpoint != 0 && endpoint != 2) { + if (endpoint != EP_CONTROL && endpoint != EP_BULK_IN) { return; } - tx = endpoint / 2; - - /* The bus is now busy. */ - pbdrv_usb_nxt_state.status = USB_BUSY; - - if (endpoint == 0) { + if (endpoint == EP_CONTROL) { packet_size = MIN(MAX_EP0_SIZE, length); } else { packet_size = MIN(MAX_SND_SIZE, length); @@ -287,18 +296,19 @@ static void pbdrv_usb_nxt_write_data(int endpoint, const void *ptr_, uint32_t le */ if (length > packet_size) { length -= packet_size; - pbdrv_usb_nxt_state.tx_data[tx] = (uint8_t *)(ptr + packet_size); - pbdrv_usb_nxt_state.tx_len[tx] = length; + pbdrv_usb_nxt_tx_data[endpoint] = (uint8_t *)(ptr + packet_size); + pbdrv_usb_nxt_tx_len[endpoint] = length; } else { - if (length == packet_size && endpoint == 0) { - // If we are sending data to the control pipe, we must terminate the data - // with a ZLP. In order to do so, we set the data pointer to non-NULL - // but the length to 0. We do not want to send ZLPs on the Pybricks bulk pipe. - pbdrv_usb_nxt_state.tx_data[tx] = (uint8_t *)(ptr); + if (length == packet_size && endpoint == EP_CONTROL) { + // If we are sending data to the control pipe, we must terminate the + // data with a ZLP. In order to do so, we set the data pointer to + // non-NULL but the length to 0. We do not want to send ZLPs on the + // CDC data pipe. + pbdrv_usb_nxt_tx_data[endpoint] = (uint8_t *)(ptr); } else { - pbdrv_usb_nxt_state.tx_data[tx] = NULL; + pbdrv_usb_nxt_tx_data[endpoint] = NULL; } - pbdrv_usb_nxt_state.tx_len[tx] = 0; + pbdrv_usb_nxt_tx_len[endpoint] = 0; } /* Push a packet into the USB FIFO, and tell the controller to send. */ @@ -319,9 +329,10 @@ static volatile uint32_t pbdrv_usb_rx_len; */ static void pbdrv_usb_rx_update(int endpoint) { - // Given our configuration, we should only get packets on endpoint 1. - // Ignore data on any other endpoint. Data from EP0 is handled separately. - if (endpoint != 1) { + // Given our configuration, we should only get packets on the bulk OUT + // endpoint. Ignore data on any other endpoint. Data from EP0 is handled + // separately. + if (endpoint != EP_BULK_OUT) { pbdrv_usb_nxt_csr_clear_flag(endpoint, AT91C_UDP_RX_DATA_BK0 | AT91C_UDP_RX_DATA_BK1); return; } @@ -330,7 +341,7 @@ static void pbdrv_usb_rx_update(int endpoint) { // Read all available bytes. for (uint16_t i = 0; i < pbdrv_usb_rx_len; i++) { - pbdrv_usb_rx_buf[i] = AT91C_UDP_FDR[1]; + pbdrv_usb_rx_buf[i] = AT91C_UDP_FDR[EP_BULK_OUT]; } // REVISIT: We could switch between RX banks to keep receiving data while @@ -345,7 +356,6 @@ static void pbdrv_usb_rx_update(int endpoint) { * On the other endpoint : Indicates to the host that the endpoint is halted */ static void pbdrv_usb_nxt_send_stall(int endpoint) { - pbdrv_usb_nxt_state.status = USB_UNINITIALIZED; pbdrv_usb_nxt_csr_set_flag(endpoint, AT91C_UDP_FORCESTALL); } @@ -354,21 +364,12 @@ static void pbdrv_usb_nxt_send_null(void) { pbdrv_usb_nxt_write_data(0, NULL, 0); } -typedef struct { - uint8_t request_attrs; /* Request characteristics. */ - uint8_t request; /* Request type. */ - uint16_t value; /* Request-specific value. */ - uint16_t index; /* Request-specific index. */ - uint16_t length; /* The number of bytes transferred in the (optional) - * second phase of the control transfer. */ -} pbdrv_usb_nxt_setup_packet_t; - -static void pbdrv_usb_handle_std_request(pbdrv_usb_nxt_setup_packet_t *packet) { +static void pbdrv_usb_handle_std_request(pbdrv_usb_setup_packet_t *packet) { uint32_t size; uint8_t index; - switch (packet->request) { - case USB_BREQUEST_GET_STATUS: { + switch (packet->bRequest) { + case GET_STATUS: { /* The host wants to know our status. * * If it wants the device status, just reply that the NXT is still @@ -379,19 +380,19 @@ static void pbdrv_usb_handle_std_request(pbdrv_usb_nxt_setup_packet_t *packet) { */ uint16_t response; - if ((packet->request_attrs & USB_BMREQUEST_RCPT) == USB_BMREQUEST_RCPT_DEV) { + if ((packet->bmRequestType & BM_REQ_RECIP_MASK) == BM_REQ_RECIP_DEV) { response = 1; } else { response = 0; } - pbdrv_usb_nxt_write_data(0, &response, 2); + pbdrv_usb_nxt_write_data(EP_CONTROL, &response, 2); } break; - case USB_BREQUEST_CLEAR_FEATURE: - case USB_BREQUEST_SET_INTERFACE: - case USB_BREQUEST_SET_FEATURE: + case CLEAR_FEATURE: + case SET_INTERFACE: + case SET_FEATURE: /* TODO: Refer back to the specs and send the right * replies. This is wrong, even though it happens to not break * on linux. @@ -399,43 +400,43 @@ static void pbdrv_usb_handle_std_request(pbdrv_usb_nxt_setup_packet_t *packet) { pbdrv_usb_nxt_send_null(); break; - case USB_BREQUEST_SET_ADDRESS: + case SET_ADDRESS: /* The host has given the NXT a new USB address. This address * must be set AFTER sending the ack packet. Therefore, we just * remember the new address, and the interrupt handler will set * it when the transmission completes. */ - pbdrv_usb_nxt_state.new_device_address = packet->value; + pbdrv_usb_nxt_new_device_address = packet->wValue; pbdrv_usb_nxt_send_null(); /* If the address change is to 0, do it immediately. * * TODO: Why? And when does this happen? */ - if (pbdrv_usb_nxt_state.new_device_address == 0) { + if (pbdrv_usb_nxt_new_device_address == 0) { *AT91C_UDP_FADDR = AT91C_UDP_FEN; *AT91C_UDP_GLBSTATE = 0; } break; - case USB_BREQUEST_GET_DESCRIPTOR: + case GET_DESCRIPTOR: /* The host requested a descriptor. */ - index = (packet->value & USB_WVALUE_INDEX); - switch ((packet->value & USB_WVALUE_TYPE) >> 8) { - case USB_DESC_TYPE_DEVICE: /* Device descriptor */ + index = (packet->wValue & 0xFF); + switch (packet->wValue >> 8) { + case DESC_TYPE_DEVICE: /* Device descriptor */ size = sizeof(pbdrv_usb_nxt_device_descriptor); - pbdrv_usb_nxt_write_data(0, &pbdrv_usb_nxt_device_descriptor, - MIN(size, packet->length)); + pbdrv_usb_nxt_write_data(EP_CONTROL, &pbdrv_usb_nxt_device_descriptor, + MIN(size, packet->wLength)); break; - case USB_DESC_TYPE_CONFIG: /* Configuration descriptor */ + case DESC_TYPE_CONFIGURATION: /* Configuration descriptor */ size = sizeof(pbdrv_usb_nxt_full_config); - pbdrv_usb_nxt_write_data(0, &pbdrv_usb_nxt_full_config, - MIN(size, packet->length)); + pbdrv_usb_nxt_write_data(EP_CONTROL, &pbdrv_usb_nxt_full_config, + MIN(size, packet->wLength)); break; - case USB_DESC_TYPE_STR: /* String or language info. */ + case DESC_TYPE_STRING: /* String or language info. */ { const void *desc = 0; switch (index) { @@ -458,174 +459,124 @@ static void pbdrv_usb_handle_std_request(pbdrv_usb_nxt_setup_packet_t *packet) { } if (desc) { - pbdrv_usb_nxt_write_data(0, desc, MIN(size, packet->length)); + pbdrv_usb_nxt_write_data(EP_CONTROL, desc, MIN(size, packet->wLength)); } else { - pbdrv_usb_nxt_send_stall(0); + pbdrv_usb_nxt_send_stall(EP_CONTROL); } } break; - case USB_DESC_TYPE_BOS: /* BOS descriptor */ - size = sizeof(pbdrv_usb_bos_desc_set.s); - pbdrv_usb_nxt_write_data(0, &pbdrv_usb_bos_desc_set, MIN(size, packet->length)); - break; - default: /* Unknown descriptor, tell the host by stalling. */ - pbdrv_usb_nxt_send_stall(0); + pbdrv_usb_nxt_send_stall(EP_CONTROL); } break; - case USB_BREQUEST_GET_CONFIG: + case GET_CONFIGURATION: /* The host wants to know the ID of the current configuration. */ - pbdrv_usb_nxt_write_data(0, (uint8_t *)&(pbdrv_usb_nxt_state.current_config), 1); + pbdrv_usb_nxt_write_data(EP_CONTROL, &pbdrv_usb_nxt_current_config, 1); break; - case USB_BREQUEST_SET_CONFIG: + case SET_CONFIGURATION: /* The host selected a new configuration. */ - pbdrv_usb_nxt_state.current_config = packet->value; + pbdrv_usb_nxt_current_config = packet->wValue; /* we ack */ pbdrv_usb_nxt_send_null(); /* we set the register in configured mode */ - *AT91C_UDP_GLBSTATE = packet->value > 0 ? + *AT91C_UDP_GLBSTATE = packet->wValue > 0 ? (AT91C_UDP_CONFG | AT91C_UDP_FADDEN) :AT91C_UDP_FADDEN; - /* TODO: Make this a little nicer. Not quite sure how. */ - - AT91C_UDP_CSR[1] = AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT; - while (AT91C_UDP_CSR[1] != (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT)) { + /* Enable the CDC data and notification endpoints. */ + AT91C_UDP_CSR[EP_BULK_OUT] = AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT; + while (AT91C_UDP_CSR[EP_BULK_OUT] != (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT)) { ; } - AT91C_UDP_CSR[2] = AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_IN; - while (AT91C_UDP_CSR[2] != (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_IN)) { + AT91C_UDP_CSR[EP_BULK_IN] = AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_IN; + while (AT91C_UDP_CSR[EP_BULK_IN] != (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_IN)) { ; } - AT91C_UDP_CSR[3] = 0; - while (AT91C_UDP_CSR[3] != 0) { + + AT91C_UDP_CSR[EP_NOTIF] = AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_INT_IN; + while (AT91C_UDP_CSR[EP_NOTIF] != (AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_INT_IN)) { ; } - pbdrv_usb_nxt_state.status = USB_READY; + pbdrv_usb_nxt_configured = packet->wValue > 0; break; - case USB_BREQUEST_GET_INTERFACE: /* TODO: This should respond, not stall. */ - case USB_BREQUEST_SET_DESCRIPTOR: + case GET_INTERFACE: /* TODO: This should respond, not stall. */ + case SET_DESCRIPTOR: default: - pbdrv_usb_nxt_send_stall(0); + pbdrv_usb_nxt_send_stall(EP_CONTROL); break; } } -static void pbdrv_usb_nxt_handle_class_request(pbdrv_usb_nxt_setup_packet_t *packet) { - switch (packet->request_attrs & USB_BMREQUEST_RCPT) { - case USB_BMREQUEST_RCPT_INT: - // Ignoring wIndex for now as we only have one interface. - switch (packet->request) { - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT: - // Standard GATT characteristic - switch (packet->value) { - case 0x2A00: { // device name - const char *name = pbdrv_bluetooth_get_hub_name(); - pbdrv_usb_nxt_write_data(0, name, - MIN(strlen(name), packet->length)); - break; - } - case 0x2A26: { // firmware revision - const char *fw = PBIO_VERSION_STR; - pbdrv_usb_nxt_write_data(0, fw, - MIN(strlen(fw), packet->length)); - break; - } - case 0x2A28: { // software revision - const char *sw = PBIO_PROTOCOL_VERSION_STR; - pbdrv_usb_nxt_write_data(0, sw, - MIN(strlen(sw), packet->length)); - break; - } - default: - pbdrv_usb_nxt_send_stall(0); - break; - } - break; - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_PYBRICKS: - // Pybricks characteristic - switch (packet->value) { - case 0x0003: { // hub capabilities - uint8_t caps[PBIO_PYBRICKS_HUB_CAPABILITIES_VALUE_SIZE]; - pbio_pybricks_hub_capabilities(caps, - MAX_RCV_SIZE - 1, - PBSYS_CONFIG_APP_FEATURE_FLAGS, - pbsys_storage_get_maximum_program_size(), - PBSYS_CONFIG_HMI_NUM_SLOTS); - pbdrv_usb_nxt_write_data(0, caps, MIN(sizeof(caps), packet->length)); - break; - } - default: - pbdrv_usb_nxt_send_stall(0); - break; - } - break; - default: - pbdrv_usb_nxt_send_stall(0); - break; - } +static void pbdrv_usb_nxt_handle_class_request(pbdrv_usb_setup_packet_t *packet) { + // CDC class requests are directed at the comm interface. + if ((packet->bmRequestType & BM_REQ_RECIP_MASK) != BM_REQ_RECIP_IF) { + pbdrv_usb_nxt_send_stall(EP_CONTROL); + return; + } + + switch (packet->bRequest) { + case USB_CDC_REQ_SET_LINE_CODING: + // The 7-byte line coding follows in an OUT data stage, which we + // read on the next EP0 RX_DATA interrupt. Do not ack yet. + pbdrv_usb_nxt_expect_line_coding = true; break; + + case USB_CDC_REQ_GET_LINE_CODING: + pbdrv_usb_nxt_write_data(EP_CONTROL, pbdrv_usb_nxt_line_coding, + MIN(sizeof(pbdrv_usb_nxt_line_coding), packet->wLength)); + break; + + case USB_CDC_REQ_SET_CONTROL_LINE_STATE: + // DTR asserted means a host app opened the serial port. This is + // the USB analog of a BLE host subscribing to notifications, and + // is how we detect connect/disconnect. + pbdrv_usb_on_dtr_changed( + (packet->wValue & USB_CDC_CONTROL_LINE_STATE_DTR) != 0); + pbdrv_usb_nxt_send_null(); + break; + default: - pbdrv_usb_nxt_send_stall(0); + pbdrv_usb_nxt_send_stall(EP_CONTROL); break; } } /* Handle receiving and responding to setup packets on EP0. */ -static uint32_t pbdrv_usb_nxt_manage_setup_packet(void) { +static void pbdrv_usb_nxt_manage_setup_packet(void) { /* The structure of a USB setup packet. */ - pbdrv_usb_nxt_setup_packet_t packet; + pbdrv_usb_setup_packet_t packet; /* Read the packet from the FIFO into the above packet struct. */ - packet.request_attrs = AT91C_UDP_FDR[0]; - packet.request = AT91C_UDP_FDR[0]; - packet.value = (AT91C_UDP_FDR[0] & 0xFF) | (AT91C_UDP_FDR[0] << 8); - packet.index = (AT91C_UDP_FDR[0] & 0xFF) | (AT91C_UDP_FDR[0] << 8); - packet.length = (AT91C_UDP_FDR[0] & 0xFF) | (AT91C_UDP_FDR[0] << 8); - - if ((packet.request_attrs & USB_BMREQUEST_DIR) == USB_BMREQUEST_D_TO_H) { - pbdrv_usb_nxt_csr_set_flag(0, AT91C_UDP_DIR); /* TODO: contradicts atmel doc p475 */ + packet.bmRequestType = AT91C_UDP_FDR[EP_CONTROL]; + packet.bRequest = AT91C_UDP_FDR[EP_CONTROL]; + packet.wValue = (AT91C_UDP_FDR[EP_CONTROL] & 0xFF) | (AT91C_UDP_FDR[EP_CONTROL] << 8); + packet.wIndex = (AT91C_UDP_FDR[EP_CONTROL] & 0xFF) | (AT91C_UDP_FDR[EP_CONTROL] << 8); + packet.wLength = (AT91C_UDP_FDR[EP_CONTROL] & 0xFF) | (AT91C_UDP_FDR[EP_CONTROL] << 8); + + if ((packet.bmRequestType & BM_REQ_DIR_MASK) == BM_REQ_DIR_D2H) { + pbdrv_usb_nxt_csr_set_flag(EP_CONTROL, AT91C_UDP_DIR); /* TODO: contradicts atmel doc p475 */ } - pbdrv_usb_nxt_csr_clear_flag(0, AT91C_UDP_RXSETUP); + pbdrv_usb_nxt_csr_clear_flag(EP_CONTROL, AT91C_UDP_RXSETUP); - switch (packet.request_attrs & USB_BMREQUEST_TYPE) { - case USB_BMREQUEST_TYPE_STD: + switch (packet.bmRequestType & BM_REQ_TYPE_MASK) { + case BM_REQ_TYPE_STANDARD: pbdrv_usb_handle_std_request(&packet); break; - case USB_BMREQUEST_TYPE_CLASS: + case BM_REQ_TYPE_CLASS: pbdrv_usb_nxt_handle_class_request(&packet); break; - case USB_BMREQUEST_TYPE_VENDOR: - switch (packet.request) { - case PBDRV_USB_VENDOR_REQ_WEBUSB: - // Since there is only one WebUSB descriptor, we ignore the index. - pbdrv_usb_nxt_write_data(0, &pbdrv_usb_webusb_landing_page, - MIN(pbdrv_usb_webusb_landing_page.s.bLength, packet.length)); - break; - case PBDRV_USB_VENDOR_REQ_MS_20: - // Since there is only one MS descriptor, we ignore the index. - pbdrv_usb_nxt_write_data(0, &pbdrv_usb_ms_20_desc_set, - MIN(sizeof(pbdrv_usb_ms_20_desc_set.s), packet.length)); - break; - default: - pbdrv_usb_nxt_send_stall(0); - break; - } - break; default: - pbdrv_usb_nxt_send_stall(0); + pbdrv_usb_nxt_send_stall(EP_CONTROL); break; } - - return packet.request; } /* The main USB interrupt handler. */ @@ -642,7 +593,7 @@ static void pbdrv_usb_nxt_isr(void) { /* End of bus reset. Starting the device setup procedure. */ if (isr & AT91C_UDP_ENDBUSRES) { - pbdrv_usb_nxt_state.status = USB_UNINITIALIZED; + pbdrv_usb_nxt_configured = false; /* Disable and clear all interruptions, reverting to the base * state. @@ -655,7 +606,7 @@ static void pbdrv_usb_nxt_isr(void) { *AT91C_UDP_RSTEP = 0; /* Reset internal state. */ - pbdrv_usb_nxt_state.current_config = 0; + pbdrv_usb_nxt_current_config = 0; /* Reset EP0 to a basic control endpoint. */ /* TODO: The while is ugly. Fix it. */ @@ -693,14 +644,11 @@ static void pbdrv_usb_nxt_isr(void) { if (isr & AT91C_UDP_RXSUSP) { *AT91C_UDP_ICR = AT91C_UDP_RXSUSP; isr &= ~AT91C_UDP_RXSUSP; - pbdrv_usb_nxt_state.pre_suspend_status = pbdrv_usb_nxt_state.status; - pbdrv_usb_nxt_state.status = USB_SUSPENDED; } if (isr & AT91C_UDP_RXRSM) { *AT91C_UDP_ICR = AT91C_UDP_RXRSM; isr &= ~AT91C_UDP_RXRSM; - pbdrv_usb_nxt_state.status = pbdrv_usb_nxt_state.pre_suspend_status; } for (endpoint = 0; endpoint < PBDRV_USB_NXT_N_ENDPOINTS; endpoint++) { @@ -712,7 +660,33 @@ static void pbdrv_usb_nxt_isr(void) { if (endpoint == 0) { if (AT91C_UDP_CSR[0] & AT91C_UDP_RXSETUP) { - csr = pbdrv_usb_nxt_manage_setup_packet(); + pbdrv_usb_nxt_manage_setup_packet(); + return; + } + + if (AT91C_UDP_CSR[0] & (AT91C_UDP_RX_DATA_BK0 | AT91C_UDP_RX_DATA_BK1)) { + /* OUT data on the control endpoint. This is either the data stage + * of a host-to-device control write (only SET_LINE_CODING for us) + * or the zero-length OUT status stage that terminates a control + * read (e.g. GET_DESCRIPTOR). + */ + if (pbdrv_usb_nxt_expect_line_coding) { + uint32_t count = (AT91C_UDP_CSR[0] & AT91C_UDP_RXBYTECNT) >> 16; + for (uint32_t i = 0; i < count && i < sizeof(pbdrv_usb_nxt_line_coding); i++) { + pbdrv_usb_nxt_line_coding[i] = AT91C_UDP_FDR[0]; + } + pbdrv_usb_nxt_expect_line_coding = false; + pbdrv_usb_nxt_csr_clear_flag(0, AT91C_UDP_RX_DATA_BK0 | AT91C_UDP_RX_DATA_BK1); + /* Acknowledge the SET_LINE_CODING data stage with a ZLP. */ + pbdrv_usb_nxt_send_null(); + } else { + /* OUT status stage of a control read. The hardware ACKs it when + * we clear the RX flag; we must NOT send anything back here, or + * we would leave a stray IN packet armed on EP0 and corrupt the + * next control transfer. + */ + pbdrv_usb_nxt_csr_clear_flag(0, AT91C_UDP_RX_DATA_BK0 | AT91C_UDP_RX_DATA_BK1); + } return; } } @@ -723,9 +697,9 @@ static void pbdrv_usb_nxt_isr(void) { if (csr & AT91C_UDP_RX_DATA_BK0 || csr & AT91C_UDP_RX_DATA_BK1) { - if (endpoint == 1) { - AT91C_UDP_CSR[1] &= ~AT91C_UDP_EPEDS; - while (AT91C_UDP_CSR[1] & AT91C_UDP_EPEDS) { + if (endpoint == EP_BULK_OUT) { + AT91C_UDP_CSR[EP_BULK_OUT] &= ~AT91C_UDP_EPEDS; + while (AT91C_UDP_CSR[EP_BULK_OUT] & AT91C_UDP_EPEDS) { ; } } @@ -741,25 +715,27 @@ static void pbdrv_usb_nxt_isr(void) { /* so first we will reset this flag */ pbdrv_usb_nxt_csr_clear_flag(endpoint, AT91C_UDP_TXCOMP); - if (pbdrv_usb_nxt_state.new_device_address > 0) { + if (pbdrv_usb_nxt_new_device_address > 0) { /* the previous message received was SET_ADDR */ /* now that the computer ACK our send_null(), we can * set this address for real */ /* we set the specified usb address in the controller */ - *AT91C_UDP_FADDR = AT91C_UDP_FEN | pbdrv_usb_nxt_state.new_device_address; + *AT91C_UDP_FADDR = AT91C_UDP_FEN | pbdrv_usb_nxt_new_device_address; /* and we tell the controller that we are in addressed mode now */ *AT91C_UDP_GLBSTATE = AT91C_UDP_FADDEN; - pbdrv_usb_nxt_state.new_device_address = 0; + pbdrv_usb_nxt_new_device_address = 0; } /* and we will send the following data */ - if (pbdrv_usb_nxt_state.tx_data[endpoint] != NULL) { - pbdrv_usb_nxt_write_data(endpoint, pbdrv_usb_nxt_state.tx_data[endpoint], - pbdrv_usb_nxt_state.tx_len[endpoint]); + if (pbdrv_usb_nxt_tx_data[endpoint] != NULL) { + pbdrv_usb_nxt_write_data(endpoint, pbdrv_usb_nxt_tx_data[endpoint], + pbdrv_usb_nxt_tx_len[endpoint]); } else { /* then it means that we sent all the data and the host has acknowledged it */ - pbdrv_usb_nxt_state.status = USB_READY; + if (endpoint == EP_BULK_IN) { + pbdrv_usb_nxt_transmitting = false; + } pbio_os_request_poll(); } return; @@ -789,11 +765,21 @@ void pbdrv_usb_init_device(void) { for (uint8_t i = 0; i < PBIO_ARRAY_SIZE(pbdrv_usb_str_desc_serial.wString); i++) { pbdrv_usb_str_desc_serial.wString[i] = bluetooth_address_string[i]; } - pbdrv_usb_str_desc_serial.bLength = PBIO_ARRAY_SIZE(pbdrv_usb_str_desc_serial.wString) * 2; - pbdrv_usb_str_desc_serial.bDescriptorType = USB_DESC_TYPE_STR, + pbdrv_usb_str_desc_serial.bLength = sizeof(pbdrv_usb_str_desc_serial); + pbdrv_usb_str_desc_serial.bDescriptorType = DESC_TYPE_STRING; pbdrv_usb_nxt_deinit(); - memset((void *)&pbdrv_usb_nxt_state, 0, sizeof(pbdrv_usb_nxt_state)); + + pbdrv_usb_nxt_configured = false; + pbdrv_usb_nxt_transmitting = false; + pbdrv_usb_nxt_expect_line_coding = false; + pbdrv_usb_nxt_new_device_address = 0; + pbdrv_usb_nxt_current_config = 0; + for (int i = 0; i < PBDRV_USB_NXT_N_ENDPOINTS; i++) { + pbdrv_usb_nxt_tx_data[i] = NULL; + pbdrv_usb_nxt_tx_len[i] = 0; + } + pbdrv_usb_rx_len = 0; uint32_t state = nx_interrupts_disable(); @@ -838,61 +824,35 @@ void pbdrv_usb_deinit_device(void) { } pbio_error_t pbdrv_usb_wait_until_configured(pbio_os_state_t *state) { - PBIO_OS_ASYNC_BEGIN(state); - - PBIO_OS_AWAIT_UNTIL(state, pbdrv_usb_nxt_state.status == USB_READY); - - PBIO_OS_ASYNC_END(PBIO_SUCCESS); + return pbdrv_usb_nxt_configured ? PBIO_SUCCESS : PBIO_ERROR_AGAIN; } bool pbdrv_usb_is_ready(void) { - return pbdrv_usb_nxt_state.status != USB_UNINITIALIZED; + return pbdrv_usb_nxt_configured; } pbdrv_usb_bcd_t pbdrv_usb_get_bcd(void) { return PBDRV_USB_BCD_NONE; } -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { static pbio_os_timer_t timer; PBIO_OS_ASYNC_BEGIN(state); - // REVISIT: Won't work if we include this check. - // if (pbdrv_usb_nxt_state.status != USB_READY) { - // return PBIO_ERROR_BUSY; - // } - - pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - pbdrv_usb_nxt_write_data(2, data, size); - - PBIO_OS_AWAIT_UNTIL(state, pbdrv_usb_nxt_state.status == USB_READY || pbio_os_timer_is_expired(&timer)); - if (pbio_os_timer_is_expired(&timer)) { - return PBIO_ERROR_TIMEDOUT; + if (pbdrv_usb_nxt_transmitting) { + return PBIO_ERROR_BUSY; } - PBIO_OS_ASYNC_END(PBIO_SUCCESS); -} - -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code) { - - static pbio_os_timer_t timer; - - static uint8_t usb_response_buf[PBIO_PYBRICKS_USB_MESSAGE_SIZE(sizeof(uint32_t))] __aligned(4) = { PBIO_PYBRICKS_IN_EP_MSG_RESPONSE }; - - PBIO_OS_ASYNC_BEGIN(state); + pbdrv_usb_nxt_transmitting = true; + pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - // REVISIT: Won't work if we include this check. - // if (pbdrv_usb_nxt_state.status != USB_READY) { - // return PBIO_ERROR_BUSY; - // } + // Transmit the raw bytes. Framing is handled by the common driver. + pbdrv_usb_nxt_write_data(EP_BULK_IN, data, size); - pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - pbio_set_uint32_le(&usb_response_buf[1], code); - pbdrv_usb_nxt_write_data(2, usb_response_buf, sizeof(usb_response_buf)); + PBIO_OS_AWAIT_UNTIL(state, !pbdrv_usb_nxt_transmitting || pbio_os_timer_is_expired(&timer)); - PBIO_OS_AWAIT_UNTIL(state, pbdrv_usb_nxt_state.status == USB_READY || pbio_os_timer_is_expired(&timer)); if (pbio_os_timer_is_expired(&timer)) { return PBIO_ERROR_TIMEDOUT; } @@ -901,8 +861,9 @@ pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t } pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { - // REVISIT: Make async. - pbdrv_usb_init_device(); + pbdrv_usb_nxt_tx_data[EP_BULK_IN] = NULL; + pbdrv_usb_nxt_tx_len[EP_BULK_IN] = 0; + pbdrv_usb_nxt_transmitting = false; return PBIO_SUCCESS; } @@ -918,9 +879,8 @@ uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { pbdrv_usb_rx_len = 0; // Get ready to receive the next message. - if (pbdrv_usb_nxt_state.status > USB_UNINITIALIZED - && pbdrv_usb_nxt_state.status != USB_SUSPENDED) { - AT91C_UDP_CSR[1] |= AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT; + if (pbdrv_usb_nxt_configured) { + AT91C_UDP_CSR[EP_BULK_OUT] |= AT91C_UDP_EPEDS | AT91C_UDP_EPTYPE_BULK_OUT; } return result; diff --git a/lib/pbio/drv/usb/usb_simulation.c b/lib/pbio/drv/usb/usb_simulation.c index b0dfd1603..0704791c8 100644 --- a/lib/pbio/drv/usb/usb_simulation.c +++ b/lib/pbio/drv/usb/usb_simulation.c @@ -10,7 +10,6 @@ #include #include -#include #include #include @@ -31,22 +30,30 @@ pbdrv_usb_bcd_t pbdrv_usb_get_bcd(void) { return PBDRV_USB_BCD_NONE; } -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { static pbio_os_timer_t timer; PBIO_OS_ASYNC_BEGIN(state); - - // Stdout also goes to native stdout. - if (size > 2 && data[0] == PBIO_PYBRICKS_IN_EP_MSG_EVENT && data[1] == PBIO_PYBRICKS_EVENT_WRITE_STDOUT) { - int ret = write(STDOUT_FILENO, data + 2, size - 2); + // The common driver hands us a COBS-encoded frame with a trailing + // delimiter. This mock only forwards stdout to the native console, so + // decode the frame and write out the payload of stdout events only. + // REVISIT: This assumes that we do one chunk per stdout event. That is + // currently true for the logic in usb.c, but we should revise this to make + // it like the RX path if we turn it into an actual stream. + uint8_t msg[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; + uint32_t msg_size = pbdrv_usb_cobs_decode(data, size - 1, msg, sizeof(msg)); + + if (msg_size >= 2 && msg[0] == PBIO_PYBRICKS_IN_EP_MSG_EVENT && + msg[1] == PBIO_PYBRICKS_EVENT_WRITE_STDOUT) { + int ret = write(STDOUT_FILENO, &msg[2], msg_size - 2); (void)ret; - } - #ifdef PBDRV_CONFIG_RPROC_VIRTUAL - pbdrv_rproc_virtual_socket_send(data + 1, size - 1); - #endif + #ifdef PBDRV_CONFIG_RPROC_VIRTUAL + pbdrv_rproc_virtual_socket_send(&msg[2], msg_size - 2); + #endif + } // Simulate some I/O time. PBIO_OS_AWAIT_MS(state, &timer, 1); @@ -54,37 +61,17 @@ pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uin PBIO_OS_ASYNC_END(PBIO_SUCCESS); } -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code) { - - static pbio_os_timer_t timer; - - static uint8_t response_buf[1 + sizeof(uint32_t)] __attribute__((aligned(4))) = - { PBIO_PYBRICKS_IN_EP_MSG_RESPONSE }; - - PBIO_OS_ASYNC_BEGIN(state); - - // Response is just the error code. - pbio_set_uint32_le(&response_buf[1], code); - - // Simulation never actually sends this. - - // Simulate some I/O time. - PBIO_OS_AWAIT_MS(state, &timer, 2); - - PBIO_OS_ASYNC_END(PBIO_SUCCESS); -} - pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { return PBIO_SUCCESS; } -static uint8_t usb_in_buf[PBDRV_CONFIG_USB_MAX_PACKET_SIZE]; +static uint8_t usb_in_buf[PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE]; static uint32_t usb_in_size; uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { // Invalid size. - if (usb_in_size > PBDRV_CONFIG_USB_MAX_PACKET_SIZE) { + if (usb_in_size > sizeof(usb_in_buf)) { usb_in_size = 0; } @@ -102,19 +89,14 @@ uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { return size; } -// Simulates incoming USB data by reading from native host stdin. In -// MicroPython, it drives the REPL. +// Simulates incoming USB data by reading the raw byte stream from native host +// stdin. In MicroPython, it drives the REPL. static pbio_error_t pbdrv_usb_test_process_thread(pbio_os_state_t *state, void *context) { static pbio_os_timer_t timer; PBIO_OS_ASYNC_BEGIN(state); - // Simulate subscribe event. - usb_in_buf[0] = PBIO_PYBRICKS_OUT_EP_MSG_SUBSCRIBE; - usb_in_buf[1] = 1; - usb_in_size = 2; - #ifdef PBDRV_CONFIG_RUN_ON_CI // CI and MicroPython test suite have lots of problems with stdin. It is // only needed for the REPL and interactive input, so don't bother on CI. @@ -129,12 +111,15 @@ static pbio_error_t pbdrv_usb_test_process_thread(pbio_os_state_t *state, void * continue; } - // This has been made non-blocking in platform.c. - ssize_t num_read = read(STDIN_FILENO, &usb_in_buf[2], sizeof(usb_in_buf) - 2); + // Read raw bytes from native stdin and present them to the common + // driver as a COBS-framed write stdin command, the same way a real + // host would. This has been made non-blocking in platform.c. + static uint8_t cmd[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; + cmd[0] = PBIO_PYBRICKS_OUT_EP_MSG_COMMAND; + cmd[1] = PBIO_PYBRICKS_COMMAND_WRITE_STDIN; + ssize_t num_read = read(STDIN_FILENO, &cmd[2], sizeof(cmd) - 2); if (num_read > 0) { - usb_in_buf[0] = PBIO_PYBRICKS_OUT_EP_MSG_COMMAND; - usb_in_buf[1] = PBIO_PYBRICKS_COMMAND_WRITE_STDIN; - usb_in_size = 2 + num_read; + usb_in_size = pbdrv_usb_cobs_encode(cmd, 2 + num_read, usb_in_buf); } } @@ -144,6 +129,9 @@ static pbio_error_t pbdrv_usb_test_process_thread(pbio_os_state_t *state, void * void pbdrv_usb_init_device(void) { static pbio_os_process_t pbdrv_usb_test_process; pbio_os_process_start(&pbdrv_usb_test_process, pbdrv_usb_test_process_thread, NULL); + + // No physical port to open, so report the connection as active right away. + pbdrv_usb_on_dtr_changed(true); } void pbdrv_usb_deinit_device(void) { diff --git a/lib/pbio/drv/usb/usb_simulation_pico.c b/lib/pbio/drv/usb/usb_simulation_pico.c index 6875a48bb..69b41e39c 100644 --- a/lib/pbio/drv/usb/usb_simulation_pico.c +++ b/lib/pbio/drv/usb/usb_simulation_pico.c @@ -26,6 +26,10 @@ static lwrb_t pbdrv_usb_simulation_pico_in_ringbuf; static volatile bool pbdrv_usb_simulation_tx_ready; +// Size of the UART receive ring buffer. This is a mock with no real USB +// hardware, so the value is arbitrary. +#define PBDRV_USB_SIMULATION_PICO_RX_RINGBUF_SIZE (128) + pbio_error_t pbdrv_usb_wait_until_configured(pbio_os_state_t *state) { return PBIO_ERROR_NOT_SUPPORTED; } @@ -38,18 +42,27 @@ pbdrv_usb_bcd_t pbdrv_usb_get_bcd(void) { return PBDRV_USB_BCD_NONE; } -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { PBIO_OS_ASYNC_BEGIN(state); - // Only care about stdout for now. - if (size < 2 || data[0] != PBIO_PYBRICKS_IN_EP_MSG_EVENT || - data[1] != PBIO_PYBRICKS_EVENT_WRITE_STDOUT) { + // The common driver hands us a COBS-encoded frame with a trailing + // delimiter. This mock only forwards stdout to the UART, so decode the + // frame and ignore anything that is not a stdout event. + // REVISIT: This assumes that we do one chunk per stdout event. That is + // currently true for the logic in usb.c, but we should revise this to make + // it like the RX path if we turn it into an actual stream. + static uint8_t msg[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; + static uint32_t msg_size; + msg_size = pbdrv_usb_cobs_decode(data, size - 1, msg, sizeof(msg)); + + if (msg_size < 2 || msg[0] != PBIO_PYBRICKS_IN_EP_MSG_EVENT || + msg[1] != PBIO_PYBRICKS_EVENT_WRITE_STDOUT) { return PBIO_SUCCESS; } static int i; - for (i = 2; i < size; i++) { + for (i = 2; i < msg_size; i++) { if (!uart_is_writable(uart_default)) { pbdrv_usb_simulation_tx_ready = false; // Enable TX interrupt to be notified when ready. @@ -57,33 +70,24 @@ pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uin PBIO_OS_AWAIT_UNTIL(state, pbdrv_usb_simulation_tx_ready); } - uart_get_hw(uart_default)->dr = data[i]; + uart_get_hw(uart_default)->dr = msg[i]; } PBIO_OS_ASYNC_END(PBIO_SUCCESS); } -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code) { - - PBIO_OS_ASYNC_BEGIN(state); - - // Not implemented. - - PBIO_OS_ASYNC_END(PBIO_SUCCESS); -} - pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { return PBIO_SUCCESS; } -static uint8_t pbdrv_usb_simulation_pico_in_buf[PBDRV_CONFIG_USB_MAX_PACKET_SIZE]; +static uint8_t pbdrv_usb_simulation_pico_in_buf[PBDRV_USB_MAX_ENCODED_MESSAGE_SIZE]; static uint32_t pbdrv_usb_simulation_pico_in_size; uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { uint32_t size = pbdrv_usb_simulation_pico_in_size; // Invalid size. - if (size > PBDRV_CONFIG_USB_MAX_PACKET_SIZE) { + if (size > sizeof(pbdrv_usb_simulation_pico_in_buf)) { pbdrv_usb_simulation_pico_in_size = 0; return 0; } @@ -106,23 +110,21 @@ uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { static pbio_error_t pbdrv_usb_test_process_thread(pbio_os_state_t *state, void *context) { PBIO_OS_ASYNC_BEGIN(state); - // Simulate subscribe event. - pbdrv_usb_simulation_pico_in_buf[0] = PBIO_PYBRICKS_OUT_EP_MSG_SUBSCRIBE; - pbdrv_usb_simulation_pico_in_buf[1] = 1; - pbdrv_usb_simulation_pico_in_size = 2; - for (;;) { static size_t available; PBIO_OS_AWAIT_UNTIL(state, pbdrv_usb_simulation_pico_in_size == 0 && (available = lwrb_get_full(&pbdrv_usb_simulation_pico_in_ringbuf)) > 0); - available = pbio_int_math_clamp(available, PBDRV_CONFIG_USB_MAX_PACKET_SIZE - 2); + available = pbio_int_math_clamp(available, PBDRV_USB_MAX_DECODED_MESSAGE_SIZE - 2); - pbdrv_usb_simulation_pico_in_buf[0] = PBIO_PYBRICKS_OUT_EP_MSG_COMMAND; - pbdrv_usb_simulation_pico_in_buf[1] = PBIO_PYBRICKS_COMMAND_WRITE_STDIN; - lwrb_read(&pbdrv_usb_simulation_pico_in_ringbuf, - &pbdrv_usb_simulation_pico_in_buf[2], available); - pbdrv_usb_simulation_pico_in_size = 2 + available; + // Wrap the raw UART bytes as a write stdin command and COBS-encode it, + // the same way a real host would, so the common driver can decode it. + static uint8_t cmd[PBDRV_USB_MAX_DECODED_MESSAGE_SIZE]; + cmd[0] = PBIO_PYBRICKS_OUT_EP_MSG_COMMAND; + cmd[1] = PBIO_PYBRICKS_COMMAND_WRITE_STDIN; + lwrb_read(&pbdrv_usb_simulation_pico_in_ringbuf, &cmd[2], available); + pbdrv_usb_simulation_pico_in_size = pbdrv_usb_cobs_encode( + cmd, 2 + available, pbdrv_usb_simulation_pico_in_buf); } PBIO_OS_ASYNC_END(PBIO_SUCCESS); @@ -147,7 +149,7 @@ static void pbdrv_usb_simulation_pico_uart_rx_irq(void) { } void pbdrv_usb_init_device(void) { - static uint8_t in_ringbuf_data[PBDRV_CONFIG_USB_MAX_PACKET_SIZE * 2]; + static uint8_t in_ringbuf_data[PBDRV_USB_SIMULATION_PICO_RX_RINGBUF_SIZE]; lwrb_init(&pbdrv_usb_simulation_pico_in_ringbuf, in_ringbuf_data, sizeof(in_ringbuf_data)); setup_default_uart(); @@ -157,6 +159,10 @@ void pbdrv_usb_init_device(void) { static pbio_os_process_t pbdrv_usb_test_process; pbio_os_process_start(&pbdrv_usb_test_process, pbdrv_usb_test_process_thread, NULL); + + // No physical port to detect, so report the host connection as active + // right away (USB analog of a BLE host subscribing). + pbdrv_usb_on_dtr_changed(true); } void pbdrv_usb_deinit_device(void) { diff --git a/lib/pbio/drv/usb/usb_stm32.c b/lib/pbio/drv/usb/usb_stm32.c index 65d3f46e9..2e571aafa 100644 --- a/lib/pbio/drv/usb/usb_stm32.c +++ b/lib/pbio/drv/usb/usb_stm32.c @@ -31,15 +31,9 @@ #include "./usb.h" #include "./usb_stm32.h" - -#if USBD_PYBRICKS_MAX_PACKET_SIZE != PBDRV_CONFIG_USB_MAX_PACKET_SIZE -#error Inconsistent USB packet size -#endif - // These buffers need to be 32-bit aligned because the USB driver moves data // to/from FIFOs in 32-bit chunks. static uint8_t usb_in_buf[USBD_PYBRICKS_MAX_PACKET_SIZE] __aligned(4); -static uint8_t usb_response_buf[PBIO_PYBRICKS_USB_MESSAGE_SIZE(sizeof(uint32_t))] __aligned(4) = { PBIO_PYBRICKS_IN_EP_MSG_RESPONSE }; static volatile uint32_t usb_in_sz; static volatile bool transmitting; @@ -217,66 +211,11 @@ static USBD_StatusTypeDef Pybricks_Itf_TransmitCplt(uint8_t *Buf, uint32_t Len, return USBD_OK; } -static USBD_StatusTypeDef Pybricks_Itf_ReadCharacteristic(USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req) { - USBD_StatusTypeDef ret = USBD_OK; - - switch (req->bRequest) { - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT: - switch (req->wValue) { - case 0x2A00: { - // GATT Device Name characteristic - const char *name = pbdrv_bluetooth_get_hub_name(); - (void)USBD_CtlSendData(pdev, (uint8_t *)name, MIN(strlen(name), req->wLength)); - } - break; - - case 0x2A26: { - // GATT Firmware Revision characteristic - const char *fw_version = PBIO_VERSION_STR; - (void)USBD_CtlSendData(pdev, (uint8_t *)fw_version, MIN(strlen(fw_version), req->wLength)); - } - break; - - case 0x2A28: { - // GATT Software Revision characteristic - const char *sw_version = PBIO_PROTOCOL_VERSION_STR; - (void)USBD_CtlSendData(pdev, (uint8_t *)sw_version, MIN(strlen(sw_version), req->wLength)); - } - break; - - default: - USBD_CtlError(pdev, req); - ret = USBD_FAIL; - break; - } - break; - case PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_PYBRICKS: - switch (req->wValue) { - case 0x0003: { - // Pybricks hub capabilities characteristic - uint8_t caps[PBIO_PYBRICKS_HUB_CAPABILITIES_VALUE_SIZE]; - pbio_pybricks_hub_capabilities(caps, - USBD_PYBRICKS_MAX_PACKET_SIZE - 1, - PBSYS_CONFIG_APP_FEATURE_FLAGS, - pbsys_storage_get_maximum_program_size(), - PBSYS_CONFIG_HMI_NUM_SLOTS); - (void)USBD_CtlSendData(pdev, caps, MIN(sizeof(caps), req->wLength)); - } - break; - - default: - USBD_CtlError(pdev, req); - ret = USBD_FAIL; - break; - } - break; - default: - USBD_CtlError(pdev, req); - ret = USBD_FAIL; - break; - } - - return ret; +static USBD_StatusTypeDef Pybricks_Itf_SetControlLineState(bool dtr) { + // The DTR signal indicates whether a host application has opened the serial + // port. This is the USB analog of a BLE host subscribing to notifications. + pbdrv_usb_on_dtr_changed(dtr); + return USBD_OK; } USBD_Pybricks_ItfTypeDef USBD_Pybricks_fops = { @@ -284,7 +223,7 @@ USBD_Pybricks_ItfTypeDef USBD_Pybricks_fops = { .DeInit = Pybricks_Itf_DeInit, .Receive = Pybricks_Itf_Receive, .TransmitCplt = Pybricks_Itf_TransmitCplt, - .ReadCharacteristic = Pybricks_Itf_ReadCharacteristic, + .SetControlLineState = Pybricks_Itf_SetControlLineState, }; pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { @@ -292,7 +231,7 @@ pbio_error_t pbdrv_usb_tx_reset(pbio_os_state_t *state) { return PBIO_SUCCESS; } -pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { +pbio_error_t pbdrv_usb_tx_chunk(pbio_os_state_t *state, const uint8_t *data, uint32_t size) { static pbio_os_timer_t timer; @@ -314,31 +253,6 @@ pbio_error_t pbdrv_usb_tx_event(pbio_os_state_t *state, const uint8_t *data, uin PBIO_OS_ASYNC_END(PBIO_SUCCESS); } -pbio_error_t pbdrv_usb_tx_response(pbio_os_state_t *state, pbio_pybricks_error_t code) { - - static pbio_os_timer_t timer; - - PBIO_OS_ASYNC_BEGIN(state); - - if (transmitting) { - return PBIO_ERROR_BUSY; - } - - transmitting = true; - pbio_os_timer_set(&timer, PBDRV_USB_TRANSMIT_TIMEOUT); - - pbio_set_uint32_le(&usb_response_buf[1], code); - - USBD_Pybricks_TransmitPacket(&husbd, usb_response_buf, sizeof(usb_response_buf)); - - PBIO_OS_AWAIT_UNTIL(state, !transmitting || pbio_os_timer_is_expired(&timer)); - if (pbio_os_timer_is_expired(&timer)) { - return PBIO_ERROR_TIMEDOUT; - } - - PBIO_OS_ASYNC_END(PBIO_SUCCESS); -} - uint32_t pbdrv_usb_get_data_and_start_receive(uint8_t *data) { if (!usb_in_sz) { diff --git a/lib/pbio/include/pbio/protocol.h b/lib/pbio/include/pbio/protocol.h index 1f0bef29c..5f8ecb3a1 100644 --- a/lib/pbio/include/pbio/protocol.h +++ b/lib/pbio/include/pbio/protocol.h @@ -472,14 +472,13 @@ extern const uint8_t pbio_nus_service_uuid[]; extern const uint8_t pbio_nus_rx_char_uuid[]; extern const uint8_t pbio_nus_tx_char_uuid[]; -/** USB bDeviceClass for Pybricks hubs */ -#define PBIO_PYBRICKS_USB_DEVICE_CLASS 0xFF -/** USB bDeviceSubClass for Pybricks hubs */ -#define PBIO_PYBRICKS_USB_DEVICE_SUBCLASS 0xC5 -/** USB bDeviceProtocol for Pybricks hubs */ -#define PBIO_PYBRICKS_USB_DEVICE_PROTOCOL 0xF5 - -/** USB bRequest for Pybricks class-specific requests */ +/** + * Characteristic namespace for ::PBIO_PYBRICKS_OUT_EP_MSG_READ requests. + * + * Selects which group the 16-bit characteristic id in a read request belongs + * to, mirroring how a BLE host distinguishes standard GATT characteristics + * from Pybricks-specific ones. + */ enum { /** Retrieve GATT characteristics */ PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT = 0x01, @@ -487,35 +486,66 @@ enum { PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_PYBRICKS = 0x02, }; -// NOTE: These enums values are sent over the wire, so cannot be changed. Also, -// 0 is skipped to avoid a zeroed buffer from being misinterpreted as a message. +// The Pybricks USB interface uses a CDC ACM data pipe (Web Serial on the host), +// which is a raw, bidirectional byte stream with no inherent message +// boundaries. Each direction therefore frames its messages with Consistent +// Overhead Byte Stuffing (COBS): every message is COBS-encoded and terminated +// with a single zero byte (0x00) delimiter. COBS guarantees the encoded payload +// never contains a zero, so the delimiter unambiguously marks the end of a +// frame. This framing is self synchronizing; a receiver that joins mid-stream +// or sees a corrupt frame simply resumes at the next delimiter. +// +// The byte stream is independent of the USB hardware packet size: a single +// frame may span several USB packets, and several small frames may share one +// packet. Only after a frame has been reassembled and decoded is its first +// byte interpreted as the message type below. -/** Hub to host messages via the Pybricks interface IN endpoint. */ +/** + * Hub to host message types. + * + * The hub to host direction is a single byte stream that multiplexes command + * responses, events and read replies, so the first byte of each message + * discriminates between them. + */ typedef enum { /** - * Analog of BLE status response. Emitted in response to every OUT message - * received. + * Reply to a ::PBIO_PYBRICKS_OUT_EP_MSG_COMMAND. The payload is the command + * error code, analogous to a BLE write response. */ PBIO_PYBRICKS_IN_EP_MSG_RESPONSE = 1, - /**Analog to BLE notification. Only emitted if subscribed. */ + /** Analog to BLE notification. Emitted while a host is connected. */ PBIO_PYBRICKS_IN_EP_MSG_EVENT = 2, + /** + * Reply to a ::PBIO_PYBRICKS_OUT_EP_MSG_READ. The payload is + * `[service, char_id_lo, char_id_hi, value...]`, echoing the selector from + * the request followed by the characteristic value. An empty value + * indicates an unknown characteristic. + */ + PBIO_PYBRICKS_IN_EP_MSG_READ_REPLY = 3, } pbio_pybricks_usb_in_ep_msg_t; -/** Host to hub messages via the Pybricks USB interface OUT endpoint. */ -typedef enum { - /** Analog of BLE Client Characteristic Configuration Descriptor (CCCD). */ - PBIO_PYBRICKS_OUT_EP_MSG_SUBSCRIBE = 1, - /** Analog of BLE Client Characteristic Write with response. */ - PBIO_PYBRICKS_OUT_EP_MSG_COMMAND = 2, -} pbio_pybricks_usb_out_ep_msg_t; - /** - * Size of USB messages for Pybricks USB interface. + * Host to hub message types. * - * USB has one extra byte header for a message type discriminator - * compared to BLE messages. + * The host to hub direction is a single byte stream, so the first byte of each + * message discriminates between a command and a characteristic read request. */ -#define PBIO_PYBRICKS_USB_MESSAGE_SIZE(n) (1 + n) +typedef enum { + /** + * A characteristic read request. The payload is + * `[service, char_id_lo, char_id_hi]`, where service is one of the + * ::PBIO_PYBRICKS_USB_INTERFACE_READ_CHARACTERISTIC_GATT values. The hub + * replies with a ::PBIO_PYBRICKS_IN_EP_MSG_READ_REPLY. This is the serial + * analog of a BLE host reading a characteristic by UUID. + */ + PBIO_PYBRICKS_OUT_EP_MSG_READ = 1, + /** + * A Pybricks command (see ::pbio_pybricks_command_t), carried in the + * remaining payload bytes. The hub replies with a + * ::PBIO_PYBRICKS_IN_EP_MSG_RESPONSE. + */ + PBIO_PYBRICKS_OUT_EP_MSG_COMMAND = 2, +} pbio_pybricks_usb_out_ep_msg_t; #endif // _PBIO_PROTOCOL_H_ diff --git a/lib/pbio/platform/build_hat/pbdrvconfig.h b/lib/pbio/platform/build_hat/pbdrvconfig.h index 0d0e5a462..a0308a8bc 100644 --- a/lib/pbio/platform/build_hat/pbdrvconfig.h +++ b/lib/pbio/platform/build_hat/pbdrvconfig.h @@ -50,8 +50,6 @@ // TODO: replace this with a serial port implementation - there is no USB hardware #define PBDRV_CONFIG_USB (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (64) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_SIMULATION_PICO (1) #define PBDRV_CONFIG_USB_MFG_STR u"Raspberry Pi" #define PBDRV_CONFIG_USB_PROD_STR u"Build HAT" diff --git a/lib/pbio/platform/essential_hub/pbdrvconfig.h b/lib/pbio/platform/essential_hub/pbdrvconfig.h index ccd41e6ff..b11538332 100644 --- a/lib/pbio/platform/essential_hub/pbdrvconfig.h +++ b/lib/pbio/platform/essential_hub/pbdrvconfig.h @@ -101,14 +101,12 @@ #define PBDRV_CONFIG_UART_STM32F4_LL_IRQ_NUM_UART (2) #define PBDRV_CONFIG_USB (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (64) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_VID LEGO_USB_VID #define PBDRV_CONFIG_USB_PID LEGO_USB_PID_SPIKE_ESSENTIAL #define PBDRV_CONFIG_USB_MFG_STR LEGO_USB_MFG_STR #define PBDRV_CONFIG_USB_PROD_STR LEGO_USB_PROD_STR_TECHNIC_SMALL_HUB " + Pybricks" #define PBDRV_CONFIG_USB_STM32F4 (1) -#define PBDRV_CONFIG_USB_CHARGE_ONLY (1) +#define PBDRV_CONFIG_USB_CHARGE_ONLY (0) #define PBDRV_CONFIG_STACK (1) #define PBDRV_CONFIG_STACK_EMBEDDED (1) diff --git a/lib/pbio/platform/ev3/pbdrvconfig.h b/lib/pbio/platform/ev3/pbdrvconfig.h index 9e994deae..79b8c5884 100644 --- a/lib/pbio/platform/ev3/pbdrvconfig.h +++ b/lib/pbio/platform/ev3/pbdrvconfig.h @@ -111,8 +111,6 @@ #define PBDRV_CONFIG_UART_EV3_NUM_UART (4) #define PBDRV_CONFIG_USB (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (512) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_EV3 (1) #define PBDRV_CONFIG_USB_VID LEGO_USB_VID #define PBDRV_CONFIG_USB_PID LEGO_USB_PID_EV3 diff --git a/lib/pbio/platform/nxt/pbdrvconfig.h b/lib/pbio/platform/nxt/pbdrvconfig.h index 5cbda7327..a0f9c6885 100644 --- a/lib/pbio/platform/nxt/pbdrvconfig.h +++ b/lib/pbio/platform/nxt/pbdrvconfig.h @@ -68,8 +68,6 @@ #define PBDRV_CONFIG_USB (1) #define PBDRV_CONFIG_USB_NXT (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (64) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_VID LEGO_USB_VID #define PBDRV_CONFIG_USB_PID LEGO_USB_PID_NXT #define PBDRV_CONFIG_USB_MFG_STR LEGO_USB_MFG_STR diff --git a/lib/pbio/platform/prime_hub/pbdrvconfig.h b/lib/pbio/platform/prime_hub/pbdrvconfig.h index f29a1532d..01e7470c7 100644 --- a/lib/pbio/platform/prime_hub/pbdrvconfig.h +++ b/lib/pbio/platform/prime_hub/pbdrvconfig.h @@ -118,8 +118,6 @@ #define PBDRV_CONFIG_UART_STM32F4_LL_IRQ_NUM_UART (6) #define PBDRV_CONFIG_USB (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (64) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_VID LEGO_USB_VID #define PBDRV_CONFIG_USB_PID 0xFFFF #define PBDRV_CONFIG_USB_PID_0 LEGO_USB_PID_SPIKE_PRIME @@ -128,7 +126,7 @@ #define PBDRV_CONFIG_USB_PROD_STR LEGO_USB_PROD_STR_TECHNIC_LARGE_HUB " + Pybricks" #define PBDRV_CONFIG_USB_STM32F4 (1) #define PBDRV_CONFIG_USB_STM32F4_HUB_VARIANT_ADDR 0x08007d80 -#define PBDRV_CONFIG_USB_CHARGE_ONLY (1) +#define PBDRV_CONFIG_USB_CHARGE_ONLY (0) #define PBDRV_CONFIG_STACK (1) #define PBDRV_CONFIG_STACK_EMBEDDED (1) diff --git a/lib/pbio/platform/virtual_hub/pbdrvconfig.h b/lib/pbio/platform/virtual_hub/pbdrvconfig.h index d71ca5238..484499de4 100644 --- a/lib/pbio/platform/virtual_hub/pbdrvconfig.h +++ b/lib/pbio/platform/virtual_hub/pbdrvconfig.h @@ -59,7 +59,5 @@ #define PBDRV_CONFIG_USB (1) #define PBDRV_CONFIG_USB_SIMULATION (1) -#define PBDRV_CONFIG_USB_MAX_PACKET_SIZE (64) -#define PBDRV_CONFIG_USB_NUM_BUFFERED_PACKETS (2) #define PBDRV_CONFIG_USB_MFG_STR u"Pybricks" #define PBDRV_CONFIG_USB_PROD_STR u"Virtual Hub" diff --git a/poetry.lock b/poetry.lock index 01654b021..7f6861679 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2385,6 +2385,21 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3489,4 +3504,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "a7130f69ba0247a060870bc3cb21635de623bdd22866218997bcfee49dd5e8a8" +content-hash = "9ff1232a58f0a61a895e31a1505ed639028023b03e25b54e31e1c3a66ad11f13" diff --git a/pyproject.toml b/pyproject.toml index be9eff2bc..abbf9e963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ package-mode = false [tool.poetry.dependencies] python = ">=3.10,<4.0" +pyserial = "^3.5" [tool.poetry.group.dev.dependencies] eventlet = "^0.40.3"