ESPHome 2026.8.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/log.h"
6
7#include <cstring>
8#include <sys/param.h>
9#include "freertos/FreeRTOS.h"
10#include "freertos/ringbuf.h"
11#include "freertos/task.h"
12#include "esp_log.h"
13
14#include "tusb.h"
15#include "tinyusb_cdc_acm.h"
16
17namespace esphome::usb_cdc_acm {
18
19static const char *const TAG = "usb_cdc_acm";
20
21// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
22static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168;
23
24static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
25static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
26
27static USBCDCACMInstance *get_instance_by_itf(int itf) {
28 if (global_usb_cdc_component == nullptr) {
29 return nullptr;
30 }
32}
33
34static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
35 USBCDCACMInstance *instance = get_instance_by_itf(itf);
36 if (instance == nullptr) {
37 ESP_LOGE(TAG, "RX callback: invalid interface %d", itf);
38 return;
39 }
40
41 size_t rx_size = 0;
42 static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0};
43
44 // read from USB
45 esp_err_t ret =
46 tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
47 ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
48#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
49 char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
50#endif
51 ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size));
52
53 if (ret == ESP_OK && rx_size > 0) {
54 RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
55 if (rx_ringbuf != nullptr) {
56 BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0);
57 if (send_res != pdTRUE) {
58 ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size);
59 } else {
60 ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size);
61 }
62 }
63 }
64}
65
66static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) {
67 USBCDCACMInstance *instance = get_instance_by_itf(itf);
68 if (instance == nullptr) {
69 ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf);
70 return;
71 }
72
73 int dtr = event->line_state_changed_data.dtr;
74 int rts = event->line_state_changed_data.rts;
75 ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts);
76
77 // Queue event for processing in main loop
78 instance->queue_line_state_event(dtr != 0, rts != 0);
79}
80
81static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) {
82 USBCDCACMInstance *instance = get_instance_by_itf(itf);
83 if (instance == nullptr) {
84 ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf);
85 return;
86 }
87
88 uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate;
89 uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits;
90 uint8_t parity = event->line_coding_changed_data.p_line_coding->parity;
91 uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits;
92 ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate,
93 stop_bits, parity, data_bits);
94
95 // Queue event for processing in main loop
96 instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits);
97}
98
99static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size,
100 TickType_t x_ticks_to_wait) {
101 size_t read_sz;
102 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz));
103
104 if (buf == nullptr) {
105 return ESP_FAIL;
106 }
107
108 memcpy(out_buf, buf, read_sz);
109 vRingbufferReturnItem(ring_buf, (void *) buf);
110 *rx_data_size = read_sz;
111
112 // Buffer's data can be wrapped, in which case we should perform another read
113 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size));
114 if (buf != nullptr) {
115 memcpy(out_buf + *rx_data_size, buf, read_sz);
116 vRingbufferReturnItem(ring_buf, (void *) buf);
117 *rx_data_size += read_sz;
118 }
119
120 return ESP_OK;
121}
122
123//==============================================================================
124// USBCDCACMInstance Implementation
125//==============================================================================
126
128 this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
129 if (this->usb_tx_ringbuf_ == nullptr) {
130 ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_);
131 this->parent_->mark_failed();
132 return;
133 }
134
135 this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
136 if (this->usb_rx_ringbuf_ == nullptr) {
137 ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_);
138 this->parent_->mark_failed();
139 return;
140 }
141
142 // Configure this CDC interface
143 const tinyusb_config_cdcacm_t acm_cfg = {
144 .cdc_port = static_cast<tinyusb_cdcacm_itf_t>(this->itf_),
145 .callback_rx = &tinyusb_cdc_rx_callback,
146 .callback_rx_wanted_char = NULL,
147 .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback,
148 .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback,
149 };
150
151 esp_err_t result = tinyusb_cdcacm_init(&acm_cfg);
152 if (result != ESP_OK) {
153 ESP_LOGE(TAG, "tinyusb_cdcacm_init failed: %d", result);
154 this->parent_->mark_failed();
155 return;
156 }
157
158 // Use a larger stack size for very verbose logging
159 constexpr size_t stack_size =
160 ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
161
162 // Create a simple, unique task name per interface
163 char task_name[] = "usb_tx_0";
164 task_name[sizeof(task_name) - 2] = format_hex_char(static_cast<char>(this->itf_));
165 xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_);
166
167 if (this->usb_tx_task_handle_ == nullptr) {
168 ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_);
169 this->parent_->mark_failed();
170 return;
171 }
172}
173
175 // Process events from the lock-free queue
176 this->process_events_();
177}
178
180
182 auto *instance = static_cast<USBCDCACMInstance *>(arg);
183 instance->usb_tx_task();
184}
185
187 uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0};
188 size_t tx_data_size = 0;
189
190 while (true) {
191 // Wait for a notification from the bridge component
192 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
193
194 // When we do wake up, we can be sure there is data in the ring buffer
195 esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0);
196
197 if (ret != ESP_OK) {
198 ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_);
199 continue;
200 } else if (tx_data_size == 0) {
201 ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_);
202 continue;
203 }
204
205 ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
206#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
207 char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
208#endif
209 ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size));
210
211 // Serial data will be split up into 64 byte chunks to be sent over USB so this
212 // usually will take multiple iterations
213 uint8_t *data_head = &data[0];
214
215 while (tx_data_size > 0) {
216 size_t queued =
217 tinyusb_cdcacm_write_queue(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), data_head, tx_data_size);
218 ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued);
219
220 tx_data_size -= queued;
221 data_head += queued;
222
223 ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_);
224 esp_err_t flush_ret =
225 tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(10));
226
227 if (flush_ret != ESP_OK) {
228 ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_);
229 tud_cdc_n_write_clear(this->itf_);
230 break;
231 }
232 }
233 }
234}
235
236//==============================================================================
237// UARTComponent Interface Implementation
238//==============================================================================
239
240void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) {
241 if (len == 0) {
242 return;
243 }
244
245 // Write data to TX ring buffer
246 BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0);
247 if (send_res != pdTRUE) {
248 ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len);
249 return;
250 }
251
252 // Notify TX task that data is available
253 if (this->usb_tx_task_handle_ != nullptr) {
254 xTaskNotifyGive(this->usb_tx_task_handle_);
255 }
256}
257
258bool USBCDCACMInstance::peek_byte(uint8_t *data) {
259 if (this->has_peek_) {
260 *data = this->peek_buffer_;
261 return true;
262 }
263
264 if (this->read_byte(&this->peek_buffer_)) {
265 *data = this->peek_buffer_;
266 this->has_peek_ = true;
267 return true;
268 }
269
270 return false;
271}
272
273bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
274 if (len == 0) {
275 return true;
276 }
277
278 size_t original_len = len;
279 size_t bytes_read = 0;
280
281 // First, use the peek buffer if available
282 if (this->has_peek_) {
283 data[0] = this->peek_buffer_;
284 this->has_peek_ = false;
285 bytes_read = 1;
286 data++;
287 if (--len == 0) { // Decrement len first, then check it...
288 return true; // No more to read
289 }
290 }
291
292 // Read remaining bytes from RX ring buffer
293 size_t rx_size = 0;
294 uint8_t *buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
295 if (buf == nullptr) {
296 return false;
297 }
298
299 memcpy(data, buf, rx_size);
300 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
301 bytes_read += rx_size;
302 data += rx_size;
303 len -= rx_size;
304 if (len == 0) {
305 return true; // No more to read
306 }
307
308 // Buffer's data may wrap around, in which case we should perform another read
309 buf = static_cast<uint8_t *>(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len));
310 if (buf == nullptr) {
311 return false;
312 }
313
314 memcpy(data, buf, rx_size);
315 vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf);
316 bytes_read += rx_size;
317
318 return bytes_read == original_len;
319}
320
322 UBaseType_t waiting = 0;
323 if (this->usb_rx_ringbuf_ != nullptr) {
324 vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
325 }
326 return waiting + (this->has_peek_ ? 1 : 0);
327}
328
330 // Wait for TX ring buffer to be empty
331 if (this->usb_tx_ringbuf_ == nullptr) {
333 }
334
335 UBaseType_t waiting = 1;
336 while (waiting > 0) {
337 vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
338 if (waiting > 0) {
339 vTaskDelay(pdMS_TO_TICKS(1));
340 }
341 }
342
343 // Also wait for USB to finish transmitting
344 esp_err_t err = tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100));
345 if (err == ESP_OK)
347 if (err == ESP_ERR_TIMEOUT)
350}
351
353
354} // namespace esphome::usb_cdc_acm
355#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:54
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
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:1263
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:340
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:1400
static void uint32_t