| /* |
| * Copyright (c) 2022 Nordic Semiconductor ASA |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/kernel.h> |
| #include <zephyr/drivers/uart.h> |
| #include <zephyr/sys/ring_buffer.h> |
| #include <zephyr/sys/byteorder.h> |
| |
| #include <zephyr/usb/usbd.h> |
| #include <zephyr/usb/usb_ch9.h> |
| #include <zephyr/usb/class/usb_cdc.h> |
| |
| #include <zephyr/drivers/usb/udc.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(usbd_cdc_acm, CONFIG_USBD_CDC_ACM_LOG_LEVEL); |
| |
| /* |
| * FIXME: buffer count per device. |
| */ |
| NET_BUF_POOL_FIXED_DEFINE(cdc_acm_ep_pool, |
| 2, 512, |
| sizeof(struct udc_buf_info), NULL); |
| |
| #define CDC_ACM_DEFAULT_LINECODING {sys_cpu_to_le32(115200), 0, 0, 8} |
| #define CDC_ACM_DEFAULT_BULK_EP_MPS 0 |
| #define CDC_ACM_DEFAULT_INT_EP_MPS 16 |
| #define CDC_ACM_DEFAULT_INT_INTERVAL 0x0A |
| |
| #define CDC_ACM_CLASS_ENABLED 0 |
| #define CDC_ACM_CLASS_SUSPENDED 1 |
| #define CDC_ACM_IRQ_RX_ENABLED 2 |
| #define CDC_ACM_IRQ_TX_ENABLED 3 |
| #define CDC_ACM_RX_FIFO_BUSY 4 |
| #define CDC_ACM_LOCK 5 |
| |
| static struct k_work_q cdc_acm_work_q; |
| static K_KERNEL_STACK_DEFINE(cdc_acm_stack, |
| CONFIG_USBD_CDC_ACM_STACK_SIZE); |
| |
| struct cdc_acm_uart_fifo { |
| struct ring_buf *rb; |
| bool irq; |
| bool altered; |
| }; |
| |
| struct cdc_acm_uart_data { |
| /* Pointer to the associated USBD class node */ |
| struct usbd_class_node *c_nd; |
| /* Line Coding Structure */ |
| struct cdc_acm_line_coding line_coding; |
| /* SetControlLineState bitmap */ |
| uint16_t line_state; |
| /* Serial state bitmap */ |
| uint16_t serial_state; |
| /* UART actual configuration */ |
| struct uart_config uart_cfg; |
| /* UART actual RTS state */ |
| bool line_state_rts; |
| /* UART actual DTR state */ |
| bool line_state_dtr; |
| /* UART API IRQ callback */ |
| uart_irq_callback_user_data_t cb; |
| /* UART API user callback data */ |
| void *cb_data; |
| /* UART API IRQ callback work */ |
| struct k_work irq_cb_work; |
| struct cdc_acm_uart_fifo rx_fifo; |
| struct cdc_acm_uart_fifo tx_fifo; |
| /* USBD CDC ACM TX fifo work */ |
| struct k_work tx_fifo_work; |
| /* USBD CDC ACM RX fifo work */ |
| struct k_work rx_fifo_work; |
| atomic_t state; |
| struct k_sem notif_sem; |
| }; |
| |
| struct usbd_cdc_acm_desc { |
| struct usb_association_descriptor iad_cdc; |
| struct usb_if_descriptor if0; |
| struct cdc_header_descriptor if0_header; |
| struct cdc_cm_descriptor if0_cm; |
| struct cdc_acm_descriptor if0_acm; |
| struct cdc_union_descriptor if0_union; |
| struct usb_ep_descriptor if0_int_ep; |
| |
| struct usb_if_descriptor if1; |
| struct usb_ep_descriptor if1_in_ep; |
| struct usb_ep_descriptor if1_out_ep; |
| |
| struct usb_desc_header nil_desc; |
| } __packed; |
| |
| static void cdc_acm_irq_rx_enable(const struct device *dev); |
| |
| struct net_buf *cdc_acm_buf_alloc(const uint8_t ep) |
| { |
| struct net_buf *buf = NULL; |
| struct udc_buf_info *bi; |
| |
| buf = net_buf_alloc(&cdc_acm_ep_pool, K_NO_WAIT); |
| if (!buf) { |
| return NULL; |
| } |
| |
| bi = udc_get_buf_info(buf); |
| memset(bi, 0, sizeof(struct udc_buf_info)); |
| bi->ep = ep; |
| |
| return buf; |
| } |
| |
| static ALWAYS_INLINE int cdc_acm_work_submit(struct k_work *work) |
| { |
| return k_work_submit_to_queue(&cdc_acm_work_q, work); |
| } |
| |
| static ALWAYS_INLINE bool check_wq_ctx(const struct device *dev) |
| { |
| return k_current_get() == k_work_queue_thread_get(&cdc_acm_work_q); |
| } |
| |
| static uint8_t cdc_acm_get_int_in(struct usbd_class_node *const c_nd) |
| { |
| struct usbd_cdc_acm_desc *desc = c_nd->data->desc; |
| |
| return desc->if0_int_ep.bEndpointAddress; |
| } |
| |
| static uint8_t cdc_acm_get_bulk_in(struct usbd_class_node *const c_nd) |
| { |
| struct usbd_cdc_acm_desc *desc = c_nd->data->desc; |
| |
| return desc->if1_in_ep.bEndpointAddress; |
| } |
| |
| static uint8_t cdc_acm_get_bulk_out(struct usbd_class_node *const c_nd) |
| { |
| struct usbd_cdc_acm_desc *desc = c_nd->data->desc; |
| |
| return desc->if1_out_ep.bEndpointAddress; |
| } |
| |
| static size_t cdc_acm_get_bulk_mps(struct usbd_class_node *const c_nd) |
| { |
| struct usbd_cdc_acm_desc *desc = c_nd->data->desc; |
| |
| return desc->if1_out_ep.wMaxPacketSize; |
| } |
| |
| static int usbd_cdc_acm_request(struct usbd_class_node *const c_nd, |
| struct net_buf *buf, int err) |
| { |
| struct usbd_contex *uds_ctx = c_nd->data->uds_ctx; |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| struct udc_buf_info *bi; |
| |
| bi = udc_get_buf_info(buf); |
| if (err) { |
| if (err == -ECONNABORTED) { |
| LOG_WRN("request ep 0x%02x, len %u cancelled", |
| bi->ep, buf->len); |
| } else { |
| LOG_ERR("request ep 0x%02x, len %u failed", |
| bi->ep, buf->len); |
| } |
| |
| if (bi->ep == cdc_acm_get_bulk_out(c_nd)) { |
| atomic_clear_bit(&data->state, CDC_ACM_RX_FIFO_BUSY); |
| } |
| |
| goto ep_request_error; |
| } |
| |
| if (bi->ep == cdc_acm_get_bulk_out(c_nd)) { |
| /* RX transfer completion */ |
| size_t done; |
| |
| LOG_HEXDUMP_INF(buf->data, buf->len, ""); |
| done = ring_buf_put(data->rx_fifo.rb, buf->data, buf->len); |
| if (done && data->cb) { |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| |
| atomic_clear_bit(&data->state, CDC_ACM_RX_FIFO_BUSY); |
| cdc_acm_work_submit(&data->rx_fifo_work); |
| } |
| |
| if (bi->ep == cdc_acm_get_bulk_in(c_nd)) { |
| /* TX transfer completion */ |
| if (data->cb) { |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| } |
| |
| if (bi->ep == cdc_acm_get_int_in(c_nd)) { |
| k_sem_give(&data->notif_sem); |
| } |
| |
| ep_request_error: |
| return usbd_ep_buf_free(uds_ctx, buf); |
| } |
| |
| static void usbd_cdc_acm_update(struct usbd_class_node *const c_nd, |
| uint8_t iface, uint8_t alternate) |
| { |
| LOG_DBG("New configuration, interface %u alternate %u", |
| iface, alternate); |
| } |
| |
| static void usbd_cdc_acm_enable(struct usbd_class_node *const c_nd) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| |
| atomic_set_bit(&data->state, CDC_ACM_CLASS_ENABLED); |
| LOG_INF("Configuration enabled"); |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED)) { |
| cdc_acm_irq_rx_enable(dev); |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED)) { |
| /* TODO */ |
| } |
| } |
| |
| static void usbd_cdc_acm_disable(struct usbd_class_node *const c_nd) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| |
| atomic_clear_bit(&data->state, CDC_ACM_CLASS_ENABLED); |
| atomic_clear_bit(&data->state, CDC_ACM_CLASS_SUSPENDED); |
| LOG_INF("Configuration disabled"); |
| } |
| |
| static void usbd_cdc_acm_suspended(struct usbd_class_node *const c_nd) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| |
| /* FIXME: filter stray suspended events earlier */ |
| atomic_set_bit(&data->state, CDC_ACM_CLASS_SUSPENDED); |
| } |
| |
| static void usbd_cdc_acm_resumed(struct usbd_class_node *const c_nd) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| |
| atomic_clear_bit(&data->state, CDC_ACM_CLASS_SUSPENDED); |
| } |
| |
| static void cdc_acm_update_uart_cfg(struct cdc_acm_uart_data *const data) |
| { |
| struct uart_config *const cfg = &data->uart_cfg; |
| |
| cfg->baudrate = sys_le32_to_cpu(data->line_coding.dwDTERate); |
| |
| switch (data->line_coding.bCharFormat) { |
| case USB_CDC_LINE_CODING_STOP_BITS_1: |
| cfg->stop_bits = UART_CFG_STOP_BITS_1; |
| break; |
| case USB_CDC_LINE_CODING_STOP_BITS_1_5: |
| cfg->stop_bits = UART_CFG_STOP_BITS_1_5; |
| break; |
| case USB_CDC_LINE_CODING_STOP_BITS_2: |
| default: |
| cfg->stop_bits = UART_CFG_STOP_BITS_2; |
| break; |
| }; |
| |
| switch (data->line_coding.bParityType) { |
| case USB_CDC_LINE_CODING_PARITY_NO: |
| default: |
| cfg->parity = UART_CFG_PARITY_NONE; |
| break; |
| case USB_CDC_LINE_CODING_PARITY_ODD: |
| cfg->parity = UART_CFG_PARITY_ODD; |
| break; |
| case USB_CDC_LINE_CODING_PARITY_EVEN: |
| cfg->parity = UART_CFG_PARITY_EVEN; |
| break; |
| case USB_CDC_LINE_CODING_PARITY_MARK: |
| cfg->parity = UART_CFG_PARITY_MARK; |
| break; |
| case USB_CDC_LINE_CODING_PARITY_SPACE: |
| cfg->parity = UART_CFG_PARITY_SPACE; |
| break; |
| }; |
| |
| switch (data->line_coding.bDataBits) { |
| case USB_CDC_LINE_CODING_DATA_BITS_5: |
| cfg->data_bits = UART_CFG_DATA_BITS_5; |
| break; |
| case USB_CDC_LINE_CODING_DATA_BITS_6: |
| cfg->data_bits = UART_CFG_DATA_BITS_6; |
| break; |
| case USB_CDC_LINE_CODING_DATA_BITS_7: |
| cfg->data_bits = UART_CFG_DATA_BITS_7; |
| break; |
| case USB_CDC_LINE_CODING_DATA_BITS_8: |
| default: |
| cfg->data_bits = UART_CFG_DATA_BITS_8; |
| break; |
| }; |
| |
| cfg->flow_ctrl = UART_CFG_FLOW_CTRL_NONE; |
| } |
| |
| static void cdc_acm_update_linestate(struct cdc_acm_uart_data *const data) |
| { |
| if (data->line_state & SET_CONTROL_LINE_STATE_RTS) { |
| data->line_state_rts = true; |
| } else { |
| data->line_state_rts = false; |
| } |
| |
| if (data->line_state & SET_CONTROL_LINE_STATE_DTR) { |
| data->line_state_dtr = true; |
| } else { |
| data->line_state_dtr = false; |
| } |
| } |
| |
| static int usbd_cdc_acm_cth(struct usbd_class_node *const c_nd, |
| const struct usb_setup_packet *const setup, |
| struct net_buf *const buf) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| size_t min_len; |
| |
| if (setup->bRequest == GET_LINE_CODING) { |
| if (buf == NULL) { |
| errno = -ENOMEM; |
| return 0; |
| } |
| |
| min_len = MIN(sizeof(data->line_coding), setup->wLength); |
| net_buf_add_mem(buf, &data->line_coding, min_len); |
| |
| return 0; |
| } |
| |
| LOG_DBG("bmRequestType 0x%02x bRequest 0x%02x unsupported", |
| setup->bmRequestType, setup->bRequest); |
| errno = -ENOTSUP; |
| |
| return 0; |
| } |
| |
| static int usbd_cdc_acm_ctd(struct usbd_class_node *const c_nd, |
| const struct usb_setup_packet *const setup, |
| const struct net_buf *const buf) |
| { |
| const struct device *dev = c_nd->data->priv; |
| struct cdc_acm_uart_data *data = dev->data; |
| size_t len; |
| |
| switch (setup->bRequest) { |
| case SET_LINE_CODING: |
| len = sizeof(data->line_coding); |
| if (setup->wLength != len) { |
| errno = -ENOTSUP; |
| return 0; |
| } |
| |
| memcpy(&data->line_coding, buf->data, len); |
| cdc_acm_update_uart_cfg(data); |
| return 0; |
| |
| case SET_CONTROL_LINE_STATE: |
| data->line_state = setup->wValue; |
| cdc_acm_update_linestate(data); |
| return 0; |
| |
| default: |
| break; |
| } |
| |
| LOG_DBG("bmRequestType 0x%02x bRequest 0x%02x unsupported", |
| setup->bmRequestType, setup->bRequest); |
| errno = -ENOTSUP; |
| |
| return 0; |
| } |
| |
| static int usbd_cdc_acm_init(struct usbd_class_node *const c_nd) |
| { |
| struct usbd_cdc_acm_desc *desc = c_nd->data->desc; |
| |
| desc->iad_cdc.bFirstInterface = desc->if0.bInterfaceNumber; |
| desc->if0_union.bControlInterface = desc->if0.bInterfaceNumber; |
| desc->if0_union.bSubordinateInterface0 = desc->if1.bInterfaceNumber; |
| |
| return 0; |
| } |
| |
| static int cdc_acm_send_notification(const struct device *dev, |
| const uint16_t serial_state) |
| { |
| struct cdc_acm_notification notification = { |
| .bmRequestType = 0xA1, |
| .bNotificationType = USB_CDC_SERIAL_STATE, |
| .wValue = 0, |
| .wIndex = 0, |
| .wLength = sys_cpu_to_le16(sizeof(uint16_t)), |
| .data = sys_cpu_to_le16(serial_state), |
| }; |
| struct cdc_acm_uart_data *data = dev->data; |
| struct usbd_class_node *c_nd = data->c_nd; |
| struct net_buf *buf; |
| uint8_t ep; |
| int ret; |
| |
| if (!atomic_test_bit(&data->state, CDC_ACM_CLASS_ENABLED)) { |
| LOG_INF("USB configuration is not enabled"); |
| return -EACCES; |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_CLASS_SUSPENDED)) { |
| LOG_INF("USB support is suspended (FIXME)"); |
| return -EACCES; |
| } |
| |
| ep = cdc_acm_get_int_in(c_nd); |
| buf = usbd_ep_buf_alloc(c_nd, ep, sizeof(struct cdc_acm_notification)); |
| if (buf == NULL) { |
| return -ENOMEM; |
| } |
| |
| net_buf_add_mem(buf, ¬ification, sizeof(struct cdc_acm_notification)); |
| ret = usbd_ep_enqueue(c_nd, buf); |
| /* FIXME: support for sync transfers */ |
| k_sem_take(&data->notif_sem, K_FOREVER); |
| |
| return ret; |
| } |
| |
| /* |
| * TX handler is triggered when the state of TX fifo has been altered. |
| */ |
| static void cdc_acm_tx_fifo_handler(struct k_work *work) |
| { |
| struct cdc_acm_uart_data *data; |
| struct usbd_class_node *c_nd; |
| struct net_buf *buf; |
| size_t len; |
| int ret; |
| |
| data = CONTAINER_OF(work, struct cdc_acm_uart_data, tx_fifo_work); |
| c_nd = data->c_nd; |
| |
| if (!atomic_test_bit(&data->state, CDC_ACM_CLASS_ENABLED)) { |
| LOG_DBG("USB configuration is not enabled"); |
| return; |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_CLASS_SUSPENDED)) { |
| LOG_INF("USB support is suspended (FIXME: submit rwup)"); |
| return; |
| } |
| |
| if (atomic_test_and_set_bit(&data->state, CDC_ACM_LOCK)) { |
| cdc_acm_work_submit(&data->tx_fifo_work); |
| return; |
| } |
| |
| buf = cdc_acm_buf_alloc(cdc_acm_get_bulk_in(c_nd)); |
| if (buf == NULL) { |
| cdc_acm_work_submit(&data->tx_fifo_work); |
| goto tx_fifo_handler_exit; |
| } |
| |
| len = ring_buf_get(data->tx_fifo.rb, buf->data, buf->size); |
| net_buf_add(buf, len); |
| |
| ret = usbd_ep_enqueue(c_nd, buf); |
| if (ret) { |
| LOG_ERR("Failed to enqueue"); |
| net_buf_unref(buf); |
| } |
| |
| tx_fifo_handler_exit: |
| atomic_clear_bit(&data->state, CDC_ACM_LOCK); |
| } |
| |
| /* |
| * RX handler should be conditionally triggered at: |
| * - (x) cdc_acm_irq_rx_enable() |
| * - (x) RX transfer completion |
| * - (x) the end of cdc_acm_irq_cb_handler |
| * - (x) USBD class API enable call |
| * - ( ) USBD class API resumed call (TODO) |
| */ |
| static void cdc_acm_rx_fifo_handler(struct k_work *work) |
| { |
| struct cdc_acm_uart_data *data; |
| struct usbd_class_node *c_nd; |
| struct net_buf *buf; |
| uint8_t ep; |
| int ret; |
| |
| data = CONTAINER_OF(work, struct cdc_acm_uart_data, rx_fifo_work); |
| c_nd = data->c_nd; |
| |
| if (!atomic_test_bit(&data->state, CDC_ACM_CLASS_ENABLED) || |
| atomic_test_bit(&data->state, CDC_ACM_CLASS_SUSPENDED)) { |
| LOG_INF("USB configuration is not enabled or suspended"); |
| return; |
| } |
| |
| if (ring_buf_space_get(data->rx_fifo.rb) < cdc_acm_get_bulk_mps(c_nd)) { |
| LOG_INF("RX buffer to small, throttle"); |
| return; |
| } |
| |
| if (atomic_test_and_set_bit(&data->state, CDC_ACM_RX_FIFO_BUSY)) { |
| LOG_WRN("RX transfer already in progress"); |
| return; |
| } |
| |
| ep = cdc_acm_get_bulk_out(c_nd); |
| buf = cdc_acm_buf_alloc(ep); |
| if (buf == NULL) { |
| return; |
| } |
| |
| ret = usbd_ep_enqueue(c_nd, buf); |
| if (ret) { |
| LOG_ERR("Failed to enqueue net_buf for 0x%02x", ep); |
| net_buf_unref(buf); |
| } |
| } |
| |
| static void cdc_acm_irq_tx_enable(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| atomic_set_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED); |
| |
| if (ring_buf_is_empty(data->tx_fifo.rb)) { |
| LOG_INF("tx_en: trigger irq_cb_work"); |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| } |
| |
| static void cdc_acm_irq_tx_disable(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| atomic_clear_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED); |
| } |
| |
| static void cdc_acm_irq_rx_enable(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| atomic_set_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED); |
| |
| /* Permit buffer to be drained regardless of USB state */ |
| if (!ring_buf_is_empty(data->rx_fifo.rb)) { |
| LOG_INF("rx_en: trigger irq_cb_work"); |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| |
| if (!atomic_test_bit(&data->state, CDC_ACM_RX_FIFO_BUSY)) { |
| LOG_INF("rx_en: trigger rx_fifo_work"); |
| cdc_acm_work_submit(&data->rx_fifo_work); |
| } |
| } |
| |
| static void cdc_acm_irq_rx_disable(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| atomic_clear_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED); |
| } |
| |
| static int cdc_acm_fifo_fill(const struct device *dev, |
| const uint8_t *const tx_data, |
| const int len) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| uint32_t done; |
| |
| if (!check_wq_ctx(dev)) { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| return 0; |
| } |
| |
| done = ring_buf_put(data->tx_fifo.rb, tx_data, len); |
| if (done) { |
| data->tx_fifo.altered = true; |
| } |
| |
| LOG_INF("UART dev %p, len %d, remaining space %u", |
| dev, len, ring_buf_space_get(data->tx_fifo.rb)); |
| |
| return done; |
| } |
| |
| static int cdc_acm_fifo_read(const struct device *dev, |
| uint8_t *const rx_data, |
| const int size) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| uint32_t len; |
| |
| LOG_INF("UART dev %p size %d length %u", |
| dev, size, ring_buf_size_get(data->rx_fifo.rb)); |
| |
| if (!check_wq_ctx(dev)) { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| return 0; |
| } |
| |
| len = ring_buf_get(data->rx_fifo.rb, rx_data, size); |
| if (len) { |
| data->rx_fifo.altered = true; |
| } |
| |
| return len; |
| } |
| |
| static int cdc_acm_irq_tx_ready(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| if (check_wq_ctx(dev)) { |
| if (ring_buf_space_get(data->tx_fifo.rb)) { |
| return 1; |
| } |
| } else { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| } |
| |
| return 0; |
| } |
| |
| static int cdc_acm_irq_rx_ready(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| if (check_wq_ctx(dev)) { |
| if (!ring_buf_is_empty(data->rx_fifo.rb)) { |
| return 1; |
| } |
| } else { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| } |
| |
| |
| return 0; |
| } |
| |
| static int cdc_acm_irq_is_pending(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| if (check_wq_ctx(dev)) { |
| if (data->tx_fifo.irq || data->rx_fifo.irq) { |
| return 1; |
| } |
| } else { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| } |
| |
| return 0; |
| } |
| |
| static int cdc_acm_irq_update(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| if (!check_wq_ctx(dev)) { |
| LOG_WRN("Invoked by inappropriate context"); |
| __ASSERT_NO_MSG(false); |
| return 0; |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED) && |
| !ring_buf_is_empty(data->rx_fifo.rb)) { |
| data->rx_fifo.irq = true; |
| } else { |
| data->rx_fifo.irq = false; |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED) && |
| ring_buf_is_empty(data->tx_fifo.rb)) { |
| data->tx_fifo.irq = true; |
| } else { |
| data->tx_fifo.irq = false; |
| } |
| |
| return 1; |
| } |
| |
| /* |
| * IRQ handler should be conditionally triggered for the TX path at: |
| * - cdc_acm_irq_tx_enable() |
| * - TX transfer completion |
| * - TX buffer is empty |
| * - USBD class API enable and resumed calls |
| * |
| * for RX path, if enabled, at: |
| * - cdc_acm_irq_rx_enable() |
| * - RX transfer completion |
| * - RX buffer is not empty |
| */ |
| static void cdc_acm_irq_cb_handler(struct k_work *work) |
| { |
| struct cdc_acm_uart_data *data; |
| struct usbd_class_node *c_nd; |
| |
| data = CONTAINER_OF(work, struct cdc_acm_uart_data, irq_cb_work); |
| c_nd = data->c_nd; |
| |
| if (data->cb == NULL) { |
| LOG_ERR("IRQ callback is not set"); |
| return; |
| } |
| |
| if (atomic_test_and_set_bit(&data->state, CDC_ACM_LOCK)) { |
| LOG_ERR("Polling is in progress"); |
| cdc_acm_work_submit(&data->irq_cb_work); |
| return; |
| } |
| |
| data->tx_fifo.altered = false; |
| data->rx_fifo.altered = false; |
| data->rx_fifo.irq = false; |
| data->tx_fifo.irq = false; |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED) || |
| atomic_test_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED)) { |
| data->cb(c_nd->data->priv, data->cb_data); |
| } |
| |
| if (data->rx_fifo.altered) { |
| LOG_DBG("rx fifo altered, submit work"); |
| cdc_acm_work_submit(&data->rx_fifo_work); |
| } |
| |
| if (data->tx_fifo.altered) { |
| LOG_DBG("tx fifo altered, submit work"); |
| cdc_acm_work_submit(&data->tx_fifo_work); |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_RX_ENABLED) && |
| !ring_buf_is_empty(data->rx_fifo.rb)) { |
| LOG_DBG("rx irq pending, submit irq_cb_work"); |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| |
| if (atomic_test_bit(&data->state, CDC_ACM_IRQ_TX_ENABLED) && |
| ring_buf_is_empty(data->tx_fifo.rb)) { |
| LOG_DBG("tx irq pending, submit irq_cb_work"); |
| cdc_acm_work_submit(&data->irq_cb_work); |
| } |
| |
| atomic_clear_bit(&data->state, CDC_ACM_LOCK); |
| } |
| |
| static void cdc_acm_irq_callback_set(const struct device *dev, |
| const uart_irq_callback_user_data_t cb, |
| void *const cb_data) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| data->cb = cb; |
| data->cb_data = cb_data; |
| } |
| |
| static int cdc_acm_poll_in(const struct device *dev, unsigned char *const c) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| uint32_t len; |
| int ret = -1; |
| |
| if (atomic_test_and_set_bit(&data->state, CDC_ACM_LOCK)) { |
| LOG_ERR("IRQ callback is used"); |
| return -1; |
| } |
| |
| if (ring_buf_is_empty(data->rx_fifo.rb)) { |
| goto poll_in_exit; |
| } |
| |
| len = ring_buf_get(data->rx_fifo.rb, c, 1); |
| if (len) { |
| cdc_acm_work_submit(&data->rx_fifo_work); |
| ret = 0; |
| } |
| |
| poll_in_exit: |
| atomic_clear_bit(&data->state, CDC_ACM_LOCK); |
| |
| return ret; |
| } |
| |
| static void cdc_acm_poll_out(const struct device *dev, const unsigned char c) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| if (atomic_test_and_set_bit(&data->state, CDC_ACM_LOCK)) { |
| LOG_ERR("IRQ callback is used"); |
| return; |
| } |
| |
| if (ring_buf_put(data->tx_fifo.rb, &c, 1)) { |
| goto poll_out_exit; |
| } |
| |
| LOG_DBG("Ring buffer full, drain buffer"); |
| if (!ring_buf_get(data->tx_fifo.rb, NULL, 1) || |
| !ring_buf_put(data->tx_fifo.rb, &c, 1)) { |
| LOG_ERR("Failed to drain buffer"); |
| __ASSERT_NO_MSG(false); |
| } |
| |
| poll_out_exit: |
| atomic_clear_bit(&data->state, CDC_ACM_LOCK); |
| cdc_acm_work_submit(&data->tx_fifo_work); |
| } |
| |
| #ifdef CONFIG_UART_LINE_CTRL |
| static int cdc_acm_line_ctrl_set(const struct device *dev, |
| const uint32_t ctrl, const uint32_t val) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| uint32_t flag = 0; |
| |
| switch (ctrl) { |
| case USB_CDC_LINE_CTRL_BAUD_RATE: |
| /* Ignore since it can not be used for notification anyway */ |
| return 0; |
| case USB_CDC_LINE_CTRL_DCD: |
| flag = USB_CDC_SERIAL_STATE_RXCARRIER; |
| break; |
| case USB_CDC_LINE_CTRL_DSR: |
| flag = USB_CDC_SERIAL_STATE_TXCARRIER; |
| break; |
| case USB_CDC_LINE_CTRL_BREAK: |
| flag = USB_CDC_SERIAL_STATE_BREAK; |
| break; |
| case USB_CDC_LINE_CTRL_RING_SIGNAL: |
| flag = USB_CDC_SERIAL_STATE_RINGSIGNAL; |
| break; |
| case USB_CDC_LINE_CTRL_FRAMING: |
| flag = USB_CDC_SERIAL_STATE_FRAMING; |
| break; |
| case USB_CDC_LINE_CTRL_PARITY: |
| flag = USB_CDC_SERIAL_STATE_PARITY; |
| break; |
| case USB_CDC_LINE_CTRL_OVER_RUN: |
| flag = USB_CDC_SERIAL_STATE_OVERRUN; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| if (val) { |
| data->serial_state |= flag; |
| } else { |
| data->serial_state &= ~flag; |
| } |
| |
| return cdc_acm_send_notification(dev, data->serial_state); |
| } |
| |
| static int cdc_acm_line_ctrl_get(const struct device *dev, |
| const uint32_t ctrl, uint32_t *const val) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| switch (ctrl) { |
| case UART_LINE_CTRL_BAUD_RATE: |
| *val = data->uart_cfg.baudrate; |
| return 0; |
| case UART_LINE_CTRL_RTS: |
| *val = data->line_state_rts; |
| return 0; |
| case UART_LINE_CTRL_DTR: |
| *val = data->line_state_dtr; |
| return 0; |
| } |
| |
| return -ENOTSUP; |
| } |
| #endif |
| |
| #ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE |
| static int cdc_acm_configure(const struct device *dev, |
| const struct uart_config *const cfg) |
| { |
| ARG_UNUSED(dev); |
| ARG_UNUSED(cfg); |
| /* |
| * We cannot implement configure API because there is |
| * no notification of configuration changes provided |
| * for the Abstract Control Model and the UART controller |
| * is only emulated. |
| * However, it allows us to use CDC ACM UART together with |
| * subsystems like Modbus which require configure API for |
| * real controllers. |
| */ |
| |
| return 0; |
| } |
| |
| static int cdc_acm_config_get(const struct device *dev, |
| struct uart_config *const cfg) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| memcpy(cfg, &data->uart_cfg, sizeof(struct uart_config)); |
| |
| return 0; |
| } |
| #endif /* CONFIG_UART_USE_RUNTIME_CONFIGURE */ |
| |
| static int usbd_cdc_acm_init_wq(void) |
| { |
| k_work_queue_init(&cdc_acm_work_q); |
| k_work_queue_start(&cdc_acm_work_q, cdc_acm_stack, |
| K_KERNEL_STACK_SIZEOF(cdc_acm_stack), |
| CONFIG_SYSTEM_WORKQUEUE_PRIORITY, NULL); |
| |
| return 0; |
| } |
| |
| static int usbd_cdc_acm_preinit(const struct device *dev) |
| { |
| struct cdc_acm_uart_data *const data = dev->data; |
| |
| ring_buf_reset(data->tx_fifo.rb); |
| ring_buf_reset(data->rx_fifo.rb); |
| |
| k_thread_name_set(&cdc_acm_work_q.thread, "cdc_acm_work_q"); |
| |
| k_work_init(&data->tx_fifo_work, cdc_acm_tx_fifo_handler); |
| k_work_init(&data->rx_fifo_work, cdc_acm_rx_fifo_handler); |
| k_work_init(&data->irq_cb_work, cdc_acm_irq_cb_handler); |
| |
| return 0; |
| } |
| |
| static const struct uart_driver_api cdc_acm_uart_api = { |
| .irq_tx_enable = cdc_acm_irq_tx_enable, |
| .irq_tx_disable = cdc_acm_irq_tx_disable, |
| .irq_tx_ready = cdc_acm_irq_tx_ready, |
| .irq_rx_enable = cdc_acm_irq_rx_enable, |
| .irq_rx_disable = cdc_acm_irq_rx_disable, |
| .irq_rx_ready = cdc_acm_irq_rx_ready, |
| .irq_is_pending = cdc_acm_irq_is_pending, |
| .irq_update = cdc_acm_irq_update, |
| .irq_callback_set = cdc_acm_irq_callback_set, |
| .poll_in = cdc_acm_poll_in, |
| .poll_out = cdc_acm_poll_out, |
| .fifo_fill = cdc_acm_fifo_fill, |
| .fifo_read = cdc_acm_fifo_read, |
| #ifdef CONFIG_UART_LINE_CTRL |
| .line_ctrl_set = cdc_acm_line_ctrl_set, |
| .line_ctrl_get = cdc_acm_line_ctrl_get, |
| #endif |
| #ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE |
| .configure = cdc_acm_configure, |
| .config_get = cdc_acm_config_get, |
| #endif |
| }; |
| |
| struct usbd_class_api usbd_cdc_acm_api = { |
| .request = usbd_cdc_acm_request, |
| .update = usbd_cdc_acm_update, |
| .enable = usbd_cdc_acm_enable, |
| .disable = usbd_cdc_acm_disable, |
| .suspended = usbd_cdc_acm_suspended, |
| .resumed = usbd_cdc_acm_resumed, |
| .control_to_host = usbd_cdc_acm_cth, |
| .control_to_dev = usbd_cdc_acm_ctd, |
| .init = usbd_cdc_acm_init, |
| }; |
| |
| #define CDC_ACM_DEFINE_DESCRIPTOR(n) \ |
| static struct usbd_cdc_acm_desc cdc_acm_desc_##n = { \ |
| .iad_cdc = { \ |
| .bLength = sizeof(struct usb_association_descriptor), \ |
| .bDescriptorType = USB_DESC_INTERFACE_ASSOC, \ |
| .bFirstInterface = 0, \ |
| .bInterfaceCount = 0x02, \ |
| .bFunctionClass = USB_BCC_CDC_CONTROL, \ |
| .bFunctionSubClass = ACM_SUBCLASS, \ |
| .bFunctionProtocol = 0, \ |
| .iFunction = 0, \ |
| }, \ |
| \ |
| .if0 = { \ |
| .bLength = sizeof(struct usb_if_descriptor), \ |
| .bDescriptorType = USB_DESC_INTERFACE, \ |
| .bInterfaceNumber = 0, \ |
| .bAlternateSetting = 0, \ |
| .bNumEndpoints = 1, \ |
| .bInterfaceClass = USB_BCC_CDC_CONTROL, \ |
| .bInterfaceSubClass = ACM_SUBCLASS, \ |
| .bInterfaceProtocol = 0, \ |
| .iInterface = 0, \ |
| }, \ |
| \ |
| .if0_header = { \ |
| .bFunctionLength = sizeof(struct cdc_header_descriptor), \ |
| .bDescriptorType = USB_DESC_CS_INTERFACE, \ |
| .bDescriptorSubtype = HEADER_FUNC_DESC, \ |
| .bcdCDC = sys_cpu_to_le16(USB_SRN_1_1), \ |
| }, \ |
| \ |
| .if0_cm = { \ |
| .bFunctionLength = sizeof(struct cdc_cm_descriptor), \ |
| .bDescriptorType = USB_DESC_CS_INTERFACE, \ |
| .bDescriptorSubtype = CALL_MANAGEMENT_FUNC_DESC, \ |
| .bmCapabilities = 0, \ |
| .bDataInterface = 1, \ |
| }, \ |
| \ |
| .if0_acm = { \ |
| .bFunctionLength = sizeof(struct cdc_acm_descriptor), \ |
| .bDescriptorType = USB_DESC_CS_INTERFACE, \ |
| .bDescriptorSubtype = ACM_FUNC_DESC, \ |
| /* See CDC PSTN Subclass Chapter 5.3.2 */ \ |
| .bmCapabilities = BIT(1), \ |
| }, \ |
| \ |
| .if0_union = { \ |
| .bFunctionLength = sizeof(struct cdc_union_descriptor), \ |
| .bDescriptorType = USB_DESC_CS_INTERFACE, \ |
| .bDescriptorSubtype = UNION_FUNC_DESC, \ |
| .bControlInterface = 0, \ |
| .bSubordinateInterface0 = 1, \ |
| }, \ |
| \ |
| .if0_int_ep = { \ |
| .bLength = sizeof(struct usb_ep_descriptor), \ |
| .bDescriptorType = USB_DESC_ENDPOINT, \ |
| .bEndpointAddress = 0x81, \ |
| .bmAttributes = USB_EP_TYPE_INTERRUPT, \ |
| .wMaxPacketSize = sys_cpu_to_le16(CDC_ACM_DEFAULT_INT_EP_MPS), \ |
| .bInterval = CDC_ACM_DEFAULT_INT_INTERVAL, \ |
| }, \ |
| \ |
| .if1 = { \ |
| .bLength = sizeof(struct usb_if_descriptor), \ |
| .bDescriptorType = USB_DESC_INTERFACE, \ |
| .bInterfaceNumber = 1, \ |
| .bAlternateSetting = 0, \ |
| .bNumEndpoints = 2, \ |
| .bInterfaceClass = USB_BCC_CDC_DATA, \ |
| .bInterfaceSubClass = 0, \ |
| .bInterfaceProtocol = 0, \ |
| .iInterface = 0, \ |
| }, \ |
| \ |
| .if1_in_ep = { \ |
| .bLength = sizeof(struct usb_ep_descriptor), \ |
| .bDescriptorType = USB_DESC_ENDPOINT, \ |
| .bEndpointAddress = 0x82, \ |
| .bmAttributes = USB_EP_TYPE_BULK, \ |
| .wMaxPacketSize = sys_cpu_to_le16(CDC_ACM_DEFAULT_BULK_EP_MPS), \ |
| .bInterval = 0, \ |
| }, \ |
| \ |
| .if1_out_ep = { \ |
| .bLength = sizeof(struct usb_ep_descriptor), \ |
| .bDescriptorType = USB_DESC_ENDPOINT, \ |
| .bEndpointAddress = 0x01, \ |
| .bmAttributes = USB_EP_TYPE_BULK, \ |
| .wMaxPacketSize = sys_cpu_to_le16(CDC_ACM_DEFAULT_BULK_EP_MPS), \ |
| .bInterval = 0, \ |
| }, \ |
| \ |
| .nil_desc = { \ |
| .bLength = 0, \ |
| .bDescriptorType = 0, \ |
| }, \ |
| } |
| |
| #define DT_DRV_COMPAT zephyr_cdc_acm_uart |
| |
| #define USBD_CDC_ACM_DT_DEVICE_DEFINE(n) \ |
| BUILD_ASSERT(DT_INST_ON_BUS(n, usb), \ |
| "node " DT_NODE_PATH(DT_DRV_INST(n)) \ |
| " is not assigned to a USB device controller"); \ |
| \ |
| CDC_ACM_DEFINE_DESCRIPTOR(n); \ |
| \ |
| static struct usbd_class_data usbd_cdc_acm_data_##n; \ |
| \ |
| USBD_DEFINE_CLASS(cdc_acm_##n, \ |
| &usbd_cdc_acm_api, \ |
| &usbd_cdc_acm_data_##n); \ |
| \ |
| RING_BUF_DECLARE(cdc_acm_rb_rx_##n, DT_INST_PROP(n, tx_fifo_size)); \ |
| RING_BUF_DECLARE(cdc_acm_rb_tx_##n, DT_INST_PROP(n, tx_fifo_size)); \ |
| \ |
| static struct cdc_acm_uart_data uart_data_##n = { \ |
| .line_coding = CDC_ACM_DEFAULT_LINECODING, \ |
| .c_nd = &cdc_acm_##n, \ |
| .rx_fifo.rb = &cdc_acm_rb_rx_##n, \ |
| .tx_fifo.rb = &cdc_acm_rb_tx_##n, \ |
| .notif_sem = Z_SEM_INITIALIZER(uart_data_##n.notif_sem, 0, 1), \ |
| }; \ |
| \ |
| static struct usbd_class_data usbd_cdc_acm_data_##n = { \ |
| .desc = (struct usb_desc_header *)&cdc_acm_desc_##n, \ |
| .priv = (void *)DEVICE_DT_GET(DT_DRV_INST(n)), \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, usbd_cdc_acm_preinit, NULL, \ |
| &uart_data_##n, NULL, \ |
| PRE_KERNEL_1, CONFIG_SERIAL_INIT_PRIORITY, \ |
| &cdc_acm_uart_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(USBD_CDC_ACM_DT_DEVICE_DEFINE); |
| |
| SYS_INIT(usbd_cdc_acm_init_wq, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); |