diff options
author | Kevin O'Connor <kevin@koconnor.net> | 2022-06-08 21:03:11 -0400 |
---|---|---|
committer | Kevin O'Connor <kevin@koconnor.net> | 2022-06-27 10:50:23 -0400 |
commit | 790ff4d8d7ac200527c272af3e58f0d9f06468e4 (patch) | |
tree | b021c1ec8517f6b16950394ca9aef91dcff7dc7d /src/generic/usb_canbus.c | |
parent | c8cc98ce5dce3771cf2728eee7b1bdb026504f75 (diff) | |
download | kutter-790ff4d8d7ac200527c272af3e58f0d9f06468e4.tar.gz kutter-790ff4d8d7ac200527c272af3e58f0d9f06468e4.tar.xz kutter-790ff4d8d7ac200527c272af3e58f0d9f06468e4.zip |
usb_canbus: Initial support for USB to CAN bridge mode
Support a USB interface that shows up as a canbus adapter to linux.
Route both local and real canbus packets over that interface.
Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
Diffstat (limited to 'src/generic/usb_canbus.c')
-rw-r--r-- | src/generic/usb_canbus.c | 675 |
1 files changed, 675 insertions, 0 deletions
diff --git a/src/generic/usb_canbus.c b/src/generic/usb_canbus.c new file mode 100644 index 00000000..1631bebe --- /dev/null +++ b/src/generic/usb_canbus.c @@ -0,0 +1,675 @@ +// Support for Linux "gs_usb" CANbus adapter emulation +// +// Copyright (C) 2018-2022 Kevin O'Connor <kevin@koconnor.net> +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include <string.h> // memmove +#include "autoconf.h" // CONFIG_USB_VENDOR_ID +#include "board/canbus.h" // canbus_notify_tx +#include "board/canserial.h" // canserial_notify_tx +#include "board/io.h" // readl +#include "board/misc.h" // console_sendf +#include "board/pgm.h" // PROGMEM +#include "board/usb_cdc_ep.h" // USB_CDC_EP_BULK_IN +#include "byteorder.h" // cpu_to_le16 +#include "generic/usbstd.h" // struct usb_device_descriptor +#include "sched.h" // sched_wake_task +#include "usb_cdc.h" // usb_notify_ep0 + + +/**************************************************************** + * Linux "gs_usb" definitions + ****************************************************************/ + +#define USB_GSUSB_1_VENDOR_ID 0x1d50 +#define USB_GSUSB_1_PRODUCT_ID 0x606f + +enum gs_usb_breq { + GS_USB_BREQ_HOST_FORMAT = 0, + GS_USB_BREQ_BITTIMING, + GS_USB_BREQ_MODE, + GS_USB_BREQ_BERR, + GS_USB_BREQ_BT_CONST, + GS_USB_BREQ_DEVICE_CONFIG, + GS_USB_BREQ_TIMESTAMP, + GS_USB_BREQ_IDENTIFY, + GS_USB_BREQ_GET_USER_ID, + GS_USB_BREQ_SET_USER_ID, + GS_USB_BREQ_DATA_BITTIMING, + GS_USB_BREQ_BT_CONST_EXT, +}; + +struct gs_host_config { + uint32_t byte_order; +} __packed; + +struct gs_device_config { + uint8_t reserved1; + uint8_t reserved2; + uint8_t reserved3; + uint8_t icount; + uint32_t sw_version; + uint32_t hw_version; +} __packed; + +struct gs_device_bt_const { + uint32_t feature; + uint32_t fclk_can; + uint32_t tseg1_min; + uint32_t tseg1_max; + uint32_t tseg2_min; + uint32_t tseg2_max; + uint32_t sjw_max; + uint32_t brp_min; + uint32_t brp_max; + uint32_t brp_inc; +} __packed; + +struct gs_device_bittiming { + uint32_t prop_seg; + uint32_t phase_seg1; + uint32_t phase_seg2; + uint32_t sjw; + uint32_t brp; +} __packed; + +struct gs_device_mode { + uint32_t mode; + uint32_t flags; +} __packed; + +struct gs_host_frame { + uint32_t echo_id; + uint32_t can_id; + + uint8_t can_dlc; + uint8_t channel; + uint8_t flags; + uint8_t reserved; + + union { + uint8_t data[8]; + uint32_t data32[2]; + }; +} __packed; + + +/**************************************************************** + * Message sending + ****************************************************************/ + +// Global storage +static struct usbcan_data { + struct task_wake wake; + + // Canbus data from host + union { + struct gs_host_frame host_frame; + uint8_t rx_frame_pad[USB_CDC_EP_BULK_OUT_SIZE]; + }; + uint8_t host_status; + + // Canbus data routed locally + uint8_t notify_local; + uint32_t assigned_id; + + // Data from physical canbus interface + uint32_t pull_pos, push_pos; + struct canbus_msg queue[8]; +} UsbCan; + +enum { + HS_TX_ECHO = 1, + HS_TX_HW = 2, + HS_TX_LOCAL = 4, +}; + +void +canbus_notify_tx(void) +{ + sched_wake_task(&UsbCan.wake); +} + +// Handle incoming data from hw canbus interface (called from IRQ handler) +void +canbus_process_data(struct canbus_msg *msg) +{ + // Add to admin command queue + uint32_t pushp = UsbCan.push_pos; + if (pushp - UsbCan.pull_pos >= ARRAY_SIZE(UsbCan.queue)) + // No space - drop message + return; + if (UsbCan.assigned_id && (msg->id & ~1) == UsbCan.assigned_id) + // Id reserved for local + return; + uint32_t pos = pushp % ARRAY_SIZE(UsbCan.queue); + memcpy(&UsbCan.queue[pos], msg, sizeof(*msg)); + UsbCan.push_pos = pushp + 1; + usb_notify_bulk_out(); +} + +// Send a message to the Linux host +static int +send_frame(struct canbus_msg *msg) +{ + struct gs_host_frame gs = {}; + gs.echo_id = 0xffffffff; + gs.can_id = msg->id; + gs.can_dlc = msg->dlc; + gs.data32[0] = msg->data32[0]; + gs.data32[1] = msg->data32[1]; + return usb_send_bulk_in(&gs, sizeof(gs)); +} + +// Send any pending hw frames to host +static int +drain_hw_queue(void) +{ + for (;;) { + uint32_t push_pos = readl(&UsbCan.push_pos); + uint32_t pull_pos = UsbCan.pull_pos; + if (push_pos != pull_pos) { + uint32_t pos = pull_pos % ARRAY_SIZE(UsbCan.queue); + int ret = send_frame(&UsbCan.queue[pos]); + if (ret < 0) + return -1; + UsbCan.pull_pos = pull_pos + 1; + continue; + } + return 0; + } +} + +void +usbcan_task(void) +{ + if (!sched_check_wake(&UsbCan.wake)) + return; + for (;;) { + // Send any pending hw frames to host + int ret = drain_hw_queue(); + if (ret < 0) + return; + + // See if previous host frame needs to be transmitted + uint_fast8_t host_status = UsbCan.host_status; + if (host_status & (HS_TX_HW | HS_TX_LOCAL)) { + struct gs_host_frame *gs = &UsbCan.host_frame; + struct canbus_msg msg; + msg.id = gs->can_id; + msg.dlc = gs->can_dlc; + msg.data32[0] = gs->data32[0]; + msg.data32[1] = gs->data32[1]; + if (host_status & HS_TX_HW) { + ret = canbus_send(&msg); + if (ret < 0) + return; + UsbCan.host_status = host_status = host_status & ~HS_TX_HW; + } + if (host_status & HS_TX_LOCAL) { + ret = canserial_process_data(&msg); + if (ret < 0) { + usb_notify_bulk_out(); + return; + } + UsbCan.host_status = host_status & ~HS_TX_LOCAL; + } + continue; + } + + // Send any previous echo frames + if (host_status) { + ret = usb_send_bulk_in(&UsbCan.host_frame + , sizeof(UsbCan.host_frame)); + if (ret < 0) + return; + UsbCan.host_status = 0; + continue; + } + + // See if can read a new frame from host + ret = usb_read_bulk_out(&UsbCan.host_frame, USB_CDC_EP_BULK_OUT_SIZE); + if (ret > 0) { + uint32_t id = UsbCan.host_frame.can_id; + UsbCan.host_status = HS_TX_ECHO | HS_TX_HW; + if (id == CANBUS_ID_ADMIN) + UsbCan.host_status = HS_TX_ECHO | HS_TX_HW | HS_TX_LOCAL; + else if (UsbCan.assigned_id && UsbCan.assigned_id == id) + UsbCan.host_status = HS_TX_ECHO | HS_TX_LOCAL; + continue; + } + + // No more work to be done + if (UsbCan.notify_local) { + UsbCan.notify_local = 0; + canserial_notify_tx(); + } + return; + } +} +DECL_TASK(usbcan_task); + +int +canserial_send(struct canbus_msg *msg) +{ + int ret = drain_hw_queue(); + if (ret < 0) + goto retry_later; + ret = send_frame(msg); + if (ret < 0) + goto retry_later; + UsbCan.notify_local = 0; + return msg->dlc; +retry_later: + UsbCan.notify_local = 1; + return -1; +} + +void +canserial_set_filter(uint32_t id) +{ + UsbCan.assigned_id = id; +} + +void +usb_notify_bulk_out(void) +{ + canbus_notify_tx(); +} + +void +usb_notify_bulk_in(void) +{ + canbus_notify_tx(); +} + + +/**************************************************************** + * USB descriptors + ****************************************************************/ + +#define CONCAT1(a, b) a ## b +#define CONCAT(a, b) CONCAT1(a, b) +#define USB_STR_MANUFACTURER u"Klipper" +#define USB_STR_PRODUCT CONCAT(u,CONFIG_MCU) +#define USB_STR_SERIAL CONCAT(u,CONFIG_USB_SERIAL_NUMBER) + +// String descriptors +enum { + USB_STR_ID_MANUFACTURER = 1, USB_STR_ID_PRODUCT, USB_STR_ID_SERIAL, +}; + +#define SIZE_cdc_string_langids (sizeof(cdc_string_langids) + 2) + +static const struct usb_string_descriptor cdc_string_langids PROGMEM = { + .bLength = SIZE_cdc_string_langids, + .bDescriptorType = USB_DT_STRING, + .data = { cpu_to_le16(USB_LANGID_ENGLISH_US) }, +}; + +#define SIZE_cdc_string_manufacturer \ + (sizeof(cdc_string_manufacturer) + sizeof(USB_STR_MANUFACTURER) - 2) + +static const struct usb_string_descriptor cdc_string_manufacturer PROGMEM = { + .bLength = SIZE_cdc_string_manufacturer, + .bDescriptorType = USB_DT_STRING, + .data = USB_STR_MANUFACTURER, +}; + +#define SIZE_cdc_string_product \ + (sizeof(cdc_string_product) + sizeof(USB_STR_PRODUCT) - 2) + +static const struct usb_string_descriptor cdc_string_product PROGMEM = { + .bLength = SIZE_cdc_string_product, + .bDescriptorType = USB_DT_STRING, + .data = USB_STR_PRODUCT, +}; + +#define SIZE_cdc_string_serial \ + (sizeof(cdc_string_serial) + sizeof(USB_STR_SERIAL) - 2) + +static const struct usb_string_descriptor cdc_string_serial PROGMEM = { + .bLength = SIZE_cdc_string_serial, + .bDescriptorType = USB_DT_STRING, + .data = USB_STR_SERIAL, +}; + +// Device descriptor +static const struct usb_device_descriptor gs_device_descriptor PROGMEM = { + .bLength = sizeof(gs_device_descriptor), + .bDescriptorType = USB_DT_DEVICE, + .bcdUSB = cpu_to_le16(0x0200), + .bMaxPacketSize0 = USB_CDC_EP0_SIZE, + .idVendor = cpu_to_le16(USB_GSUSB_1_VENDOR_ID), + .idProduct = cpu_to_le16(USB_GSUSB_1_PRODUCT_ID), + .iManufacturer = USB_STR_ID_MANUFACTURER, + .iProduct = USB_STR_ID_PRODUCT, + .iSerialNumber = USB_STR_ID_SERIAL, + .bNumConfigurations = 1, +}; + +// Config descriptor +static const struct config_s { + struct usb_config_descriptor config; + struct usb_interface_descriptor iface0; + struct usb_endpoint_descriptor ep1; + struct usb_endpoint_descriptor ep2; +} PACKED gs_config_descriptor PROGMEM = { + .config = { + .bLength = sizeof(gs_config_descriptor.config), + .bDescriptorType = USB_DT_CONFIG, + .wTotalLength = cpu_to_le16(sizeof(gs_config_descriptor)), + .bNumInterfaces = 1, + .bConfigurationValue = 1, + .bmAttributes = 0xC0, + .bMaxPower = 50, + }, + .iface0 = { + .bLength = sizeof(gs_config_descriptor.iface0), + .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 0, + .bNumEndpoints = 2, + .bInterfaceClass = 255, + .bInterfaceSubClass = 255, + .bInterfaceProtocol = 255, + }, + .ep1 = { + .bLength = sizeof(gs_config_descriptor.ep1), + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_CDC_EP_BULK_OUT, + .bmAttributes = USB_ENDPOINT_XFER_BULK, + .wMaxPacketSize = cpu_to_le16(USB_CDC_EP_BULK_OUT_SIZE), + }, + .ep2 = { + .bLength = sizeof(gs_config_descriptor.ep2), + .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = USB_CDC_EP_BULK_IN | USB_DIR_IN, + .bmAttributes = USB_ENDPOINT_XFER_BULK, + .wMaxPacketSize = cpu_to_le16(USB_CDC_EP_BULK_IN_SIZE), + }, +}; + +// List of available descriptors +static const struct descriptor_s { + uint_fast16_t wValue; + uint_fast16_t wIndex; + const void *desc; + uint_fast8_t size; +} usb_descriptors[] PROGMEM = { + { USB_DT_DEVICE<<8, 0x0000, + &gs_device_descriptor, sizeof(gs_device_descriptor) }, + { USB_DT_CONFIG<<8, 0x0000, + &gs_config_descriptor, sizeof(gs_config_descriptor) }, + { USB_DT_STRING<<8, 0x0000, + &cdc_string_langids, SIZE_cdc_string_langids }, + { (USB_DT_STRING<<8) | USB_STR_ID_MANUFACTURER, USB_LANGID_ENGLISH_US, + &cdc_string_manufacturer, SIZE_cdc_string_manufacturer }, + { (USB_DT_STRING<<8) | USB_STR_ID_PRODUCT, USB_LANGID_ENGLISH_US, + &cdc_string_product, SIZE_cdc_string_product }, +#if !CONFIG_USB_SERIAL_NUMBER_CHIPID + { (USB_DT_STRING<<8) | USB_STR_ID_SERIAL, USB_LANGID_ENGLISH_US, + &cdc_string_serial, SIZE_cdc_string_serial }, +#endif +}; + +// Fill in a USB serial string descriptor from a chip id +void +usb_fill_serial(struct usb_string_descriptor *desc, int strlen, void *id) +{ + desc->bLength = sizeof(*desc) + strlen * sizeof(desc->data[0]); + desc->bDescriptorType = USB_DT_STRING; + + uint8_t *src = id; + int i; + for (i = 0; i < strlen; i++) { + uint8_t c = i & 1 ? src[i/2] & 0x0f : src[i/2] >> 4; + desc->data[i] = c < 10 ? c + '0' : c - 10 + 'A'; + } +} + + +/**************************************************************** + * USB endpoint 0 control message handling + ****************************************************************/ + +// State tracking +enum { + UX_READ = 1<<0, UX_SEND = 1<<1, UX_SEND_PROGMEM = 1<<2, UX_SEND_ZLP = 1<<3 +}; + +static void *usb_xfer_data; +static uint8_t usb_xfer_size, usb_xfer_flags; + +// Set the USB "stall" condition +static void +usb_do_stall(void) +{ + usb_stall_ep0(); + usb_xfer_flags = 0; +} + +// Transfer data on the usb endpoint 0 +static void +usb_do_xfer(void *data, uint_fast8_t size, uint_fast8_t flags) +{ + for (;;) { + uint_fast8_t xs = size; + if (xs > USB_CDC_EP0_SIZE) + xs = USB_CDC_EP0_SIZE; + int_fast8_t ret; + if (flags & UX_READ) + ret = usb_read_ep0(data, xs); + else if (NEED_PROGMEM && flags & UX_SEND_PROGMEM) + ret = usb_send_ep0_progmem(data, xs); + else + ret = usb_send_ep0(data, xs); + if (ret == xs) { + // Success + data += xs; + size -= xs; + if (!size) { + // Entire transfer completed successfully + if (flags & UX_READ) { + // Send status packet at end of read + flags = UX_SEND; + continue; + } + if (xs == USB_CDC_EP0_SIZE && flags & UX_SEND_ZLP) + // Must send zero-length-packet + continue; + usb_xfer_flags = 0; + usb_notify_ep0(); + return; + } + continue; + } + if (ret == -1) { + // Interface busy - retry later + usb_xfer_data = data; + usb_xfer_size = size; + usb_xfer_flags = flags; + return; + } + // Error + usb_do_stall(); + return; + } +} + +static void +usb_req_get_descriptor(struct usb_ctrlrequest *req) +{ + if (req->bRequestType != USB_DIR_IN) + goto fail; + void *desc = NULL; + uint_fast8_t flags, size, i; + for (i=0; i<ARRAY_SIZE(usb_descriptors); i++) { + const struct descriptor_s *d = &usb_descriptors[i]; + if (READP(d->wValue) == req->wValue + && READP(d->wIndex) == req->wIndex) { + flags = NEED_PROGMEM ? UX_SEND_PROGMEM : UX_SEND; + size = READP(d->size); + desc = (void*)READP(d->desc); + } + } + if (CONFIG_USB_SERIAL_NUMBER_CHIPID + && req->wValue == ((USB_DT_STRING<<8) | USB_STR_ID_SERIAL) + && req->wIndex == USB_LANGID_ENGLISH_US) { + struct usb_string_descriptor *usbserial_serialid; + usbserial_serialid = usbserial_get_serialid(); + flags = UX_SEND; + size = usbserial_serialid->bLength; + desc = (void*)usbserial_serialid; + } + if (desc) { + if (size > req->wLength) + size = req->wLength; + else if (size < req->wLength) + flags |= UX_SEND_ZLP; + usb_do_xfer(desc, size, flags); + return; + } +fail: + usb_do_stall(); +} + +static void +usb_req_set_address(struct usb_ctrlrequest *req) +{ + if (req->bRequestType || req->wIndex || req->wLength) { + usb_do_stall(); + return; + } + usb_set_address(req->wValue); +} + +static void +usb_req_set_configuration(struct usb_ctrlrequest *req) +{ + if (req->bRequestType || req->wValue != 1 || req->wIndex || req->wLength) { + usb_do_stall(); + return; + } + usb_set_configure(); + usb_notify_bulk_in(); + usb_notify_bulk_out(); + usb_do_xfer(NULL, 0, UX_SEND); +} + +struct gs_host_config host_config; + +static void +gs_breq_host_format(struct usb_ctrlrequest *req) +{ + // Like candlightfw, little-endian is always used. Read and ignore value. + usb_do_xfer(&host_config, sizeof(host_config), UX_READ); +} + +static const struct gs_device_config device_config PROGMEM = { + .sw_version = 2, + .hw_version = 1, +}; + +static void +gs_breq_device_config(struct usb_ctrlrequest *req) +{ + usb_do_xfer((void*)&device_config, sizeof(device_config), UX_SEND); +} + +static const struct gs_device_bt_const bt_const PROGMEM = { + // These are just dummy values for now + .feature = 0, + .fclk_can = 48000000, + .tseg1_min = 1, + .tseg1_max = 16, + .tseg2_min = 1, + .tseg2_max = 8, + .sjw_max = 4, + .brp_min = 1, + .brp_max = 1024, + .brp_inc = 1, +}; + +static void +gs_breq_bt_const(struct usb_ctrlrequest *req) +{ + usb_do_xfer((void*)&bt_const, sizeof(bt_const), UX_SEND); +} + +struct gs_device_bittiming device_bittiming; + +static void +gs_breq_bittiming(struct usb_ctrlrequest *req) +{ + // Bit timing is ignored for now + usb_do_xfer(&device_bittiming, sizeof(device_bittiming), UX_READ); +} + +struct gs_device_mode device_mode; + +static void +gs_breq_mode(struct usb_ctrlrequest *req) +{ + // Mode is ignored for now + usb_do_xfer(&device_mode, sizeof(device_mode), UX_READ); +} + +static void +usb_state_ready(void) +{ + struct usb_ctrlrequest req; + int_fast8_t ret = usb_read_ep0_setup(&req, sizeof(req)); + if (ret != sizeof(req)) + return; + uint32_t req_type = req.bRequestType & USB_TYPE_MASK; + if (req_type == USB_TYPE_STANDARD) { + switch (req.bRequest) { + case USB_REQ_GET_DESCRIPTOR: usb_req_get_descriptor(&req); break; + case USB_REQ_SET_ADDRESS: usb_req_set_address(&req); break; + case USB_REQ_SET_CONFIGURATION: usb_req_set_configuration(&req); break; + default: usb_do_stall(); break; + } + } else if (req_type == USB_TYPE_VENDOR) { + switch (req.bRequest) { + case GS_USB_BREQ_HOST_FORMAT: gs_breq_host_format(&req); break; + case GS_USB_BREQ_DEVICE_CONFIG: gs_breq_device_config(&req); break; + case GS_USB_BREQ_BT_CONST: gs_breq_bt_const(&req); break; + case GS_USB_BREQ_BITTIMING: gs_breq_bittiming(&req); break; + case GS_USB_BREQ_MODE: gs_breq_mode(&req); break; + default: usb_do_stall(); break; + } + } else { + usb_do_stall(); + } +} + +// State tracking dispatch +static struct task_wake usb_ep0_wake; + +void +usb_notify_ep0(void) +{ + sched_wake_task(&usb_ep0_wake); +} + +void +usb_ep0_task(void) +{ + if (!sched_check_wake(&usb_ep0_wake)) + return; + if (usb_xfer_flags) + usb_do_xfer(usb_xfer_data, usb_xfer_size, usb_xfer_flags); + else + usb_state_ready(); +} +DECL_TASK(usb_ep0_task); + +void +usb_shutdown(void) +{ + usb_notify_bulk_in(); + usb_notify_bulk_out(); + usb_notify_ep0(); +} +DECL_SHUTDOWN(usb_shutdown); |