Skip to content

stm32/tinyusb: Add High-Speed USB support and fix CDC/VBUS issues.#26

Merged
andrewleech merged 55 commits into
masterfrom
stm32_tinyusb_hs_clean
May 16, 2026
Merged

stm32/tinyusb: Add High-Speed USB support and fix CDC/VBUS issues.#26
andrewleech merged 55 commits into
masterfrom
stm32_tinyusb_hs_clean

Conversation

@andrewleech
Copy link
Copy Markdown
Owner

@andrewleech andrewleech commented Mar 15, 2026

Summary

The STM32 port's TinyUSB integration only supports the FS controller. Boards with a HS controller (PYBD_SF6, STM32F429DISC, OLIMEX_H407, or ULPI-based designs) cannot use TinyUSB — the RHPORT configuration is hardcoded to RHPORT 0 / full-speed only.

This PR adds proper RHPORT mode selection in tusb_config.h based on MICROPY_HW_USB_HS and MICROPY_HW_USB_HS_IN_FS board config, routes HS IRQ handlers to the correct RHPORT (including STM32N6 which maps HS to RHPORT 0), and adds VBUS sensing configuration for the HS-in-FS path on STM32F4/F7 (mirroring the existing FS path, with support for both VBDEN and legacy NOVBUSSENS register variants).

Additional issues found and fixed during testing (each is a separate commit and can be split into its own PR if preferred):

  • CDC reconnect stall: When a host closes and reopens the CDC serial port (e.g. repeated mpremote connect/disconnect cycles), the IN endpoint can remain stalled from a prior runtime USB disconnect. The device becomes unresponsive until reset. Fixed by clearing endpoint stall on DTR high. Additionally, on DTR low (host close) the TX FIFO is now flushed so stale data from a previous session does not accumulate and block writes on the next connection.

  • DFU bootloader serial number mismatch: The TinyUSB serial descriptor uses a raw hex dump of all 12 UID bytes (24-char lowercase), while the legacy USB stack and ST's onboard DFU bootloader use a condensed algorithm that selects 6 bytes with two additions (12-char uppercase). This causes the device to report a different serial number depending on which USB stack is active, breaking tools that identify devices by serial (udev rules, mpremote, dfu-util). Fixed by using the ST DFU bootloader algorithm.

  • TinyUSB-specific boot.py template: The factory reset boot.py template references pyb.usb_mode() which does not exist on the TinyUSB stack. A TinyUSB-specific template is added that uses machine.USBDevice with BUILTIN_DEFAULT.

Testing

Built with CFLAGS_EXTRA=-DMICROPY_HW_TINYUSB_STACK=1 for:

  • PYBD_SF6 (F7, HS-in-FS) — flashed and verified CDC serial, machine.USBDevice API, filesystem access
  • OPENMV_N6 (N6, HS-in-FS) — flashed and verified CDC serial, machine.USBDevice API, filesystem access
  • STM32F429DISC (F4, HS-in-FS) — build only
  • OLIMEX_H407 (F4, HS-in-FS) — build only
  • NUCLEO_H563ZI (H5, FS only) — build only, regression check

ULPI boards (STM32H747I_DISCO) are tested on hardware as part of the original development in micropython#18303 — this branch is split out from that work.

CDC serial throughput (OPENMV_N6, HS link at 480 Mbit/s)

Compared TinyUSB vs legacy USB stack using tests/serial_test.py methodology.

Read (device → host):

bufsize TinyUSB KB/s Legacy KB/s Ratio
256 5198 816 6.4x
512 5490 817 6.7x
1024 6050 811 7.5x
2048 7560 817 9.3x
4096 9448 811 11.6x
8192 8422 815 10.3x
16384 8086 787 10.3x

Write (host → device):

bufsize TinyUSB KB/s Legacy KB/s Ratio
256 186 464 0.4x
512 218 592 0.4x
1024 193 671 0.3x
2048 185 733 0.3x
4096 184 763 0.2x
8192 178 786 0.2x
16384 179 800 0.2x

TinyUSB read is 6-12x faster. Write is 2.5-4.5x slower due to a pre-existing limitation in the TinyUSB CDC stdin path — mp_hal_stdin_rx_chr() reads byte-by-byte through a 512-byte ringbuffer, causing backpressure on the USB OUT endpoint. This affects all TinyUSB ports, not just HS, and is being tracked separately.

Trade-offs and Alternatives

The RHPORT configuration uses #ifndef guards so boards like STM32N6 that pre-define CFG_TUSB_RHPORT0_MODE in mpconfigboard_common.h pass through unchanged. This avoids needing per-family #if chains but means any future HS-capable family (e.g. STM32U5) needs its own board-level override.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 15, 2026

Code size report:

Reference:  docs/develop/porting: Update session log for example port. [9f396bb]
Comparison: stm32,extmod: Use full path for shared/tinyusb/mp_usbd.h include. [merge of d2e0d20]
  mpy-cross:   +80 +0.021% 
   bare-arm:   +52 +0.092% 
minimal x86:   +71 +0.038% 
   unix x64:   +96 +0.011% standard
      stm32:   +56 +0.014% PYBV10
      esp32:   +44 +0.003% ESP32_GENERIC
     mimxrt:  +104 +0.027% TEENSY40
        rp2:   +80 +0.009% RPI_PICO_W
       samd: +2588 +0.941% ADAFRUIT_ITSYBITSY_M4_EXPRESS[incl +36(data) +284(bss)]
  qemu rv32:   +63 +0.014% VIRT_RV32

@andrewleech andrewleech force-pushed the stm32_tinyusb_hs_clean branch 6 times, most recently from bb25aee to 4c3e95f Compare March 16, 2026 01:45
@andrewleech andrewleech force-pushed the stm32_tinyusb_hs_clean branch from 4c3e95f to 1e59002 Compare March 27, 2026 19:35
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 27, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.47%. Comparing base (9f396bb) to head (d2e0d20).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #26      +/-   ##
==========================================
+ Coverage   98.46%   98.47%   +0.01%     
==========================================
  Files         176      176              
  Lines       22811    22831      +20     
==========================================
+ Hits        22460    22483      +23     
+ Misses        351      348       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrewleech andrewleech force-pushed the stm32_tinyusb_hs_clean branch 3 times, most recently from 105a90a to 2fae783 Compare April 20, 2026 09:01
@andrewleech andrewleech force-pushed the master branch 7 times, most recently from ce2c0c9 to 9f396bb Compare May 1, 2026 23:21
@andrewleech andrewleech force-pushed the stm32_tinyusb_hs_clean branch 2 times, most recently from 2e5b8dc to f6345b7 Compare May 2, 2026 09:48
robert-hh added 9 commits May 5, 2026 14:49
Used for allocation of DMA channels. It will be needed for planned
modules and methods like adc_timed(), dac_timed(), I2S.

It includes management code for DMA IRQ handlers, similar to what
was made for Sercom.

Signed-off-by: robert-hh <robert@hammelrath.com>
These functions are use to allocate, free and configure a set
of TC counter instances. The SAMxx MCU have between 3 to 5 (SAMD21) and
4 to 8 (SAMD51) TC instances. Two of them are used for the µs counter,
the remaining 1 - 6 instances are administered here for use by
various functions, like timed DMA transfers.

Signed-off-by: robert-hh <robert@hammelrath.com>
Used as:

    dac.write_timed(data, freq [, count])
    dac.deinit()

Working range for dac_timed():

    SAMD21: 1 Hz - 100 kHz (1 MHz clock, 10 bit)
    SAMD51: 1 Hz - ~500 kHz (8 MHz clock, 12 bit)

The buffer has to be a byte array or a halfword array,
and the data is sent once.

The default for count is 1. If set to a value > 0, the data will be
transmitted count times. If set to 0 or  < 0, the date will be
transmitted until deliberately stopped. The playback
can be stopped with dac.deinit().

dac.deinit() just releases the timer and DMA channel needed by
dac_timed(). The DAC object itself does not have to be released.

Signed-off-by: robert-hh <robert@hammelrath.com>
Used as:

    adc.read_timed(buffer, freq)

Buffer must be preallocated. The size determines the number of 16 bit
words to be read. The numeric range of the results is that of the raw
ADC. The call returns immediately, and the data transfer is done by DMA.
The caller must wait sufficiently long until the data is sampled
and can be noticed by a callback. No internal checks are made for
a too-high freq value.

Read speeds depends on Average and bit length setting:

    SAMD21: Max. 350kS/s (8 bit, Average 1)
    SAMD51: Max. 1 MS/s (8 bit, Average 1)

Signed-off-by: robert-hh <robert@hammelrath.com>
The callback is called when a dac_timed() sequence finishes. It will be
reset with callback=None or omitting the callback option in the
constructor.

Side change: Set the clock freq. to 48Mhz.

Signed-off-by: robert-hh <robert@hammelrath.com>
Enabling a callback that will be called when a adc.read_timed_into() run
is finished. That's especially useful with slow sampling rates and/or
many samples, avoiding to guess the sampling time.
Raise an error is adc.read_u16() is called while a read_timed_into()
is active.

Other ADC changes:
- SAMD51: use ADC1 if both ADC1 and ADC0 are available at a Pin.

Signed-off-by: robert-hh <robert@hammelrath.com>
These return True, while a timed action is ongoing.

Side change:
Reorder some code in machine_dac.c and do not reset DAC twice.

Signed-off-by: robert-hh <robert@hammelrath.com>
Signed-off-by: robert-hh <robert@hammelrath.com>
Since the two channels of a SAMD51 are not completely independent,
dac.deinit() now clears both channels, and both channels have to
be re-instantiated after a deinit().

Side change:
- rearrange some code lines.

Signed-off-by: robert-hh <robert@hammelrath.com>
pi-anl and others added 28 commits May 7, 2026 23:31
Add fallback define for NRF_FPU_S which was removed from the
nRF9160 device headers in nrfx v3.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Replace the nrf port's custom Pin.irq implementation with the
shared mp_irq_obj_t pattern used by rp2 and alif ports. Adds
support for the hard kwarg (soft IRQs via mp_sched_schedule
when hard=False), makes handler optional, and returns a proper
IRQ object with flags() and trigger() methods. Boards without
MICROPY_ENABLE_SCHEDULER retain the direct ISR-only path.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Use -1 sentinels for SPI init parameters so that calling
spi.init(baudrate=X) no longer resets polarity, phase, and
other settings to defaults. Matches the rp2 port behavior.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Accept the timeout kwarg (default 50ms) for compatibility with
rp2 and alif ports. Also fix TWI peripheral not being disabled
on the transfer error path.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
The old `nrfutil` runs only under CPython 2.x and has been replaced by a
new stand-alone `nrfutil` executable, available directly from Nordic.

Update the Makefile to work with this new version, and also update the
README to describe how to install it.

Signed-off-by: Damien George <damien@micropython.org>
The `sys.platform` variable on bare-metal ports is uniformly the port name
(except for stm32 which uses "pyboard" for `sys.platform`), regardless of
the board.

Change the nrf port to follow this convention.

Among other things, this gets the test auto-detection working properly on
nrf boards, which relies on the value of `sys.platform` (eg for target
wiring selection).

Signed-off-by: Damien George <damien@micropython.org>
Apply the same workaround as added in
cc7eb1a for other boards to the
NRF51-Dongle, since it also suffers from the same limitation.  This change
makes it possible to run unit tests against it.

Signed-off-by: Daniël van de Giessen <daniel@dvdgiessen.nl>
Change tcsetattr from TCSAFLUSH to TCSANOW in mp_hal_stdio_mode_raw
and mp_hal_stdio_mode_orig. The TCSAFLUSH flag calls tcdrain() before
applying terminal settings, which blocks indefinitely on macOS when
used with PTY devices if the master side has not read all pending
output. This is a known macOS kernel behavior (XNU ttywait loops
until the output queue is consumed by the master).

The drain serves no practical purpose on the unix port since output
is written via write() syscalls that complete before tcsetattr is
called. The input flush is also unnecessary since MicroPython's
readline handles stale input gracefully.

Two repl_ test .exp files are updated to account for the newline
after Ctrl-D (end paste mode) no longer being discarded by the input
flush; it produces an extra empty prompt line.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Adds a "# sigint:" directive for repl_ tests that need Ctrl-C to
generate SIGINT via the PTY terminal driver. When present, the child
process is set up with the PTY as its controlling terminal (via
setsid/TIOCSCTTY/tcsetpgrp) so that \x03 written to the PTY master
generates SIGINT for the child's process group.

This works because MicroPython's REPL restores original terminal
settings (with ISIG enabled) before executing user code, allowing the
terminal driver to convert \x03 into SIGINT during blocking operations.

Test added:
- repl_ctrl_c_interrupt_execution.py: Verifies Ctrl-C interrupts a
  blocking time.sleep() call and the REPL remains functional afterward.

Also wraps PTY fd handling in try/finally for all repl_ tests.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Some socket implementations (eg `extmod/modsocket.c`) don't (and can't
easily) raise an exception for an invalid socket type.  So just test that
passing such a value doesn't crash the device.

Signed-off-by: Damien George <damien@micropython.org>
For example, the ESP8266_GENERIC FLASH_512K variant doesn't have the
`vfs` module.

Signed-off-by: Damien George <damien@micropython.org>
Eg running this on `ADAFRUIT_ITSYBITSY_M0_EXPRESS` would previously crash
with "NameError: name not defined" due to the lookup of `complex` as a
global.  With the change here, that test is skipped automatically.

Also, provide better skip messages with the reason for the skip.

Signed-off-by: Damien George <damien@micropython.org>
This test is unreliable with 5 UDP groups because on bare-metal lwIP
targets there's only enough buffer space for 4 groups, see
`extmod/modlwip.c`:

    // Total queue length for buffered UDP/raw incoming packets.
    #define LWIP_INCOMING_PACKET_QUEUE_LEN (4)

Signed-off-by: Damien George <damien@micropython.org>
The nrf port doesn't implement the `freq` argument.

Signed-off-by: Damien George <damien@micropython.org>
Two changes here to improve running this test on targets with low memory:

- Print the results at end of test, so if a MemoryError occurs part-way-
  through then the SKIP works correctly.

- Reorganise the order of the sub-tests and preallocate the large buffer
  so it can be reused.

This gets the test running and passing on ESP32_GENERIC with the native
emitter (and also still works with the bytecode emitter).

Signed-off-by: Damien George <damien@micropython.org>
This decreases the variation in the time taken to send, making the test
more reliable.

Without this change the nrf port is flaky at 2400 baud, eg the
`ARDUINO_NANO_33_BLE_SENSE` board fails this test more often than not.

Signed-off-by: Damien George <damien@micropython.org>
Prior to this change this test fails on `ADAFRUIT_ITSYBITSY_M0_EXPRESS`
with the native emitter, because it doesn't have enough heap to allocate
the backing buffer.

Signed-off-by: Damien George <damien@micropython.org>
Prior to this change this test fails on `ADAFRUIT_ITSYBITSY_M0_EXPRESS`
with the native emitter, because it doesn't have enough heap to allocate
the read/write buffers.

Signed-off-by: Damien George <damien@micropython.org>
Detect the target's error reporting capabilities, and skip tests as
appropriate.  For example, all samd boards are configured with
`MICROPY_ERROR_REPORTING_TERSE` so need to skip these four tests.

Signed-off-by: Damien George <damien@micropython.org>
Allows running with terse error reporting (and normal and detailed).

Signed-off-by: Damien George <damien@micropython.org>
For example, `ADAFRUIT_ITSYBITSY_M0_EXPRESS` only has about 20k heap and
runs out of memory part way through the integration loop.

Signed-off-by: Damien George <damien@micropython.org>
Clang -m32 rounds some floating point reprs differently, likely due to x87
temporary excess precision.  Accept this value in addition to the other
values that are accepted.

Closes: micropython#19120

Signed-off-by: Jeff Epler <jepler@unpythonic.net>
CPython's dir() converts the result of __dir__ to a list and sorts
it; MicroPython returns the value as-is.  Document the difference
with a class whose __dir__ returns a non-list iterable.

Signed-off-by: Jeongseop Lim <jseoplim@gmail.com>
Implements mapping from MICROPY_HW_USB_MAIN_DEV to TinyUSB RHPORT
configuration, enabling board-specific USB PHY selection for TinyUSB
stack. Adds support for HS-in-FS mode (High Speed controller running
at Full Speed) which is the default for STM32 boards without external
ULPI PHY.

Thi STM32F4/F7/H7 high-speed RHPORT mode selection is placed in
ports/stm32/tinyusb_port/tusb_config.h, following the alif/nrf pattern.
Includes py/mpconfig.h to ensure board config macros are available when
TinyUSB processes the header.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
When a host closes and reopens the CDC serial port, the IN endpoint may
remain stalled from a prior runtime USB disconnect (e.g. mpremote
connect/disconnect cycles). Clear the stall on DTR high so the
connection recovers without requiring a device reset.

On DTR low (host close), flush the TX FIFO so stale data does not
accumulate and block writes on the next connection.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
The TinyUSB serial descriptor used a raw hex dump of all 12 UID bytes
in sequential order (24-char lowercase), while the legacy USB stack and
ST's onboard DFU bootloader use a condensed algorithm that selects 6
bytes with two additions (12-char uppercase).

This mismatch caused the device to report a different serial number
depending on which USB stack was active, breaking tools that identify
devices by serial (e.g. udev rules, mpremote, dfu-util).

Use the ST DFU bootloader algorithm for consistency.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Removes the need for -I$(TOP)/shared/tinyusb/ in the stm32 Makefile
by using an explicit path in the two files that include mp_usbd.h
outside of the shared/tinyusb/ directory itself.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
@dpgeorge dpgeorge force-pushed the stm32_tinyusb_hs_clean branch from f6345b7 to d2e0d20 Compare May 7, 2026 16:23
@andrewleech andrewleech merged commit d2e0d20 into master May 16, 2026
141 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.