Ken Van Hoeylandt cb4ce5246d
USB host refactored (#684)
- Create `USB_HOST_TYPE` base driver in TactilityKernel
- Refactor esp32_usbhost dts format, and create child devices dynamically
- Use PSRAM for task stacks where possible: saving 20 kB from just the task stacks, but roughly 30 kB when nothing is connected.
- Allow the user to disable USB host interface in the USB settings app, saving an additionaly 20 kB of IRAM when nothing was connected.
- Change listener-based APIs to subscribe and poll/await pattern. This avoids callstacks of apps and services leaking into the USB API

This results in big IRAM savings: 30 kB when USB host is on and nothing is connected, and 50 kB total when USB host is disabled.
This brings the total free IRAM after boot from roughly 170 kB to 200 kB (host enabled) or 220 kB (host disabled).
2026-10-10 13:33:32 +02:00

545 lines
18 KiB
C++

#include <sdkconfig.h>
#ifdef CONFIG_SOC_USB_OTG_SUPPORTED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/usb_host.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <usb/usb_host.h>
#include <esp_intr_alloc.h>
#define TAG "esp32_usbhost"
#define GET_CONFIG(device) ((const Esp32UsbHostConfig*)(device)->config)
#define USB_HOST_AUDIO_SUPPORTED (CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3)
constexpr auto USB_LIB_TASK_STACK = 4096;
constexpr auto USB_LIB_TASK_PRIORITY = 10;
constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500;
constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000;
constexpr auto USB_HOST_STOP_RETRY_MS = 1000;
constexpr auto USB_WORKER_TASK_STACK = 4096;
constexpr auto USB_WORKER_TASK_PRIORITY = 5;
constexpr auto USB_WORKER_QUEUE_SIZE = 4;
constexpr auto USB_WORKER_IDLE_TIMEOUT_MS = 1000;
constexpr auto USB_HOST_CLASS_COUNT = USB_HOST_CLASS_AUDIO + 1;
constexpr auto DEVICE_DESTRUCT_RETRIES = 100;
struct UsbHostWorkerJob {
// nullptr stops the worker
void (*function)(void* context);
void* context;
StaticSemaphore_t done_buffer;
SemaphoreHandle_t done;
};
struct UsbHostContext {
// Exists only while there is work, guarded by worker_mutex
TaskHandle_t worker_task = nullptr;
QueueHandle_t worker_queue = nullptr;
SemaphoreHandle_t worker_mutex = nullptr;
TaskHandle_t lib_task = nullptr;
SemaphoreHandle_t lib_task_done = nullptr;
// Backend contexts indexed by UsbClass, nullptr when not running
void* backends[USB_HOST_CLASS_COUNT] = {};
};
// Stack may be in PSRAM: no flash access allowed from this task.
static void usbLibTask(void* arg) {
auto* ctx = static_cast<UsbHostContext*>(arg);
LOG_I(TAG, "lib task started");
while (true) {
uint32_t flags = 0;
esp_err_t err = usb_host_lib_handle_events(pdMS_TO_TICKS(USB_LIB_EVENT_TIMEOUT_MS), &flags);
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
LOG_W(TAG, "usb_host_lib_handle_events: %s", esp_err_to_name(err));
}
if (flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) {
LOG_I(TAG, "no more USB clients, freeing all devices");
usb_host_device_free_all();
}
if (flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) {
LOG_I(TAG, "all USB devices freed");
}
if (ulTaskNotifyTake(pdFALSE, 0) > 0) {
break;
}
}
LOG_I(TAG, "lib task stopping");
xSemaphoreGive(ctx->lib_task_done);
vTaskDeleteWithCaps(nullptr);
}
// region Worker
// Runs device lifecycle work for the class drivers, so device listeners never run on a USB task.
// The stack stays in internal RAM because listeners may access flash.
// Created on demand and exits when idle, so it uses no memory while nothing is plugged in or out.
static void usbWorkerTask(void* arg) {
auto* ctx = static_cast<UsbHostContext*>(arg);
while (true) {
UsbHostWorkerJob* job = nullptr;
if (xQueueReceive(ctx->worker_queue, &job, pdMS_TO_TICKS(USB_WORKER_IDLE_TIMEOUT_MS)) != pdTRUE) {
// Jobs are queued under worker_mutex, so none can arrive between this check and the exit
xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY);
const bool idle = uxQueueMessagesWaiting(ctx->worker_queue) == 0;
if (idle) {
ctx->worker_task = nullptr;
}
xSemaphoreGive(ctx->worker_mutex);
if (idle) {
break;
}
continue;
}
if (job->function == nullptr) {
xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY);
ctx->worker_task = nullptr;
xSemaphoreGive(ctx->worker_mutex);
xSemaphoreGive(job->done);
break;
}
job->function(job->context);
xSemaphoreGive(job->done);
}
vTaskDelete(nullptr);
}
static void wait_for_job(UsbHostWorkerJob* job) {
xSemaphoreTake(job->done, portMAX_DELAY);
vSemaphoreDelete(job->done);
}
static error_t create_worker_queue(UsbHostContext* ctx) {
ctx->worker_mutex = xSemaphoreCreateMutex();
ctx->worker_queue = xQueueCreate(USB_WORKER_QUEUE_SIZE, sizeof(UsbHostWorkerJob*));
if (ctx->worker_mutex == nullptr || ctx->worker_queue == nullptr) {
LOG_E(TAG, "failed to create worker queue");
if (ctx->worker_mutex != nullptr) vSemaphoreDelete(ctx->worker_mutex);
if (ctx->worker_queue != nullptr) vQueueDelete(ctx->worker_queue);
ctx->worker_mutex = nullptr;
ctx->worker_queue = nullptr;
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
// No jobs may be submitted anymore
static void delete_worker_queue(UsbHostContext* ctx) {
UsbHostWorkerJob job = { .function = nullptr, .context = nullptr, .done_buffer = {}, .done = nullptr };
job.done = xSemaphoreCreateBinaryStatic(&job.done_buffer);
UsbHostWorkerJob* job_pointer = &job;
xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY);
const bool running = ctx->worker_task != nullptr;
if (running) {
xQueueSend(ctx->worker_queue, &job_pointer, portMAX_DELAY);
}
xSemaphoreGive(ctx->worker_mutex);
if (running) {
wait_for_job(&job);
} else {
vSemaphoreDelete(job.done);
}
vQueueDelete(ctx->worker_queue);
vSemaphoreDelete(ctx->worker_mutex);
ctx->worker_queue = nullptr;
ctx->worker_mutex = nullptr;
}
error_t esp32_usbhost_run_on_worker(struct Device* host, void (*function)(void* context), void* context) {
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(host));
if (ctx == nullptr || ctx->worker_queue == nullptr) {
return ERROR_INVALID_STATE;
}
UsbHostWorkerJob job = { .function = function, .context = context, .done_buffer = {}, .done = nullptr };
UsbHostWorkerJob* job_pointer = &job;
xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY);
if (ctx->worker_task != nullptr && xTaskGetCurrentTaskHandle() == ctx->worker_task) {
xSemaphoreGive(ctx->worker_mutex);
function(context);
return ERROR_NONE;
}
if (ctx->worker_task == nullptr
&& xTaskCreate(usbWorkerTask, "usb_worker", USB_WORKER_TASK_STACK, ctx, USB_WORKER_TASK_PRIORITY, &ctx->worker_task) != pdPASS) {
ctx->worker_task = nullptr;
xSemaphoreGive(ctx->worker_mutex);
LOG_E(TAG, "failed to create worker task");
return ERROR_RESOURCE;
}
job.done = xSemaphoreCreateBinaryStatic(&job.done_buffer);
xQueueSend(ctx->worker_queue, &job_pointer, portMAX_DELAY);
xSemaphoreGive(ctx->worker_mutex);
wait_for_job(&job);
return ERROR_NONE;
}
// endregion
// region Devices
struct DeviceCreateJob {
Device* device;
Device* parent;
const char* name;
Driver* driver;
void* driver_data;
error_t result;
};
static void device_create_on_worker(void* context) {
auto* job = static_cast<DeviceCreateJob*>(context);
Device* device = job->device;
*device = Device {};
device->name = job->name;
job->result = device_construct(device);
if (job->result != ERROR_NONE) {
LOG_E(TAG, "failed to construct %s", job->name);
return;
}
device_set_driver_data(device, job->driver_data);
device_set_parent(device, job->parent);
device_set_driver(device, job->driver);
job->result = device_add(device);
if (job->result != ERROR_NONE) {
LOG_E(TAG, "failed to add %s", job->name);
device_destruct(device);
return;
}
job->result = device_start(device);
if (job->result != ERROR_NONE) {
LOG_E(TAG, "failed to start %s", job->name);
device_remove(device);
device_destruct(device);
}
}
struct DeviceDestroyJob {
Device* device;
error_t result;
};
static void device_destroy_on_worker(void* context) {
auto* job = static_cast<DeviceDestroyJob*>(context);
Device* device = job->device;
if (device_is_added(device)) {
error_t error = device_stop(device);
if (error == ERROR_NONE) {
error = device_remove(device);
}
if (error != ERROR_NONE) {
LOG_E(TAG, "failed to stop %s: %s", device->name, error_to_string(error));
job->result = error;
return;
}
}
// Short-lived device_get() references block destruction
error_t error = device_destruct(device);
for (int i = 0; error == ERROR_RESOURCE_BUSY && i < DEVICE_DESTRUCT_RETRIES; i++) {
vTaskDelay(pdMS_TO_TICKS(10));
error = device_destruct(device);
}
if (error != ERROR_NONE) {
LOG_E(TAG, "failed to destruct %s: %s", device->name, error_to_string(error));
}
job->result = error;
}
error_t esp32_usbhost_device_create(Device* host, Device* device, Device* parent, const char* name, Driver* driver, void* driver_data) {
DeviceCreateJob job = { device, parent, name, driver, driver_data, ERROR_NONE };
error_t error = esp32_usbhost_run_on_worker(host, device_create_on_worker, &job);
return error != ERROR_NONE ? error : job.result;
}
error_t esp32_usbhost_device_destroy(Device* host, Device* device) {
DeviceDestroyJob job = { device, ERROR_NONE };
error_t error = esp32_usbhost_run_on_worker(host, device_destroy_on_worker, &job);
return error != ERROR_NONE ? error : job.result;
}
// endregion
// region Backends
static const Esp32UsbHostBackend* get_backend(UsbClass usb_class) {
switch (usb_class) {
case USB_HOST_CLASS_HID: return &esp32_usbhost_hid_backend;
case USB_HOST_CLASS_MIDI: return &esp32_usbhost_midi_backend;
case USB_HOST_CLASS_MSC: return &esp32_usbhost_msc_backend;
case USB_HOST_CLASS_AUDIO:
#if USB_HOST_AUDIO_SUPPORTED
return &esp32_usbhost_uac_backend;
#else
return nullptr;
#endif
}
return nullptr;
}
static bool is_class_configured(const Esp32UsbHostConfig* config, UsbClass usb_class) {
switch (usb_class) {
case USB_HOST_CLASS_HID: return config->hid;
case USB_HOST_CLASS_MIDI: return config->midi;
case USB_HOST_CLASS_MSC: return config->msc;
case USB_HOST_CLASS_AUDIO: return config->audio;
}
return false;
}
// endregion
// region Lib
static error_t install_lib(UsbHostContext* ctx, const Esp32UsbHostConfig* cfg) {
usb_host_config_t host_cfg = {
.skip_phy_setup = false,
.root_port_unpowered = false,
.intr_flags = ESP_INTR_FLAG_LEVEL1,
.enum_filter_cb = nullptr,
.fifo_settings_custom = {},
.peripheral_map = cfg->peripheral_map,
};
esp_err_t ret = usb_host_install(&host_cfg);
if (ret != ESP_OK) {
LOG_E(TAG, "usb_host_install failed: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
ctx->lib_task_done = xSemaphoreCreateBinary();
if (!ctx->lib_task_done) {
LOG_E(TAG, "failed to create lib_task_done semaphore");
usb_host_uninstall();
return ERROR_RESOURCE;
}
BaseType_t result = esp32_usbhost_task_create_psram(usbLibTask, "usb_lib", USB_LIB_TASK_STACK,
ctx, USB_LIB_TASK_PRIORITY, &ctx->lib_task);
if (result != pdPASS) {
LOG_E(TAG, "failed to create usb_lib task");
vSemaphoreDelete(ctx->lib_task_done);
ctx->lib_task_done = nullptr;
usb_host_uninstall();
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t uninstall_lib(UsbHostContext* ctx) {
xTaskNotifyGive(ctx->lib_task);
bool exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_TIMEOUT_MS)) == pdTRUE);
if (!exited) {
// Free all devices to unblock the NO_CLIENTS / ALL_FREE flags, then retry.
usb_host_device_free_all();
exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_RETRY_MS)) == pdTRUE);
}
if (!exited) {
LOG_E(TAG, "lib task stop timed out after %dms, force terminating — USB host restart required",
USB_HOST_STOP_TIMEOUT_MS + USB_HOST_STOP_RETRY_MS);
vTaskDeleteWithCaps(ctx->lib_task);
vTaskDelay(pdMS_TO_TICKS(50));
// Skip usb_host_uninstall — USB stack is in undefined state.
ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done);
ctx->lib_task_done = nullptr;
return ERROR_RESOURCE;
}
ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done);
ctx->lib_task_done = nullptr;
esp_err_t uninstall_err = usb_host_uninstall();
if (uninstall_err != ESP_OK) {
LOG_W(TAG, "usb_host_uninstall: %s", esp_err_to_name(uninstall_err));
}
return ERROR_NONE;
}
// endregion
// region API
// Serializes backend changes with host start and stop, which create and delete the context
static SemaphoreHandle_t get_backends_mutex() {
static StaticSemaphore_t buffer;
static SemaphoreHandle_t mutex = xSemaphoreCreateMutexStatic(&buffer);
return mutex;
}
static bool is_class_supported_by(const Esp32UsbHostConfig* config, UsbClass usb_class) {
return is_class_configured(config, usb_class) && get_backend(usb_class) != nullptr;
}
static error_t start_backend(Device* device, UsbHostContext* ctx, UsbClass usb_class) {
error_t error = get_backend(usb_class)->start(device, &ctx->backends[usb_class]);
if (error != ERROR_NONE) {
LOG_E(TAG, "failed to start class %d", usb_class);
ctx->backends[usb_class] = nullptr;
}
return error;
}
static error_t stop_backend(UsbHostContext* ctx, UsbClass usb_class) {
// A retained backend still uses the lib and the worker
error_t error = get_backend(usb_class)->stop(ctx->backends[usb_class]);
if (error != ERROR_NONE) {
LOG_E(TAG, "failed to stop class %d: %s", usb_class, error_to_string(error));
return error;
}
ctx->backends[usb_class] = nullptr;
return ERROR_NONE;
}
static error_t is_class_supported(struct Device* device, enum UsbClass usb_class, bool* supported) {
if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) {
return ERROR_INVALID_ARGUMENT;
}
*supported = is_class_supported_by(GET_CONFIG(device), usb_class);
return ERROR_NONE;
}
static error_t is_class_enabled(struct Device* device, enum UsbClass usb_class, bool* enabled) {
if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) {
return ERROR_INVALID_ARGUMENT;
}
xSemaphoreTake(get_backends_mutex(), portMAX_DELAY);
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(device));
if (ctx != nullptr) {
*enabled = ctx->backends[usb_class] != nullptr;
}
xSemaphoreGive(get_backends_mutex());
return ctx != nullptr ? ERROR_NONE : ERROR_INVALID_STATE;
}
static error_t set_class_enabled(struct Device* device, enum UsbClass usb_class, bool enabled) {
if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) {
return ERROR_INVALID_ARGUMENT;
}
if (!is_class_supported_by(GET_CONFIG(device), usb_class)) {
return ERROR_NOT_SUPPORTED;
}
xSemaphoreTake(get_backends_mutex(), portMAX_DELAY);
error_t error = ERROR_NONE;
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(device));
if (ctx == nullptr) {
error = ERROR_INVALID_STATE;
} else if (enabled && ctx->backends[usb_class] == nullptr) {
error = start_backend(device, ctx, usb_class);
} else if (!enabled && ctx->backends[usb_class] != nullptr) {
error = stop_backend(ctx, usb_class);
}
xSemaphoreGive(get_backends_mutex());
return error;
}
// endregion
extern "C" {
static error_t start_device(struct Device* device) {
auto* cfg = GET_CONFIG(device);
if (!cfg) {
LOG_E(TAG, "device config is null");
return ERROR_INVALID_ARGUMENT;
}
xSemaphoreTake(get_backends_mutex(), portMAX_DELAY);
auto* ctx = new UsbHostContext();
if (create_worker_queue(ctx) != ERROR_NONE) {
xSemaphoreGive(get_backends_mutex());
delete ctx;
return ERROR_RESOURCE;
}
if (install_lib(ctx, cfg) != ERROR_NONE) {
xSemaphoreGive(get_backends_mutex());
delete_worker_queue(ctx);
delete ctx;
return ERROR_RESOURCE;
}
device_set_driver_data(device, ctx);
for (int i = 0; i < USB_HOST_CLASS_COUNT; i++) {
const auto usb_class = static_cast<UsbClass>(i);
if (is_class_supported_by(cfg, usb_class)) {
start_backend(device, ctx, usb_class);
}
}
xSemaphoreGive(get_backends_mutex());
LOG_I(TAG, "started (peripheral_map=0x%02x)", cfg->peripheral_map);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
xSemaphoreTake(get_backends_mutex(), portMAX_DELAY);
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(device));
if (!ctx) {
xSemaphoreGive(get_backends_mutex());
return ERROR_NONE;
}
for (int i = USB_HOST_CLASS_COUNT - 1; i >= 0; i--) {
if (ctx->backends[i] == nullptr) {
continue;
}
error_t error = stop_backend(ctx, static_cast<UsbClass>(i));
if (error != ERROR_NONE) {
xSemaphoreGive(get_backends_mutex());
return error;
}
}
device_set_driver_data(device, nullptr);
xSemaphoreGive(get_backends_mutex());
error_t result = uninstall_lib(ctx);
delete_worker_queue(ctx);
delete ctx;
LOG_I(TAG, "stopped");
return result;
}
static const UsbHostApi usb_host_api = {
.is_class_supported = is_class_supported,
.is_class_enabled = is_class_enabled,
.set_class_enabled = set_class_enabled,
};
Driver esp32_usbhost_driver = {
.name = "esp32_usbhost",
.compatible = (const char*[]) { "espressif,esp32-usbhost", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.probe = nullptr,
.api = &usb_host_api,
.device_type = &USB_HOST_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED