mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge remote-tracking branch 'clydebarrow/usb-uart' into integration
This commit is contained in:
		| @@ -55,7 +55,7 @@ static const uint8_t USB_DIR_IN = 1 << 7; | |||||||
| static const uint8_t USB_DIR_OUT = 0; | static const uint8_t USB_DIR_OUT = 0; | ||||||
| static const size_t SETUP_PACKET_SIZE = 8; | 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"); | 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. | // 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. | // 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. | // 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; | 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_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) | 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<void(const TransferStatus &)>; | using transfer_cb_t = std::function<void(const TransferStatus &)>; | ||||||
|  |  | ||||||
| enum TransferResult : uint8_t { |  | ||||||
|   TRANSFER_OK = 0, |  | ||||||
|   TRANSFER_ERROR_NO_SLOTS, |  | ||||||
|   TRANSFER_ERROR_SUBMIT_FAILED, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class USBClient; | class USBClient; | ||||||
|  |  | ||||||
| // struct used to capture all data needed for a transfer | // 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; } |   float get_setup_priority() const override { return setup_priority::IO; } | ||||||
|   void on_opened(uint8_t addr); |   void on_opened(uint8_t addr); | ||||||
|   void on_removed(usb_device_handle_t handle); |   void on_removed(usb_device_handle_t handle); | ||||||
|   void control_transfer_callback(const usb_transfer_t *xfer) const; |   bool transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length); | ||||||
|   TransferResult 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 transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length); |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void release_trq(TransferRequest *trq); |   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, |   bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback, | ||||||
|                         const std::vector<uint8_t> &data = {}); |                         const std::vector<uint8_t> &data = {}); | ||||||
|  |  | ||||||
| @@ -153,7 +148,6 @@ class USBClient : public Component { | |||||||
|   EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; |   EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool register_(); |  | ||||||
|   TransferRequest *get_trq_();  // Lock-free allocation using atomic bitmask (multi-consumer safe) |   TransferRequest *get_trq_();  // Lock-free allocation using atomic bitmask (multi-consumer safe) | ||||||
|   virtual void disconnect(); |   virtual void disconnect(); | ||||||
|   virtual void on_connected() {} |   virtual void on_connected() {} | ||||||
| @@ -164,7 +158,7 @@ class USBClient : public Component { | |||||||
|  |  | ||||||
|   // USB task management |   // USB task management | ||||||
|   static void usb_task_fn(void *arg); |   static void usb_task_fn(void *arg); | ||||||
|   void usb_task_loop(); |   [[noreturn]] void usb_task_loop() const; | ||||||
|  |  | ||||||
|   TaskHandle_t usb_task_handle_{nullptr}; |   TaskHandle_t usb_task_handle_{nullptr}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -188,9 +188,9 @@ void USBClient::setup() { | |||||||
|   } |   } | ||||||
|   // Pre-allocate USB transfer buffers for all slots at startup |   // Pre-allocate USB transfer buffers for all slots at startup | ||||||
|   // This avoids any dynamic allocation during runtime |   // This avoids any dynamic allocation during runtime | ||||||
|   for (size_t i = 0; i < MAX_REQUESTS; i++) { |   for (auto &request : this->requests_) { | ||||||
|     usb_host_transfer_alloc(64, 0, &this->requests_[i].transfer); |     usb_host_transfer_alloc(64, 0, &request.transfer); | ||||||
|     this->requests_[i].client = this;  // Set once, never changes |     request.client = this;  // Set once, never changes | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Create and start USB task |   // Create and start USB task | ||||||
| @@ -210,8 +210,7 @@ void USBClient::usb_task_fn(void *arg) { | |||||||
|   auto *client = static_cast<USBClient *>(arg); |   auto *client = static_cast<USBClient *>(arg); | ||||||
|   client->usb_task_loop(); |   client->usb_task_loop(); | ||||||
| } | } | ||||||
|  | void USBClient::usb_task_loop() const { | ||||||
| void USBClient::usb_task_loop() { |  | ||||||
|   while (true) { |   while (true) { | ||||||
|     usb_host_client_handle_events(this->handle_, portMAX_DELAY); |     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 |   // 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 |   // We use a while loop to allow retrying the same slot after CAS failure | ||||||
|   size_t i = 0; |   for (;;) { | ||||||
|   while (i != MAX_REQUESTS) { |     if (mask == ALL_REQUESTS_IN_USE) { | ||||||
|     if (mask & (static_cast<trq_bitmask_t>(1) << i)) { |       ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS); | ||||||
|       // Slot is in use, move to next slot |       return nullptr; | ||||||
|       i++; |  | ||||||
|       continue; |  | ||||||
|     } |     } | ||||||
|  |     // find the least significant zero bit | ||||||
|  |     trq_bitmask_t lsb = ~mask & (mask + 1); | ||||||
|  |  | ||||||
|     // Slot i appears available, try to claim it atomically |     // Slot i appears available, try to claim it atomically | ||||||
|     trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(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 |       // Successfully claimed slot i - prepare the TransferRequest | ||||||
|       auto *trq = &this->requests_[i]; |       auto *trq = &this->requests_[i]; | ||||||
|       trq->transfer->context = trq; |       trq->transfer->context = trq; | ||||||
| @@ -358,13 +358,9 @@ TransferRequest *USBClient::get_trq_() { | |||||||
|     } |     } | ||||||
|     // CAS failed - another thread modified the bitmask |     // CAS failed - another thread modified the bitmask | ||||||
|     // mask was already updated by compare_exchange_weak with the current value |     // 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() { | void USBClient::disconnect() { | ||||||
|   this->on_disconnected(); |   this->on_disconnected(); | ||||||
|   auto err = usb_host_device_close(this->handle_, this->device_handle_); |   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 ep_address The endpoint address. | ||||||
|  * @param callback The callback function to be called when the transfer is complete. |  * @param callback The callback function to be called when the transfer is complete. | ||||||
|  * @param length The length of the data to be transferred. |  * @param length The length of the data to be transferred. | ||||||
|  * @return TransferResult indicating success or specific failure reason |  | ||||||
|  * |  * | ||||||
|  * @throws None. |  * @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_(); |   auto *trq = this->get_trq_(); | ||||||
|   if (trq == nullptr) { |   if (trq == nullptr) { | ||||||
|     ESP_LOGE(TAG, "Too many requests queued"); |     ESP_LOGE(TAG, "Too many requests queued"); | ||||||
|     return TRANSFER_ERROR_NO_SLOTS; |     return false; | ||||||
|   } |   } | ||||||
|   trq->callback = callback; |   trq->callback = callback; | ||||||
|   trq->transfer->callback = transfer_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) { |   if (err != ESP_OK) { | ||||||
|     ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); |     ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); | ||||||
|     this->release_trq(trq); |     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. |  * @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_(); |   auto *trq = this->get_trq_(); | ||||||
|   if (trq == nullptr) { |   if (trq == nullptr) { | ||||||
|     ESP_LOGE(TAG, "Too many requests queued"); |     ESP_LOGE(TAG, "Too many requests queued"); | ||||||
|     return; |     return false; | ||||||
|   } |   } | ||||||
|   trq->callback = callback; |   trq->callback = callback; | ||||||
|   trq->transfer->callback = transfer_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) { |   if (err != ESP_OK) { | ||||||
|     ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); |     ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); | ||||||
|     this->release_trq(trq); |     this->release_trq(trq); | ||||||
|  |     return false; | ||||||
|   } |   } | ||||||
|  |   return true; | ||||||
| } | } | ||||||
| void USBClient::dump_config() { | void USBClient::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
| @@ -508,7 +505,7 @@ void USBClient::dump_config() { | |||||||
| // - Main loop: When transfer submission fails | // - Main loop: When transfer submission fails | ||||||
| // | // | ||||||
| // THREAD SAFETY: Lock-free using atomic AND to clear bit | // 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) { | void USBClient::release_trq(TransferRequest *trq) { | ||||||
|   if (trq == nullptr) |   if (trq == nullptr) | ||||||
|     return; |     return; | ||||||
| @@ -520,10 +517,10 @@ void USBClient::release_trq(TransferRequest *trq) { | |||||||
|     return; |     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 |   // fetch_and with inverted bitmask clears the bit atomically | ||||||
|   trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index; |   trq_bitmask_t mask = ~(static_cast<trq_bitmask_t>(1) << index); | ||||||
|   this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release); |   this->trq_in_use_.fetch_and(mask, std::memory_order_release); | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace usb_host | }  // namespace usb_host | ||||||
|   | |||||||
| @@ -324,9 +324,58 @@ void USBUartComponent::start_input(USBUartChannel *channel) { | |||||||
|   // |   // | ||||||
|   // The underlying transfer_in() uses lock-free atomic allocation from the |   // The underlying transfer_in() uses lock-free atomic allocation from the | ||||||
|   // TransferRequest pool, making this multi-threaded access safe |   // TransferRequest pool, making this multi-threaded access safe | ||||||
|  | <<<<<<< HEAD | ||||||
|  |  | ||||||
|   // Do the actual work (input_started_ already set to true by CAS above) |   // Do the actual work (input_started_ already set to true by CAS above) | ||||||
|   this->do_start_input_(channel); |   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) { | 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_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); |     usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); | ||||||
|     channel->initialised_.store(false); |     // Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts | ||||||
|     channel->input_started_.store(false); |     channel->input_started_.store(true); | ||||||
|     channel->output_started_.store(false); |     channel->output_started_.store(true); | ||||||
|     channel->input_buffer_.clear(); |     channel->input_buffer_.clear(); | ||||||
|     channel->output_buffer_.clear(); |     channel->output_buffer_.clear(); | ||||||
|  |     channel->initialised_.store(false); | ||||||
|   } |   } | ||||||
|   USBClient::on_disconnected(); |   USBClient::on_disconnected(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | usb_host: | ||||||
|  |   max_transfer_requests: 32 | ||||||
|  |  | ||||||
| usb_uart: | usb_uart: | ||||||
|   - id: uart_0 |   - id: uart_0 | ||||||
|     type: cdc_acm |     type: cdc_acm | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user