Description
cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c updates the interrupt-enable
register with a whole-register assign from both the thread and the ISR, with no
lock and no read-modify-write:
static ssize_t _write(const void *buffer, size_t len)
{
tsrb_add(&serial_tx_rb, buffer, len);
USB_SERIAL_JTAG.int_ena.val = IRQ_MASK; /* enable IN_EMPTY */
return len;
}
In _serial_intr_handler(), after tsrb_get_one() returns empty:
USB_SERIAL_JTAG.int_ena.val = IRQ_MASK & ~USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY;
tsrb_* itself is IRQ-safe. The bug is the two unsynchronized stores to
int_ena.
The JTAG IRQ is routed to PRO_CPU (intr_matrix_set(PRO_CPU_NUM, ...)).
_write() (every printf) can run on APP_CPU. On classic ESP32 that is a
dual-core lost-update:
- ISR on PRO_CPU sees an empty ring and is about to clear
IN_EMPTY.
- APP_CPU
_write() adds bytes and sets int_ena = IRQ_MASK.
- The ISR store lands last and turns
IN_EMPTY off again.
Result: serial_tx_rb is not empty, but the TX-empty IRQ is disabled. The last
console bytes sit in the ring until the next _write().
This is a hot path (every stdio write), not a “do not re-init a running
peripheral” API issue.
Suggested direction: RMW / bit set-clear under a critical section, or only
clear IN_EMPTY after re-checking that the ring is still empty.
File: cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c
Line numbers on current master, in cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c:
_write(): lines 39 to 45 (assigns int_ena)
_serial_intr_handler(): lines 65 to 70 (clears the IN_EMPTY enable)
_init(): line 96 (binds the IRQ to PRO_CPU)
Steps to reproduce the issue
On ESP32 (dual-core), with stdio_usb_serial_jtag:
- Have the TX ISR drain the ring to empty and take the “disable IN_EMPTY”
path.
- Concurrently call
_write() / printf on the other core so it tsrb_add()s
and then assigns int_ena = IRQ_MASK.
- Let the ISR disable store complete after that assign.
- Observe: ring non-empty,
IN_EMPTY still clear, no further TX IRQ until the
next write.
Expected results
If the TX ring has data, IN_EMPTY should stay enabled so the ISR keeps
draining.
Actual results
A later ISR store can disable IN_EMPTY after _write() re-enabled it. Stdio
TX stalls until the next _write().
Versions
- RIOT: current
master (cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c)
- OS: Linux
- Most relevant on original ESP32 SMP (IRQ pinned to PRO_CPU).
Declaration of AI-Tools / LLMs usage:
AI-Tools / LLMs that were used are:
- xAI Grok for drafting this issue text, with user review
Description
cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.cupdates the interrupt-enableregister with a whole-register assign from both the thread and the ISR, with no
lock and no read-modify-write:
In
_serial_intr_handler(), aftertsrb_get_one()returns empty:tsrb_*itself is IRQ-safe. The bug is the two unsynchronized stores toint_ena.The JTAG IRQ is routed to PRO_CPU (
intr_matrix_set(PRO_CPU_NUM, ...))._write()(everyprintf) can run on APP_CPU. On classic ESP32 that is adual-core lost-update:
IN_EMPTY._write()adds bytes and setsint_ena = IRQ_MASK.IN_EMPTYoff again.Result:
serial_tx_rbis not empty, but the TX-empty IRQ is disabled. The lastconsole bytes sit in the ring until the next
_write().This is a hot path (every stdio write), not a “do not re-init a running
peripheral” API issue.
Suggested direction: RMW / bit set-clear under a critical section, or only
clear
IN_EMPTYafter re-checking that the ring is still empty.File:
cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.cLine numbers on current master, in
cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c:_write(): lines 39 to 45 (assignsint_ena)_serial_intr_handler(): lines 65 to 70 (clears the IN_EMPTY enable)_init(): line 96 (binds the IRQ toPRO_CPU)Steps to reproduce the issue
On ESP32 (dual-core), with
stdio_usb_serial_jtag:path.
_write()/printfon the other core so ittsrb_add()sand then assigns
int_ena = IRQ_MASK.IN_EMPTYstill clear, no further TX IRQ until thenext write.
Expected results
If the TX ring has data,
IN_EMPTYshould stay enabled so the ISR keepsdraining.
Actual results
A later ISR store can disable
IN_EMPTYafter_write()re-enabled it. StdioTX stalls until the next
_write().Versions
master(cpu/esp32/stdio_usb_serial_jtag/usb_serial_jtag.c)Declaration of AI-Tools / LLMs usage:
AI-Tools / LLMs that were used are: