From ae56a386bf35514601041f881339a22595633948 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 27 Aug 2026 13:37:24 +1000 Subject: [PATCH 1/3] esp32: Fix USB Serial/jTAG 64-byte tx race triggering stalled input. Update to the fix in eb5d89cd83. The sequence of events which caused this problem: 1. USJ has no data to send to host (TXFIFO/IN EP), so the SERIAL_IN_EMPTY interrupt bit is set. 2. MicroPython sends exactly 64 bytes to host, queued into the TXFIFO. The hardware will start flushing to the host. 3. The SERIAL_IN_EMPTY interrupt is enabled, but because the bit was already set it triggers. 4. ISR calls txfifo_flush() but this is a no-op as the TXFIFO is already being flushed. 5. ISR is now disabled until the next time MicroPython tries to send data. Because there is exactly 64 bytes in the buffer (i.e. EP size), the host will assume there is more data to come and will wait for either a Zero Length Packet (ZLP) or more data. This won't happen and the data won't be received until the next time MicroPython tries to TX serial data to the host. Fix is to clear the SERIAL_IN_EMPTY interrupt status to avoid stale interrupts, and to be explicit about when we flush immediately on send versus when we need to wait for the TXFIFO to empty. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/usb_serial_jtag.c | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ports/esp32/usb_serial_jtag.c b/ports/esp32/usb_serial_jtag.c index 86c89385fae..548d77a49f0 100644 --- a/ports/esp32/usb_serial_jtag.c +++ b/ports/esp32/usb_serial_jtag.c @@ -77,12 +77,14 @@ static void usb_serial_jtag_isr_handler(void *arg) { } if (flags & USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY) { - // As per the ESP-IDF driver, allow for the possibility the USJ just sent a full - // 64-bit endpoint to the host and now it's waiting for another ZLP to flush the result - // to the OS + // This interrupt is only enabled after we've sent a full IN EP buffer to the host. + + // As per the ESP-IDF driver, this is necessary if the USJ has just + // sent a full 64-byte EP to the host and now the host is waiting + // for another ZLP to signal that the transfer is actually done. usb_serial_jtag_ll_txfifo_flush(); - // Disable this interrupt until next time we write into the FIFO + // Disable this interrupt until the next time we write a full EP buffer usb_serial_jtag_ll_disable_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); } } @@ -122,9 +124,22 @@ void usb_serial_jtag_tx_strn(const char *str, size_t len) { l = usb_serial_jtag_ll_write_txfifo((const uint8_t *)str, l); str += l; len -= l; + } + + // Clear any old "serial in empty" interrupt, as we're about to either + // manually flush or enable the interrupt if the TXFIFO is full (no risk of + // a lost flush as we check if the TXFIFO is full after we do this.) + usb_serial_jtag_ll_clr_intsts_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + + if (usb_serial_jtag_ll_txfifo_writable()) { + // If we haven't written a full EP buffer, manually flush it to send to the host + usb_serial_jtag_ll_txfifo_flush(); + } else { + // If we have written a full EP buffer to send to the host then the hardware will flush + // automatically, but enable the "serial in empty" interrupt so we will send a ZLP once + // the full buffer has been sent. usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); } - usb_serial_jtag_ll_txfifo_flush(); } #endif // MICROPY_HW_ESP_USB_SERIAL_JTAG From 48156411cff61857763a3de1a9496cc9779d70d3 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 27 Aug 2026 14:30:52 +1000 Subject: [PATCH 2/3] tests/serial_test: Allow more time for soft reset of board. Necessary on ESP32 when testing a native USB or USJ, as the UART TX buffer can be quite large and is flushed before the soft reset completes. (Which from the perspective of the native port just looks like it doesn't respond.) This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- tests/serial_test.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/serial_test.py b/tests/serial_test.py index eebea402fa4..d04c8d0f9fc 100755 --- a/tests/serial_test.py +++ b/tests/serial_test.py @@ -15,6 +15,13 @@ from test_utils import test_instance_epilog, convert_device_shortcut_to_real_device +TEST_READ_TIMEOUT = 1 + +# For ESP32 boards with both native/USJ USB and a UART serial port, soft +# reset may flush the UART buffer before it completes - this can be quite large +# if it's backed up a lot of data during the previous test. +SOFT_RESET_TIMEOUT = 5 + echo_test_script = """ import sys bytes_min=%u @@ -98,9 +105,25 @@ def drain_input(ser): time.sleep(0.1) +def reset_into_raw_repl(ser): + try: + ser.timeout = SOFT_RESET_TIMEOUT + ser.flushInput() + ser.write(b"\x03\x01\x04") # break, raw-repl, soft-reboot + EXPECTED1 = b"MPY: soft reboot\r\n" + EXPECTED2 = b"raw REPL; CTRL-B to exit\r\n>" + r1 = ser.read_until(EXPECTED1) + r2 = b"" + if r1.endswith(EXPECTED1): + r2 = ser.read_until(EXPECTED2) + if not r2.endswith(EXPECTED2): + raise TestError("could not reset to raw REPL", r1 + r2) + finally: + ser.timeout = TEST_READ_TIMEOUT + + def send_script(ser, script): - ser.write(b"\x03\x01\x04") # break, raw-repl, soft-reboot - drain_input(ser) + reset_into_raw_repl(ser) chunk_size = 32 for i in range(0, len(script), chunk_size): ser.write(script[i : i + chunk_size]) @@ -270,13 +293,13 @@ def write_test(ser_repl, ser_data, bufsize, nbuf, verified): def do_test(dev_repl, dev_data=None, time_per_subtest=1): if dev_data is None: print("REPL and data on", dev_repl) - ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=1) + ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=TEST_READ_TIMEOUT) ser_data = ser_repl else: print("REPL on", dev_repl) print("data on", dev_data) - ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=1) - ser_data = serial.Serial(dev_data, baudrate=115200, timeout=1) + ser_repl = serial.Serial(dev_repl, baudrate=115200, timeout=TEST_READ_TIMEOUT) + ser_data = serial.Serial(dev_data, baudrate=115200, timeout=TEST_READ_TIMEOUT) # Do echo test first, and abort if it doesn't pass. echo_test(ser_repl, ser_data) From 6e22f5a129620d8bb3ec47989a4d7e6813f2aff2 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 27 Aug 2026 15:13:42 +1000 Subject: [PATCH 3/3] esp32/usb_serial_jtag: Fix tick overflow issue in timeout. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- ports/esp32/usb_serial_jtag.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/esp32/usb_serial_jtag.c b/ports/esp32/usb_serial_jtag.c index 548d77a49f0..44b3412cd52 100644 --- a/ports/esp32/usb_serial_jtag.c +++ b/ports/esp32/usb_serial_jtag.c @@ -115,7 +115,7 @@ void usb_serial_jtag_tx_strn(const char *str, size_t len) { TickType_t start_tick = xTaskGetTickCount(); while (!usb_serial_jtag_ll_txfifo_writable()) { TickType_t now_tick = xTaskGetTickCount(); - if (!terminal_connected || now_tick > (start_tick + pdMS_TO_TICKS(200))) { + if (!terminal_connected || (now_tick - start_tick) > pdMS_TO_TICKS(200)) { terminal_connected = false; return; }