Skip to content

Commit d22111c

Browse files
author
Vyacheslav Furyst
committed
fix(usbh_cdc/ecm): fix data pipe stalls for CDC-ECM on SW reboot and alt-setting switch
Three related fixes for CDC-ECM bulk data pipe reliability: 1. in_xfer_cb: handle CANCELED separately — resubmit the IN transfer so the receive chain restarts automatically after SET_INTERFACE flushes the endpoint during alt=0→alt=1 switch. Remove the retry logic from the default (error) case: leave the pipe HALTED so the USB host client's USBH_EP_CMD_FLUSH can run without hitting ESP_ERR_INVALID_STATE. 2. Add usbh_cdc_restart_in_transfer(): called by the ECM layer after SET_INTERFACE(alt=1) completes to clear the HALTED IN pipe and resubmit the IN transfer chain. 3. Add usbh_cdc_restart_out_transfer(): on a software reboot the device is not power-cycled, so the OUT pipe may be left HALTED from the previous session. Clears the halt and releases the semaphore, guarded by a semaphore-count check so it is a no-op on a healthy pipe (safe to call on both HW and SW reboots). 4. Set USB_TRANSFER_FLAG_ZERO_PACK on the bulk OUT transfer so a ZLP is sent when payload size is an exact multiple of the 64-byte MPS, as required by CDC-ECM spec §3.3.1. Both restart functions are called from iot_usbh_ecm after SET_INTERFACE(alt=1) completes. Tested on ESP32-S3 with a USB modem (VID:1F94 PID:3002),verified on both hardware power-cycle and software reboot (esp_restart()).
1 parent ab9021a commit d22111c

3 files changed

Lines changed: 116 additions & 6 deletions

File tree

components/usb/iot_usbh_cdc/include/iot_usbh_cdc.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,34 @@ esp_err_t usbh_cdc_get_dev_handle(usbh_cdc_port_handle_t cdc_port_handle, usb_de
381381
*/
382382
esp_err_t usbh_cdc_port_get_intf_desc(usbh_cdc_port_handle_t port_handle, const usb_intf_desc_t **notif_intf, const usb_intf_desc_t **data_intf);
383383

384+
/**
385+
* @brief Restart the bulk IN transfer chain after SET_INTERFACE(alt=1) completes.
386+
*
387+
* The IN endpoint is unavailable while the data interface is in alt=0. Any
388+
* pending IN transfer fails with STATUS_ERROR and leaves the pipe HALTED.
389+
* Call this from the ECM layer once alt=1 is active so the receive path
390+
* becomes live again.
391+
*
392+
* @param[in] cdc_port_handle CDC port handle
393+
* @return ESP_OK on success, ESP_ERR_INVALID_STATE if handle or transfer is NULL
394+
*/
395+
esp_err_t usbh_cdc_restart_in_transfer(usbh_cdc_port_handle_t cdc_port_handle);
396+
397+
/**
398+
* @brief Recover the bulk OUT pipe after a software reboot.
399+
*
400+
* On a software reboot the USB device is not power-cycled. A failed OUT
401+
* transfer from the previous session can leave the pipe HALTED, blocking all
402+
* future transmits. This function clears the halt and releases the semaphore
403+
* only when the pipe is actually stalled (semaphore count == 0), so it is
404+
* safe to call unconditionally after SET_INTERFACE(alt=1) on both HW and SW
405+
* reboots.
406+
*
407+
* @param[in] cdc_port_handle CDC port handle
408+
* @return ESP_OK on success, ESP_ERR_INVALID_STATE if handle or transfer is NULL
409+
*/
410+
esp_err_t usbh_cdc_restart_out_transfer(usbh_cdc_port_handle_t cdc_port_handle);
411+
384412
#ifdef __cplusplus
385413
}
386414
#endif

components/usb/iot_usbh_cdc/iot_usbh_cdc.c

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -570,15 +570,36 @@ static void in_xfer_cb(usb_transfer_t *in_xfer)
570570
return;
571571
}
572572
case USB_TRANSFER_STATUS_NO_DEVICE:
573-
case USB_TRANSFER_STATUS_CANCELED:
574573
return;
575-
default:
576-
// Any other error
577-
ESP_LOGW(TAG, "IN Transfer failed, EP:%d, status %d", in_xfer->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK, in_xfer->status);
578-
if (retry_count++ < CONFIG_USBH_CDC_IN_EP_RETRY_COUNT) {
579-
_cdc_reset_transfer_endpoint(cdc_port->cdc_dev->dev_hdl, in_xfer);
574+
case USB_TRANSFER_STATUS_CANCELED:
575+
/*
576+
* CANCELED fires when the USB host flushes the endpoint during
577+
* SET_INTERFACE (alt=0 → alt=1 for CDC-ECM). Resubmit so the IN
578+
* transfer chain restarts automatically once alt=1 is active and the
579+
* bulk endpoint physically exists. Skip if the port is being closed.
580+
*/
581+
if (!cdc_port->to_close) {
582+
retry_count = 0;
580583
_cdc_host_transfer_submit(cdc_port, in_xfer);
581584
}
585+
return;
586+
default:
587+
/*
588+
* IN transfer failed (typically STATUS_ERROR on alt=0 before SET_INTERFACE).
589+
*
590+
* Do NOT clear the pipe or resubmit here. _handle_pending_ep (USB host
591+
* client error handler) calls USBH_EP_CMD_FLUSH on the endpoint, which
592+
* requires the pipe to be in HALTED state. Calling endpoint_clear
593+
* (HALTED→ACTIVE) before that FLUSH runs causes an ESP_ERR_INVALID_STATE
594+
* crash.
595+
*
596+
* The pipe stays HALTED after every error. The ECM layer calls
597+
* usbh_cdc_restart_in_transfer() after SET_INTERFACE completes (alt=1
598+
* is active) to restart the IN transfer chain cleanly.
599+
*/
600+
ESP_LOGW(TAG, "IN Transfer failed, EP:%d, status %d",
601+
in_xfer->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_NUM_MASK,
602+
in_xfer->status);
582603
break;
583604
}
584605
}
@@ -608,6 +629,49 @@ static void out_xfer_cb(usb_transfer_t *out_xfer)
608629
}
609630
}
610631

632+
/*
633+
* Restart the IN bulk transfer chain after the interface has switched to alt=1.
634+
* Must be called from the ECM layer once SET_INTERFACE(alt=1) has completed and
635+
* the bulk IN endpoint is physically present.
636+
*/
637+
esp_err_t usbh_cdc_restart_in_transfer(usbh_cdc_port_handle_t handle)
638+
{
639+
usbh_cdc_port_t *cdc_port = (usbh_cdc_port_t *)handle;
640+
if (!cdc_port || !cdc_port->data.in_xfer) {
641+
return ESP_ERR_INVALID_STATE;
642+
}
643+
/* Clear HALTED→ACTIVE before submitting */
644+
usb_host_endpoint_clear(cdc_port->cdc_dev->dev_hdl,
645+
cdc_port->data.in_xfer->bEndpointAddress);
646+
return _cdc_host_transfer_submit(cdc_port, cdc_port->data.in_xfer);
647+
}
648+
649+
/*
650+
* Recover the OUT bulk pipe after a software reboot.
651+
*
652+
* On a software reboot the USB device is not power-cycled, so a failed OUT
653+
* transfer from the previous session can leave the pipe in HALTED state.
654+
* This function clears the halt and releases the semaphore so future writes
655+
* can proceed. The semaphore-count guard prevents touching a healthy pipe
656+
* (which would produce a spurious ESP_ERR_INVALID_STATE from the USB host
657+
* layer on a normal hardware reboot).
658+
*
659+
* Must be called from the ECM layer after SET_INTERFACE(alt=1) completes.
660+
*/
661+
esp_err_t usbh_cdc_restart_out_transfer(usbh_cdc_port_handle_t handle)
662+
{
663+
usbh_cdc_port_t *cdc_port = (usbh_cdc_port_t *)handle;
664+
if (!cdc_port || !cdc_port->data.out_xfer || cdc_port->out_ringbuf_handle) {
665+
return ESP_ERR_INVALID_STATE;
666+
}
667+
if (uxSemaphoreGetCount(cdc_port->data.out_xfer_free_sem) == 0) {
668+
usb_host_endpoint_clear(cdc_port->cdc_dev->dev_hdl,
669+
cdc_port->data.out_xfer->bEndpointAddress);
670+
xSemaphoreGive(cdc_port->data.out_xfer_free_sem);
671+
}
672+
return ESP_OK;
673+
}
674+
611675
static void _cdc_tx_xfer_submit(usb_transfer_t *out_xfer)
612676
{
613677
usbh_cdc_port_t *cdc_port = (usbh_cdc_port_t *) out_xfer->context;
@@ -706,6 +770,11 @@ static esp_err_t _cdc_transfers_allocate(usbh_cdc_port_t *cdc_port, const usb_ep
706770
cdc_port->data.out_xfer->context = cdc_port;
707771
cdc_port->data.out_xfer->num_bytes = out_buf_len;
708772
cdc_port->data.out_xfer->callback = out_xfer_cb;
773+
/* CDC-ECM spec §3.3.1: host MUST send a ZLP when a segment size is
774+
* an exact multiple of the bulk OUT MPS, so the device can detect
775+
* end-of-transfer. Without this flag the device's BULK OUT endpoint
776+
* stalls permanently on such transfers. */
777+
cdc_port->data.out_xfer->flags = USB_TRANSFER_FLAG_ZERO_PACK;
709778
}
710779
}
711780
return ret;

components/usb/iot_usbh_ecm/iot_usbh_ecm.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,19 @@ static void _usbh_ecm_task(void *arg)
265265
if (events & ECM_DEV_CONNECTED) {
266266
_ecm_config_intf(ecm);
267267
connected_time_ms = pdTICKS_TO_MS(xTaskGetTickCount());
268+
/*
269+
* The IN bulk endpoint fails with STATUS_ERROR while the interface
270+
* is in alt=0 (no physical endpoint), leaving the pipe HALTED.
271+
* The OUT pipe may also be HALTED after a software reboot (device
272+
* not power-cycled, stale HALTED state from previous session).
273+
*
274+
* SET_INTERFACE(alt=1) is now complete — restart both transfer
275+
* chains so the data path is live before "Connected" fires.
276+
*/
277+
if (ecm->cdc_port) {
278+
usbh_cdc_restart_in_transfer(ecm->cdc_port);
279+
usbh_cdc_restart_out_transfer(ecm->cdc_port);
280+
}
268281
}
269282
if (events & ECM_STOP) {
270283
ESP_LOGI(TAG, "USB ECM stop");

0 commit comments

Comments
 (0)