ESPHome 2026.1.0-dev
Loading...
Searching...
No Matches
usb_cdc_acm.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#include "usb_cdc_acm.h"
4#include "esphome/core/log.h"
5
6#include <sys/param.h>
7#include "freertos/FreeRTOS.h"
8#include "freertos/ringbuf.h"
9#include "freertos/task.h"
10#include "esp_log.h"
11
12#include "tusb.h"
13#include "tusb_cdc_acm.h"
14
16
17static const char *TAG = "usb_cdc_acm";
18
19static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
20static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
21
22// Global component instance for managing USB device
24
25static USBCDCACMInstance *get_instance_by_itf(int itf) {
26 if (global_usb_cdc_component == nullptr) {
27 return nullptr;
28 }
30}
31
32static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
33 USBCDCACMInstance *instance = get_instance_by_itf(itf);
34 if (instance == nullptr) {
35 ESP_LOGE(TAG, "RX callback: invalid interface %d", itf);
36 return;
37 }
38
39 size_t rx_size = 0;
40 static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0};
41
42 // read from USB
43 esp_err_t ret =
44 tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
45 ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
46 ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str());
47
48 if (ret == ESP_OK && rx_size > 0) {
49 RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
50 if (rx_ringbuf != nullptr) {
51 BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0);
52 if (send_res != pdTRUE) {
53 ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size);
54 } else {
55 ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size);
56 }
57 }
58 }
59}
60
61static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
62 USBCDCACMInstance *instance = get_instance_by_itf(itf);
63 if (instance == nullptr) {
64 ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf);
65 return;
66 }
67
68 int dtr = event->line_state_changed_data.dtr;
69 int rts = event->line_state_changed_data.rts;
70 ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts);
71
72 // Queue event for processing in main loop
73 instance->queue_line_state_event(dtr != 0, rts != 0);
74}
75
76static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) {
77 USBCDCACMInstance *instance = get_instance_by_itf(itf);
78 if (instance == nullptr) {
79 ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf);
80 return;
81 }
82
83 uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate;
84 uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits;
85 uint8_t parity = event->line_coding_changed_data.p_line_coding->parity;
86 uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits;
87 ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate,
88 stop_bits, parity, data_bits);
89
90 // Queue event for processing in main loop
91 instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
92}
93
94static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
95 TickType_t xTicksToWait) {
96 size_t read_sz;
97 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz));
98
99 if (buf == nullptr) {
100 return ESP_FAIL;
101 }
102
103 memcpy(out_buf, buf, read_sz);
104 vRingbufferReturnItem(ring_buf, (void *) buf);
105 *rx_data_size = read_sz;
106
107 // Buffer's data can be wrapped, in which case we should perform another read
108 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
109 if (buf != nullptr) {
110 memcpy(out_buf + *rx_data_size, buf, read_sz);
111 vRingbufferReturnItem(ring_buf, (void *) buf);
112 *rx_data_size += read_sz;
113 }
114
115 return ESP_OK;
116}
117
118//==============================================================================
119// USBCDCACMInstance Implementation
120//==============================================================================
121
123 this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
124 if (this->usb_tx_ringbuf_ == nullptr) {
125 ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
126 this->parent_->mark_failed();
127 return;
128 }
129
130 this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
131 if (this->usb_rx_ringbuf_ == nullptr) {
132 ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
133 this->parent_->mark_failed();
134 return;
135 }
136
137 // Configure this CDC interface
138 const tinyusb_config_cdcacm_t acm_cfg = {
139 .usb_dev = TINYUSB_USBDEV_0,
140 .cdc_port = this->itf_,
141 .callback_rx = &tinyusb_cdc_rx_callback,
142 .callback_rx_wanted_char = NULL,
143 .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
144 .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
145 };
146
147 esp_err_t result = tusb_cdc_acm_init(&acm_cfg);
148 if (result != ESP_OK) {
149 ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result);
150 this->parent_->mark_failed();
151 return;
152 }
153
154 // Use a larger stack size for (very) verbose logging
155 const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
156
157 // Create a simple, unique task name per interface
158 char task_name[] = "usb_tx_0";
159 task_name[sizeof(task_name) - 1] = format_hex_char(static_cast<char>(this->itf_));
160 xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
161
162 if (this->usb_tx_task_handle_ == nullptr) {
163 ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
164 this->parent_->mark_failed();
165 return;
166 }
167}
168
170 // Process events from the lock-free queue
171 this->process_events_();
172}
173
175 // Allocate event from pool
176 CDCEvent *event = this->event_pool_.allocate();
177 if (event == nullptr) {
178 ESP_LOGW(TAG, "Event pool exhausted, line state event dropped (itf=%d)", this->itf_);
179 return;
180 }
181
183 event->data.line_state.dtr = dtr;
184 event->data.line_state.rts = rts;
185
186 if (!this->event_queue_.push(event)) {
187 ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
188 // Return event to pool since we couldn't queue it
189 this->event_pool_.release(event);
190 } else {
191 // Wake main loop immediately to process event
192#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
194#endif
195 }
196}
197
198void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
199 uint8_t data_bits) {
200 // Allocate event from pool
201 CDCEvent *event = this->event_pool_.allocate();
202 if (event == nullptr) {
203 ESP_LOGW(TAG, "Event pool exhausted, line coding event dropped (itf=%d)", this->itf_);
204 return;
205 }
206
208 event->data.line_coding.bit_rate = bit_rate;
209 event->data.line_coding.stop_bits = stop_bits;
210 event->data.line_coding.parity = parity;
211 event->data.line_coding.data_bits = data_bits;
212
213 if (!this->event_queue_.push(event)) {
214 ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
215 // Return event to pool since we couldn't queue it
216 this->event_pool_.release(event);
217 } else {
218 // Wake main loop immediately to process event
219#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
221#endif
222 }
223}
224
226 // Process all pending events from the queue
227 CDCEvent *event;
228 while ((event = this->event_queue_.pop()) != nullptr) {
229 switch (event->type) {
231 bool dtr = event->data.line_state.dtr;
232 bool rts = event->data.line_state.rts;
233
234 // Invoke user callback in main loop context
235 if (this->line_state_callback_ != nullptr) {
236 this->line_state_callback_(dtr, rts);
237 }
238 break;
239 }
241 uint32_t bit_rate = event->data.line_coding.bit_rate;
242 uint8_t stop_bits = event->data.line_coding.stop_bits;
243 uint8_t parity = event->data.line_coding.parity;
244 uint8_t data_bits = event->data.line_coding.data_bits;
245
246 // Update UART configuration based on CDC line coding
247 this->baud_rate_ = bit_rate;
248 this->data_bits_ = data_bits;
249
250 // Convert CDC stop bits to UART stop bits format
251 // CDC: 0=1 stop bit, 1=1.5 stop bits, 2=2 stop bits
252 this->stop_bits_ = (stop_bits == 0) ? 1 : (stop_bits == 1) ? 1 : 2;
253
254 // Convert CDC parity to UART parity format
255 // CDC: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space
256 switch (parity) {
257 case 0:
259 break;
260 case 1:
262 break;
263 case 2:
265 break;
266 default:
267 // Mark and Space parity are not commonly supported, default to None
269 break;
270 }
271
272 // Invoke user callback in main loop context
273 if (this->line_coding_callback_ != nullptr) {
274 this->line_coding_callback_(bit_rate, stop_bits, parity, data_bits);
275 }
276 break;
277 }
278 }
279 // Return event to pool for reuse
280 this->event_pool_.release(event);
281 }
282}
283
285 auto *instance = static_cast<USBCDCACMInstance *>(arg);
286 instance->usb_tx_task();
287}
288
290 uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
291 size_t tx_data_size = 0;
292
293 while (1) {
294 // Wait for a notification from the bridge component
295 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
296
297 // When we do wake up, we can be sure there is data in the ring buffer
298 esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
299
300 if (ret != ESP_OK) {
301 ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
302 continue;
303 } else if (tx_data_size == 0) {
304 ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
305 continue;
306 }
307
308 ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
309 ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str());
310
311 // Serial data will be split up into 64 byte chunks to be sent over USB so this
312 // usually will take multiple iterations
313 uint8_t *data_head = &data[0];
314
315 while (tx_data_size > 0) {
316 size_t queued = tinyusb_cdcacm_write_queue(this->itf_, data_head, tx_data_size);
317 ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
318
319 tx_data_size -= queued;
320 data_head += queued;
321
322 ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
323 esp_err_t flush_ret = tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(10));
324
325 if (flush_ret != ESP_OK) {
326 ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_);
327 tud_cdc_n_write_clear(this->itf_);
328 break;
329 }
330 }
331 }
332}
333
334//==============================================================================
335// UARTComponent Interface Implementation
336//==============================================================================
337
338void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
339 if (len == 0) {
340 return;
341 }
342
343 // Write data to TX ring buffer
344 BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
345 if (send_res != pdTRUE) {
346 ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len);
347 return;
348 }
349
350 // Notify TX task that data is available
351 if (this->usb_tx_task_handle_ != nullptr) {
352 xTaskNotifyGive(this->usb_tx_task_handle_);
353 }
354}
355
356bool USBCDCACMInstance::peek_byte(uint8_t *data) {
357 if (this->has_peek_) {
358 *data = this->peek_buffer_;
359 return true;
360 }
361
362 if (this->read_byte(&this->peek_buffer_)) {
363 *data = this->peek_buffer_;
364 this->has_peek_ = true;
365 return true;
366 }
367
368 return false;
369}
370
371bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
372 if (len == 0) {
373 return true;
374 }
375
376 size_t original_len = len;
377 size_t bytes_read = 0;
378
379 // First, use the peek buffer if available
380 if (this->has_peek_) {
381 data[0] = this->peek_buffer_;
382 this->has_peek_ = false;
383 bytes_read = 1;
384 data++;
385 if (--len == 0) { // Decrement len first, then check it...
386 return true; // No more to read
387 }
388 }
389
390 // Read remaining bytes from RX ring buffer
391 size_t rx_size = 0;
392 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
393 if (buf == nullptr) {
394 return false;
395 }
396
397 memcpy(data, buf, rx_size);
398 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
399 bytes_read += rx_size;
400 data += rx_size;
401 len -= rx_size;
402 if (len == 0) {
403 return true; // No more to read
404 }
405
406 // Buffer's data may wrap around, in which case we should perform another read
407 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
408 if (buf == nullptr) {
409 return false;
410 }
411
412 memcpy(data, buf, rx_size);
413 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
414 bytes_read += rx_size;
415
416 return bytes_read == original_len;
417}
418
420 UBaseType_t waiting = 0;
421 if (this->usb_rx_ringbuf_ != nullptr) {
422 vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
423 }
424 return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0);
425}
426
428 // Wait for TX ring buffer to be empty
429 if (this->usb_tx_ringbuf_ == nullptr) {
430 return;
431 }
432
433 UBaseType_t waiting = 1;
434 while (waiting > 0) {
435 vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
436 if (waiting > 0) {
437 vTaskDelay(pdMS_TO_TICKS(1));
438 }
439 }
440
441 // Also wait for USB to finish transmitting
442 tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(100));
443}
444
445//==============================================================================
446// USBCDCACMComponent Implementation
447//==============================================================================
448
450
452 // Setup all registered interfaces
453 for (auto interface : this->interfaces_) {
454 if (interface != nullptr) {
455 interface->setup();
456 }
457 }
458}
459
461 // Call loop() on all registered interfaces to process events
462 for (auto interface : this->interfaces_) {
463 if (interface != nullptr) {
464 interface->loop();
465 }
466 }
467}
468
470 ESP_LOGCONFIG(TAG,
471 "USB CDC-ACM:\n"
472 " Number of Interfaces: %d",
473 this->interfaces_[MAX_USB_CDC_INSTANCES - 1] != nullptr ? MAX_USB_CDC_INSTANCES : 1);
474}
475
477 uint8_t itf_num = static_cast<uint8_t>(interface->get_itf());
478 if (itf_num < MAX_USB_CDC_INSTANCES) {
479 this->interfaces_[itf_num] = interface;
480 } else {
481 ESP_LOGE(TAG, "Interface number must be less than %u", MAX_USB_CDC_INSTANCES);
482 }
483}
484
486 for (auto interface : this->interfaces_) {
487 if ((interface != nullptr) && (interface->get_itf() == static_cast<tinyusb_cdcacm_itf_t>(itf))) {
488 return interface;
489 }
490 }
491 return nullptr;
492}
493
494} // namespace esphome::usb_cdc_acm
495#endif
void wake_loop_threadsafe()
Wake the main event loop from a FreeRTOS task Thread-safe, can be called from task context to immedia...
bool read_byte(uint8_t *data)
Main USB CDC ACM component that manages the USB device and all CDC interfaces.
void add_interface(USBCDCACMInstance *interface)
USBCDCACMInstance * get_interface_by_number(uint8_t itf)
std::array< USBCDCACMInstance *, MAX_USB_CDC_INSTANCES > interfaces_
Represents a single CDC ACM interface instance.
Definition usb_cdc_acm.h:54
bool read_array(uint8_t *data, size_t len) override
EventPool< CDCEvent, EVENT_QUEUE_SIZE > event_pool_
bool peek_byte(uint8_t *data) override
tinyusb_cdcacm_itf_t get_itf() const
Definition usb_cdc_acm.h:62
LockFreeQueue< CDCEvent, EVENT_QUEUE_SIZE > event_queue_
void queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits)
void write_array(const uint8_t *data, size_t len) override
void queue_line_state_event(bool dtr, bool rts)
USBCDCACMComponent * global_usb_cdc_component
std::string size_t len
Definition helpers.h:518
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length)
Format a byte array in pretty-printed, human-readable hex format.
Definition helpers.cpp:326
char format_hex_char(uint8_t v)
Convert a nibble (0-15) to lowercase hex char.
Definition helpers.h:654
Application App
Global storage of Application pointer - only one Application can exist.