From 3a3709d765dc7bbacd87e56b9b32623084da3334 Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 21:45:14 -0800 Subject: [PATCH 1/6] Fix USB VCP lockup on disconnect (STM32F4) Two issues fixed: 1. serial.c: serialIsConnected() was not returning the result from the vtable isConnected call, always returning true regardless of actual connection state. 2. vcpf4/usbd_cdc_vcp.c: VCP_DataTx() had infinite loops waiting for TX state and buffer space. If USB was disconnected, these loops would never exit, locking up the flight controller. Added 50ms timeout to both blocking loops. On timeout, returns USBD_FAIL and CDC_Send_DATA returns 0 to indicate failure. Tested on SPEEDYBEEF405WING - FC continues operating normally after USB disconnect instead of locking up. Co-Authored-By: Claude Opus 4.6 --- src/main/drivers/serial.c | 2 +- src/main/vcpf4/usbd_cdc_vcp.c | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/drivers/serial.c b/src/main/drivers/serial.c index dc625aaa354..590a36002b2 100644 --- a/src/main/drivers/serial.c +++ b/src/main/drivers/serial.c @@ -111,7 +111,7 @@ void serialEndWrite(serialPort_t *instance) bool serialIsConnected(const serialPort_t *instance) { if (instance->vTable->isConnected) - instance->vTable->isConnected(instance); + return(instance->vTable->isConnected(instance)); // If API is not defined - assume connected return true; diff --git a/src/main/vcpf4/usbd_cdc_vcp.c b/src/main/vcpf4/usbd_cdc_vcp.c index 15c1f7e39d9..a29eb70f4c4 100644 --- a/src/main/vcpf4/usbd_cdc_vcp.c +++ b/src/main/vcpf4/usbd_cdc_vcp.c @@ -178,7 +178,9 @@ static uint16_t VCP_Ctrl(uint32_t Cmd, uint8_t* Buf, uint32_t Len) *******************************************************************************/ uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength) { - VCP_DataTx(ptrBuffer, sendLength); + if (VCP_DataTx(ptrBuffer, sendLength) != USBD_OK) { + return 0; + } return sendLength; } @@ -194,18 +196,29 @@ uint32_t CDC_Send_FreeBytes(void) * @param Len: Number of data to be sent (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else VCP_FAIL */ +#define VCP_WRITE_TIMEOUT_MS 50 + static uint16_t VCP_DataTx(const uint8_t* Buf, uint32_t Len) { + uint32_t start = millis(); + /* make sure that any paragraph end frame is not in play could just check for: USB_CDC_ZLP, but better to be safe and wait for any existing transmission to complete. */ - while (USB_Tx_State != 0); + while (USB_Tx_State != 0) { + if (millis() - start > VCP_WRITE_TIMEOUT_MS) { + return USBD_FAIL; + } + } for (uint32_t i = 0; i < Len; i++) { // Stall if the ring buffer is full while (((APP_Rx_ptr_in + 1) % APP_RX_DATA_SIZE) == APP_Rx_ptr_out) { + if (millis() - start > VCP_WRITE_TIMEOUT_MS) { + return USBD_FAIL; + } delay(1); } From 692d6400f73efd1c21e9340c7967ddfbd60fb722 Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 21:49:34 -0800 Subject: [PATCH 2/6] Fix USB VCP lockup on disconnect (STM32F7/H7) Same fix as STM32F4: added 50ms timeout to blocking loops in CDC_Send_DATA() that wait for TX state and buffer space. On timeout, returns partial byte count (or 0) instead of blocking forever when USB is disconnected. Co-Authored-By: Claude Opus 4.6 --- src/main/vcp_hal/usbd_cdc_interface.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/vcp_hal/usbd_cdc_interface.c b/src/main/vcp_hal/usbd_cdc_interface.c index dbc093fb641..7ad16084ad9 100644 --- a/src/main/vcp_hal/usbd_cdc_interface.c +++ b/src/main/vcp_hal/usbd_cdc_interface.c @@ -415,6 +415,8 @@ uint32_t CDC_Send_FreeBytes(void) * @param sendLength: Number of data to be sent (in bytes) * @retval Bytes sent */ +#define VCP_WRITE_TIMEOUT_MS 50 + uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength) { #if defined(STM32H7) @@ -423,13 +425,22 @@ uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength) USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)USBD_Device.pCDC_ClassData; #endif - while (hcdc->TxState != 0); + uint32_t start = millis(); + + while (hcdc->TxState != 0) { + if (millis() - start > VCP_WRITE_TIMEOUT_MS) { + return 0; + } + } for (uint32_t i = 0; i < sendLength; i++) { UserTxBuffer[UserTxBufPtrIn] = ptrBuffer[i]; UserTxBufPtrIn = (UserTxBufPtrIn + 1) % APP_TX_DATA_SIZE; while (CDC_Send_FreeBytes() == 0) { + if (millis() - start > VCP_WRITE_TIMEOUT_MS) { + return i; // Return partial count + } delay(1); } } From 502095ada9ab8a2656b2230a231ca93f2cfada11 Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 21:54:54 -0800 Subject: [PATCH 3/6] Fix CDC_Send_FreeBytes buffer calculation (STM32F4) The previous implementation incorrectly subtracted RX buffer usage from TX buffer size. Fixed to properly calculate free space in the outbound (APP_Rx) circular buffer using the correct pointer math. Co-Authored-By: Claude Opus 4.6 --- src/main/vcpf4/usbd_cdc_vcp.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/vcpf4/usbd_cdc_vcp.c b/src/main/vcpf4/usbd_cdc_vcp.c index a29eb70f4c4..7511f617eb4 100644 --- a/src/main/vcpf4/usbd_cdc_vcp.c +++ b/src/main/vcpf4/usbd_cdc_vcp.c @@ -186,7 +186,16 @@ uint32_t CDC_Send_DATA(const uint8_t *ptrBuffer, uint32_t sendLength) uint32_t CDC_Send_FreeBytes(void) { - return APP_RX_DATA_SIZE - CDC_Receive_BytesAvailable(); + // Calculate free space in APP_Rx_Buffer (outbound to host) + // Using correct circular buffer math with volatile pointers + uint32_t ptr_in = APP_Rx_ptr_in; + uint32_t ptr_out = APP_Rx_ptr_out; + + if (ptr_out > ptr_in) { + return ptr_out - ptr_in - 1; + } else { + return APP_RX_DATA_SIZE + ptr_out - ptr_in - 1; + } } /** From bc913862a3054b26c6ad6782a6b6ab100d92f7de Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 21:55:01 -0800 Subject: [PATCH 4/6] Add DTR tracking for USB VCP connection detection Register callback for CDC control line state changes to track when the host has actually opened the COM port (DTR signal). This provides more reliable connection detection than just checking USB enumeration state. usbVcpIsConnected() now returns true only when: - USB is connected (hardware) - USB is configured (enumerated) - Host has opened COM port (DTR high) This helps prevent writes to USB when no host application is listening, which can cause buffer buildup and delays. Co-Authored-By: Claude Opus 4.6 --- src/main/drivers/serial_usb_vcp.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/drivers/serial_usb_vcp.c b/src/main/drivers/serial_usb_vcp.c index 7fdbad2a114..9a98e86854d 100644 --- a/src/main/drivers/serial_usb_vcp.c +++ b/src/main/drivers/serial_usb_vcp.c @@ -51,6 +51,16 @@ USBD_HandleTypeDef USBD_Device; static vcpPort_t vcpPort; +// Track DTR (Data Terminal Ready) state - indicates if host has COM port open +static volatile bool cdcPortOpened = false; + +static void cdcCtrlLineStateCallback(void *context, uint16_t ctrlLineState) +{ + UNUSED(context); + // DTR is bit 0 of control line state + cdcPortOpened = (ctrlLineState & 0x01) != 0; +} + static void usbVcpSetBaudRate(serialPort_t *instance, uint32_t baudRate) { UNUSED(instance); @@ -103,7 +113,8 @@ static uint8_t usbVcpRead(serialPort_t *instance) static bool usbVcpIsConnected(const serialPort_t *instance) { (void)instance; - return usbIsConnected() && usbIsConfigured(); + // Check USB hardware state AND whether host has opened the COM port (DTR) + return usbIsConnected() && usbIsConfigured() && cdcPortOpened; } static void usbVcpWriteBuf(serialPort_t *instance, const void *data, int count) @@ -209,6 +220,9 @@ void usbVcpInitHardware(void) IOInit(IOGetByTag(IO_TAG(PA11)), OWNER_USB, RESOURCE_INPUT, 0); IOInit(IOGetByTag(IO_TAG(PA12)), OWNER_USB, RESOURCE_OUTPUT, 0); USBD_Init(&USB_OTG_dev, USB_OTG_FS_CORE_ID, &USR_desc, &USBD_CDC_cb, &USR_cb); + + // Register callback for DTR state changes + CDC_SetCtrlLineStateCb(cdcCtrlLineStateCallback, NULL); #elif defined(STM32F7) || defined(STM32H7) usbGenerateDisconnectPulse(); @@ -225,7 +239,10 @@ void usbVcpInitHardware(void) /* Start Device Process */ USBD_Start(&USBD_Device); - + + // Register callback for DTR state changes + CDC_SetCtrlLineStateCb(cdcCtrlLineStateCallback, NULL); + #ifdef STM32H7 HAL_PWREx_EnableUSBVoltageDetector(); delay(100); // Cold boot failures observed without this, even when USB cable is not connected From 5a81c9bdbca2b52254f3e145db91d99c154913ed Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 21:57:54 -0800 Subject: [PATCH 5/6] Add suspend detection as disconnect fallback (STM32F4) On boards without VBUS sensing, USB hardware disconnect may not be detected. Treat USB suspend event as a disconnect to prevent blocking on writes when cable is unplugged. When suspend occurs, set bDeviceState = UNCONNECTED so that usbIsConnected() returns false and writes are skipped. Co-Authored-By: Claude Opus 4.6 --- src/main/vcpf4/usbd_usr.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/vcpf4/usbd_usr.c b/src/main/vcpf4/usbd_usr.c index 24c5f17c206..15aa47fb257 100644 --- a/src/main/vcpf4/usbd_usr.c +++ b/src/main/vcpf4/usbd_usr.c @@ -21,6 +21,7 @@ #include "usbd_usr.h" #include "usbd_ioreq.h" +#include "usbd_cdc_vcp.h" USBD_Usr_cb_TypeDef USR_cb = { @@ -102,13 +103,17 @@ void USBD_USR_DeviceDisconnected (void) /** * @brief USBD_USR_DeviceSuspended -* Displays the message on LCD on device suspend Event +* Handle device suspend event - treat as disconnect for safety. +* This helps on boards without VBUS sensing where hardware +* disconnect detection may not work. * @param None * @retval None */ void USBD_USR_DeviceSuspended(void) { - /* Users can do their application actions here for the USB-Reset */ + // Treat suspend as disconnect - prevents blocking on USB writes + // when cable is unplugged on boards without VBUS sensing + bDeviceState = UNCONNECTED; } From bf81e90a3dcbe08bc7010357cf83088768404c49 Mon Sep 17 00:00:00 2001 From: daijoubu Date: Fri, 20 Feb 2026 22:15:37 -0800 Subject: [PATCH 6/6] Fix DTR tracking default - assume connected initially Default cdcPortOpened to true so MSP works immediately on connection. DTR state will be updated when host explicitly sets/clears it. Previous default of false broke MSP on tools that don't assert DTR. Co-Authored-By: Claude Opus 4.6 --- src/main/drivers/serial_usb_vcp.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/drivers/serial_usb_vcp.c b/src/main/drivers/serial_usb_vcp.c index 9a98e86854d..f076e0bfee2 100644 --- a/src/main/drivers/serial_usb_vcp.c +++ b/src/main/drivers/serial_usb_vcp.c @@ -52,7 +52,8 @@ USBD_HandleTypeDef USBD_Device; static vcpPort_t vcpPort; // Track DTR (Data Terminal Ready) state - indicates if host has COM port open -static volatile bool cdcPortOpened = false; +// Default to true - assume connected until host explicitly clears DTR +static volatile bool cdcPortOpened = true; static void cdcCtrlLineStateCallback(void *context, uint16_t ctrlLineState) {