ESPHome 2026.11.0-dev
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
20ESPHOME_LOG_TAG(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
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
107//==============================================================================
108// USBCDCACMInstance Implementation
109//==============================================================================
110
112 this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
113 if (this->usb_tx_ringbuf_ == nullptr) {
114 ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
115 this->parent_->mark_failed();
116 return;
117 }
118
119 this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
120 if (this->usb_rx_ringbuf_ == nullptr) {
121 ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
122 this->parent_->mark_failed();
123 return;
124 }
125
126 // Configure this CDC interface
127 const tinyusb_config_cdcacm_t acm_cfg = {
128 .cdc_port = static_cast<tinyusb_cdcacm_itf_t>(this->itf_),
129 .callback_rx = &tinyusb_cdc_rx_callback,
130 .callback_rx_wanted_char = NULL,
131 .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
132 .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
133 };
134
135 esp_err_t result = tinyusb_cdcacm_init(&acm_cfg);
136 if (result != ESP_OK) {
137 ESP_LOGE(TAG, "tinyusb_cdcacm_init failed: %d", result);
138 this->parent_->mark_failed();
139 return;
140 }
141
142 // Use a larger stack size for very verbose logging
143 constexpr size_t stack_size =
144 ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
145
146 // Create a simple, unique task name per interface
147 char task_name[] = "usb_tx_0";
148 task_name[sizeof(task_name) - 2] = format_hex_char(static_cast<char>(this->itf_));
149 xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
150
151 if (this->usb_tx_task_handle_ == nullptr) {
152 ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
153 this->parent_->mark_failed();
154 return;
155 }
156}
157
159 // Process events from the lock-free queue
160 this->process_events_();
161}
162
164
166 auto *instance = static_cast<USBCDCACMInstance *>(arg);
167 instance->usb_tx_task();
168}
169
171 uint8_t *data = this->usb_tx_staging_.data();
172 size_t tx_data_size = 0;
173 // Back-dated so a stall within the first LOG_THROTTLE_MS of uptime still logs
174 // immediately (unsigned arithmetic keeps this wrap-safe).
175 uint32_t stall_log_ms = millis() - LOG_THROTTLE_MS;
176
177 while (true) {
178 // Not holding any data while blocked waiting for more.
179 this->usb_tx_busy_ = 0;
180
181 // Wait for a notification from the bridge component
182 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
183
184 // Raise the busy flag before pulling data out of the ring buffer, so at every
185 // instant flush() sees pending bytes in the ring buffer count or in this flag.
186 this->usb_tx_busy_ = 1;
187
188 // When we do wake up, we can be sure there is data in the ring buffer
189 esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
190
191 if (ret != ESP_OK) {
192 ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
193 continue;
194 } else if (tx_data_size == 0) {
195 ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
196 continue;
197 }
198
199 ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
200#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
201 char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
202#endif
203 ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size));
204
205 // Serial data will be split up into 64 byte chunks to be sent over USB so this
206 // usually will take multiple iterations
207 uint8_t *data_head = &data[0];
208
209 while (tx_data_size > 0) {
210 size_t queued =
211 tinyusb_cdcacm_write_queue(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), data_head, tx_data_size);
212 ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
213
214 tx_data_size -= queued;
215 data_head += queued;
216
217 ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
218 esp_err_t flush_ret =
219 tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(10));
220
221 if (flush_ret == ESP_OK) {
222 continue;
223 }
224
225 // Bytes not yet handed to TinyUSB plus bytes still sitting in its transmit FIFO.
226 // tud_cdc_n_write_occupied() is not public API in the pinned TinyUSB release, so
227 // derive the occupancy from the FIFO depth TinyUSB itself is configured with.
228 const size_t pending = tx_data_size + (CFG_TUD_CDC_TX_BUFSIZE - tud_cdc_n_write_available(this->itf_));
229
230 // A flush timeout only means TinyUSB's transmit FIFO did not fully drain within
231 // the wait window; the queued bytes are untouched and TinyUSB keeps sending them
232 // from its transfer-complete callback once the host polls again. Clearing the
233 // FIFO here would discard the tail of a frame whose head is already on the wire,
234 // corrupting the stream mid-frame. Hold the data and retry instead; sustained
235 // backpressure then propagates to the ring buffer, which drops whole writes with
236 // a warning instead of splitting a frame.
237 //
238 // Gate the retry on DTR (tud_cdc_n_connected()) rather than tud_ready(): an
239 // enumerated-but-idle host (no application holding the port open) never polls
240 // the IN endpoint, so retrying on tud_ready() alone would wedge this task -- and
241 // stall every write_array()/flush() caller behind a full ring buffer -- for as
242 // long as the board sits plugged into an idle PC. DTR means an application has
243 // the port open and is expected to eventually read.
244 if (flush_ret == ESP_ERR_TIMEOUT && tud_cdc_n_connected(this->itf_)) {
245 const uint32_t now = millis();
246 if ((now - stall_log_ms) >= LOG_THROTTLE_MS) {
247 stall_log_ms = now;
248 ESP_LOGW(TAG, "USB TX itf=%d: host not reading; %zu bytes pending", this->itf_, pending);
249 }
250 continue;
251 }
252
253 if (flush_ret == ESP_ERR_TIMEOUT) {
254 // No application has the port open (DTR deasserted) or the device is detached,
255 // so the data cannot be delivered. TinyUSB does not clear its transmit FIFO on
256 // bus reset; drop the data here so a stale partial frame is not replayed when
257 // the port is (re)opened.
258 ESP_LOGW(TAG, "USB TX itf=%d: not connected; dropping %zu bytes", this->itf_, pending);
259 } else {
260 ESP_LOGE(TAG, "USB TX itf=%d: flush failed (%s); dropping %zu bytes", this->itf_, esp_err_to_name(flush_ret),
261 pending);
262 }
263 tud_cdc_n_write_clear(this->itf_);
264 break;
265 }
266 }
267}
268
269//==============================================================================
270// UARTComponent Interface Implementation
271//==============================================================================
272
273void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
274 if (len == 0) {
275 return;
276 }
277
278 // Write data to TX ring buffer
279 BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
280 if (send_res != pdTRUE) {
281 // During a sustained host stall the ring buffer stays full (that is the intended
282 // backpressure), so this path runs for every write; throttle the warning so the
283 // log stays readable. The counter is a running total that is never reset: each
284 // line reports all bytes dropped so far, so bytes dropped in the tail of one
285 // stall are still accounted for by the next line, whenever that is. It also makes
286 // the very first drop since boot detectable, which is logged unthrottled.
287 const bool first_drop = this->tx_dropped_bytes_ == 0;
288 this->tx_dropped_bytes_ += len;
289 const uint32_t now = millis();
290 if (first_drop || (now - this->tx_dropped_log_ms_) >= LOG_THROTTLE_MS) {
291 this->tx_dropped_log_ms_ = now;
292 ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %" PRIu32 " bytes dropped total", this->itf_, this->tx_dropped_bytes_);
293 }
294 return;
295 }
296
297 // Notify TX task that data is available
298 if (this->usb_tx_task_handle_ != nullptr) {
299 xTaskNotifyGive(this->usb_tx_task_handle_);
300 }
301}
302
303bool USBCDCACMInstance::peek_byte(uint8_t *data) {
304 if (this->has_peek_) {
305 *data = this->peek_buffer_;
306 return true;
307 }
308
309 if (this->read_byte(&this->peek_buffer_)) {
310 *data = this->peek_buffer_;
311 this->has_peek_ = true;
312 return true;
313 }
314
315 return false;
316}
317
318bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
319 if (len == 0) {
320 return true;
321 }
322
323 size_t original_len = len;
324 size_t bytes_read = 0;
325
326 // First, use the peek buffer if available
327 if (this->has_peek_) {
328 data[0] = this->peek_buffer_;
329 this->has_peek_ = false;
330 bytes_read = 1;
331 data++;
332 if (--len == 0) { // Decrement len first, then check it...
333 return true; // No more to read
334 }
335 }
336
337 // Read remaining bytes from RX ring buffer
338 size_t rx_size = 0;
339 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
340 if (buf == nullptr) {
341 return false;
342 }
343
344 memcpy(data, buf, rx_size);
345 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
346 bytes_read += rx_size;
347 data += rx_size;
348 len -= rx_size;
349 if (len == 0) {
350 return true; // No more to read
351 }
352
353 // Buffer's data may wrap around, in which case we should perform another read
354 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
355 if (buf == nullptr) {
356 return false;
357 }
358
359 memcpy(data, buf, rx_size);
360 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
361 bytes_read += rx_size;
362
363 return bytes_read == original_len;
364}
365
367 UBaseType_t waiting = 0;
368 if (this->usb_rx_ringbuf_ != nullptr) {
369 vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
370 }
371 return waiting + (this->has_peek_ ? 1 : 0);
372}
373
374// True while TX bytes have not yet reached TinyUSB's FIFO: still counted in the ring
375// buffer, or held by the TX task (usb_tx_busy_) between pulling them from the ring
376// buffer and handing them to TinyUSB -- there they are in neither the ring buffer
377// count nor TinyUSB's FIFO.
379 UBaseType_t waiting = 0;
380 vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
381 return waiting != 0 || this->usb_tx_busy_ != 0;
382}
383
385 if (this->usb_tx_ringbuf_ == nullptr) {
387 }
388
389 // Bound the wait: when the host stalls or disconnects, the TX task holds on to
390 // pending data rather than discarding it, so the ring buffer may not drain for as
391 // long as the host stays away. flush() runs on the caller's (typically the main
392 // loop) task and must not block indefinitely. Signed tick differences keep the
393 // deadline arithmetic wrap-safe.
394 TickType_t now = xTaskGetTickCount();
395 const TickType_t deadline = now + pdMS_TO_TICKS(FLUSH_TIMEOUT_MS);
396 while (this->tx_pending_()) {
397 if (static_cast<int32_t>(now - deadline) >= 0) {
399 }
400 vTaskDelay(pdMS_TO_TICKS(1));
401 now = xTaskGetTickCount();
402 }
403
404 // Also wait for USB to finish transmitting, within whatever remains of the budget.
405 // Floor at one tick: a zero-tick timeout takes esp_tinyusb's non-blocking branch,
406 // whose return contract is that library's internal detail and may differ between
407 // releases. One tick keeps the call on the blocking branch (ESP_OK/ESP_ERR_TIMEOUT)
408 // at the cost of at most one tick over budget.
409 const int32_t remaining = static_cast<int32_t>(deadline - now);
410 const TickType_t flush_ticks = remaining > 0 ? static_cast<TickType_t>(remaining) : 1;
411 switch (tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), flush_ticks)) {
412 case ESP_OK:
414 case ESP_ERR_TIMEOUT:
415 // ESP_ERR_NOT_FINISHED is the non-blocking branch's "still draining" result;
416 // mapped like a timeout in case a future esp_tinyusb release returns it here.
417 case ESP_ERR_NOT_FINISHED:
419 default:
421 }
422}
423
425
426} // namespace esphome::usb_cdc_acm
427#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:87
std::array< uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE > usb_tx_staging_
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
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.
ESPHOME_LOG_TAG(TAG, "usb_cdc_acm")
esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, TickType_t x_ticks_to_wait)
Definition usb_cdc_acm.h:25
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:1380
const void size_t len
Definition hal.h:68
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:498
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:1533
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t