Fix ESP32-S2/S3 raw-REPL paste corruption - #11354
Conversation
On espressif, tud_task() runs in its own FreeRTOS task at the same priority as the VM. TinyUSB releases the OUT endpoint before cdcd_xfer_cb copies the received packet into its fifo, so a tud_cdc_read from the VM in that window re-arms DMA into the same buffer: one 64-byte packet is lost and the next one repeated (hathach/tinyusb#1292). Measured as 3.5% of whole-file raw-REPL pastes failing with a SyntaxError on Metro ESP32-S3. Move all console CDC reads onto the task that runs tud_task(): tud_cdc_rx_cb drains the fifo into a small single-producer/single-consumer ring buffer and serial_read()/serial_bytes_available() read that. If the ring is full the leftover is drained after the next tud_task(), which on espressif now times out instead of blocking so a full ring cannot stall. Ctrl-C clears the ring too. This is the structure MicroPython uses (micropython#14462). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On espressif tud_cdc_rx_wanted_cb runs on the TinyUSB task, so moving the ring tail there gave the tail two writers. Ctrl-C now sets a flag and the VM moves the tail on its next read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dhalbert
left a comment
There was a problem hiding this comment.
There is a already a (non-atomic) ringbuf implementation in py/ringbuf.{c,h}, which we use several other places. Could you use that?
Replace the custom ring with the existing ringbuf, guarded with common_hal_mcu_disable_interrupts() the same way the espressif BLE CharacteristicBuffer guards its ringbuf. The USB task reads the TinyUSB fifo outside the critical section, since that takes a mutex, and puts the chunk into the ringbuf inside it. Ctrl-C clears the ringbuf directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only espressif runs tud_task() in its own task, so the ringbuf is now built only when CFG_TUSB_OS is OPT_OS_FREERTOS; other ports read the TinyUSB fifo directly again, as on main. The espressif usbd loop calls the drain on every pass, which replaces the pending flag. The drain does nothing when the console is disabled, so a data serial at index 0 keeps its input, and usb_cdc.console reads, in_waiting and reset_input_buffer() use the ringbuf. The copy chunk is a fixed 64 bytes so a high-speed endpoint size does not put 512 bytes on the usbd task stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dhalbert
left a comment
There was a problem hiding this comment.
The conditional FREERTOS checks in the changes make the shared in supervisor/shared/... be less authentic. Previously there was only one tiny conditional, having to do with FreeRTOS tasking.
Can the code be refactored so there is less port-specifc code again, maybe with default impls of the usb_cdc_rx_... routines, and then espressif-specific impls? Could other ports switch to using usb_cdc_rx... calls (without the ringbuf) instead of usb_cdc...? Or could some abstraction be made to hide the details here?
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KwTBwHF9YvQvN18ctYzJ7U
Shared code now calls usb_cdc_rx_read/available/clear/drain unconditionally. The default implementations in supervisor/shared/usb/usb_device.c are weak pass-throughs to the TinyUSB fifo, the same behavior as before on ports that run tud_task() on the VM task. ports/espressif/supervisor/usb.c overrides them with the console ringbuf. No CFG_TUSB_OS conditionals remain in supervisor/shared or shared-module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KwTBwHF9YvQvN18ctYzJ7U
|
Done in 6a8a2d8: shared defaults are pass-throughs, espressif overrides with the ringbuf. The console object reads the ring, the drain skips when the console is off, and ARM keeps the direct fifo read since a ring there stalled a 2 KB paste on the M0. Merged main. S2 and S3 0/2000, M0 and RP2350 clean, 20 to 40 bytes flash on tight boards. |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Comments trimmed in 3e0a43e. |
Fixes raw-REPL paste corruption on ESP32-S2/S3: console input is now read on the TinyUSB task, not the VM task.
Cause
On espressif
tud_task()runs in its own FreeRTOS task, so a VM read can re-arm the endpoint mid-copy.lib/tinyusb/src/device/usbd.ccdcd_xfer_cbthen copiesep_bufinto the RX fifolib/tinyusb/src/class/cdc/cdc_device.ctud_cdc_read_char()re-arms DMA into the sameep_buflib/tinyusb/src/tusb.ctu_edpt_stream_read_xferChange
Espressif only (
CFG_TUSB_OS == OPT_OS_FREERTOS). Other ports read the fifo directly, unchanged from main.supervisor/shared/usb/usb_device.cpy/ringbuf, filled on the TinyUSB task, every access insidecommon_hal_mcu_disable_interrupts()like espressif_bleio/CharacteristicBuffer.cports/espressif/supervisor/usb.ctud_task_ext(10, false)so fifo input held back by a full ringbuf moves in without a new USB event (the loop's existingtud_cdc_write_flush()now runs too)supervisor/shared/serial.cshared-module/usb_cdc/Serial.cusb_cdc.consoleread,in_waiting,reset_input_buffer()use the ringbufHardware tested
Raw-REPL paste harness, 40 tests x 50 iterations per row. Host: Ubuntu 24.04.5 LTS.
usb_cdc.console.in_waiting/read()from a REPL loopusb_cdc.enable(console=False, data=True), echo onusb_cdc.dataadafruit_p4gpio, ESP32-C3 (no native USB)bless_dev_board_multi_sensor,circuitbrains_deluxe_m4,kicksat-spriteNot covered
usb_cdc.datareads still come from the VM task on espressif. Same race class, separate change.zephyr-tests / nrf54lm20bsimAI assistance
Written with Claude Code. I verified the mechanism in the source and ran every measurement above on my test boards.
🤖 Generated with Claude Code