ESPHome 2025.11.4
Loading...
Searching...
No Matches
usb_uart.cpp
Go to the documentation of this file.
1// Should not be needed, but it's required to pass CI clang-tidy checks
2#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
3#include "usb_uart.h"
4#include "esphome/core/log.h"
7
8#include <cinttypes>
9
10namespace esphome {
11namespace usb_uart {
12
20static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
21 int conf_offset, ep_offset;
22 // look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out)
23 CdcEps eps{};
24 eps.bulk_interface_number = 0xFF;
25 for (;;) {
26 const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
27 if (!intf_desc) {
28 ESP_LOGE(TAG, "usb_parse_interface_descriptor failed");
29 return nullopt;
30 }
31 ESP_LOGD(TAG, "intf_desc: bInterfaceClass=%02X, bInterfaceSubClass=%02X, bInterfaceProtocol=%02X, bNumEndpoints=%d",
32 intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol,
33 intf_desc->bNumEndpoints);
34 for (uint8_t i = 0; i != intf_desc->bNumEndpoints; i++) {
35 ep_offset = conf_offset;
36 const auto *ep = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &ep_offset);
37 if (!ep) {
38 ESP_LOGE(TAG, "Ran out of interfaces at %d before finding all endpoints", i);
39 return nullopt;
40 }
41 ESP_LOGD(TAG, "ep: bEndpointAddress=%02X, bmAttributes=%02X", ep->bEndpointAddress, ep->bmAttributes);
42 if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
43 eps.notify_ep = ep;
44 eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
45 } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
46 (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
47 eps.in_ep = ep;
48 eps.bulk_interface_number = intf_desc->bInterfaceNumber;
49 } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
50 (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
51 eps.out_ep = ep;
52 eps.bulk_interface_number = intf_desc->bInterfaceNumber;
53 } else {
54 ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
55 continue;
56 }
57 }
58 if (eps.in_ep != nullptr && eps.out_ep != nullptr && eps.notify_ep != nullptr)
59 return eps;
60 }
61}
62
63std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev_hdl) {
64 const usb_config_desc_t *config_desc;
65 const usb_device_desc_t *device_desc;
66 int desc_offset = 0;
67 std::vector<CdcEps> cdc_devs{};
68
69 // Get required descriptors
70 if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
71 ESP_LOGE(TAG, "get_device_descriptor failed");
72 return {};
73 }
74 if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
75 ESP_LOGE(TAG, "get_active_config_descriptor failed");
76 return {};
77 }
78 if (device_desc->bDeviceClass == USB_CLASS_COMM || device_desc->bDeviceClass == USB_CLASS_VENDOR_SPEC) {
79 // single CDC-ACM device
80 if (auto eps = get_cdc(config_desc, 0)) {
81 ESP_LOGV(TAG, "Found CDC-ACM device");
82 cdc_devs.push_back(*eps);
83 }
84 return cdc_devs;
85 }
86 if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) &&
87 (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) ||
88 ((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) &&
89 (device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) {
90 // This is a composite device, that uses Interface Association Descriptor
91 const auto *this_desc = reinterpret_cast<const usb_standard_desc_t *>(config_desc);
92 for (;;) {
93 this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength,
94 USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset);
95 if (!this_desc)
96 break;
97 const auto *iad_desc = reinterpret_cast<const usb_iad_desc_t *>(this_desc);
98
99 if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) {
100 ESP_LOGV(TAG, "Found CDC-ACM device in composite device");
101 if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface))
102 cdc_devs.push_back(*eps);
103 }
104 }
105 }
106 return cdc_devs;
107}
108
109void RingBuffer::push(uint8_t item) {
110 this->buffer_[this->insert_pos_] = item;
111 this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
112}
113void RingBuffer::push(const uint8_t *data, size_t len) {
114 for (size_t i = 0; i != len; i++) {
115 this->buffer_[this->insert_pos_] = *data++;
116 this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
117 }
118}
119
121 uint8_t item = this->buffer_[this->read_pos_];
122 this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
123 return item;
124}
125size_t RingBuffer::pop(uint8_t *data, size_t len) {
126 len = std::min(len, this->get_available());
127 for (size_t i = 0; i != len; i++) {
128 *data++ = this->buffer_[this->read_pos_];
129 this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
130 }
131 return len;
132}
133void USBUartChannel::write_array(const uint8_t *data, size_t len) {
134 if (!this->initialised_.load()) {
135 ESP_LOGV(TAG, "Channel not initialised - write ignored");
136 return;
137 }
138 while (this->output_buffer_.get_free_space() != 0 && len-- != 0) {
139 this->output_buffer_.push(*data++);
140 }
141 len++;
142 if (len > 0) {
143 ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len);
144 }
145 this->parent_->start_output(this);
146}
147
148bool USBUartChannel::peek_byte(uint8_t *data) {
149 if (this->input_buffer_.is_empty()) {
150 return false;
151 }
152 *data = this->input_buffer_.peek();
153 return true;
154}
155bool USBUartChannel::read_array(uint8_t *data, size_t len) {
156 if (!this->initialised_.load()) {
157 ESP_LOGV(TAG, "Channel not initialised - read ignored");
158 return false;
159 }
160 auto available = this->available();
161 bool status = true;
162 if (len > available) {
163 ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available);
164 len = available;
165 status = false;
166 }
167 for (size_t i = 0; i != len; i++) {
168 *data++ = this->input_buffer_.pop();
169 }
170 this->parent_->start_input(this);
171 return status;
172}
173void USBUartComponent::setup() { USBClient::setup(); }
175 USBClient::loop();
176
177 // Process USB data from the lock-free queue
178 UsbDataChunk *chunk;
179 while ((chunk = this->usb_data_queue_.pop()) != nullptr) {
180 auto *channel = chunk->channel;
181
182#ifdef USE_UART_DEBUGGER
183 if (channel->debug_) {
184 uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX, std::vector<uint8_t>(chunk->data, chunk->data + chunk->length),
185 ','); // NOLINT()
186 }
187#endif
188
189 // Push data to ring buffer (now safe in main loop)
190 channel->input_buffer_.push(chunk->data, chunk->length);
191
192 // Return chunk to pool for reuse
193 this->chunk_pool_.release(chunk);
194 }
195
196 // Log dropped USB data periodically
197 uint16_t dropped = this->usb_data_queue_.get_and_reset_dropped_count();
198 if (dropped > 0) {
199 ESP_LOGW(TAG, "Dropped %u USB data chunks due to buffer overflow", dropped);
200 }
201}
203 USBClient::dump_config();
204 for (auto &channel : this->channels_) {
205 ESP_LOGCONFIG(TAG,
206 " UART Channel %d\n"
207 " Baud Rate: %" PRIu32 " baud\n"
208 " Data Bits: %u\n"
209 " Parity: %s\n"
210 " Stop bits: %s\n"
211 " Debug: %s\n"
212 " Dummy receiver: %s",
213 channel->index_, channel->baud_rate_, channel->data_bits_, PARITY_NAMES[channel->parity_],
214 STOP_BITS_NAMES[channel->stop_bits_], YESNO(channel->debug_), YESNO(channel->dummy_receiver_));
215 }
216}
218 if (!channel->initialised_.load())
219 return;
220 // THREAD CONTEXT: Called from both USB task and main loop threads
221 // - USB task: Immediate restart after successful transfer for continuous data flow
222 // - Main loop: Controlled restart after consuming data (backpressure mechanism)
223 //
224 // This dual-thread access is intentional for performance:
225 // - USB task restarts avoid context switch delays for high-speed data
226 // - Main loop restarts provide flow control when buffers are full
227 //
228 // The underlying transfer_in() uses lock-free atomic allocation from the
229 // TransferRequest pool, making this multi-threaded access safe
230
231 // if already started, don't restart. A spurious failure in compare_exchange_weak
232 // is not a problem, as it will be retried on the next read_array()
233 auto started = false;
234 if (!channel->input_started_.compare_exchange_weak(started, true))
235 return;
236 const auto *ep = channel->cdc_dev_.in_ep;
237 // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
238 auto callback = [this, channel](const usb_host::TransferStatus &status) {
239 ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
240 if (!status.success) {
241 ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code));
242 // On failure, don't restart - let next read_array() trigger it
243 channel->input_started_.store(false);
244 return;
245 }
246
247 if (!channel->dummy_receiver_ && status.data_len > 0) {
248 // Allocate a chunk from the pool
249 UsbDataChunk *chunk = this->chunk_pool_.allocate();
250 if (chunk == nullptr) {
251 // No chunks available - queue is full or we're out of memory
252 this->usb_data_queue_.increment_dropped_count();
253 // Mark input as not started so we can retry
254 channel->input_started_.store(false);
255 return;
256 }
257
258 // Copy data to chunk (this is fast, happens in USB task)
259 memcpy(chunk->data, status.data, status.data_len);
260 chunk->length = status.data_len;
261 chunk->channel = channel;
262
263 // Push to lock-free queue for main loop processing
264 // Push always succeeds because pool size == queue size
265 this->usb_data_queue_.push(chunk);
266
267 // Wake main loop immediately to process USB data instead of waiting for select() timeout
268#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
270#endif
271 }
272
273 // On success, restart input immediately from USB task for performance
274 // The lock-free queue will handle backpressure
275 channel->input_started_.store(false);
276 this->start_input(channel);
277 };
278 if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
279 channel->input_started_.store(false);
280 }
281}
282
284 // IMPORTANT: This function must only be called from the main loop!
285 // The output_buffer_ is not thread-safe and can only be accessed from main loop.
286 // USB callbacks use defer() to ensure this function runs in the correct context.
287 if (channel->output_started_.load())
288 return;
289 if (channel->output_buffer_.is_empty()) {
290 return;
291 }
292 const auto *ep = channel->cdc_dev_.out_ep;
293 // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
294 auto callback = [this, channel](const usb_host::TransferStatus &status) {
295 ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
296 channel->output_started_.store(false);
297 // Defer restart to main loop (defer is thread-safe)
298 this->defer([this, channel] { this->start_output(channel); });
299 };
300 channel->output_started_.store(true);
301 uint8_t data[ep->wMaxPacketSize];
302 auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
303 this->transfer_out(ep->bEndpointAddress, callback, data, len);
304#ifdef USE_UART_DEBUGGER
305 if (channel->debug_) {
306 uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector<uint8_t>(data, data + len), ','); // NOLINT()
307 }
308#endif
309 ESP_LOGV(TAG, "Output %d bytes started", len);
310}
311
316static void fix_mps(const usb_ep_desc_t *ep) {
317 if (ep != nullptr) {
318 auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep);
319 if (ep->wMaxPacketSize > 64) {
320 ESP_LOGW(TAG, "Corrected MPS of EP 0x%02X from %u to 64", static_cast<uint8_t>(ep->bEndpointAddress & 0xFF),
321 ep->wMaxPacketSize);
322 ep_mutable->wMaxPacketSize = 64;
323 }
324 }
325}
327 auto cdc_devs = this->parse_descriptors(this->device_handle_);
328 if (cdc_devs.empty()) {
329 this->status_set_error("No CDC-ACM device found");
330 this->disconnect();
331 return;
332 }
333 ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size());
334 size_t i = 0;
335 for (auto *channel : this->channels_) {
336 if (i == cdc_devs.size()) {
337 ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
338 this->status_set_warning("No configuration found for channel");
339 break;
340 }
341 channel->cdc_dev_ = cdc_devs[i++];
342 fix_mps(channel->cdc_dev_.in_ep);
343 fix_mps(channel->cdc_dev_.out_ep);
344 channel->initialised_.store(true);
345 auto err =
346 usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0);
347 if (err != ESP_OK) {
348 ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
349 channel->cdc_dev_.bulk_interface_number);
350 this->status_set_error("usb_host_interface_claim failed");
351 this->disconnect();
352 return;
353 }
354 }
355 this->enable_channels();
356}
357
359 for (auto *channel : this->channels_) {
360 if (channel->cdc_dev_.in_ep != nullptr) {
361 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
362 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
363 }
364 if (channel->cdc_dev_.out_ep != nullptr) {
365 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
366 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
367 }
368 if (channel->cdc_dev_.notify_ep != nullptr) {
369 usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
370 usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
371 }
372 usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
373 // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
374 channel->input_started_.store(true);
375 channel->output_started_.store(true);
376 channel->input_buffer_.clear();
377 channel->output_buffer_.clear();
378 channel->initialised_.store(false);
379 }
380 USBClient::on_disconnected();
381}
382
384 for (auto *channel : this->channels_) {
385 if (!channel->initialised_.load())
386 continue;
387 channel->input_started_.store(false);
388 channel->output_started_.store(false);
389 this->start_input(channel);
390 }
391}
392
393} // namespace usb_uart
394} // namespace esphome
395#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
uint8_t status
Definition bl0942.h:8
void wake_loop_threadsafe()
Wake the main event loop from a FreeRTOS task Thread-safe, can be called from task context to immedia...
void status_set_warning(const char *message=nullptr)
void defer(const std::string &name, std::function< void()> &&f)
Defer a callback to the next loop() call.
void status_set_error(const char *message=nullptr)
static void log_hex(UARTDirection direction, std::vector< uint8_t > bytes, uint8_t separator)
Log the bytes as hex values, separated by the provided separator character.
usb_host_client_handle_t handle_
Definition usb_host.h:165
bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length)
Performs an output transfer operation.
bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length)
Performs a transfer input operation.
usb_device_handle_t device_handle_
Definition usb_host.h:166
void push(uint8_t item)
Definition usb_uart.cpp:109
size_t get_free_space() const
Definition usb_uart.h:60
size_t get_available() const
Definition usb_uart.h:57
std::atomic< bool > input_started_
Definition usb_uart.h:115
std::atomic< bool > initialised_
Definition usb_uart.h:117
bool peek_byte(uint8_t *data) override
Definition usb_uart.cpp:148
void write_array(const uint8_t *data, size_t len) override
Definition usb_uart.cpp:133
bool read_array(uint8_t *data, size_t len) override
Definition usb_uart.cpp:155
std::atomic< bool > output_started_
Definition usb_uart.h:116
EventPool< UsbDataChunk, USB_DATA_QUEUE_SIZE > chunk_pool_
Definition usb_uart.h:140
std::vector< USBUartChannel * > channels_
Definition usb_uart.h:143
LockFreeQueue< UsbDataChunk, USB_DATA_QUEUE_SIZE > usb_data_queue_
Definition usb_uart.h:139
void start_output(USBUartChannel *channel)
Definition usb_uart.cpp:283
void start_input(USBUartChannel *channel)
Definition usb_uart.cpp:217
virtual std::vector< CdcEps > parse_descriptors(usb_device_handle_t dev_hdl)
Definition usb_uart.cpp:63
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:486
Application App
Global storage of Application pointer - only one Application can exist.
const nullopt_t nullopt((nullopt_t::init()))
const usb_ep_desc_t * out_ep
Definition usb_uart.h:31
const usb_ep_desc_t * in_ep
Definition usb_uart.h:30
uint8_t data[MAX_CHUNK_SIZE]
Definition usb_uart.h:78