diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index bf68c71206..31bdde2df8 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7; static const uint8_t USB_DIR_OUT = 0; static const size_t SETUP_PACKET_SIZE = 8; -static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. +static constexpr size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible. static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32"); // Select appropriate bitmask type for tracking allocation of TransferRequest slots. @@ -65,6 +65,7 @@ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be bet // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32. // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated. using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type; +static constexpr trq_bitmask_t ALL_REQUESTS_IN_USE = MAX_REQUESTS == 32 ? ~0 : (1 << MAX_REQUESTS) - 1; static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples) @@ -82,12 +83,6 @@ struct TransferStatus { using transfer_cb_t = std::function; -enum TransferResult : uint8_t { - TRANSFER_OK = 0, - TRANSFER_ERROR_NO_SLOTS, - TRANSFER_ERROR_SUBMIT_FAILED, -}; - class USBClient; // struct used to capture all data needed for a transfer @@ -139,11 +134,11 @@ class USBClient : public Component { float get_setup_priority() const override { return setup_priority::IO; } void on_opened(uint8_t addr); void on_removed(usb_device_handle_t handle); - void control_transfer_callback(const usb_transfer_t *xfer) const; - TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); - void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); + bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); + bool transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); void dump_config() override; void release_trq(TransferRequest *trq); + trq_bitmask_t get_trq_in_use() const { return trq_in_use_; } bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, const std::vector &data = {}); @@ -153,7 +148,6 @@ class USBClient : public Component { EventPool event_pool; protected: - bool register_(); TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe) virtual void disconnect(); virtual void on_connected() {} @@ -164,7 +158,7 @@ class USBClient : public Component { // USB task management static void usb_task_fn(void *arg); - void usb_task_loop(); + [[noreturn]] void usb_task_loop() const; TaskHandle_t usb_task_handle_{nullptr}; diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 2bfdb64b54..dc216a209d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,9 +188,9 @@ void USBClient::setup() { } // Pre-allocate USB transfer buffers for all slots at startup // This avoids any dynamic allocation during runtime - for (size_t i = 0; i < MAX_REQUESTS; i++) { - usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); - this->requests_[i].client = this; // Set once, never changes + for (auto &request : this->requests_) { + usb_host_transfer_alloc(64, 0, &request.transfer); + request.client = this; // Set once, never changes } // Create and start USB task @@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) { auto *client = static_cast(arg); client->usb_task_loop(); } - -void USBClient::usb_task_loop() { +void USBClient::usb_task_loop() const { while (true) { usb_host_client_handle_events(this->handle_, portMAX_DELAY); } @@ -338,18 +337,19 @@ TransferRequest *USBClient::get_trq_() { // Find first available slot (bit = 0) and try to claim it atomically // We use a while loop to allow retrying the same slot after CAS failure - size_t i = 0; - while (i != MAX_REQUESTS) { - if (mask & (static_cast(1) << i)) { - // Slot is in use, move to next slot - i++; - continue; + for (;;) { + if (mask == ALL_REQUESTS_IN_USE) { + ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); + return nullptr; } + // find the least significant zero bit + trq_bitmask_t lsb = ~mask & (mask + 1); // Slot i appears available, try to claim it atomically - trq_bitmask_t desired = mask | (static_cast(1) << i); // Set bit i to mark as in-use + trq_bitmask_t desired = mask | lsb; - if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) { + if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order::acquire)) { + auto i = __builtin_ctz(lsb); // count trailing zeroes // Successfully claimed slot i - prepare the TransferRequest auto *trq = &this->requests_[i]; trq->transfer->context = trq; @@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() { } // CAS failed - another thread modified the bitmask // mask was already updated by compare_exchange_weak with the current value - // No need to reload - the CAS already did that for us - i = 0; } - - ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); - return nullptr; } + void USBClient::disconnect() { this->on_disconnected(); auto err = usb_host_device_close(this->handle_, this->device_handle_); @@ -443,15 +439,14 @@ static void transfer_callback(usb_transfer_t *xfer) { * @param ep_address The endpoint address. * @param callback The callback function to be called when the transfer is complete. * @param length The length of the data to be transferred. - * @return TransferResult indicating success or specific failure reason * * @throws None. */ -TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { +bool USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return TRANSFER_ERROR_NO_SLOTS; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -461,9 +456,9 @@ TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &c if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); - return TRANSFER_ERROR_SUBMIT_FAILED; + return false; } - return TRANSFER_OK; + return true; } /** @@ -479,11 +474,11 @@ TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &c * * @throws None. */ -void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { +bool USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); - return; + return false; } trq->callback = callback; trq->transfer->callback = transfer_callback; @@ -494,7 +489,9 @@ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); + return false; } + return true; } void USBClient::dump_config() { ESP_LOGCONFIG(TAG, @@ -508,7 +505,7 @@ void USBClient::dump_config() { // - Main loop: When transfer submission fails // // THREAD SAFETY: Lock-free using atomic AND to clear bit -// Thread-safe atomic operation allows multi-threaded deallocation +// Thread-safe atomic operation allows multithreaded deallocation void USBClient::release_trq(TransferRequest *trq) { if (trq == nullptr) return; @@ -520,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) { return; } - // Atomically clear bit i to mark slot as available + // Atomically clear the bit to mark slot as available // fetch_and with inverted bitmask clears the bit atomically - trq_bitmask_t bit = static_cast(1) << index; - this->trq_in_use_.fetch_and(static_cast(~bit), std::memory_order_release); + trq_bitmask_t mask = ~(static_cast(1) << index); + this->trq_in_use_.fetch_and(mask, std::memory_order_release); } } // namespace usb_host diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 46bb5728a7..dc9ba4af7a 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -324,9 +324,58 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // // The underlying transfer_in() uses lock-free atomic allocation from the // TransferRequest pool, making this multi-threaded access safe +<<<<<<< HEAD // Do the actual work (input_started_ already set to true by CAS above) this->do_start_input_(channel); +======= + + // if already started, don't restart. A spurious failure in compare_exchange_weak + // is not a problem, as it will be retried on the next read_array() + auto started = false; + if (!channel->input_started_.compare_exchange_weak(started, true)) + return; + const auto *ep = channel->cdc_dev_.in_ep; + // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback + auto callback = [this, channel](const usb_host::TransferStatus &status) { + ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); + if (!status.success) { + ESP_LOGE(TAG, "Input transfer failed, status=%s", esp_err_to_name(status.error_code)); + // On failure, don't restart - let next read_array() trigger it + channel->input_started_.store(false); + return; + } + + if (!channel->dummy_receiver_ && status.data_len > 0) { + // Allocate a chunk from the pool + UsbDataChunk *chunk = this->chunk_pool_.allocate(); + if (chunk == nullptr) { + // No chunks available - queue is full or we're out of memory + this->usb_data_queue_.increment_dropped_count(); + // Mark input as not started so we can retry + channel->input_started_.store(false); + return; + } + + // Copy data to chunk (this is fast, happens in USB task) + memcpy(chunk->data, status.data, status.data_len); + chunk->length = status.data_len; + chunk->channel = channel; + + // Push to lock-free queue for main loop processing + // Push always succeeds because pool size == queue size + this->usb_data_queue_.push(chunk); + } + + // On success, restart input immediately from USB task for performance + // The lock-free queue will handle backpressure + channel->input_started_.store(false); + this->start_input(channel); + }; + if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) { + channel->input_started_.store(false); + } +>>>>>>> clydebarrow/usb-uart } void USBUartComponent::start_output(USBUartChannel *channel) { @@ -419,11 +468,12 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); - channel->initialised_.store(false); - channel->input_started_.store(false); - channel->output_started_.store(false); + // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts + channel->input_started_.store(true); + channel->output_started_.store(true); channel->input_buffer_.clear(); channel->output_buffer_.clear(); + channel->initialised_.store(false); } USBClient::on_disconnected(); } diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 46ad6291f9..474c3f5c8d 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -1,3 +1,6 @@ +usb_host: + max_transfer_requests: 32 + usb_uart: - id: uart_0 type: cdc_acm