ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
cdc_acm_uart_bridge.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)
4#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6
7#include <algorithm>
8#include "freertos/FreeRTOS.h"
9#include "freertos/task.h"
10#include "freertos/ringbuf.h"
11#include "driver/uart.h"
12#include "soc/soc_caps.h"
13
15
16ESPHOME_LOG_TAG(TAG, "cdc_acm_uart");
17
18static constexpr size_t UART_TASK_STACK_SIZE = 4096;
19static constexpr size_t RINGBUF_RETRY_CHUNK_SIZE = 64;
20static constexpr uint32_t LOG_THROTTLE_MS = 1000;
21static constexpr uint32_t UART_RELOAD_SETTLE_MS = 20;
22// Above the default priority but below the USB/Wi-Fi system tasks.
23static constexpr UBaseType_t TASK_PRIORITY = 4;
24
25static bool should_log_now(uint32_t *last_ms, uint32_t interval_ms) {
26 uint32_t now = millis();
27 if ((now - *last_ms) >= interval_ms) {
28 *last_ms = now;
29 return true;
30 }
31 return false;
32}
33
34static bool ringbuf_send_with_retry(RingbufHandle_t ringbuf, const uint8_t *data, size_t len, uint32_t *log_ms) {
35 if (len == 0) {
36 return true;
37 }
38
39 if (xRingbufferSend(ringbuf, data, len, pdMS_TO_TICKS(1)) == pdTRUE) {
40 return true;
41 }
42
43 size_t offset = 0;
44 while (offset < len) {
45 size_t chunk = std::min(RINGBUF_RETRY_CHUNK_SIZE, len - offset);
46 if (xRingbufferSend(ringbuf, data + offset, chunk, pdMS_TO_TICKS(1)) != pdTRUE) {
47 if (should_log_now(log_ms, LOG_THROTTLE_MS)) {
48 ESP_LOGW(TAG, "USB TX buffer full; some data is lost");
49 }
50 return false;
51 }
52 offset += chunk;
53 }
54 return true;
55}
56
58 // Line state starts deasserted (no host yet); active-low DTR#/RTS# wiring is
59 // handled by configuring the pins inverted, so deasserted idles HIGH.
60 if (this->dtr_pin_ != nullptr) {
61 this->dtr_pin_->setup();
62 this->dtr_pin_->digital_write(false);
63 }
64
65 if (this->rts_pin_ != nullptr) {
66 this->rts_pin_->setup();
67 this->rts_pin_->digital_write(false);
68 }
69
70 // A failed UART never assigned its port number, so the worker tasks would run
71 // against an indeterminate port.
72 if (this->uart_parent_->is_failed()) {
73 ESP_LOGE(TAG, "UART parent failed; aborting");
74 this->mark_failed();
75 return;
76 }
77
82
83 // usb_cdc_acm sets up first (priority IO > HARDWARE). Any interface failing marks
84 // the hub failed, and a failed hub no longer runs loop(), so line coding and line
85 // state events would never reach this bridge even if its own interface is healthy.
86 if (this->usb_cdc_parent_->get_parent()->is_failed()) {
87 ESP_LOGE(TAG, "USB CDC ACM failed; aborting");
88 this->mark_failed();
89 return;
90 }
91
92 // Per-instance task names (keyed on the CDC interface number) keep task dumps
93 // unambiguous with multiple bridges.
94 char tx_task_name[] = "cdc_uart_tx_0";
95 char rx_task_name[] = "cdc_uart_rx_0";
96 const char itf_char = format_hex_char(this->usb_cdc_parent_->get_itf());
97 tx_task_name[sizeof(tx_task_name) - 2] = itf_char;
98 rx_task_name[sizeof(rx_task_name) - 2] = itf_char;
99
100 xTaskCreate(uart_tx_task_fn, tx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_tx_task_handle_);
101 if (this->uart_tx_task_handle_ == nullptr) {
102 ESP_LOGE(TAG, "Failed to create UART TX task");
103 this->mark_failed();
104 return;
105 }
106
107 xTaskCreate(uart_rx_task_fn, rx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_rx_task_handle_);
108 if (this->uart_rx_task_handle_ == nullptr) {
109 ESP_LOGE(TAG, "Failed to create UART RX task");
110 vTaskDelete(this->uart_tx_task_handle_);
111 this->uart_tx_task_handle_ = nullptr;
112 this->mark_failed();
113 return;
114 }
115
116 // Only register callbacks once both tasks exist, so a failed setup never drives
117 // DTR/RTS from a dead bridge.
118 this->usb_cdc_parent_->set_line_state_callback([this](bool dtr, bool rts) { this->set_line_state(dtr, rts); });
119 this->usb_cdc_parent_->set_line_coding_callback([this](uint32_t, uint8_t, uint8_t, uint8_t) {
120 this->host_coding_seen_ = true;
121 // Another component owns the UART's framing while paused; resume() re-syncs.
122 if (this->paused_ == 0) {
123 this->set_line_coding();
124 }
125 });
126
127 // Release the workers only now: until here a failed setup may still delete the TX
128 // task, which is safe only while it is parked and owns nothing in the driver.
129 xTaskNotifyGive(this->uart_tx_task_handle_);
130 xTaskNotifyGive(this->uart_rx_task_handle_);
131
132 // loop() only services line-coding reloads; stay off the main loop until one is
133 // scheduled.
134 this->disable_loop();
135}
136
138 ESP_LOGCONFIG(TAG,
139 "CDC-ACM UART Bridge:\n"
140 " UART Bus: %u\n"
141 " USB CDC Interface: %u",
142 this->uart_parent_->get_hw_serial_number(), this->usb_cdc_parent_->get_itf());
143 LOG_PIN(" DTR Pin: ", this->dtr_pin_);
144 LOG_PIN(" RTS Pin: ", this->rts_pin_);
145}
146
148 // The UART (BUS) shuts down after this component (HARDWARE) and deletes its driver,
149 // freeing the ring buffer and mutexes the worker tasks block on. Suspending the
150 // tasks unlinks them from those objects first.
151 if (this->uart_rx_task_handle_ != nullptr) {
152 vTaskSuspend(this->uart_rx_task_handle_);
153 }
154 if (this->uart_tx_task_handle_ != nullptr) {
155 vTaskSuspend(this->uart_tx_task_handle_);
156 }
157}
158
160 switch (this->state_) {
162 if ((App.get_loop_component_start_time() - this->reload_requested_at_) < UART_RELOAD_SETTLE_MS) {
163 return;
164 }
165 // Deliberately not gated on tx_idle_(): a host that re-codes the line mid-stream
166 // wants the new framing now, and its own in-flight bytes are its concern.
167 // apply_settings_live() rewrites the framing registers without reinstalling the
168 // driver, so the worker tasks blocked inside it are undisturbed.
171 break;
174 // Let a host write that was in flight drain, FIFO included, before a reload
175 // flushes the FIFOs and truncates it.
176 if (!this->tx_idle_()) {
177 return;
178 }
182 } else {
183 this->finish_resume_();
184 }
185 break;
186 default:
187 break;
188 }
189 this->disable_loop();
190}
191
193 if (!this->sync_host_framing_()) {
194 return;
195 }
196 // Coalesce rapid line-coding updates from the host.
199 // Main-loop context (via USBCDCACMInstance::process_events_).
200 this->enable_loop();
201}
202
204 // usb_cdc_acm has already translated the wire coding onto the CDC instance (main
205 // loop); mirror it here so the framing translation has a single source of truth.
206 bool changed = false;
207
208 // Reject 0 (the CDC B0/hang-up encoding; older IDF revisions divide by the rate)
209 // and rates above the SoC ceiling. Anything in between is the driver's call,
210 // matching what a YAML-configured UART accepts.
211 const uint32_t baud = this->usb_cdc_parent_->get_baud_rate();
212 if (baud == 0 || baud > SOC_UART_BITRATE_MAX) {
213 ESP_LOGW(TAG, "Ignoring unsupported baud rate %" PRIu32 " from host; keeping %" PRIu32, baud,
214 this->uart_parent_->get_baud_rate());
215 } else if (this->uart_parent_->get_baud_rate() != baud) {
216 this->uart_parent_->set_baud_rate(baud);
217 changed = true;
218 }
219
220 const uint8_t stop_bits = this->usb_cdc_parent_->get_stop_bits();
221 if (this->uart_parent_->get_stop_bits() != stop_bits) {
222 this->uart_parent_->set_stop_bits(stop_bits);
223 changed = true;
224 }
225
226 const auto parity = this->usb_cdc_parent_->get_parity();
227 if (this->uart_parent_->get_parity() != parity) {
228 this->uart_parent_->set_parity(parity);
229 changed = true;
230 }
231
232 // USB CDC permits data-bit counts the UART cannot represent (up to 16).
233 const uint8_t data_bits = this->usb_cdc_parent_->get_data_bits();
234 if (data_bits < 5 || data_bits > 8) {
235 ESP_LOGW(TAG, "Ignoring unsupported data bits %u from host; keeping %u", data_bits,
236 this->uart_parent_->get_data_bits());
237 } else if (this->uart_parent_->get_data_bits() != data_bits) {
238 this->uart_parent_->set_data_bits(data_bits);
239 changed = true;
240 }
241
242 if (changed) {
243 ESP_LOGV(TAG, "Line coding: baud=%" PRIu32 ", data_bits=%u, stop_bits=%u, parity=%u",
244 this->uart_parent_->get_baud_rate(), this->uart_parent_->get_data_bits(),
245 this->uart_parent_->get_stop_bits(), static_cast<uint8_t>(this->uart_parent_->get_parity()));
246 }
247 return changed;
248}
249
252 return;
253 }
254 this->paused_ = 1;
255 // A null RX task means setup() has not completed (or failed): nothing to stop, and
256 // the framing snapshot does not exist yet. Should setup() run later, the RX task
257 // starts parked.
258 if (this->uart_rx_task_handle_ == nullptr) {
260 return;
261 }
262 // Drops a coalesced host reload or a pending resume; loop() restores the framing
263 // once any host write in flight has drained.
265 this->enable_loop();
266}
267
270 return;
271 }
272 if (this->uart_rx_task_handle_ == nullptr) {
273 this->paused_ = 0;
275 return;
276 }
277 // A restore still waiting on the TX side is moot: the host's framing is kept.
278 if (!this->tx_idle_()) {
280 this->enable_loop();
281 return;
282 }
283 this->finish_resume_();
284 this->disable_loop();
285}
286
288 // Take the bus back at a known framing before either task runs again: the host's
289 // if it ever sent one, else the YAML framing (the other owner may have changed it).
290 if (this->host_coding_seen_) {
291 this->sync_host_framing_();
293 } else {
295 }
296 this->paused_ = 0;
298 this->drive_line_state_();
299 xTaskNotifyGive(this->uart_rx_task_handle_);
300}
301
303 const auto uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
304 return this->tx_busy_ == 0 && uart_wait_tx_done(uart_num, 0) == ESP_OK;
305}
306
308 // Always applied: the cached settings can lead the hardware by a pending reload,
309 // so they are no proof of what is live.
315}
316
317void CDCACMUARTBridge::set_line_state(bool dtr, bool rts) {
318 ESP_LOGV(TAG, "Line state: DTR=%d, RTS=%d", dtr, rts);
319 this->host_dtr_ = dtr;
320 this->host_rts_ = rts;
321 // Frozen while paused: a host opening the port must not reset a peer that another
322 // component is talking to.
323 if (this->paused_ == 0) {
324 this->drive_line_state_();
325 }
326}
327
329 if (this->dtr_pin_ != nullptr) {
330 this->dtr_pin_->digital_write(this->host_dtr_);
331 }
332 if (this->rts_pin_ != nullptr) {
333 this->rts_pin_->digital_write(this->host_rts_);
334 }
335}
336
338 auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
339 bridge->uart_rx_task_();
340}
341
343 auto *bridge = static_cast<CDCACMUARTBridge *>(arg);
344 bridge->uart_tx_task_();
345}
346
348 TaskHandle_t usb_tx_handle = this->usb_cdc_parent_->get_tx_task_handle();
349 RingbufHandle_t usb_tx_ringbuf = this->usb_cdc_parent_->get_tx_ringbuf();
350 uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
351 // Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
352 uint32_t tx_full_log_ms = millis() - LOG_THROTTLE_MS;
353 uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
354
355 uint8_t *data = this->uart_rx_buffer_.data();
356 const size_t buf_size = this->uart_rx_buffer_.size();
357
358 // Released by setup() once both tasks exist.
359 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
360
361 while (true) {
362 if (this->paused_ != 0) {
363 // Parked until resume() notifies; nothing is read, so the other owner sees
364 // every byte.
365 this->rx_parked_ = 1;
366 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
367 this->rx_parked_ = 0;
368 continue;
369 }
370
371 // Block until at least one byte is available from UART.
372 int total_rx_size = uart_read_bytes(uart_num, data, 1, pdMS_TO_TICKS(UART_RX_WAIT_MS));
373 if (total_rx_size < 0) {
374 if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
375 ESP_LOGE(TAG, "UART read failed: %d", total_rx_size);
376 }
377 vTaskDelay(pdMS_TO_TICKS(10));
378 continue;
379 }
380 if (total_rx_size == 0) {
381 continue;
382 }
383 // pause() landed during the read: don't forward a byte to a host that is gone.
384 if (this->paused_ != 0) {
385 continue;
386 }
387
388 // Drain the currently buffered burst without waiting.
389 while (true) {
390 int rx_data_size = uart_read_bytes(uart_num, data + total_rx_size, buf_size - total_rx_size, 0);
391 if (rx_data_size < 0) {
392 if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
393 ESP_LOGE(TAG, "UART read failed: %d", rx_data_size);
394 }
395 break;
396 }
397 if (rx_data_size == 0) {
398 break;
399 }
400 ESP_LOGV(TAG, "UART RX: %d bytes", rx_data_size);
401 total_rx_size += rx_data_size;
402 if (total_rx_size >= (int) buf_size) {
403 break;
404 }
405 }
406
407 ringbuf_send_with_retry(usb_tx_ringbuf, data, total_rx_size, &tx_full_log_ms);
408
409 ESP_LOGV(TAG, "UART RX: waking up USB TX task");
410 xTaskNotifyGive(usb_tx_handle);
411 }
412}
413
415 RingbufHandle_t usb_rx_ringbuf = this->usb_cdc_parent_->get_rx_ringbuf();
416 uart_port_t uart_num = static_cast<uart_port_t>(this->uart_parent_->get_hw_serial_number());
417 uint8_t *data_to_uart = this->uart_tx_buffer_.data();
418 const size_t buf_size = this->uart_tx_buffer_.size();
419 size_t rx_size;
420 // Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs.
421 uint32_t err_log_ms = millis() - LOG_THROTTLE_MS;
422 uint32_t drop_log_ms = millis() - LOG_THROTTLE_MS;
423
424 // Released by setup() once both tasks exist.
425 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
426
427 while (true) {
428 ESP_LOGV(TAG, "Waiting for data to send to UART");
429 esp_err_t ret = usb_cdc_acm::ringbuf_read_bytes(usb_rx_ringbuf, data_to_uart, buf_size, &rx_size, portMAX_DELAY);
430
431 if (ret != ESP_OK) {
432 if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
433 ESP_LOGE(TAG, "USB RX RingBuf read failed");
434 }
435 // Yield: this task runs above the main loop, so a persistent failure must not
436 // become a tight loop.
437 vTaskDelay(pdMS_TO_TICKS(10));
438 continue;
439 }
440
441 // Another component owns the UART; host bytes must not interleave with its traffic.
442 // tx_busy_ goes up before the check so is_paused() cannot miss a write in flight.
443 this->tx_busy_ = 1;
444 if (this->paused_ != 0) {
445 this->tx_busy_ = 0;
446 if (should_log_now(&drop_log_ms, LOG_THROTTLE_MS)) {
447 ESP_LOGW(TAG, "Paused; dropping %zu bytes from host", rx_size);
448 }
449 continue;
450 }
451
452 ESP_LOGV(TAG, "Sending %zu bytes to UART", rx_size);
453 // Signed: uart_write_bytes() returns -1 on error.
454 int xfer_size = uart_write_bytes(uart_num, data_to_uart, rx_size);
455 this->tx_busy_ = 0;
456
457 if (xfer_size < 0) {
458 if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) {
459 ESP_LOGE(TAG, "UART write failed: %d", xfer_size);
460 }
461 } else if (static_cast<size_t>(xfer_size) != rx_size) {
462 ESP_LOGW(TAG, "UART write incomplete (%d/%zu bytes)", xfer_size, rx_size);
463 }
464 }
465}
466
467} // namespace esphome::cdc_acm_uart
468#endif
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
void enable_loop()
Enable this component's loop.
Definition component.h:248
void disable_loop()
Disable this component's loop.
virtual void setup()=0
virtual void digital_write(bool value)=0
T * get_parent() const
Get the parent of this object.
Definition helpers.h:2003
std::array< uint8_t, CONFIG_TINYUSB_CDC_RX_BUFSIZE > uart_tx_buffer_
void resume()
Re-apply the host's line coding and line state, then resume forwarding.
usb_cdc_acm::USBCDCACMInstance * usb_cdc_parent_
void pause()
Stop forwarding in both directions and hand the UART back to its configured framing,...
std::array< uint8_t, CONFIG_TINYUSB_CDC_TX_BUFSIZE > uart_rx_buffer_
esp_err_t apply_settings_live()
Apply the current framing (baud rate, parity, data/stop bits) to the installed driver in place,...
UARTParityOptions get_parity() const
void set_baud_rate(uint32_t baud_rate)
void set_parity(UARTParityOptions parity)
void set_stop_bits(uint8_t stop_bits)
void set_data_bits(uint8_t data_bits)
RingbufHandle_t get_rx_ringbuf() const
Definition usb_cdc_acm.h:98
RingbufHandle_t get_tx_ringbuf() const
Definition usb_cdc_acm.h:97
void set_line_state_callback(LineStateCallback callback)
void set_line_coding_callback(LineCodingCallback callback)
int ret
ESPHOME_LOG_TAG(TAG, "cdc_acm_uart")
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
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
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t