Summary
When passing explicit descriptors (device, full_speed_config, string) via tinyusb_config_t.descriptor to tinyusb_driver_install() for an NCM-only target, the chassis-side state confirms the pointers correctly reach s_desc_cfg (verified by tinyusb_descriptors_set's own boot-summary log), yet the host receives esp_tinyusb's default descriptors (idProduct=0x4001, "Espressif Device", "Espressif CDC Device", CDC-ACM subclass=2) on the wire.
The bug appears specific to this device + full_speed_config + string-all-non-NULL path with CONFIG_TINYUSB_CDC_ENABLED=n. Reproduces on both Linux (6.12) and macOS hosts.
Environment
- ESP-IDF: v6.0
- esp_tinyusb: pinned
^2.2.0 (resolved by IDF Component Manager)
- Target: ESP32-S3 (native USB-OTG)
- Hosts tested: Linux 6.12 (Debian Trixie,
cdc_ncm module available) + macOS Big Sur+ — both see defaults
sdkconfig.defaults:
CONFIG_TINYUSB_NET_MODE_NCM=y
CONFIG_TINYUSB_CDC_ENABLED=n
No unknown kconfig symbol warnings during build — both keys recognized in 2.2.0.
Minimal reproducer
#include \"tinyusb.h\"
#include \"tinyusb_default_config.h\" /* TINYUSB_TASK_DEFAULT() */
#include \"tusb.h\"
#include \"class/net/net_device.h\" /* CFG_TUD_NET_MTU */
enum { STRID_LANGID, STRID_MFG, STRID_PROD, STRID_SER, STRID_IF, STRID_MAC };
enum { ITF_NUM_CDC, ITF_NUM_CDC_DATA, ITF_NUM_TOTAL };
#define EPNUM_NOTIF 0x81
#define EPNUM_OUT 0x02
#define EPNUM_IN 0x82
#define NCM_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_NCM_DESC_LEN)
static const tusb_desc_device_t my_device_desc = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0201,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A,
.idProduct = 0x4022, /* unique magic to distinguish our descriptor from default 0x4001 */
.bcdDevice = 0x0100,
.iManufacturer = STRID_MFG,
.iProduct = STRID_PROD,
.iSerialNumber = STRID_SER,
.bNumConfigurations = 1,
};
static const uint8_t my_ncm_config_desc[] = {
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, NCM_TOTAL_LEN, 0, 100),
/* 9-arg form (no trailing bInterval) — what 2.2.0's TinyUSB fork accepts */
TUD_CDC_NCM_DESCRIPTOR(ITF_NUM_CDC, STRID_IF, STRID_MAC,
EPNUM_NOTIF, 64,
EPNUM_IN, EPNUM_OUT,
64, CFG_TUD_NET_MTU),
};
static char mac_str[13] = \"020000000001\";
static const char *string_desc[] = {
(const char[]) {0x09, 0x04}, /* 0: LANGID English (US) */
\"MyVendor\", /* 1: Manufacturer */
\"MyProduct (NCM)\", /* 2: Product */
\"0001\", /* 3: Serial */
\"MyNCMInterface\", /* 4: Interface */
mac_str, /* 5: MAC */
};
void app_main(void)
{
/* (NVS / esp_netif / WiFi-STA setup omitted for brevity) */
const tinyusb_config_t tusb_cfg = {
.port = TINYUSB_PORT_FULL_SPEED_0,
.phy = { .skip_setup = false, .self_powered = false, .vbus_monitor_io = -1 },
.task = TINYUSB_TASK_DEFAULT(),
.descriptor = {
.device = &my_device_desc,
.qualifier = NULL,
.string = string_desc,
.string_count = sizeof(string_desc) / sizeof(string_desc[0]),
.full_speed_config = my_ncm_config_desc,
.high_speed_config = NULL,
},
.event_cb = NULL,
.event_arg = NULL,
};
ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
/* Then tinyusb_net_init(&net_cfg) for NCM. */
}
idf_component.yml:
dependencies:
espressif/esp_tinyusb:
version: \"^2.2.0\"
idf: \">=5.0\"
Expected
Host enumerates with our descriptor: idProduct=0x4022, Product=\"MyProduct (NCM)\", bInterfaceClass=2 / bInterfaceSubClass=13 (CDC-NCM). Linux loads cdc_ncm and a usb0 virtual NIC appears.
Actual
Chassis-side: pointers correctly reach s_desc_cfg (no using default warnings)
tinyusb_descriptors_set in descriptors_control.c logs its summary table with our values:
I (545) tusb_desc:
┌─────────────────────────────────┐
│ USB Device Descriptor Summary │
├───────────────────┬─────────────┤
│bDeviceClass │ 239 │
│bDeviceSubClass │ 2 │
│bDeviceProtocol │ 1 │
│bMaxPacketSize0 │ 64 │
│idVendor │ 0x303a │
│idProduct │ 0x4022 │ ← OUR value (default would be 0x4001)
│bcdDevice │ 0x100 │
│iManufacturer │ 0x1 │
│iProduct │ 0x2 │
│iSerialNumber │ 0x3 │
│bNumConfigurations │ 0x1 │
└───────────────────┴─────────────┘
I (710) TinyUSB: TinyUSB Driver installed on port 0
No No Device descriptor provided, using default / No Full-speed configuration descriptor provided, using default / No String descriptors provided, using default warnings — all three of our pointers reach tinyusb_descriptors_set non-NULL and get assigned to s_desc_cfg.{dev, fs_cfg, str}.
Host-side: defaults on the wire (Linux `lsusb -v`, fresh enumeration)
After a true physical USB disconnect+reconnect (kernel logs usb 3-2: USB disconnect, device number 5 followed by usb 3-2: new full-speed USB device number 7 — confirming no caching):
Bus 003 Device 007: ID 303a:4001 Espressif Systems Espressif Device
Device Descriptor:
bcdUSB 2.00
bDeviceClass 239 Miscellaneous Device
bDeviceSubClass 2 [unknown]
bDeviceProtocol 1 Interface Association
idVendor 0x303a Espressif Systems
idProduct 0x4001 Espressif Device ← default, NOT 0x4022
iManufacturer 1 Espressif Systems
iProduct 2 Espressif Device ← default, NOT \"MyProduct (NCM)\"
iSerial 3 123456 ← default, NOT \"0001\"
Configuration Descriptor:
wTotalLength 0x004b ← 75 bytes (default ACM); ours would be ~94
Interface Descriptor:
bInterfaceClass 2 Communications
bInterfaceSubClass 2 Abstract (modem) ← CDC-ACM (default), NOT NCM (subclass 13)
iInterface 4 Espressif CDC Device ← default, NOT \"MyNCMInterface\"
Linux kernel binds cdc_acm 3-2:1.0: ttyACM2: USB ACM device. macOS binds AppleUSBACMControl similarly. No usb0 / enX virtual NIC enumerates on either OS.
What we ruled out
- Host-side USB caching: kernel saw a true disconnect with new device-number assignment;
lsusb -v reads raw GET_DEVICE_DESCRIPTOR + GET_CONFIGURATION_DESCRIPTOR bytes off the wire (no kernel cache).
- OS-specific bug: identical wrong descriptor on macOS Big Sur+ AND Linux 6.12.
- Kconfig silent-no-op:
CONFIG_TINYUSB_CDC_ENABLED=n is accepted in 2.2.0 (no unknown kconfig symbol warning during build), but the wire behavior is unchanged whether it's set or not.
- Stale chassis flash: rebuilt fresh + flashed via UART; chassis's own boot-summary log shows our
idProduct=0x4022 value being stored, proving our pointer was received.
- NULL pointer slipping in: no
using default warnings logged by tinyusb_descriptors_set, so all three of our pointers are received non-NULL.
Question
When CONFIG_TINYUSB_CDC_ENABLED=n and we provide explicit tinyusb_config_t.descriptor.{device, full_speed_config, string} pointers, where does esp_tinyusb actually source the bytes it serves to tud_descriptor_device_cb() / tud_descriptor_configuration_cb() / tud_descriptor_string_cb()?
descriptors_control.c::tud_descriptor_device_cb() returns (uint8_t const *)s_desc_cfg.dev, which (per the chassis-side summary log) holds our pointer. Yet lsusb -v shows defaults.
- Is there a runtime override path that bypasses
s_desc_cfg.{dev, fs_cfg, str} and serves descriptor_*_default directly?
- Is there a call-order or flag we're missing for the NCM-only-target case?
- Is the
device + full_speed_config + string-all-non-NULL path expected to work, or is it implicitly tied to CONFIG_TINYUSB_CDC_ENABLED=y?
Happy to provide additional traces (full boot log, agent build log with all warnings, dmesg from the Linux host) if helpful.
Summary
When passing explicit descriptors (
device,full_speed_config,string) viatinyusb_config_t.descriptortotinyusb_driver_install()for an NCM-only target, the chassis-side state confirms the pointers correctly reachs_desc_cfg(verified bytinyusb_descriptors_set's own boot-summary log), yet the host receives esp_tinyusb's default descriptors (idProduct=0x4001,"Espressif Device","Espressif CDC Device", CDC-ACM subclass=2) on the wire.The bug appears specific to this
device + full_speed_config + string-all-non-NULL path withCONFIG_TINYUSB_CDC_ENABLED=n. Reproduces on both Linux (6.12) and macOS hosts.Environment
^2.2.0(resolved by IDF Component Manager)cdc_ncmmodule available) + macOS Big Sur+ — both see defaultssdkconfig.defaults:unknown kconfig symbolwarnings during build — both keys recognized in 2.2.0.Minimal reproducer
idf_component.yml:Expected
Host enumerates with our descriptor:
idProduct=0x4022,Product=\"MyProduct (NCM)\",bInterfaceClass=2 / bInterfaceSubClass=13(CDC-NCM). Linux loadscdc_ncmand ausb0virtual NIC appears.Actual
Chassis-side: pointers correctly reach
s_desc_cfg(nousing defaultwarnings)tinyusb_descriptors_setindescriptors_control.clogs its summary table with our values:No
No Device descriptor provided, using default/No Full-speed configuration descriptor provided, using default/No String descriptors provided, using defaultwarnings — all three of our pointers reachtinyusb_descriptors_setnon-NULL and get assigned tos_desc_cfg.{dev, fs_cfg, str}.Host-side: defaults on the wire (Linux `lsusb -v`, fresh enumeration)
After a true physical USB disconnect+reconnect (kernel logs
usb 3-2: USB disconnect, device number 5followed byusb 3-2: new full-speed USB device number 7— confirming no caching):Linux kernel binds
cdc_acm 3-2:1.0: ttyACM2: USB ACM device. macOS bindsAppleUSBACMControlsimilarly. Nousb0/enXvirtual NIC enumerates on either OS.What we ruled out
lsusb -vreads raw GET_DEVICE_DESCRIPTOR + GET_CONFIGURATION_DESCRIPTOR bytes off the wire (no kernel cache).CONFIG_TINYUSB_CDC_ENABLED=nis accepted in 2.2.0 (nounknown kconfig symbolwarning during build), but the wire behavior is unchanged whether it's set or not.idProduct=0x4022value being stored, proving our pointer was received.using defaultwarnings logged bytinyusb_descriptors_set, so all three of our pointers are received non-NULL.Question
When
CONFIG_TINYUSB_CDC_ENABLED=nand we provide explicittinyusb_config_t.descriptor.{device, full_speed_config, string}pointers, where does esp_tinyusb actually source the bytes it serves totud_descriptor_device_cb()/tud_descriptor_configuration_cb()/tud_descriptor_string_cb()?descriptors_control.c::tud_descriptor_device_cb()returns(uint8_t const *)s_desc_cfg.dev, which (per the chassis-side summary log) holds our pointer. Yetlsusb -vshows defaults.s_desc_cfg.{dev, fs_cfg, str}and servesdescriptor_*_defaultdirectly?device + full_speed_config + string-all-non-NULL path expected to work, or is it implicitly tied toCONFIG_TINYUSB_CDC_ENABLED=y?Happy to provide additional traces (full boot log, agent build log with all warnings,
dmesgfrom the Linux host) if helpful.