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