Skip to content

UDD_Send() (uotghs_device.c) has an unbounded busy-wait on TXINI — permanent MCU hang if host doesn't set it (native USB CDC line-state related) #153

Description

@MrLeftarion

Summary

UDD_Send() in system/libsam/source/uotghs_device.c contains an unbounded busy-wait loop with no timeout, waiting for the TXINI (Transmit IN complete) flag on the target endpoint. If the host does not set this flag (e.g. around a USB line-state change such as DTR/RTS toggling on the native CDC port), the MCU hangs permanently in this loop, with interrupts still enabled and no way to recover except a full reset/power cycle.

uint32_t UDD_Send(uint32_t ep, const void* data, uint32_t len)
{
    const uint8_t *ptr_src = data;
    uint8_t *ptr_dest = (uint8_t *) &udd_get_endpoint_fifo_access8(ep);
    uint32_t i;

    while( UOTGHS_DEVEPTISR_TXINI != (UOTGHS->UOTGHS_DEVEPTISR[ep] & UOTGHS_DEVEPTISR_TXINI )) {}
    ...

(https://github.com/arduino/ArduinoCore-sam/blob/master/system/libsam/source/uotghs_device.c)

This is called from USBD_Send() in USBCore.cpp, which is the underlying function used by every SerialUSB.write()/println() call (via Serial_::write in CDC.cpp), as well as HID reports.

Related but similarly unbounded waits exist elsewhere in the same file (UDD_WaitIN, UDD_WaitOUT, UDD_WaitForINOrOUT) — all are plain while(...); with no timeout or bail-out condition.

How I found this

I have an Arduino Due (genuine, Rev3) that reliably hangs during extended native-USB operation on specific Windows 11 hosts (newer Intel xHCI controllers — Tiger Lake/Alder Lake generation). Full investigation history, including USB traffic capture (Wireshark/USBPcap) and elimination of third-party software as a cause, is documented here: #71 (comment)

Using an ST-Link V2 + OpenOCD + GDB attached via the DEBUG (SWD) header, I was able to halt the MCU while it was hung (native USB unresponsive, but the rest of loop() — an independent LED blink — still running, i.e. not a full lockup: DHCSR.S_LOCKUP was 0, core was in normal Thread mode with interrupts enabled).

Register/symbol dump at the hang:

pc = 0x00081b06   -> inside UDD_Send+18
lr = 0x00080e99   -> called from USBD_Send() at USBCore.cpp:204
xpsr = 0x01000000 -> Thread mode, not inside an ISR
primask/faultmask = 0 -> interrupts not disabled

info line *0x00080e99 resolved cleanly to USBCore.cpp:204 inside USBD_Send, confirming the call chain Serial_::write → USBD_Send → UDD_Send, stuck spinning on the TXINI wait with no way out.

Reliable(-ish) repro

A minimal sketch (no HID/Joystick needed, plain CDC is enough):

bool state;
uint32_t Tim;
uint32_t TimLog;

void setup() {
  pinMode(13, OUTPUT);
  delay(1000);
  SerialUSB.println(1);
}

void loop() {
  if (millis() - Tim >= 500) {
    Tim = millis();
    digitalWrite(13, state);
    state = !state;
  }
  if (millis() - TimLog >= 5000) {
    TimLog = millis();
    SerialUSB.println(millis());
  }
}
  • Left running for ~15-25 minutes on an affected Windows 11 host: hangs organically.
  • Faster/more reliable trigger: from the host, open the native CDC COM port via .NET System.IO.Ports.SerialPort without setting DtrEnable/RtsEnable to true before .Open() — this toggles the CDC line state in a way that reliably triggers the hang within seconds in my testing.
  • Does not reproduce on Linux or in Windows Safe Mode on the same physical hardware — consistent with the theory that it's a host-timing-dependent race on whether TXINI gets set before the firmware's next attempt to send.

Suggested direction for a fix

At minimum, every unbounded while(...) wait in uotghs_device.c (UDD_Send, UDD_WaitIN, UDD_WaitOUT, UDD_WaitForINOrOUT) should have a bounded timeout (e.g. based on millis() or a cycle counter) and return an error/abort the transfer instead of spinning forever when the expected hardware flag never arrives. I understand this changes the function signatures/call sites (currently void/unconditional uint32_t return used as byte count, not status) — happy to discuss/help test a patch if a maintainer can advise on the preferred approach (e.g. optional timeout parameter, or a global "USB write timeout" behavior similar to how other Serial implementations use setTimeout()).

Environment

  • Board: Arduino Due (genuine, Rev3, native USB port)
  • Core: Arduino SAM Boards (32-bit ARM Cortex-M3), package version installed via Boards Manager — 1.6.12
  • Debug setup: ST-Link V2 (clone) via the onboard 4-pin SWD DEBUG header, OpenOCD 0.12.0, arm-none-eabi-gdb 4.8.3 (bundled with Arduino IDE's SAM toolchain)
  • Host: Windows 11 (build 26100), Intel USB 3.10/3.20 xHCI controllers (PCI\VEN_8086&DEV_51ED, PCI\VEN_8086&DEV_461E)

Happy to provide more register dumps, the .elf/.map files, or the full Wireshark capture if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions