Skip to content

feat(esp_tinyusb/msc): Add optional asynchronous storage IO - #614

Draft
omnitex wants to merge 5 commits into
masterfrom
feat/usb-device-msc-async-io
Draft

omnitex wants to merge 5 commits into
masterfrom
feat/usb-device-msc-async-io

Conversation

@omnitex

@omnitex omnitex commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Main idea

TinyUSB runs every class in a single task (tud_task). Without async IO, MSC READ10/WRITE10 callbacks access the storage medium inside that task. While an SD card or flash is busy, CDC, HID and the other classes on the device are not serviced.

TinyUSB 0.19 added async MSC IO (hathach/tinyusb#2967): the callback returns TUD_MSC_RET_ASYNC, and the storage access finishes later with tud_msc_async_io_done(). This PR uses that API in esp_tinyusb, so applications need no code changes:

  • CONFIG_TINYUSB_MSC_ASYNC_IO runs storage IO in a dedicated worker task. The TinyUSB task keeps servicing other classes, and write errors are reported to the host.
  • CONFIG_TINYUSB_MSC_ASYNC_WRITE_BEHIND (default y) accepts a write chunk into the existing per-storage buffer and writes it in the background, so the host sends the next chunk in the meantime. This keeps write throughput without extra RAM. A failed buffered write is reported on the next command to the LUN.
  • Extra cost: about 4.5 KB heap, mostly the worker stack. Measured peak use of that stack is 972 of 4096 bytes (24 %).

Results (ESP32-P4, SD card, 40 MHz)

Example: 104 MB file copied from the host while CDC sends a heartbeat every 100 ms (REMOVE log commit)

Async IO off Async IO on
Worst CDC latency during writes 88.3 ms 1.0 ms
Heartbeat lines above 10 ms 14 0
Write rate 2.6 MB/s 2.6 MB/s

Benchmark: CDC echo latency under MSC load, async with write-behind (REMOVE CSV commit)

Extra delay per storage access Off: p50 / p99 / max On: p50 / p99 / max Write MB/s, off → on
0 ms (card only) 1.69 / 21.98 / 76.3 ms 0.33 / 0.49 / 0.89 ms 2.01 → 2.93
10 ms 11.35 / 15.02 / 97.0 ms 0.33 / 0.45 / 0.54 ms 0.53 → 0.56

With async IO off, CDC latency follows the storage access time. With async IO on, it stays at the idle value of about 0.33 ms, and SD write throughput does not drop (it goes up by 46 % at 0 ms delay). The 20 ms async run had one outlier at 65 ms; I haven't investigated it yet.

Notes

  • Requires TinyUSB >= 0.19.0.
  • On internal SPI flash, a few stalls remain even with async IO, because flash writes and erases disable the cache on both cores. The SD card is not affected.

Checklist

Before submitting a Pull Request, please ensure the following:

  • 🚨 This PR does not introduce breaking changes.
  • All CI checks (GH Actions) pass.
  • Documentation is updated as needed.
  • Tests are updated or added as necessary.
  • Code is well-commented, especially in complex areas.
  • Git history is clean — commits are squashed to the minimum necessary.

- CONFIG_TINYUSB_MSC_ASYNC_IO runs READ10/WRITE10 in a worker task,
  so the TinyUSB task keeps serving other classes. Needs TinyUSB >= 0.19.
- CONFIG_TINYUSB_MSC_ASYNC_WRITE_BEHIND (default y) keeps sync write
  throughput by reusing the existing storage buffer.
- Document both options and their RAM cost.
Measures CDC latency and MSC throughput with async IO off/on,
on a RAM disk with optional delay or an SD card. Results in README.
CDC prints USB latency while a file is copied to the drive,
so async IO off vs on is visible at a glance.
SD card based storage, copied 104 MB file from host.
@omnitex
omnitex force-pushed the feat/usb-device-msc-async-io branch from 60b65f8 to 03d8b48 Compare October 6, 2026 15:01

This branch has not been deployed

No deployments
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.

1 participant