ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
usb_cdc_acm_esp32.cpp
Go to the documentation of this file.
1#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
2 defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
3#include "usb_cdc_acm.h"
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
7
8#include <cstring>
9#include <sys/param.h>
10#include "freertos/FreeRTOS.h"
11#include "freertos/ringbuf.h"
12#include "freertos/task.h"
13#include "esp_log.h"
14
15#include "tusb.h"
16#include "tinyusb_cdc_acm.h"
17
18namespace esphome::usb_cdc_acm {
19
20static const char *const TAG = "usb_cdc_acm";
21
22// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
23static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168;
24
25static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
26static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
27
28// Upper bound on how long flush() may block in total: the TX ring buffer drain and
29// the final TinyUSB flush share this budget.
30static constexpr uint32_t FLUSH_TIMEOUT_MS = 100;
31
32// Minimum interval between repeated warnings while a host stall persists.
33static constexpr uint32_t LOG_THROTTLE_MS = 1000;
34
35static USBCDCACMInstance *get_instance_by_itf(int itf) {
36 if (global_usb_cdc_component == nullptr) {
37 return nullptr;
38 }
40}
41
42static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
43 USBCDCACMInstance *instance = get_instance_by_itf(itf);
44 if (instance == nullptr) {
45 ESP_LOGE(TAG, "RX callback: invalid interface %d", itf);
46 return;
47 }
48
49 size_t rx_size = 0;
50 static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0};
51
52 // read from USB
53 esp_err_t ret =
54 tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
55 ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
56#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
57 char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
58#endif
59 ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size));
60
61 if (ret == ESP_OK && rx_size > 0) {
62 RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
63 if (rx_ringbuf != nullptr) {
64 BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0);
65 if (send_res != pdTRUE) {
66 ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size);
67 } else {
68 ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size);
69 }
70 }
71 }
72}
73
74static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
75 USBCDCACMInstance *instance = get_instance_by_itf(itf);
76 if (instance == nullptr) {
77 ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf);
78 return;
79 }
80
81 int dtr = event->line_state_changed_data.dtr;
82 int rts = event->line_state_changed_data.rts;
83 ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts);
84
85 // Queue event for processing in main loop
86 instance->queue_line_state_event(dtr != 0, rts != 0);
87}
88
89static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) {
90 USBCDCACMInstance *instance = get_instance_by_itf(itf);
91 if (instance == nullptr) {
92 ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf);
93 return;
94 }
95
96 uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate;
97 uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits;
98 uint8_t parity = event->line_coding_changed_data.p_line_coding->parity;
99 uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits;
100 ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate,
101 stop_bits, parity, data_bits);
102
103 // Queue event for processing in main loop
104 instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
105}
106
107static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
108 TickType_t x_ticks_to_wait) {
109 size_t read_sz;
110 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz));
111
112 if (buf == nullptr) {
113 return ESP_FAIL;
114 }
115
116 memcpy(out_buf, buf, read_sz);
117 vRingbufferReturnItem(ring_buf, (void *) buf);
118 *rx_data_size = read_sz;
119
120 // Buffer's data can be wrapped, in which case we should perform another read
121 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
122 if (buf != nullptr) {
123 memcpy(out_buf + *rx_data_size, buf, read_sz);
124 vRingbufferReturnItem(ring_buf, (void *) buf);
125 *rx_data_size += read_sz;
126 }
127
128 return ESP_OK;
129}
130
131//==============================================================================
132// USBCDCACMInstance Implementation
133//==============================================================================
134
136 this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
137 if (this->usb_tx_ringbuf_ == nullptr) {
138 ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
139 this->parent_->mark_failed();
140 return;
141 }
142
143 this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
144 if (this->usb_rx_ringbuf_ == nullptr) {
145 ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
146 this->parent_->mark_failed();
147 return;
148 }
149
150 // Configure this CDC interface
151 const tinyusb_config_cdcacm_t acm_cfg = {
152 .cdc_port = static_cast<tinyusb_cdcacm_itf_t>(this->itf_),
153 .callback_rx = &tinyusb_cdc_rx_callback,
154 .callback_rx_wanted_char = NULL,
155 .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
156 .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
157 };
158
159 esp_err_t result = tinyusb_cdcacm_init(&acm_cfg);
160 if (result != ESP_OK) {
161 ESP_LOGE(TAG, "tinyusb_cdcacm_init failed: %d", result);
162 this->parent_->mark_failed();
163 return;
164 }
165
166 // Use a larger stack size for very verbose logging
167 constexpr size_t stack_size =
168 ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
169
170 // Create a simple, unique task name per interface
171 char task_name[] = "usb_tx_0";
172 task_name[sizeof(task_name) - 2] = format_hex_char(static_cast<char>(this->itf_));
173 xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
174
175 if (this->usb_tx_task_handle_ == nullptr) {
176 ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
177 this->parent_->mark_failed();
178 return;
179 }
180}
181
183 // Process events from the lock-free queue
184 this->process_events_();
185}
186
188
190 auto *instance = static_cast<USBCDCACMInstance *>(arg);
191 instance->usb_tx_task();
192}
193
195 uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
196 size_t tx_data_size = 0;
197 // Back-dated so a stall within the first LOG_THROTTLE_MS of uptime still logs
198 // immediately (unsigned arithmetic keeps this wrap-safe).
199 uint32_t stall_log_ms = millis() - LOG_THROTTLE_MS;
200
201 while (true) {
202 // Not holding any data while blocked waiting for more.
203 this->usb_tx_busy_ = 0;
204
205 // Wait for a notification from the bridge component
206 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
207
208 // Raise the busy flag before pulling data out of the ring buffer, so at every
209 // instant flush() sees pending bytes in the ring buffer count or in this flag.
210 this->usb_tx_busy_ = 1;
211
212 // When we do wake up, we can be sure there is data in the ring buffer
213 esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
214
215 if (ret != ESP_OK) {
216 ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
217 continue;
218 } else if (tx_data_size == 0) {
219 ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
220 continue;
221 }
222
223 ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
224#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
225 char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
226#endif
227 ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size));
228
229 // Serial data will be split up into 64 byte chunks to be sent over USB so this
230 // usually will take multiple iterations
231 uint8_t *data_head = &data[0];
232
233 while (tx_data_size > 0) {
234 size_t queued =
235 tinyusb_cdcacm_write_queue(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), data_head, tx_data_size);
236 ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
237
238 tx_data_size -= queued;
239 data_head += queued;
240
241 ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
242 esp_err_t flush_ret =
243 tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(10));
244
245 if (flush_ret == ESP_OK) {
246 continue;
247 }
248
249 // Bytes not yet handed to TinyUSB plus bytes still sitting in its transmit FIFO.
250 // tud_cdc_n_write_occupied() is not public API in the pinned TinyUSB release, so
251 // derive the occupancy from the FIFO depth TinyUSB itself is configured with.
252 const size_t pending = tx_data_size + (CFG_TUD_CDC_TX_BUFSIZE - tud_cdc_n_write_available(this->itf_));
253
254 // A flush timeout only means TinyUSB's transmit FIFO did not fully drain within
255 // the wait window; the queued bytes are untouched and TinyUSB keeps sending them
256 // from its transfer-complete callback once the host polls again. Clearing the
257 // FIFO here would discard the tail of a frame whose head is already on the wire,
258 // corrupting the stream mid-frame. Hold the data and retry instead; sustained
259 // backpressure then propagates to the ring buffer, which drops whole writes with
260 // a warning instead of splitting a frame.
261 //
262 // Gate the retry on DTR (tud_cdc_n_connected()) rather than tud_ready(): an
263 // enumerated-but-idle host (no application holding the port open) never polls
264 // the IN endpoint, so retrying on tud_ready() alone would wedge this task -- and
265 // stall every write_array()/flush() caller behind a full ring buffer -- for as
266 // long as the board sits plugged into an idle PC. DTR means an application has
267 // the port open and is expected to eventually read.
268 if (flush_ret == ESP_ERR_TIMEOUT && tud_cdc_n_connected(this->itf_)) {
269 const uint32_t now = millis();
270 if ((now - stall_log_ms) >= LOG_THROTTLE_MS) {
271 stall_log_ms = now;
272 ESP_LOGW(TAG, "USB TX itf=%d: host not reading; %zu bytes pending", this->itf_, pending);
273 }
274 continue;
275 }
276
277 if (flush_ret == ESP_ERR_TIMEOUT) {
278 // No application has the port open (DTR deasserted) or the device is detached,
279 // so the data cannot be delivered. TinyUSB does not clear its transmit FIFO on
280 // bus reset; drop the data here so a stale partial frame is not replayed when
281 // the port is (re)opened.
282 ESP_LOGW(TAG, "USB TX itf=%d: not connected; dropping %zu bytes", this->itf_, pending);
283 } else {
284 ESP_LOGE(TAG, "USB TX itf=%d: flush failed (%s); dropping %zu bytes", this->itf_, esp_err_to_name(flush_ret),
285 pending);
286 }
287 tud_cdc_n_write_clear(this->itf_);
288 break;
289 }
290 }
291}
292
293//==============================================================================
294// UARTComponent Interface Implementation
295//==============================================================================
296
297void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
298 if (len == 0) {
299 return;
300 }
301
302 // Write data to TX ring buffer
303 BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
304 if (send_res != pdTRUE) {
305 // During a sustained host stall the ring buffer stays full (that is the intended
306 // backpressure), so this path runs for every write; throttle the warning so the
307 // log stays readable. The counter is a running total that is never reset: each
308 // line reports all bytes dropped so far, so bytes dropped in the tail of one
309 // stall are still accounted for by the next line, whenever that is. It also makes
310 // the very first drop since boot detectable, which is logged unthrottled.
311 const bool first_drop = this->tx_dropped_bytes_ == 0;
312 this->tx_dropped_bytes_ += len;
313 const uint32_t now = millis();
314 if (first_drop || (now - this->tx_dropped_log_ms_) >= LOG_THROTTLE_MS) {
315 this->tx_dropped_log_ms_ = now;
316 ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %" PRIu32 " bytes dropped total", this->itf_, this->tx_dropped_bytes_);
317 }
318 return;
319 }
320
321 // Notify TX task that data is available
322 if (this->usb_tx_task_handle_ != nullptr) {
323 xTaskNotifyGive(this->usb_tx_task_handle_);
324 }
325}
326
327bool USBCDCACMInstance::peek_byte(uint8_t *data) {
328 if (this->has_peek_) {
329 *data = this->peek_buffer_;
330 return true;
331 }
332
333 if (this->read_byte(&this->peek_buffer_)) {
334 *data = this->peek_buffer_;
335 this->has_peek_ = true;
336 return true;
337 }
338
339 return false;
340}
341
342bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
343 if (len == 0) {
344 return true;
345 }
346
347 size_t original_len = len;
348 size_t bytes_read = 0;
349
350 // First, use the peek buffer if available
351 if (this->has_peek_) {
352 data[0] = this->peek_buffer_;
353 this->has_peek_ = false;
354 bytes_read = 1;
355 data++;
356 if (--len == 0) { // Decrement len first, then check it...
357 return true; // No more to read
358 }
359 }
360
361 // Read remaining bytes from RX ring buffer
362 size_t rx_size = 0;
363 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
364 if (buf == nullptr) {
365 return false;
366 }
367
368 memcpy(data, buf, rx_size);
369 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
370 bytes_read += rx_size;
371 data += rx_size;
372 len -= rx_size;
373 if (len == 0) {
374 return true; // No more to read
375 }
376
377 // Buffer's data may wrap around, in which case we should perform another read
378 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
379 if (buf == nullptr) {
380 return false;
381 }
382
383 memcpy(data, buf, rx_size);
384 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
385 bytes_read += rx_size;
386
387 return bytes_read == original_len;
388}
389
391 UBaseType_t waiting = 0;
392 if (this->usb_rx_ringbuf_ != nullptr) {
393 vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
394 }
395 return waiting + (this->has_peek_ ? 1 : 0);
396}
397
398// True while TX bytes have not yet reached TinyUSB's FIFO: still counted in the ring
399// buffer, or held by the TX task (usb_tx_busy_) between pulling them from the ring
400// buffer and handing them to TinyUSB -- there they are in neither the ring buffer
401// count nor TinyUSB's FIFO.
403 UBaseType_t waiting = 0;
404 vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
405 return waiting != 0 || this->usb_tx_busy_ != 0;
406}
407
409 if (this->usb_tx_ringbuf_ == nullptr) {
411 }
412
413 // Bound the wait: when the host stalls or disconnects, the TX task holds on to
414 // pending data rather than discarding it, so the ring buffer may not drain for as
415 // long as the host stays away. flush() runs on the caller's (typically the main
416 // loop) task and must not block indefinitely. Signed tick differences keep the
417 // deadline arithmetic wrap-safe.
418 TickType_t now = xTaskGetTickCount();
419 const TickType_t deadline = now + pdMS_TO_TICKS(FLUSH_TIMEOUT_MS);
420 while (this->tx_pending_()) {
421 if (static_cast<int32_t>(now - deadline) >= 0) {
423 }
424 vTaskDelay(pdMS_TO_TICKS(1));
425 now = xTaskGetTickCount();
426 }
427
428 // Also wait for USB to finish transmitting, within whatever remains of the budget.
429 // Floor at one tick: a zero-tick timeout takes esp_tinyusb's non-blocking branch,
430 // whose return contract is that library's internal detail and may differ between
431 // releases. One tick keeps the call on the blocking branch (ESP_OK/ESP_ERR_TIMEOUT)
432 // at the cost of at most one tick over budget.
433 const int32_t remaining = static_cast<int32_t>(deadline - now);
434 const TickType_t flush_ticks = remaining > 0 ? static_cast<TickType_t>(remaining) : 1;
435 switch (tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), flush_ticks)) {
436 case ESP_OK:
438 case ESP_ERR_TIMEOUT:
439 // ESP_ERR_NOT_FINISHED is the non-blocking branch's "still draining" result;
440 // mapped like a timeout in case a future esp_tinyusb release returns it here.
441 case ESP_ERR_NOT_FINISHED:
443 default:
445 }
446}
447
449
450} // namespace esphome::usb_cdc_acm
451#endif
bool read_byte(uint8_t *data)
USBCDCACMInstance * get_interface_by_number(uint8_t itf)
Represents a single CDC ACM interface instance.
Definition usb_cdc_acm.h:55
bool read_array(uint8_t *data, size_t len) override
uart::UARTFlushResult flush() override
void write_array(const uint8_t *data, size_t len) override
int ret
const char *const TAG
Definition spi.cpp:7
UARTFlushResult
Result of a flush() call.
@ UART_FLUSH_RESULT_ASSUMED_SUCCESS
Platform cannot report result; success is assumed.
@ UART_FLUSH_RESULT_SUCCESS
Confirmed: all bytes left the TX FIFO.
@ UART_FLUSH_RESULT_FAILED
Confirmed: driver or hardware error.
@ UART_FLUSH_RESULT_TIMEOUT
Confirmed: timed out before TX completed.
USBCDCACMComponent * global_usb_cdc_component
ESPHOME_ALWAYS_INLINE char format_hex_char(uint8_t v, char base)
Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase)
Definition helpers.h:1273
const void size_t len
Definition hal.h:64
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:406
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1426
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t