diff --git a/subsys/usb/device_next/class/usbd_cdc_acm.c b/subsys/usb/device_next/class/usbd_cdc_acm.c index 933396a597e0..0d3051af66e1 100644 --- a/subsys/usb/device_next/class/usbd_cdc_acm.c +++ b/subsys/usb/device_next/class/usbd_cdc_acm.c @@ -383,6 +383,10 @@ static void usbd_cdc_acm_disable(struct usbd_class_data *const c_data) atomic_clear_bit(&data->state, CDC_ACM_CLASS_ENABLED); atomic_clear_bit(&data->state, CDC_ACM_CLASS_SUSPENDED); + /* Clear TX_FIFO_BUSY on disable so a stuck IN transfer does not block + * the TX path after the host reconnects and re-enables the class. + */ + atomic_clear_bit(&data->state, CDC_ACM_TX_FIFO_BUSY); LOG_INF("Configuration disabled"); } @@ -653,6 +657,13 @@ static void cdc_acm_tx_fifo_handler(struct k_work *work) if (atomic_test_and_set_bit(&data->state, CDC_ACM_TX_FIFO_BUSY)) { LOG_DBG("TX transfer already in progress"); + /* Reschedule if data is waiting - guards against a stuck IN transfer + * where the host stops issuing IN tokens (e.g. macOS flow-control). + * The retry is a no-op once the completion fires and clears BUSY. + */ + if (!ring_buf_is_empty(data->tx_fifo.rb)) { + cdc_acm_work_schedule(&data->tx_fifo_work, K_MSEC(10)); + } return; } @@ -998,6 +1009,7 @@ static void cdc_acm_poll_out(const struct device *dev, const unsigned char c) struct cdc_acm_uart_data *const data = dev->data; k_spinlock_key_t key; uint32_t wrote; + int retries = 20; while (true) { key = k_spin_lock(&data->lock); @@ -1008,7 +1020,15 @@ static void cdc_acm_poll_out(const struct device *dev, const unsigned char c) break; } - if (k_is_in_isr() || !data->flow_ctrl) { + /* Bounded wait: with an attached-but-stalled host session (macOS + * ceases IN polling for minutes at a time), an unbounded sleep-retry + * here makes every console write take the full stall duration. Any + * thread that logs (event text loggers, assert reporting) then backs + * up its own queues and cascades into a system-wide com livelock. + * After ~20 ms of backpressure, treat the console as best-effort and + * discard, exactly like the detached (!flow_ctrl) case below. + */ + if (k_is_in_isr() || !data->flow_ctrl || retries-- <= 0) { LOG_WRN_ONCE("Ring buffer full, discard data"); break; }