summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author54fire <[email protected]>2026-08-17 14:48:32 +0800
committerGitHub <[email protected]>2026-08-17 14:48:32 +0800
commit60911ffd9ed6aaacf768dfd557508d0b40170664 (patch)
tree7e38cdec71f07b3afc42d206585ba4809d522516
parent1bfc83ad46301553c8b626446417273b18abca6e (diff)
feat(class/cdc/usbd_ncm): 添加usb device ncm功能 (#433)
* 添加usb device ncm功能 * fix(class/cdc/usbd_ncm): address review findings - fix compile failure when CONFIG_USBDEV_CDC_NCM_USING_LWIP is disabled - fix GET/SET_NTB_INPUT_SIZE operating on the OUT direction size, and honor the negotiated IN limit in the transmit path - set tx busy flag before submitting the transfer to avoid a race that could block transmit permanently - follow the ndp chain via wNextNdpIndex so chained datagrams are not dropped, and access ntb fields with LE helpers instead of assuming a little-endian target - reject oversized frames instead of transmitting truncated ones - register endpoints only once when initializing both interfaces - wait for tx completion with a timeout in the demo instead of an unbounded busy wait - make usbd_cdc_ncm.h self-contained Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
-rw-r--r--Kconfig1
-rw-r--r--class/cdc/usb_cdc.h3
-rw-r--r--class/cdc/usbd_cdc_ncm.c545
-rw-r--r--class/cdc/usbd_cdc_ncm.h41
-rw-r--r--common/usb_util.h20
-rw-r--r--demo/cdc_ncm_template.c384
6 files changed, 993 insertions, 1 deletions
diff --git a/Kconfig b/Kconfig
index ad48c93e..807165be 100644
--- a/Kconfig
+++ b/Kconfig
@@ -122,7 +122,6 @@ if CHERRYUSB
config CHERRYUSB_DEVICE_CDC_NCM
bool
prompt "Enable usb cdc ncm device"
- depends on !IDF_CMAKE
default n
config CHERRYUSB_DEVICE_MTP
diff --git a/class/cdc/usb_cdc.h b/class/cdc/usb_cdc.h
index a21bc3b2..c80a107e 100644
--- a/class/cdc/usb_cdc.h
+++ b/class/cdc/usb_cdc.h
@@ -275,6 +275,9 @@
#define CDC_ECM_NOTIFY_CODE_RESPONSE_AVAILABLE 0x01
#define CDC_ECM_NOTIFY_CODE_CONNECTION_SPEED_CHANGE 0x2A
+#define CDC_NCM_CONNECT_SPEED_UPSTREAM 0x004C4B40U /* 5Mbps */
+#define CDC_NCM_CONNECT_SPEED_DOWNSTREAM 0x004C4B40U /* 5Mbps */
+
#define CDC_NCM_NTH16_SIGNATURE 0x484D434E
#define CDC_NCM_NDP16_SIGNATURE_NCM0 0x304D434E
#define CDC_NCM_NDP16_SIGNATURE_NCM1 0x314D434E
diff --git a/class/cdc/usbd_cdc_ncm.c b/class/cdc/usbd_cdc_ncm.c
new file mode 100644
index 00000000..68a272ee
--- /dev/null
+++ b/class/cdc/usbd_cdc_ncm.c
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2026, 54fire
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#include <string.h>
+#include "usbd_core.h"
+#include "usbd_cdc_ncm.h"
+
+#define CDC_NCM_OUT_EP_IDX 0
+#define CDC_NCM_IN_EP_IDX 1
+#define CDC_NCM_INT_EP_IDX 2
+
+#define CONFIG_CDC_NCM_ETH_MAX_SEGSZE 1514U
+#define CONFIG_CDC_NCM_NTB_MAX_SIZE 2048U
+#define CDC_NCM_NTH16_LEN 12U
+#define CDC_NCM_NDP16_LEN 16U
+#define CDC_NCM_DATAGRAM_OFFSET 16U
+
+static struct usbd_endpoint cdc_ncm_ep_data[3];
+
+#ifdef CONFIG_USBDEV_CDC_NCM_USING_LWIP
+static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t g_cdc_ncm_rx_buffer[USB_ALIGN_UP(CONFIG_CDC_NCM_NTB_MAX_SIZE, CONFIG_USB_ALIGN_SIZE)];
+static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t g_cdc_ncm_tx_buffer[USB_ALIGN_UP(CONFIG_CDC_NCM_NTB_MAX_SIZE, CONFIG_USB_ALIGN_SIZE)];
+#endif
+static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t g_cdc_ncm_ctrl_buf[USB_ALIGN_UP(32, CONFIG_USB_ALIGN_SIZE)];
+static USB_NOCACHE_RAM_SECTION USB_MEM_ALIGNX uint8_t g_cdc_ncm_notify_buf[USB_ALIGN_UP(16, CONFIG_USB_ALIGN_SIZE)];
+
+static volatile uint32_t g_cdc_ncm_rx_ntb_length = 0;
+static volatile uint32_t g_cdc_ncm_tx_ntb_length = 0;
+static volatile bool g_cdc_ncm_data_alt_active = false;
+static uint8_t *g_cdc_ncm_rx_data_buffer = NULL;
+static uint32_t g_cdc_ncm_rx_total_length = 0;
+static uint16_t g_cdc_ncm_rx_datagram_pos = 0;
+static uint16_t g_cdc_ncm_rx_ndp_index = 0;
+static uint16_t g_cdc_ncm_rx_sequence = 0;
+static uint16_t g_cdc_ncm_tx_sequence = 0;
+static uint16_t g_cdc_ncm_max_datagram_size = CONFIG_CDC_NCM_ETH_MAX_SEGSZE;
+static uint32_t g_cdc_ncm_ntb_in_max_size = CONFIG_CDC_NCM_NTB_MAX_SIZE;
+static uint32_t g_cdc_ncm_ntb_out_max_size = CONFIG_CDC_NCM_NTB_MAX_SIZE;
+static uint16_t g_cdc_ncm_ntb_format = 0;
+static uint16_t g_cdc_ncm_packet_filter = 0;
+static volatile uint8_t g_current_net_status = 0;
+static volatile uint8_t g_cmd_intf = 0;
+static uint8_t g_cdc_ncm_mac[6] = { 0 };
+static bool g_cdc_ncm_ep_registered = false;
+
+static uint32_t g_connect_speed_table[2] = { CDC_NCM_CONNECT_SPEED_UPSTREAM, CDC_NCM_CONNECT_SPEED_DOWNSTREAM };
+
+static void usbd_cdc_ncm_send_notify(uint8_t notifycode, uint8_t value, uint32_t *speed)
+{
+ struct cdc_eth_notification *notify = (struct cdc_eth_notification *)g_cdc_ncm_notify_buf;
+ uint8_t bytes2send = 0;
+
+ memset(g_cdc_ncm_notify_buf, 0, 16);
+ notify->bmRequestType = CDC_ECM_BMREQUEST_TYPE_ECM;
+ notify->bNotificationType = notifycode;
+ notify->wIndex = g_cmd_intf;
+
+ switch (notifycode) {
+ case CDC_ECM_NOTIFY_CODE_NETWORK_CONNECTION:
+ notify->wValue = value;
+ bytes2send = 8;
+ break;
+ case CDC_ECM_NOTIFY_CODE_RESPONSE_AVAILABLE:
+ bytes2send = 8;
+ break;
+ case CDC_ECM_NOTIFY_CODE_CONNECTION_SPEED_CHANGE:
+ notify->wLength = 8;
+ if (speed) {
+ memcpy(notify->data, speed, 8);
+ }
+ bytes2send = 16;
+ break;
+ default:
+ break;
+ }
+
+ if (usb_device_is_configured(0) && bytes2send) {
+ usbd_ep_start_write(0, cdc_ncm_ep_data[CDC_NCM_INT_EP_IDX].ep_addr, g_cdc_ncm_notify_buf, bytes2send);
+ }
+}
+
+static void usbd_cdc_ncm_fill_ntb_parameters(void)
+{
+ memset(g_cdc_ncm_ctrl_buf, 0, 32);
+ SET_LE16(&g_cdc_ncm_ctrl_buf[0], 28); /* wLength */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[2], 0x0001); /* NTB16 only */
+ SET_LE32(&g_cdc_ncm_ctrl_buf[4], g_cdc_ncm_ntb_in_max_size); /* device-to-host NTB */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[8], 4); /* wNdbInDivisor */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[10], 0); /* wNdbInPayloadRemainder */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[12], 4); /* wNdbInAlignment */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[14], 0); /* wReserved */
+ SET_LE32(&g_cdc_ncm_ctrl_buf[16], g_cdc_ncm_ntb_out_max_size);/* host-to-device NTB */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[20], 4); /* wNdbOutDivisor */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[22], 0); /* wNdbOutPayloadRemainder */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[24], 4); /* wNdbOutAlignment */
+ SET_LE16(&g_cdc_ncm_ctrl_buf[26], 1); /* one datagram preferred */
+}
+
+static int cdc_ncm_class_interface_request_handler(uint8_t busid, struct usb_setup_packet *setup, uint8_t **data, uint32_t *len)
+{
+ (void)busid;
+
+ g_cmd_intf = LO_BYTE(setup->wIndex);
+
+ switch (setup->bRequest) {
+ case CDC_REQUEST_GET_NTB_PARAMETERS:
+ usbd_cdc_ncm_fill_ntb_parameters();
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 28;
+ break;
+ case CDC_REQUEST_GET_NTB_FORMAT:
+ SET_LE16(g_cdc_ncm_ctrl_buf, g_cdc_ncm_ntb_format);
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 2;
+ break;
+ case CDC_REQUEST_SET_NTB_FORMAT:
+ if (setup->wValue != 0) {
+ return -1;
+ }
+ g_cdc_ncm_ntb_format = 0;
+ break;
+ case CDC_REQUEST_GET_NTB_INPUT_SIZE:
+ SET_LE32(g_cdc_ncm_ctrl_buf, g_cdc_ncm_ntb_in_max_size);
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 4;
+ break;
+ case CDC_REQUEST_SET_NTB_INPUT_SIZE:
+ if (setup->wLength >= 4 && data && *data) {
+ uint32_t ntb_size = GET_LE32(*data);
+ if (ntb_size >= (CDC_NCM_DATAGRAM_OFFSET + CDC_NCM_NDP16_LEN) && ntb_size <= CONFIG_CDC_NCM_NTB_MAX_SIZE) {
+ g_cdc_ncm_ntb_in_max_size = ntb_size;
+ }
+ }
+ break;
+ case CDC_REQUEST_GET_MAX_DATAGRAM_SIZE:
+ SET_LE16(g_cdc_ncm_ctrl_buf, g_cdc_ncm_max_datagram_size);
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 2;
+ break;
+ case CDC_REQUEST_SET_MAX_DATAGRAM_SIZE:
+ if (setup->wLength >= 2 && data && *data) {
+ uint16_t max_datagram_size = GET_LE16(*data);
+ if (max_datagram_size >= 64 && max_datagram_size <= CONFIG_CDC_NCM_ETH_MAX_SEGSZE) {
+ g_cdc_ncm_max_datagram_size = max_datagram_size;
+ }
+ } else if (setup->wValue >= 64 && setup->wValue <= CONFIG_CDC_NCM_ETH_MAX_SEGSZE) {
+ g_cdc_ncm_max_datagram_size = setup->wValue;
+ }
+ break;
+ case CDC_REQUEST_GET_CRC_MODE:
+ SET_LE16(g_cdc_ncm_ctrl_buf, 0);
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 2;
+ break;
+ case CDC_REQUEST_SET_CRC_MODE:
+ if (setup->wValue != 0) {
+ return -1;
+ }
+ break;
+ case CDC_REQUEST_GET_NET_ADDRESS:
+ memcpy(g_cdc_ncm_ctrl_buf, g_cdc_ncm_mac, 6);
+ *data = g_cdc_ncm_ctrl_buf;
+ *len = 6;
+ break;
+ case CDC_REQUEST_SET_NET_ADDRESS:
+ if (setup->wLength >= 6 && data && *data) {
+ memcpy(g_cdc_ncm_mac, *data, 6);
+ }
+ break;
+ case CDC_REQUEST_SET_ETHERNET_PACKET_FILTER:
+ g_cdc_ncm_packet_filter = setup->wValue;
+ g_connect_speed_table[0] = 100000000;
+ g_connect_speed_table[1] = 100000000;
+ usbd_cdc_ncm_set_connect(g_cdc_ncm_packet_filter != 0, g_connect_speed_table);
+ break;
+ case CDC_REQUEST_SET_ETHERNET_MULTICAST_FILTERS:
+ case CDC_REQUEST_SET_ETHERNET_PMP_FILTER:
+ break;
+ default:
+ USB_LOG_WRN("Unhandled CDC NCM Class bRequest 0x%02x\r\n", setup->bRequest);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void cdc_ncm_notify_handler(uint8_t busid, uint8_t event, void *arg)
+{
+ (void)busid;
+
+ switch (event) {
+ case USBD_EVENT_RESET:
+ case USBD_EVENT_DISCONNECTED:
+ g_current_net_status = 0;
+ g_cdc_ncm_rx_ntb_length = 0;
+ g_cdc_ncm_tx_ntb_length = 0;
+ g_cdc_ncm_data_alt_active = false;
+ g_cdc_ncm_rx_datagram_pos = 0;
+ g_cdc_ncm_rx_ndp_index = 0;
+ break;
+ case USBD_EVENT_SET_INTERFACE: {
+#ifdef CONFIG_USBDEV_CDC_NCM_USING_LWIP
+ struct usb_interface_descriptor *desc = (struct usb_interface_descriptor *)arg;
+
+ if (desc && desc->bAlternateSetting == 1) {
+ g_cdc_ncm_data_alt_active = true;
+ usbd_cdc_ncm_start_read(g_cdc_ncm_rx_buffer, g_cdc_ncm_ntb_out_max_size);
+ } else {
+ g_cdc_ncm_data_alt_active = false;
+ g_cdc_ncm_tx_ntb_length = 0;
+ }
+#else
+ (void)arg;
+#endif
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void cdc_ncm_bulk_out(uint8_t busid, uint8_t ep, uint32_t nbytes)
+{
+ (void)busid;
+ (void)ep;
+
+ g_cdc_ncm_rx_ntb_length = nbytes;
+ g_cdc_ncm_rx_datagram_pos = 0;
+ g_cdc_ncm_rx_ndp_index = 0;
+ usbd_cdc_ncm_data_recv_done(g_cdc_ncm_rx_ntb_length);
+}
+
+static void cdc_ncm_bulk_in(uint8_t busid, uint8_t ep, uint32_t nbytes)
+{
+ (void)busid;
+
+ if ((nbytes % usbd_get_ep_mps(0, ep)) == 0 && nbytes) {
+ usbd_ep_start_write(0, ep, NULL, 0);
+ } else {
+ usbd_cdc_ncm_data_send_done(g_cdc_ncm_tx_ntb_length);
+ g_cdc_ncm_tx_ntb_length = 0;
+ }
+}
+
+static void cdc_ncm_int_in(uint8_t busid, uint8_t ep, uint32_t nbytes)
+{
+ (void)busid;
+ (void)ep;
+ (void)nbytes;
+
+ if (g_current_net_status == 2) {
+ g_current_net_status = 3;
+ usbd_cdc_ncm_send_notify(CDC_ECM_NOTIFY_CODE_CONNECTION_SPEED_CHANGE, 0, g_connect_speed_table);
+ } else {
+ g_current_net_status = 0;
+ }
+}
+
+int usbd_cdc_ncm_start_write(uint8_t *buf, uint32_t len)
+{
+ int ret;
+
+ if (!usb_device_is_configured(0) || !g_cdc_ncm_data_alt_active) {
+ return -USB_ERR_NOTCONN;
+ }
+
+ if (g_cdc_ncm_tx_ntb_length > 0) {
+ return -USB_ERR_BUSY;
+ }
+
+ /* mark busy before submitting: the transfer may complete in interrupt
+ context before usbd_ep_start_write() returns */
+ g_cdc_ncm_tx_ntb_length = len;
+ ret = usbd_ep_start_write(0, cdc_ncm_ep_data[CDC_NCM_IN_EP_IDX].ep_addr, buf, len);
+ if (ret != 0) {
+ g_cdc_ncm_tx_ntb_length = 0;
+ return ret;
+ }
+
+ USB_LOG_DBG("txlen:%d\r\n", g_cdc_ncm_tx_ntb_length);
+ return 0;
+}
+
+int usbd_cdc_ncm_start_read(uint8_t *buf, uint32_t len)
+{
+ if (!usb_device_is_configured(0)) {
+ return -USB_ERR_NOTCONN;
+ }
+
+ g_cdc_ncm_rx_data_buffer = buf;
+ g_cdc_ncm_rx_total_length = len;
+ g_cdc_ncm_rx_ntb_length = 0;
+ g_cdc_ncm_rx_datagram_pos = 0;
+ g_cdc_ncm_rx_ndp_index = 0;
+ return usbd_ep_start_read(0, cdc_ncm_ep_data[CDC_NCM_OUT_EP_IDX].ep_addr, buf, len);
+}
+
+#ifdef CONFIG_USBDEV_CDC_NCM_USING_LWIP
+static bool cdc_ncm_check_ndp16(uint16_t ndp_index)
+{
+ struct cdc_ncm_ndp16 *ndp16;
+ uint32_t signature;
+ uint16_t ndp_len;
+
+ if (ndp_index == 0 || (uint32_t)ndp_index + sizeof(struct cdc_ncm_ndp16) > g_cdc_ncm_rx_ntb_length) {
+ return false;
+ }
+
+ ndp16 = (struct cdc_ncm_ndp16 *)&g_cdc_ncm_rx_data_buffer[ndp_index];
+ signature = GET_LE32((uint8_t *)&ndp16->dwSignature);
+ ndp_len = GET_LE16((uint8_t *)&ndp16->wLength);
+
+ if ((signature != CDC_NCM_NDP16_SIGNATURE_NCM0) &&
+ (signature != CDC_NCM_NDP16_SIGNATURE_NCM1)) {
+ USB_LOG_WRN("invalid ncm ndp16 signature\r\n");
+ return false;
+ }
+
+ if (ndp_len < 12 || (uint32_t)ndp_index + ndp_len > g_cdc_ncm_rx_ntb_length) {
+ USB_LOG_WRN("invalid ncm ndp16 len\r\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool cdc_ncm_prepare_rx_ndp(void)
+{
+ struct cdc_ncm_nth16 *nth16;
+ uint16_t ndp_index;
+
+ if (g_cdc_ncm_rx_ntb_length < (CDC_NCM_NTH16_LEN + CDC_NCM_NDP16_LEN)) {
+ return false;
+ }
+
+ nth16 = (struct cdc_ncm_nth16 *)g_cdc_ncm_rx_data_buffer;
+ ndp_index = GET_LE16((uint8_t *)&nth16->wNdpIndex);
+ if (GET_LE32((uint8_t *)&nth16->dwSignature) != CDC_NCM_NTH16_SIGNATURE ||
+ GET_LE16((uint8_t *)&nth16->wHeaderLength) != CDC_NCM_NTH16_LEN ||
+ (uint32_t)GET_LE16((uint8_t *)&nth16->wBlockLength) > g_cdc_ncm_rx_ntb_length ||
+ !cdc_ncm_check_ndp16(ndp_index)) {
+ USB_LOG_WRN("invalid ncm nth16\r\n");
+ return false;
+ }
+
+ g_cdc_ncm_rx_ndp_index = ndp_index;
+ g_cdc_ncm_rx_sequence = GET_LE16((uint8_t *)&nth16->wSequence);
+ (void)g_cdc_ncm_rx_sequence;
+ return true;
+}
+
+struct pbuf *usbd_cdc_ncm_eth_rx(void)
+{
+ struct cdc_ncm_ndp16 *ndp16;
+ uint16_t ndp_len;
+ uint16_t next_ndp_index;
+
+ if (g_cdc_ncm_rx_ntb_length == 0 || g_cdc_ncm_rx_data_buffer == NULL) {
+ return NULL;
+ }
+
+ if (g_cdc_ncm_rx_ndp_index == 0 && !cdc_ncm_prepare_rx_ndp()) {
+ usbd_cdc_ncm_start_read(g_cdc_ncm_rx_buffer, g_cdc_ncm_ntb_out_max_size);
+ return NULL;
+ }
+
+ for (;;) {
+ ndp16 = (struct cdc_ncm_ndp16 *)&g_cdc_ncm_rx_data_buffer[g_cdc_ncm_rx_ndp_index];
+ ndp_len = GET_LE16((uint8_t *)&ndp16->wLength);
+
+ while ((8 + 4 * (g_cdc_ncm_rx_datagram_pos + 1)) <= ndp_len) {
+ struct cdc_ncm_ndp16_datagram *datagram;
+ uint16_t index;
+ uint16_t length;
+ struct pbuf *p;
+
+ datagram = (struct cdc_ncm_ndp16_datagram *)&g_cdc_ncm_rx_data_buffer[g_cdc_ncm_rx_ndp_index + 8 + 4 * g_cdc_ncm_rx_datagram_pos];
+ g_cdc_ncm_rx_datagram_pos++;
+ index = GET_LE16((uint8_t *)&datagram->wDatagramIndex);
+ length = GET_LE16((uint8_t *)&datagram->wDatagramLength);
+
+ if (index == 0 || length == 0) {
+ break;
+ }
+ if ((uint32_t)index + length > g_cdc_ncm_rx_ntb_length || length > g_cdc_ncm_max_datagram_size) {
+ USB_LOG_WRN("invalid ncm datagram index:%u len:%u\r\n", index, length);
+ continue;
+ }
+
+ p = pbuf_alloc(PBUF_RAW, length, PBUF_POOL);
+ if (p == NULL) {
+ USB_LOG_WRN("ncm pbuf alloc failed\r\n");
+ usbd_cdc_ncm_start_read(g_cdc_ncm_rx_buffer, g_cdc_ncm_ntb_out_max_size);
+ return NULL;
+ }
+ if (pbuf_take(p, &g_cdc_ncm_rx_data_buffer[index], length) != ERR_OK) {
+ pbuf_free(p);
+ continue;
+ }
+
+ USB_LOG_DBG("rxlen:%d\r\n", length);
+ return p;
+ }
+
+ /* current ndp is exhausted, follow the ndp chain if present */
+ next_ndp_index = GET_LE16((uint8_t *)&ndp16->wNextNdpIndex);
+ if (next_ndp_index == 0 || !cdc_ncm_check_ndp16(next_ndp_index)) {
+ break;
+ }
+ g_cdc_ncm_rx_ndp_index = next_ndp_index;
+ g_cdc_ncm_rx_datagram_pos = 0;
+ }
+
+ usbd_cdc_ncm_start_read(g_cdc_ncm_rx_buffer, g_cdc_ncm_ntb_out_max_size);
+ return NULL;
+}
+
+int usbd_cdc_ncm_eth_tx(struct pbuf *p)
+{
+ struct cdc_ncm_nth16 *nth16;
+ struct cdc_ncm_ndp16 *ndp16;
+ struct cdc_ncm_ndp16_datagram *datagram;
+ struct pbuf *q;
+ uint8_t *buffer;
+ uint16_t payload_len;
+ uint16_t aligned_payload_len;
+ uint16_t ndp_index;
+ uint16_t block_len;
+
+ if (!usb_device_is_configured(0)) {
+ return -USB_ERR_NOTCONN;
+ }
+
+ if (g_cdc_ncm_tx_ntb_length > 0) {
+ return -USB_ERR_BUSY;
+ }
+
+ if (p->tot_len > g_cdc_ncm_max_datagram_size) {
+ USB_LOG_WRN("ncm tx frame too large:%u\r\n", p->tot_len);
+ return -USB_ERR_INVAL;
+ }
+
+ payload_len = p->tot_len;
+ aligned_payload_len = USB_ALIGN_UP(payload_len, 4);
+ ndp_index = CDC_NCM_DATAGRAM_OFFSET + aligned_payload_len;
+ block_len = ndp_index + CDC_NCM_NDP16_LEN;
+ if (block_len > sizeof(g_cdc_ncm_tx_buffer) || block_len > g_cdc_ncm_ntb_in_max_size) {
+ return -USB_ERR_INVAL;
+ }
+
+ memset(g_cdc_ncm_tx_buffer, 0, block_len);
+ buffer = &g_cdc_ncm_tx_buffer[CDC_NCM_DATAGRAM_OFFSET];
+ for (q = p; q != NULL && payload_len > 0; q = q->next) {
+ uint16_t copy_len = MIN(q->len, payload_len);
+ usb_memcpy(buffer, q->payload, copy_len);
+ buffer += copy_len;
+ payload_len -= copy_len;
+ }
+ payload_len = p->tot_len;
+
+ nth16 = (struct cdc_ncm_nth16 *)&g_cdc_ncm_tx_buffer[0];
+ SET_LE32((uint8_t *)&nth16->dwSignature, CDC_NCM_NTH16_SIGNATURE);
+ SET_LE16((uint8_t *)&nth16->wHeaderLength, CDC_NCM_NTH16_LEN);
+ SET_LE16((uint8_t *)&nth16->wSequence, g_cdc_ncm_tx_sequence++);
+ SET_LE16((uint8_t *)&nth16->wBlockLength, block_len);
+ SET_LE16((uint8_t *)&nth16->wNdpIndex, ndp_index);
+
+ ndp16 = (struct cdc_ncm_ndp16 *)&g_cdc_ncm_tx_buffer[ndp_index];
+ SET_LE32((uint8_t *)&ndp16->dwSignature, CDC_NCM_NDP16_SIGNATURE_NCM0);
+ SET_LE16((uint8_t *)&ndp16->wLength, CDC_NCM_NDP16_LEN);
+ SET_LE16((uint8_t *)&ndp16->wNextNdpIndex, 0);
+
+ datagram = (struct cdc_ncm_ndp16_datagram *)&g_cdc_ncm_tx_buffer[ndp_index + 8];
+ SET_LE16((uint8_t *)&datagram[0].wDatagramIndex, CDC_NCM_DATAGRAM_OFFSET);
+ SET_LE16((uint8_t *)&datagram[0].wDatagramLength, payload_len);
+ datagram[1].wDatagramIndex = 0;
+ datagram[1].wDatagramLength = 0;
+
+ return usbd_cdc_ncm_start_write(g_cdc_ncm_tx_buffer, block_len);
+}
+#endif
+
+struct usbd_interface *usbd_cdc_ncm_init_intf(struct usbd_interface *intf,
+ const uint8_t int_ep,
+ const uint8_t out_ep,
+ const uint8_t in_ep,
+ uint8_t mac[6])
+{
+ memcpy(g_cdc_ncm_mac, mac, 6);
+
+ intf->class_interface_handler = cdc_ncm_class_interface_request_handler;
+ intf->class_endpoint_handler = NULL;
+ intf->vendor_handler = NULL;
+ intf->notify_handler = cdc_ncm_notify_handler;
+
+ /* control and data interfaces share the same endpoints, register them only once */
+ if (!g_cdc_ncm_ep_registered) {
+ g_cdc_ncm_ep_registered = true;
+
+ cdc_ncm_ep_data[CDC_NCM_OUT_EP_IDX].ep_addr = out_ep;
+ cdc_ncm_ep_data[CDC_NCM_OUT_EP_IDX].ep_cb = cdc_ncm_bulk_out;
+ cdc_ncm_ep_data[CDC_NCM_IN_EP_IDX].ep_addr = in_ep;
+ cdc_ncm_ep_data[CDC_NCM_IN_EP_IDX].ep_cb = cdc_ncm_bulk_in;
+ cdc_ncm_ep_data[CDC_NCM_INT_EP_IDX].ep_addr = int_ep;
+ cdc_ncm_ep_data[CDC_NCM_INT_EP_IDX].ep_cb = cdc_ncm_int_in;
+
+ usbd_add_endpoint(0, &cdc_ncm_ep_data[CDC_NCM_OUT_EP_IDX]);
+ usbd_add_endpoint(0, &cdc_ncm_ep_data[CDC_NCM_IN_EP_IDX]);
+ usbd_add_endpoint(0, &cdc_ncm_ep_data[CDC_NCM_INT_EP_IDX]);
+ }
+
+ return intf;
+}
+
+int usbd_cdc_ncm_set_connect(bool connect, uint32_t speed[2])
+{
+ if (!usb_device_is_configured(0)) {
+ return -USB_ERR_NOTCONN;
+ }
+
+ if (connect) {
+ g_current_net_status = 2;
+ if (speed) {
+ memcpy(g_connect_speed_table, speed, 8);
+ }
+ usbd_cdc_ncm_send_notify(CDC_ECM_NOTIFY_CODE_NETWORK_CONNECTION, CDC_ECM_NET_CONNECTED, NULL);
+ } else {
+ g_current_net_status = 1;
+ usbd_cdc_ncm_send_notify(CDC_ECM_NOTIFY_CODE_NETWORK_CONNECTION, CDC_ECM_NET_DISCONNECTED, NULL);
+ }
+
+ return 0;
+}
+
+__WEAK void usbd_cdc_ncm_data_recv_done(uint32_t len)
+{
+ (void)len;
+}
+
+__WEAK void usbd_cdc_ncm_data_send_done(uint32_t len)
+{
+ (void)len;
+}
diff --git a/class/cdc/usbd_cdc_ncm.h b/class/cdc/usbd_cdc_ncm.h
new file mode 100644
index 00000000..7a232c32
--- /dev/null
+++ b/class/cdc/usbd_cdc_ncm.h
@@ -0,0 +1,41 @@
+/*
+ * CherryUSB CDC NCM device class glue
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#ifndef USBD_CDC_NCM_H
+#define USBD_CDC_NCM_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "usb_cdc.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct usbd_interface *usbd_cdc_ncm_init_intf(struct usbd_interface *intf,
+ const uint8_t int_ep,
+ const uint8_t out_ep,
+ const uint8_t in_ep,
+ uint8_t mac[6]);
+
+int usbd_cdc_ncm_set_connect(bool connect, uint32_t speed[2]);
+
+void usbd_cdc_ncm_data_recv_done(uint32_t len);
+void usbd_cdc_ncm_data_send_done(uint32_t len);
+int usbd_cdc_ncm_start_write(uint8_t *buf, uint32_t len);
+int usbd_cdc_ncm_start_read(uint8_t *buf, uint32_t len);
+
+#ifdef CONFIG_USBDEV_CDC_NCM_USING_LWIP
+#include "lwip/netif.h"
+#include "lwip/pbuf.h"
+struct pbuf *usbd_cdc_ncm_eth_rx(void);
+int usbd_cdc_ncm_eth_tx(struct pbuf *p);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* USBD_CDC_NCM_H */
diff --git a/common/usb_util.h b/common/usb_util.h
index 521bf27f..97e0917c 100644
--- a/common/usb_util.h
+++ b/common/usb_util.h
@@ -185,6 +185,26 @@
(field)[3] = (uint8_t)((value) >> 0); \
} while (0)
+#define GET_LE16(field) \
+ (((uint16_t)(field)[0] << 0) | ((uint16_t)(field)[1] << 8))
+
+#define GET_LE32(field) \
+ (((uint32_t)(field)[0] << 0) | ((uint32_t)(field)[1] << 8) | ((uint32_t)(field)[2] << 16) | ((uint32_t)(field)[3] << 24))
+
+#define SET_LE16(field, value) \
+ do { \
+ (field)[0] = (uint8_t)((value) >> 0); \
+ (field)[1] = (uint8_t)((value) >> 8); \
+ } while (0)
+
+#define SET_LE32(field, value) \
+ do { \
+ (field)[0] = (uint8_t)((value) >> 0); \
+ (field)[1] = (uint8_t)((value) >> 8); \
+ (field)[2] = (uint8_t)((value) >> 16); \
+ (field)[3] = (uint8_t)((value) >> 24); \
+ } while (0)
+
#define WBVAL(x) ((x) & 0xFF), (((x) >> 8) & 0xFF)
#define DBVAL(x) ((x) & 0xFF), (((x) >> 8) & 0xFF), (((x) >> 16) & 0xFF), (((x) >> 24) & 0xFF)
diff --git a/demo/cdc_ncm_template.c b/demo/cdc_ncm_template.c
new file mode 100644
index 00000000..17327716
--- /dev/null
+++ b/demo/cdc_ncm_template.c
@@ -0,0 +1,384 @@
+/*
+ * Copyright (c) 2026, 54fire
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#include "usbd_core.h"
+#include "usbd_cdc_ncm.h"
+
+#ifndef CONFIG_USBDEV_CDC_NCM_USING_LWIP
+#error "Please enable CONFIG_USBDEV_CDC_NCM_USING_LWIP for this demo"
+#endif
+
+/*!< endpoint address */
+#define CDC_IN_EP 0x81
+#define CDC_OUT_EP 0x02
+#define CDC_INT_EP 0x83
+
+#define USBD_VID 0xFFFF
+#define USBD_PID 0xFFFF
+#define USBD_MAX_POWER 100
+#define USBD_LANGID_STRING 1033
+
+/*!< config descriptor size */
+#define USB_CONFIG_SIZE (9 + CDC_NCM_DESCRIPTOR_LEN)
+
+#ifdef CONFIG_USB_HS
+#define CDC_MAX_MPS 512
+#else
+#define CDC_MAX_MPS 64
+#endif
+
+/* str idx = 4 is for mac address: aa:bb:cc:dd:ee:ff*/
+#define CDC_NCM_MAC_STRING_INDEX 4
+
+static const uint8_t device_descriptor[] = {
+ USB_DEVICE_DESCRIPTOR_INIT(USB_2_0, 0xEF, 0x02, 0x01, USBD_VID, USBD_PID, 0x0100, 0x01)
+};
+
+static const uint8_t config_descriptor[] = {
+ USB_CONFIG_DESCRIPTOR_INIT(USB_CONFIG_SIZE, 0x02, 0x01, USB_CONFIG_BUS_POWERED, USBD_MAX_POWER),
+ CDC_NCM_DESCRIPTOR_INIT(0x00, CDC_INT_EP, CDC_OUT_EP, CDC_IN_EP, CDC_MAX_MPS, CDC_NCM_MAC_STRING_INDEX)
+};
+
+static const uint8_t device_quality_descriptor[] = {
+ ///////////////////////////////////////
+ /// device qualifier descriptor
+ ///////////////////////////////////////
+ 0x0a,
+ USB_DESCRIPTOR_TYPE_DEVICE_QUALIFIER,
+ 0x00,
+ 0x02,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x40,
+ 0x00,
+ 0x00,
+};
+
+static const char *string_descriptors[] = {
+ (const char[]){ 0x09, 0x04 }, /* Langid */
+ "CherryUSB", /* Manufacturer */
+ "CherryUSB CDC NCM DEMO", /* Product */
+ "2026123456", /* Serial Number */
+ "aabbccddeeff", /* ncm mac address */
+};
+
+static const uint8_t *device_descriptor_callback(uint8_t speed)
+{
+ return device_descriptor;
+}
+
+static const uint8_t *config_descriptor_callback(uint8_t speed)
+{
+ return config_descriptor;
+}
+
+static const uint8_t *device_quality_descriptor_callback(uint8_t speed)
+{
+ return device_quality_descriptor;
+}
+
+static const char *string_descriptor_callback(uint8_t speed, uint8_t index)
+{
+ if (index >= (sizeof(string_descriptors) / sizeof(char *))) {
+ return NULL;
+ }
+ return string_descriptors[index];
+}
+
+const struct usb_descriptor cdc_ncm_descriptor = {
+ .device_descriptor_callback = device_descriptor_callback,
+ .config_descriptor_callback = config_descriptor_callback,
+ .device_quality_descriptor_callback = device_quality_descriptor_callback,
+ .string_descriptor_callback = string_descriptor_callback
+};
+
+uint8_t mac[6] = { 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
+
+#ifdef RT_USING_LWIP
+
+#ifndef RT_LWIP_DHCP
+#error cdc_ncm must enable RT_LWIP_DHCP
+#endif
+
+#ifndef LWIP_USING_DHCPD
+#error cdc_ncm must enable LWIP_USING_DHCPD
+#endif
+
+#include <rtthread.h>
+#include <rtdevice.h>
+#include <netif/ethernetif.h>
+#include <dhcp_server.h>
+
+#define CDC_NCM_TX_TIMEOUT_MS 1000
+
+struct eth_device cdc_ncm_dev;
+static struct rt_semaphore cdc_ncm_tx_sem;
+
+void usbd_cdc_ncm_data_send_done(uint32_t len)
+{
+ (void)len;
+ rt_sem_release(&cdc_ncm_tx_sem);
+}
+
+static rt_err_t rt_usbd_cdc_ncm_control(rt_device_t dev, int cmd, void *args)
+{
+ switch (cmd) {
+ case NIOCTL_GADDR:
+
+ /* get mac address */
+ if (args) {
+ uint8_t *mac_dev = (uint8_t *)args;
+ rt_memcpy(mac_dev, mac, 6);
+ mac_dev[5] = ~mac_dev[5]; /* device mac can't same as host. */
+ } else
+ return -RT_ERROR;
+
+ break;
+
+ default:
+ break;
+ }
+
+ return RT_EOK;
+}
+
+struct pbuf *rt_usbd_cdc_ncm_eth_rx(rt_device_t dev)
+{
+ return usbd_cdc_ncm_eth_rx();
+}
+
+rt_err_t rt_usbd_cdc_ncm_eth_tx(rt_device_t dev, struct pbuf *p)
+{
+ int ret;
+
+ ret = usbd_cdc_ncm_eth_tx(p);
+ if (ret == 0) {
+ if (rt_sem_take(&cdc_ncm_tx_sem, rt_tick_from_millisecond(CDC_NCM_TX_TIMEOUT_MS)) != RT_EOK) {
+ return -RT_ETIMEOUT;
+ }
+ return RT_EOK;
+ } else
+ return -RT_ERROR;
+}
+
+void cdc_ncm_lwip_init(void)
+{
+ rt_sem_init(&cdc_ncm_tx_sem, "ncm_tx", 0, RT_IPC_FLAG_PRIO);
+
+ cdc_ncm_dev.parent.control = rt_usbd_cdc_ncm_control;
+ cdc_ncm_dev.eth_rx = rt_usbd_cdc_ncm_eth_rx;
+ cdc_ncm_dev.eth_tx = rt_usbd_cdc_ncm_eth_tx;
+
+ eth_device_init(&cdc_ncm_dev, "u0");
+
+ eth_device_linkchange(&cdc_ncm_dev, RT_TRUE);
+ dhcpd_start("u0");
+}
+
+void usbd_cdc_ncm_data_recv_done(uint32_t len)
+{
+ eth_device_ready(&cdc_ncm_dev);
+}
+
+#else
+#include "netif/etharp.h"
+#include "lwip/init.h"
+#include "lwip/netif.h"
+#include "lwip/pbuf.h"
+
+#include "dhserver.h"
+#include "dnserver.h"
+
+/* rough bounded wait for the polled demo; in a real application use an os semaphore with timeout */
+#define CDC_NCM_TX_TIMEOUT_LOOPS 1000000UL
+
+volatile bool cdc_ncm_tx_done = false;
+
+void usbd_cdc_ncm_data_send_done(uint32_t len)
+{
+ (void)len;
+ cdc_ncm_tx_done = true;
+}
+
+/*Static IP ADDRESS: IP_ADDR0.IP_ADDR1.IP_ADDR2.IP_ADDR3 */
+#define IP_ADDR0 (uint8_t)192
+#define IP_ADDR1 (uint8_t)168
+#define IP_ADDR2 (uint8_t)7
+#define IP_ADDR3 (uint8_t)1
+
+/*NETMASK*/
+#define NETMASK_ADDR0 (uint8_t)255
+#define NETMASK_ADDR1 (uint8_t)255
+#define NETMASK_ADDR2 (uint8_t)255
+#define NETMASK_ADDR3 (uint8_t)0
+
+/*Gateway Address*/
+#define GW_ADDR0 (uint8_t)0
+#define GW_ADDR1 (uint8_t)0
+#define GW_ADDR2 (uint8_t)0
+#define GW_ADDR3 (uint8_t)0
+
+const ip_addr_t ipaddr = IPADDR4_INIT_BYTES(IP_ADDR0, IP_ADDR1, IP_ADDR2, IP_ADDR3);
+const ip_addr_t netmask = IPADDR4_INIT_BYTES(NETMASK_ADDR0, NETMASK_ADDR1, NETMASK_ADDR2, NETMASK_ADDR3);
+const ip_addr_t gateway = IPADDR4_INIT_BYTES(GW_ADDR0, GW_ADDR1, GW_ADDR2, GW_ADDR3);
+
+#define NUM_DHCP_ENTRY 3
+
+static dhcp_entry_t entries[NUM_DHCP_ENTRY] = {
+ /* mac ip address subnet mask lease time */
+ { { 0 }, { 192, 168, 7, 2 }, { 255, 255, 255, 0 }, 24 * 60 * 60 },
+ { { 0 }, { 192, 168, 7, 3 }, { 255, 255, 255, 0 }, 24 * 60 * 60 },
+ { { 0 }, { 192, 168, 7, 4 }, { 255, 255, 255, 0 }, 24 * 60 * 60 }
+};
+
+static dhcp_config_t dhcp_config = {
+ { 192, 168, 7, 1 }, /* server address */
+ 67, /* port */
+ { 192, 168, 7, 1 }, /* dns server */
+ "cherry", /* dns suffix */
+ NUM_DHCP_ENTRY, /* num entry */
+ entries /* entries */
+};
+
+static bool dns_query_proc(const char *name, ip_addr_t *addr)
+{
+ if (strcmp(name, "cdc_ncm.cherry") == 0 || strcmp(name, "www.cdc_ncm.cherry") == 0) {
+ addr->addr = ipaddr.addr;
+ return true;
+ }
+ return false;
+}
+
+static struct netif cdc_ncm_netif; //network interface
+
+/* Network interface name */
+#define IFNAME0 'N'
+#define IFNAME1 'X'
+
+err_t linkoutput_fn(struct netif *netif, struct pbuf *p)
+{
+ int ret;
+ uint32_t timeout = CDC_NCM_TX_TIMEOUT_LOOPS;
+
+ cdc_ncm_tx_done = false;
+ ret = usbd_cdc_ncm_eth_tx(p);
+ if (ret == 0) {
+ while (!cdc_ncm_tx_done) {
+ if (--timeout == 0) {
+ return ERR_TIMEOUT;
+ }
+ }
+ return ERR_OK;
+ } else
+ return ERR_BUF;
+}
+
+err_t cdc_ncm_if_init(struct netif *netif)
+{
+ LWIP_ASSERT("netif != NULL", (netif != NULL));
+
+ netif->mtu = 1500;
+ netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP | NETIF_FLAG_UP;
+ netif->state = NULL;
+ netif->name[0] = IFNAME0;
+ netif->name[1] = IFNAME1;
+ netif->output = etharp_output;
+ netif->linkoutput = linkoutput_fn;
+ return ERR_OK;
+}
+
+err_t cdc_ncm_if_input(struct netif *netif)
+{
+ err_t err;
+ struct pbuf *p;
+
+ p = usbd_cdc_ncm_eth_rx();
+ if (p != NULL) {
+ err = netif->input(p, netif);
+ if (err != ERR_OK) {
+ pbuf_free(p);
+ }
+ } else {
+ return ERR_BUF;
+ }
+ return err;
+}
+
+void cdc_ncm_lwip_init(void)
+{
+ struct netif *netif = &cdc_ncm_netif;
+
+ lwip_init();
+
+ netif->hwaddr_len = 6;
+ memcpy(netif->hwaddr, mac, 6);
+ netif->hwaddr[5] = ~netif->hwaddr[5]; /* device mac can't same as host. */
+
+ netif = netif_add(netif, &ipaddr, &netmask, &gateway, NULL, cdc_ncm_if_init, netif_input);
+ netif_set_default(netif);
+ while (!netif_is_up(netif)) {
+ }
+
+ while (dhserv_init(&dhcp_config)) {
+ }
+
+ while (dnserv_init(IP_ADDR_ANY, 53, dns_query_proc)) {
+ }
+}
+
+void usbd_cdc_ncm_data_recv_done(uint32_t len)
+{
+}
+
+void cdc_ncm_input_poll(void)
+{
+ cdc_ncm_if_input(&cdc_ncm_netif);
+}
+#endif
+
+static void usbd_event_handler(uint8_t busid, uint8_t event)
+{
+ switch (event) {
+ case USBD_EVENT_RESET:
+ break;
+ case USBD_EVENT_CONNECTED:
+ break;
+ case USBD_EVENT_DISCONNECTED:
+ break;
+ case USBD_EVENT_RESUME:
+ break;
+ case USBD_EVENT_SUSPEND:
+ break;
+ case USBD_EVENT_CONFIGURED:
+ break;
+ case USBD_EVENT_SET_REMOTE_WAKEUP:
+ break;
+ case USBD_EVENT_CLR_REMOTE_WAKEUP:
+ break;
+
+ default:
+ break;
+ }
+}
+
+struct usbd_interface intf0;
+struct usbd_interface intf1;
+
+/* ncm is supported in linux, windows and macos. In linux, you should input the following command
+ *
+ * sudo ifconfig enxaabbccddeeff up
+ * sudo dhclient enxaabbccddeeff
+*/
+void cdc_ncm_init(uint8_t busid, uintptr_t reg_base)
+{
+ cdc_ncm_lwip_init();
+
+ usbd_desc_register(busid, &cdc_ncm_descriptor);
+
+ usbd_add_interface(busid, usbd_cdc_ncm_init_intf(&intf0, CDC_INT_EP, CDC_OUT_EP, CDC_IN_EP, mac));
+ usbd_add_interface(busid, usbd_cdc_ncm_init_intf(&intf1, CDC_INT_EP, CDC_OUT_EP, CDC_IN_EP, mac));
+ usbd_initialize(busid, reg_base, usbd_event_handler);
+}