AI generated issue-ticket
- part of the CLI-exit bug found thinking it was HELIO only, but not. and not wholly configurator bug.
Summary
CDC_Send_DATA() in src/main/vcp_hal/usbd_cdc_interface.c contains an indefinite blocking spin that stalls the STM32 task scheduler when the USB TX ring buffer is full. If the scheduler is blocked long enough, the WWDG (Window Watchdog) times out and resets the MCU — producing a spurious USB disconnect that is indistinguishable from a user-initiated reboot.
This is reproduced reliably when leaving the CLI tab and switching to the Sensors tab in EmuConfigurator: the sensors tab begins high-frequency MSP polling immediately, which fills the 2048-byte USB TX ring faster than the host drains it, triggering the block.
Steps to Reproduce
- Connect FC over USB (STM32F4 or F7 target).
- Open EmuConfigurator → CLI tab. Wait for autocomplete build.
- Exit CLI tab → navigate to Sensors tab.
- Within 2–5 seconds: second USB disconnect occurs. Sensors gyro goes flat.
- Log shows:
SERIAL: Connection with ID: 1 closed / PortHandler - Removed: /dev/ttyACM0
Reproducible with stock EmuFlight on any high-polling-rate tab after CLI exit.
Root Cause
CDC_Send_DATA() — indefinite block
// src/main/vcp_hal/usbd_cdc_interface.c
uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength)
{
for (uint32_t i = 0; i < sendLength; i++) {
while (CDC_Send_FreeBytes() == 0) {
delay(1); // <-- spins indefinitely if host is not draining
}
ATOMIC_BLOCK(NVIC_BUILD_PRIORITY(6, 0)) {
UserTxBuffer[UserTxBufPtrIn] = ptrBuffer[i];
UserTxBufPtrIn = (UserTxBufPtrIn + 1) % APP_TX_DATA_SIZE;
}
}
return sendLength;
}
When CDC_Send_FreeBytes() == 0, the function never returns. The STM32 scheduler is blocked. The WWDG is not fed. The MCU resets.
USB_TIMEOUT guard is unreachable
usbVcpWriteBuf() has a 50ms timeout that appears to protect against this:
while (count > 0) {
uint32_t txed = CDC_Send_DATA(p, count); // never returns partial
count -= txed;
if (millis() - start > USB_TIMEOUT) break; // never reached
}
CDC_Send_DATA does not return until it has written all bytes. The timeout check in the outer loop never executes.
isUsbVcpTransmitBufferEmpty() is a stub
// src/main/drivers/serial_usb_vcp.c
static bool isUsbVcpTransmitBufferEmpty(const serialPort_t *instance) {
UNUSED(instance);
return true; // always reports empty
}
waitForSerialPortToFinishTransmitting() in cliRebootEx() is therefore a no-op for VCP ports.
Proposed Fix
1. Add per-byte timeout to CDC_Send_DATA — return partial count
uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength)
{
uint32_t sent = 0;
uint32_t start = millis();
for (uint32_t i = 0; i < sendLength; i++) {
while (CDC_Send_FreeBytes() == 0) {
if (millis() - start > USB_TIMEOUT) {
return sent; // partial write — caller handles via outer loop
}
delay(1);
}
ATOMIC_BLOCK(NVIC_BUILD_PRIORITY(6, 0)) {
UserTxBuffer[UserTxBufPtrIn] = ptrBuffer[i];
UserTxBufPtrIn = (UserTxBufPtrIn + 1) % APP_TX_DATA_SIZE;
}
sent++;
}
return sent;
}
This returns partial sent count on timeout. usbVcpWriteBuf's outer count -= txed + USB_TIMEOUT check becomes correctly reachable, no other callers need changes.
2. Fix isUsbVcpTransmitBufferEmpty stub
static bool isUsbVcpTransmitBufferEmpty(const serialPort_t *instance)
{
UNUSED(instance);
return CDC_Send_FreeBytes() == APP_TX_DATA_SIZE - 1;
}
Impact
| Scenario |
Before |
After |
| Sensors tab after CLI exit |
WWDG reset → spurious disconnect |
TX bytes dropped on timeout, no reset |
| USB host disconnects mid-session |
FC wedges in spin loop → reset |
Graceful timeout bailout |
| Normal MSP traffic |
No regression |
Identical — buffer never fills under normal load |
waitForSerialPortToFinishTransmitting |
No-op (always true) |
Actually waits for TX ring to drain |
Related
- Same bug confirmed in Betaflight:
src/platform/STM32/vcp_hal/usbd_cdc_interface.c and src/platform/STM32/vcpf4/usbd_cdc_vcp.c — identical blocking pattern.
- EmuConfigurator configurator-side CLI exit fixes: nerdCopter/EmuConfigurator_nerdRepo
rethink-WM branch.
Identified during EmuConfigurator Electron/Forge migration analysis — 2026-04-04
AI generated issue-ticket
Summary
CDC_Send_DATA()insrc/main/vcp_hal/usbd_cdc_interface.ccontains an indefinite blocking spin that stalls the STM32 task scheduler when the USB TX ring buffer is full. If the scheduler is blocked long enough, the WWDG (Window Watchdog) times out and resets the MCU — producing a spurious USB disconnect that is indistinguishable from a user-initiated reboot.This is reproduced reliably when leaving the CLI tab and switching to the Sensors tab in EmuConfigurator: the sensors tab begins high-frequency MSP polling immediately, which fills the 2048-byte USB TX ring faster than the host drains it, triggering the block.
Steps to Reproduce
SERIAL: Connection with ID: 1 closed/PortHandler - Removed: /dev/ttyACM0Reproducible with stock EmuFlight on any high-polling-rate tab after CLI exit.
Root Cause
CDC_Send_DATA()— indefinite blockWhen
CDC_Send_FreeBytes() == 0, the function never returns. The STM32 scheduler is blocked. The WWDG is not fed. The MCU resets.USB_TIMEOUTguard is unreachableusbVcpWriteBuf()has a 50ms timeout that appears to protect against this:CDC_Send_DATAdoes not return until it has written all bytes. The timeout check in the outer loop never executes.isUsbVcpTransmitBufferEmpty()is a stubwaitForSerialPortToFinishTransmitting()incliRebootEx()is therefore a no-op for VCP ports.Proposed Fix
1. Add per-byte timeout to
CDC_Send_DATA— return partial countThis returns partial
sentcount on timeout.usbVcpWriteBuf's outercount -= txed+USB_TIMEOUTcheck becomes correctly reachable, no other callers need changes.2. Fix
isUsbVcpTransmitBufferEmptystubImpact
waitForSerialPortToFinishTransmittingRelated
src/platform/STM32/vcp_hal/usbd_cdc_interface.candsrc/platform/STM32/vcpf4/usbd_cdc_vcp.c— identical blocking pattern.rethink-WMbranch.Identified during EmuConfigurator Electron/Forge migration analysis — 2026-04-04