Device hotplug support

This commit is contained in:
Ken Van Hoeylandt 2026-09-19 15:13:22 +02:00
parent 996aea8dac
commit a264a96321
6 changed files with 265 additions and 3 deletions

View File

@ -113,6 +113,7 @@ error_t device_remove(struct Device* device);
* device's lock for its entire body, so doing so would deadlock. * device's lock for its entire body, so doing so would deadlock.
* @param[in,out] device non-null device pointer * @param[in,out] device non-null device pointer
* @retval ERROR_INVALID_STATE if the device is already started or not added * @retval ERROR_INVALID_STATE if the device is already started or not added
* @retval ERROR_NOT_FOUND probe() reported the device isn't present
* @retval ERROR_RESOURCE when driver binding fails * @retval ERROR_RESOURCE when driver binding fails
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
*/ */
@ -389,6 +390,32 @@ bool device_has_active_by_type(const struct DeviceType* type);
*/ */
error_t device_get_first_by_compatible(const char* compatible, struct Device** out_device); error_t device_get_first_by_compatible(const char* compatible, struct Device** out_device);
/** Consecutive device_hotplug_poll_once() calls a probe() result must hold before it takes effect. */
#define DEVICE_HOTPLUG_DEBOUNCE_COUNT 2
/**
* Registers @a device with the hotplug poller (see device_hotplug_poll_once()): started once
* probe() (or immediately if NULL) reports present, stopped when it reports absent. A device
* never registered here is never touched by the poller.
* @warning Call only at boot, before polling starts.
*/
void device_hotplug_register(struct Device* device);
/** Reverses device_hotplug_register(). No-op if not registered. */
void device_hotplug_unregister(struct Device* device);
/**
* Starts/stops every device_hotplug_register()'d device per its driver's probe() (or
* unconditionally, once, if probe is NULL), debounced across DEVICE_HOTPLUG_DEBOUNCE_COUNT
* calls. Never removes/destructs a device.
* @retval ERROR_NONE on success
* @retval (other) a mandatory (probe == NULL) device failed to start
*/
error_t device_hotplug_poll_once(void);
/** Starts a periodic timer that calls device_hotplug_poll_once(). */
void device_hotplug_start_polling(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -23,6 +23,12 @@ struct Driver {
error_t (*start_device)(struct Device* dev); error_t (*start_device)(struct Device* dev);
/** Function to deinitialize the driver for a device */ /** Function to deinitialize the driver for a device */
error_t (*stop_device)(struct Device* dev); error_t (*stop_device)(struct Device* dev);
/**
* Optional: reports whether the hardware is physically present. Checked by driver_bind()
* and periodically by device_hotplug_poll_once(). NULL means always present.
* @warning Must not call device_add()/device_remove() - may run while the ledger is locked.
*/
bool (*probe)(struct Device* device);
/** Contains the driver's functions */ /** Contains the driver's functions */
const void* api; const void* api;
/** Which type of devices this driver creates (can be NULL) */ /** Which type of devices this driver creates (can be NULL) */
@ -100,6 +106,7 @@ error_t driver_remove_destruct(struct Driver* driver);
* *
* @param driver The driver to bind. * @param driver The driver to bind.
* @param device The device to bind to. * @param device The device to bind to.
* @retval ERROR_NOT_FOUND probe() reported the device isn't present
* @return ERROR_NONE if successful, or an error code otherwise. * @return ERROR_NONE if successful, or an error code otherwise.
*/ */
error_t driver_bind(struct Driver* driver, struct Device* device); error_t driver_bind(struct Driver* driver, struct Device* device);

View File

@ -7,6 +7,7 @@
#include <tactility/log.h> #include <tactility/log.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/concurrent/recursive_mutex.h> #include <tactility/concurrent/recursive_mutex.h>
#include <tactility/concurrent/timer.h>
#include <ranges> #include <ranges>
#include <cassert> #include <cassert>
@ -43,6 +44,11 @@ struct DeviceInternal {
} state; } state;
/** Attached child devices */ /** Attached child devices */
std::vector<Device*> children {}; std::vector<Device*> children {};
// device_hotplug_poll_once() debounce state.
struct {
uint8_t consecutive = 0; // probe() results differing from `present` so far
bool present = false;
} hotplug;
// Outstanding device_get() holders. Guarded by `mutex`. Independent of state.started - // Outstanding device_get() holders. Guarded by `mutex`. Independent of state.started -
// device_get()/device_put() bracket construct/destruct, not start/stop, so a ref can be held // device_get()/device_put() bracket construct/destruct, not start/stop, so a ref can be held
// across a device_stop(). device_destruct() refuses to run while this is > 0. // across a device_stop(). device_destruct() refuses to run while this is > 0.
@ -225,7 +231,7 @@ error_t device_start(Device* device) {
unlock_internal(internal); unlock_internal(internal);
if (bind_error != ERROR_NONE) { if (bind_error != ERROR_NONE) {
return ERROR_RESOURCE; return bind_error == ERROR_NOT_FOUND ? ERROR_NOT_FOUND : ERROR_RESOURCE;
} }
device_listener_notify(device, DEVICE_EVENT_STARTED); device_listener_notify(device, DEVICE_EVENT_STARTED);
@ -559,4 +565,82 @@ error_t device_get_first_by_compatible(const char* compatible, Device** out_devi
return error; return error;
} }
struct HotplugAction {
Device* device;
bool start; // vs. stop
bool mandatory; // probe == nullptr; a start failure here is a real error
};
// Boot-time-only writes, so no lock: registration finishes before polling starts.
static std::vector<Device*> hotplug_registered_devices;
static Timer* hotplug_timer = nullptr;
static void hotplug_timer_callback(void*) {
device_hotplug_poll_once();
}
void device_hotplug_register(Device* device) {
hotplug_registered_devices.push_back(device);
}
void device_hotplug_unregister(Device* device) {
const auto iterator = std::ranges::find(hotplug_registered_devices, device);
if (iterator != hotplug_registered_devices.end()) {
hotplug_registered_devices.erase(iterator);
}
}
error_t device_hotplug_poll_once(void) {
// Collect actions first: start_device()/stop_device() may add/remove child devices
// (ledger_lock), so don't call them mid-iteration.
std::vector<HotplugAction> actions;
for (Device* device : hotplug_registered_devices) {
auto* internal = device->internal;
auto* driver = internal->driver;
if (driver == nullptr) {
continue;
}
if (driver->probe == nullptr) {
if (!internal->state.started) {
actions.push_back({ device, true, true });
}
continue;
}
bool present = driver->probe(device);
if (present == internal->hotplug.present) {
internal->hotplug.consecutive = 0;
continue;
}
if (++internal->hotplug.consecutive < DEVICE_HOTPLUG_DEBOUNCE_COUNT) {
continue;
}
internal->hotplug.present = present;
internal->hotplug.consecutive = 0;
actions.push_back({ device, present, false });
}
error_t first_mandatory_error = ERROR_NONE;
for (const auto& action : actions) {
error_t error = action.start ? device_start(action.device) : device_stop(action.device);
if (action.mandatory && action.start && error != ERROR_NONE && first_mandatory_error == ERROR_NONE) {
first_mandatory_error = error;
}
}
return first_mandatory_error;
}
void device_hotplug_start_polling(void) {
if (hotplug_timer != nullptr) {
return;
}
hotplug_timer = timer_alloc(TIMER_TYPE_PERIODIC, pdMS_TO_TICKS(500), hotplug_timer_callback, nullptr);
timer_start(hotplug_timer);
}
} // extern "C" } // extern "C"

View File

@ -129,6 +129,11 @@ error_t driver_bind(Driver* driver, Device* device) {
goto error; goto error;
} }
if (driver->probe != nullptr && !driver->probe(device)) {
error = ERROR_NOT_FOUND;
goto error;
}
if (driver->start_device != nullptr) { if (driver->start_device != nullptr) {
error = driver->start_device(device); error = driver->start_device(device);
if (error != ERROR_NONE) { if (error != ERROR_NONE) {

View File

@ -65,13 +65,16 @@ error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[])
dts_module++; dts_module++;
} }
// Starting is deferred to device_hotplug_poll_once() below so probe() can run first.
// DISABLED devices are never registered, so the poller never starts them.
const DtsDevice* dts_device = dts_devices; const DtsDevice* dts_device = dts_devices;
while (dts_device->device != nullptr) { while (dts_device->device != nullptr) {
if (dts_device->status == DTS_DEVICE_STATUS_OKAY) { if (dts_device->status == DTS_DEVICE_STATUS_OKAY) {
if (device_construct_add_start(dts_device->device, dts_device->compatible) != ERROR_NONE) { if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) {
LOG_E(TAG, "kernel_init failed to construct+add+start device: %s (%s)", dts_device->device->name, dts_device->compatible); LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible);
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
device_hotplug_register(dts_device->device);
} else if (dts_device->status == DTS_DEVICE_STATUS_DISABLED) { } else if (dts_device->status == DTS_DEVICE_STATUS_DISABLED) {
if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) { if (device_construct_add(dts_device->device, dts_device->compatible) != ERROR_NONE) {
LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible); LOG_E(TAG, "kernel_init failed to construct+add device: %s (%s)", dts_device->device->name, dts_device->compatible);
@ -83,6 +86,13 @@ error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[])
dts_device++; dts_device++;
} }
// A mandatory (probe == NULL) device's start failure aborts boot, as before.
if (device_hotplug_poll_once() != ERROR_NONE) {
LOG_E(TAG, "kernel_init failed to start a mandatory device");
return ERROR_RESOURCE;
}
device_hotplug_start_polling();
LOG_I(TAG, "init done"); LOG_I(TAG, "init done");
return ERROR_NONE; return ERROR_NONE;
} }

View File

@ -0,0 +1,129 @@
#include "doctest.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/module.h>
static Module hotplug_test_module = {
.name = "hotplug_test_module",
.start = nullptr,
.stop = nullptr
};
static bool probe_present = false;
static int probe_driver_start_called = 0;
static int probe_driver_stop_called = 0;
static bool probe_driver_probe(Device*) { return probe_present; }
static error_t probe_driver_start(Device*) { probe_driver_start_called++; return ERROR_NONE; }
static error_t probe_driver_stop(Device*) { probe_driver_stop_called++; return ERROR_NONE; }
static Driver probe_driver = {
.name = "hotplug_test_probe_driver",
.compatible = (const char*[]) { "hotplug_test,probe", nullptr },
.start_device = probe_driver_start,
.stop_device = probe_driver_stop,
.probe = probe_driver_probe,
.api = nullptr,
.device_type = nullptr,
.owner = &hotplug_test_module,
.internal = nullptr,
};
TEST_CASE("device_hotplug_poll_once starts/stops a probe-capable device after debounce") {
probe_present = false;
probe_driver_start_called = 0;
probe_driver_stop_called = 0;
static Device device {
.name = "hotplug_probe_device",
.config = nullptr,
.parent = nullptr,
};
CHECK_EQ(driver_construct_add(&probe_driver), ERROR_NONE);
CHECK_EQ(device_construct_add(&device, "hotplug_test,probe"), ERROR_NONE);
device_hotplug_register(&device);
// Not present: repeated polling never starts it.
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_EQ(probe_driver_start_called, 0);
CHECK_FALSE(device_is_ready(&device));
// Present: needs DEVICE_HOTPLUG_DEBOUNCE_COUNT consecutive polls before it actually starts.
probe_present = true;
for (int i = 0; i < DEVICE_HOTPLUG_DEBOUNCE_COUNT - 1; i++) {
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_FALSE(device_is_ready(&device));
}
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK(device_is_ready(&device));
CHECK_EQ(probe_driver_start_called, 1);
// Already started: further polls while still present don't restart it.
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_EQ(probe_driver_start_called, 1);
// Absent again: needs the same debounce before it stops.
probe_present = false;
for (int i = 0; i < DEVICE_HOTPLUG_DEBOUNCE_COUNT - 1; i++) {
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK(device_is_ready(&device));
}
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_FALSE(device_is_ready(&device));
CHECK_EQ(probe_driver_stop_called, 1);
// Only started/stopped, never removed/destructed.
CHECK(device_is_added(&device));
device_hotplug_unregister(&device);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
CHECK_EQ(driver_remove_destruct(&probe_driver), ERROR_NONE);
}
static int mandatory_driver_start_called = 0;
static error_t mandatory_driver_start(Device*) { mandatory_driver_start_called++; return ERROR_NONE; }
static error_t mandatory_driver_stop(Device*) { return ERROR_NONE; }
static Driver mandatory_driver = {
.name = "hotplug_test_mandatory_driver",
.compatible = (const char*[]) { "hotplug_test,mandatory", nullptr },
.start_device = mandatory_driver_start,
.stop_device = mandatory_driver_stop,
.probe = nullptr,
.api = nullptr,
.device_type = nullptr,
.owner = &hotplug_test_module,
.internal = nullptr,
};
TEST_CASE("device_hotplug_poll_once starts a mandatory (probe == NULL) device unconditionally on the first call") {
mandatory_driver_start_called = 0;
static Device device {
.name = "hotplug_mandatory_device",
.config = nullptr,
.parent = nullptr,
};
CHECK_EQ(driver_construct_add(&mandatory_driver), ERROR_NONE);
CHECK_EQ(device_construct_add(&device, "hotplug_test,mandatory"), ERROR_NONE);
device_hotplug_register(&device);
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK(device_is_ready(&device));
CHECK_EQ(mandatory_driver_start_called, 1);
// Already started: further polls don't restart it.
CHECK_EQ(device_hotplug_poll_once(), ERROR_NONE);
CHECK_EQ(mandatory_driver_start_called, 1);
device_hotplug_unregister(&device);
CHECK_EQ(device_stop(&device), ERROR_NONE);
CHECK_EQ(device_remove(&device), ERROR_NONE);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
CHECK_EQ(driver_remove_destruct(&mandatory_driver), ERROR_NONE);
}