mirror of
https://github.com/esphome/esphome.git
synced 2025-10-31 23:21:54 +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