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