Skip to content

Bug: ESP32 USB-Serial-JTAG stdio can lose IN_EMPTY enable and stall TX #22607

Description

@andrewlihhh

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:

  1. ISR on PRO_CPU sees an empty ring and is about to clear IN_EMPTY.
  2. APP_CPU _write() adds bytes and sets int_ena = IRQ_MASK.
  3. 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:

  1. Have the TX ISR drain the ring to empty and take the “disable IN_EMPTY”
    path.
  2. Concurrently call _write() / printf on the other core so it tsrb_add()s
    and then assigns int_ena = IRQ_MASK.
  3. Let the ISR disable store complete after that assign.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions