Skip to content

Commit 031ecba

Browse files
committed
extmod/machine_usb_device: Add dynamic VID/PID configuration support.
This commit adds VID/PID (Vendor ID/Product ID) configuration support to the USB device implementation, allowing both runtime and build-time customization of USB device identification. Features include runtime VID/PID properties (usb.vid, usb.pid), build-time VID/PID override macros, dynamic descriptor patching, and automatic VID/PID reset on USB deactivation. The implementation uses a fallback system where custom values override runtime defaults, which override compile-time defaults. Also fixes USB disconnect/connect in static mode by adding tud_disconnect() and tud_connect() calls to properly handle device activation and deactivation, enabling runtime VID/PID changes to take effect. Signed-off-by: Andrew Leech <[email protected]>
1 parent fe06a1c commit 031ecba

File tree

4 files changed

+218
-28
lines changed

4 files changed

+218
-28
lines changed

docs/library/machine.USBDevice.rst

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class USBDevice -- USB Device driver
1010

1111
USBDevice provides a low-level Python API for implementing USB device functions using
1212
Python code. This includes both runtime device descriptor configuration and
13-
build-time customization of USB configuration.
13+
build-time customization of USB Vendor ID (VID) and Product ID (PID).
1414

1515
.. warning:: This low-level API assumes familiarity with the USB standard. There
1616
are high-level `usb driver modules in micropython-lib`_ which provide a
@@ -412,5 +412,134 @@ Usage Examples
412412
usb.builtin_driver = usb.BUILTIN_CDC | usb.BUILTIN_NCM
413413
usb.active(True)
414414

415+
**Custom VID/PID (Runtime Mode):**
416+
::
417+
418+
import machine
419+
420+
# Create custom device descriptor with your VID/PID
421+
custom_device_desc = bytearray([
422+
18, # bLength
423+
1, # bDescriptorType (Device)
424+
0x00, 0x02, # bcdUSB (USB 2.0)
425+
0x02, # bDeviceClass (CDC)
426+
0x00, # bDeviceSubClass
427+
0x00, # bDeviceProtocol
428+
64, # bMaxPacketSize0
429+
0x34, 0x12, # idVendor (0x1234)
430+
0x78, 0x56, # idProduct (0x5678)
431+
0x00, 0x01, # bcdDevice (1.0)
432+
1, # iManufacturer
433+
2, # iProduct
434+
3, # iSerialNumber
435+
1 # bNumConfigurations
436+
])
437+
438+
usb = machine.USBDevice()
439+
usb.active(False)
440+
usb.config(desc_dev=custom_device_desc)
441+
usb.builtin_driver = usb.BUILTIN_CDC
442+
usb.active(True)
443+
444+
Dynamic VID/PID Configuration
445+
-----------------------------
446+
447+
**Runtime VID/PID Properties (All Modes):**
448+
449+
Both runtime and non-runtime USB device modes support dynamic VID/PID
450+
configuration using simple Python properties:
451+
452+
::
453+
454+
import machine
455+
456+
usb = machine.USBDevice()
457+
usb.active(False)
458+
459+
# Set custom VID/PID dynamically
460+
usb.vid = 0x1234
461+
usb.pid = 0x5678
462+
463+
usb.builtin_driver = usb.BUILTIN_CDC
464+
usb.active(True)
465+
466+
# Check current values
467+
print(f"VID: 0x{usb.vid:04X}, PID: 0x{usb.pid:04X}")
468+
469+
# Reset to firmware defaults
470+
usb.active(False)
471+
usb.vid = 0 # 0 means use builtin default
472+
usb.pid = 0
473+
usb.active(True)
474+
475+
**Build-time VID/PID Override:**
476+
477+
For additional customization, VID/PID defaults can be set at build time
478+
using the ``MICROPY_HW_USB_RUNTIME_VID`` and ``MICROPY_HW_USB_RUNTIME_PID``
479+
macros:
480+
481+
**Override VID/PID at Build Time:**
482+
::
483+
484+
# Build with custom VID/PID
485+
make CFLAGS_EXTRA="-DMICROPY_HW_USB_RUNTIME_VID=0x1234 -DMICROPY_HW_USB_RUNTIME_PID=0x5678"
486+
487+
**Port-specific Configuration:**
488+
::
489+
490+
# In mpconfigport.h or boards/BOARD/mpconfigboard.h
491+
#define MICROPY_HW_USB_RUNTIME_VID (0x1234)
492+
#define MICROPY_HW_USB_RUNTIME_PID (0x5678)
493+
494+
This allows customizing the USB VID/PID defaults even when ``MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE``
495+
is disabled and only built-in drivers are available. If not specified, these
496+
macros default to ``MICROPY_HW_USB_VID`` and ``MICROPY_HW_USB_PID``.
497+
498+
**VID/PID Property Notes:**
499+
500+
- Properties can only be set when ``usb.active`` is ``False``
501+
- Setting VID/PID to ``0`` uses the firmware default value
502+
- Valid range is 1-65535 (0x0001-0xFFFF)
503+
- Custom values persist across active()/inactive() cycles
504+
- Works in both runtime and non-runtime USB device modes
505+
506+
.. important::
507+
**Runtime VID/PID changes work correctly.**
508+
509+
VID/PID can be changed at runtime using ``active(False)`` → set VID/PID →
510+
``active(True)``. The device will re-enumerate with the new VID/PID values.
511+
512+
**Note:** On some operating systems (e.g., Windows with WSL), you may need to
513+
manually forward the new USB device when the VID/PID changes.
514+
515+
**Runtime usage:**
516+
::
517+
518+
import machine
519+
520+
usb = machine.USBDevice()
521+
usb.active(False)
522+
usb.vid = 0x1234 # Your custom VID
523+
usb.pid = 0x5678 # Your custom PID
524+
usb.builtin_driver = usb.BUILTIN_CDC
525+
usb.active(True)
526+
# Device will enumerate with VID:0x1234 PID:0x5678
527+
528+
**Persistent configuration (recommended for production):**
529+
530+
To ensure the custom VID/PID is applied on every boot, place the
531+
configuration in ``boot.py``::
532+
533+
import machine
534+
535+
usb = machine.USBDevice()
536+
usb.active(False)
537+
usb.vid = 0x1234 # Your custom VID
538+
usb.pid = 0x5678 # Your custom PID
539+
usb.builtin_driver = usb.BUILTIN_CDC
540+
usb.active(True)
541+
542+
After saving to ``boot.py``, the device will enumerate with the custom
543+
VID/PID on every subsequent boot.
415544

416545
.. _usb driver modules in micropython-lib: https://github.com/micropython/micropython-lib/tree/master/micropython/usb#readme

extmod/machine_usb_device.c

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ static mp_obj_t usb_device_make_new(const mp_obj_type_t *type, size_t n_args, si
101101
o->builtin_driver = MP_OBJ_FROM_PTR(&builtin_none_obj);
102102
}
103103
#endif
104+
o->custom_vid = 0; // 0 = use builtin default
105+
o->custom_pid = 0; // 0 = use builtin default
104106

105107
#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
106108
// Initialize runtime-only fields
@@ -221,9 +223,17 @@ static mp_obj_t usb_device_active(size_t n_args, const mp_obj_t *args) {
221223
mp_obj_usb_builtin_t *builtin = MP_OBJ_TO_PTR(usbd->builtin_driver);
222224
mp_usbd_update_class_state(builtin->flags);
223225
}
226+
227+
// Connect to USB host
228+
tud_connect();
224229
} else {
230+
// Disconnect from USB host first
231+
tud_disconnect();
232+
225233
// Disable all classes when deactivating
226234
mp_usbd_update_class_state(USB_BUILTIN_FLAG_NONE);
235+
// Note: custom_vid/pid are NOT reset here - they persist
236+
// across deactivation so users can set them before activating
227237
}
228238
#endif
229239
}
@@ -574,22 +584,29 @@ static void usb_device_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
574584
// Load attribute.
575585
if (attr == MP_QSTR_builtin_driver) {
576586
dest[0] = self->builtin_driver;
587+
} else if (attr == MP_QSTR_vid) {
588+
uint16_t vid = self->custom_vid;
589+
if (vid == 0) {
590+
vid = MICROPY_HW_USB_RUNTIME_VID;
591+
}
592+
dest[0] = MP_OBJ_NEW_SMALL_INT(vid);
593+
} else if (attr == MP_QSTR_pid) {
594+
uint16_t pid = self->custom_pid;
595+
if (pid == 0) {
596+
pid = MICROPY_HW_USB_RUNTIME_PID;
597+
}
598+
dest[0] = MP_OBJ_NEW_SMALL_INT(pid);
577599
} else {
578600
// Continue lookup in locals_dict.
579601
dest[1] = MP_OBJ_SENTINEL;
580602
}
581603
} else if (dest[1] != MP_OBJ_NULL) {
582604
// Store attribute.
583605
if (attr == MP_QSTR_builtin_driver) {
584-
#if MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
585-
// In runtime mode, require deactivation before changing builtin_driver
606+
// Require deactivation before changing builtin_driver in both modes
586607
if (self->active) {
587-
mp_raise_OSError(MP_EINVAL); // Need to deactivate first
608+
mp_raise_OSError(MP_EINVAL);
588609
}
589-
#else
590-
// In static mode, allow changing builtin_driver when active
591-
// This will update the class state and trigger re-enumeration
592-
#endif
593610

594611
// Handle new bitfield builtin types and legacy constants
595612
uint8_t flags = 0;
@@ -632,23 +649,31 @@ static void usb_device_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
632649
// Update the internal class state based on flags
633650
mp_usbd_update_class_state(flags);
634651

635-
#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
636-
// In static mode, if USB is active, trigger re-enumeration
637-
// to update the host with the new configuration
638-
if (self->active) {
639-
#ifndef NO_QSTR
640-
// Disconnect and reconnect to trigger enumeration with new descriptors
641-
tud_disconnect();
642-
// Small delay to ensure host recognizes disconnection
643-
mp_hal_delay_ms(100);
644-
tud_connect();
645-
#endif
646-
}
647-
#endif
648-
649652
// Store the builtin_driver (could be legacy constant or new bitfield object)
650653
self->builtin_driver = dest[1];
651654
dest[0] = MP_OBJ_NULL;
655+
} else if (attr == MP_QSTR_vid) {
656+
// Require deactivation before changing VID in both modes
657+
if (self->active) {
658+
mp_raise_OSError(MP_EINVAL);
659+
}
660+
mp_int_t vid = mp_obj_get_int(dest[1]);
661+
if (vid < 0 || vid > 0xFFFF) {
662+
mp_raise_ValueError(MP_ERROR_TEXT("VID must be 0-65535"));
663+
}
664+
self->custom_vid = (uint16_t)vid;
665+
dest[0] = MP_OBJ_NULL;
666+
} else if (attr == MP_QSTR_pid) {
667+
// Require deactivation before changing PID in both modes
668+
if (self->active) {
669+
mp_raise_OSError(MP_EINVAL);
670+
}
671+
mp_int_t pid = mp_obj_get_int(dest[1]);
672+
if (pid < 0 || pid > 0xFFFF) {
673+
mp_raise_ValueError(MP_ERROR_TEXT("PID must be 0-65535"));
674+
}
675+
self->custom_pid = (uint16_t)pid;
676+
dest[0] = MP_OBJ_NULL;
652677
}
653678
}
654679
}

shared/tinyusb/mp_usbd.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ static inline void mp_usbd_init_tud(void) {
9797
#endif
9898
}
9999

100+
// Allow runtime override of VID/PID defaults
101+
#ifndef MICROPY_HW_USB_RUNTIME_VID
102+
#define MICROPY_HW_USB_RUNTIME_VID MICROPY_HW_USB_VID
103+
#endif
104+
#ifndef MICROPY_HW_USB_RUNTIME_PID
105+
#define MICROPY_HW_USB_RUNTIME_PID MICROPY_HW_USB_PID
106+
#endif
107+
100108
// Individual USB class flags for bitfield operations
101109
#define USB_BUILTIN_FLAG_NONE 0x00
102110
#define USB_BUILTIN_FLAG_CDC 0x01
@@ -148,6 +156,9 @@ typedef struct {
148156
bool active; // Has the user set the USB device active?
149157
bool trigger; // Has the user requested the active state change (or re-activate)?
150158

159+
uint16_t custom_vid; // Custom VID (0 = use builtin default)
160+
uint16_t custom_pid; // Custom PID (0 = use builtin default)
161+
151162

152163
// Temporary pointers for xfer data in progress on each endpoint
153164
// Ensuring they aren't garbage collected until the xfer completes
@@ -172,6 +183,8 @@ typedef struct {
172183
mp_obj_base_t base;
173184
mp_obj_t builtin_driver; // Points to one of mp_type_usb_device_builtin_nnn
174185
bool active; // Has the user set the USB device active?
186+
uint16_t custom_vid; // Custom VID (0 = use builtin default)
187+
uint16_t custom_pid; // Custom PID (0 = use builtin default)
175188
} mp_obj_usb_device_t;
176189

177190
static inline void mp_usbd_init(void) {

shared/tinyusb/mp_usbd_descriptor.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ const tusb_desc_device_t mp_usbd_builtin_desc_dev = {
4646
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
4747
.bDeviceProtocol = MISC_PROTOCOL_IAD,
4848
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
49-
.idVendor = MICROPY_HW_USB_VID,
50-
.idProduct = MICROPY_HW_USB_PID,
49+
.idVendor = MICROPY_HW_USB_RUNTIME_VID,
50+
.idProduct = MICROPY_HW_USB_RUNTIME_PID,
5151
.bcdDevice = 0x0100,
5252
.iManufacturer = USBD_STR_MANUF,
5353
.iProduct = USBD_STR_PRODUCT,
@@ -141,35 +141,41 @@ static const uint8_t *mp_usbd_generate_desc_cfg_unified(uint8_t flags, uint8_t *
141141
*desc++ = 0x80; // bmAttributes (bit 7 must be 1)
142142
*desc++ = USBD_MAX_POWER_MA / 2; // bMaxPower (in 2mA units)
143143

144-
// Add enabled class descriptors
144+
// Track current interface number for dynamic assignment
145+
uint8_t itf_num = 0;
146+
147+
// Add enabled class descriptors with dynamically calculated interface numbers
145148
#if CFG_TUD_CDC
146149
if (flags & USB_BUILTIN_FLAG_CDC) {
147150
const uint8_t cdc_desc[] = {
148-
TUD_CDC_DESCRIPTOR(USBD_ITF_CDC, USBD_STR_CDC, USBD_CDC_EP_CMD,
151+
TUD_CDC_DESCRIPTOR(itf_num, USBD_STR_CDC, USBD_CDC_EP_CMD,
149152
USBD_CDC_CMD_MAX_SIZE, USBD_CDC_EP_OUT, USBD_CDC_EP_IN, USBD_CDC_IN_OUT_MAX_SIZE)
150153
};
151154
memcpy(desc, cdc_desc, sizeof(cdc_desc));
152155
desc += sizeof(cdc_desc);
156+
itf_num += 2; // CDC uses 2 interfaces
153157
}
154158
#endif
155159

156160
#if CFG_TUD_MSC
157161
if (flags & USB_BUILTIN_FLAG_MSC) {
158162
const uint8_t msc_desc[] = {
159-
TUD_MSC_DESCRIPTOR(USBD_ITF_MSC, USBD_STR_MSC, USBD_MSC_EP_OUT, USBD_MSC_EP_IN, USBD_MSC_IN_OUT_MAX_SIZE)
163+
TUD_MSC_DESCRIPTOR(itf_num, USBD_STR_MSC, USBD_MSC_EP_OUT, USBD_MSC_EP_IN, USBD_MSC_IN_OUT_MAX_SIZE)
160164
};
161165
memcpy(desc, msc_desc, sizeof(msc_desc));
162166
desc += sizeof(msc_desc);
167+
itf_num += 1; // MSC uses 1 interface
163168
}
164169
#endif
165170

166171
#if CFG_TUD_NCM
167172
if (flags & USB_BUILTIN_FLAG_NCM) {
168173
const uint8_t ncm_desc[] = {
169-
TUD_CDC_NCM_DESCRIPTOR(USBD_ITF_NET, USBD_STR_NET, USBD_STR_NET_MAC, USBD_NET_EP_CMD, 64, USBD_NET_EP_OUT, USBD_NET_EP_IN, USBD_NET_IN_OUT_MAX_SIZE, CFG_TUD_NET_MTU)
174+
TUD_CDC_NCM_DESCRIPTOR(itf_num, USBD_STR_NET, USBD_STR_NET_MAC, USBD_NET_EP_CMD, 64, USBD_NET_EP_OUT, USBD_NET_EP_IN, USBD_NET_IN_OUT_MAX_SIZE, CFG_TUD_NET_MTU)
170175
};
171176
memcpy(desc, ncm_desc, sizeof(ncm_desc));
172177
desc += sizeof(ncm_desc);
178+
itf_num += 2; // NCM uses 2 interfaces (control + data)
173179
}
174180
#endif
175181

@@ -345,6 +351,23 @@ const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
345351
#if !MICROPY_HW_ENABLE_USB_RUNTIME_DEVICE
346352

347353
const uint8_t *tud_descriptor_device_cb(void) {
354+
// Check if custom VID/PID is set (0 means use default)
355+
if (MP_STATE_VM(usbd) != MP_OBJ_NULL) {
356+
mp_obj_usb_device_t *usbd = MP_OBJ_TO_PTR(MP_STATE_VM(usbd));
357+
if (usbd->custom_vid != 0 || usbd->custom_pid != 0) {
358+
// Patch the default descriptor with custom VID/PID values.
359+
// We override whichever fields are non-zero (0 = keep default).
360+
static tusb_desc_device_t custom_desc;
361+
custom_desc = mp_usbd_builtin_desc_dev; // Copy default
362+
if (usbd->custom_vid != 0) {
363+
custom_desc.idVendor = usbd->custom_vid;
364+
}
365+
if (usbd->custom_pid != 0) {
366+
custom_desc.idProduct = usbd->custom_pid;
367+
}
368+
return (const uint8_t*)&custom_desc;
369+
}
370+
}
348371
return (const void *)&mp_usbd_builtin_desc_dev;
349372
}
350373

0 commit comments

Comments
 (0)