Skip to content

esp_tinyusb 2.2.0: explicit tinyusb_config_t.descriptor.* pointers stored in s_desc_cfg but defaults served on the wire (IEC-537) #491

@ff4415

Description

@ff4415

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions