aboutsummaryrefslogtreecommitdiff
path: root/docs/guides
diff options
context:
space:
mode:
Diffstat (limited to 'docs/guides')
-rw-r--r--docs/guides/device-drivers.rst250
1 files changed, 249 insertions, 1 deletions
diff --git a/docs/guides/device-drivers.rst b/docs/guides/device-drivers.rst
index 61a91ddf..3d3da182 100644
--- a/docs/guides/device-drivers.rst
+++ b/docs/guides/device-drivers.rst
@@ -1,2 +1,250 @@
Implementing a Device Driver
-============================ \ No newline at end of file
+============================
+
+This guide walks through the core of TeachOS's device/driver model — the four types that make up the device tree, how a driver gets matched and bound to a device, and a complete worked example already living in the tree.
+It assumes the facet-dispatch mechanism (``facet_id``, ``device.facet<T>()``, ``query_facet()``) is already familiar; if it is not, read *Technical Brief 0003, "Facet-Based Capability Dispatch in the Device Model"* first (:doc:`/briefs/tb0003-facet-based-capability-dispatch`) — this guide builds directly on it and does not re-derive it.
+Two related topics — the declarative resource model (``kapi::devices::resource``, for MMIO/port/IRQ/DMA assignment, :doc:`/briefs/tb0005-device-resources`) and the linker-section-based driver self-registration mechanism (:doc:`/briefs/tb0006-compile-time-driver-self-registration`) — are used here only as much as the worked example needs.
+
+The Four Building Blocks
+--------------------------
+
+Every part of the device tree is built from exactly four types, all in ``kapi::devices`` (``kapi/kapi/devices/*.hpp``, included together as ``<kapi/devices.hpp>``):
+
+* **A device** (``device``) is an inert tree node. It carries an identity (a tree name, not a stable one — see below), a lifecycle state, a weak back-pointer to its parent, an optional bound driver, opaque driver-owned data, and a list of assigned resources. It does no work on its own; every behavior a concrete device type has beyond that comes from the facets it implements (:doc:`/briefs/tb0003-facet-based-capability-dispatch`).
+* **A bus** (``bus : device``) is a device that owns children. It holds them by ``shared_ptr`` (a bus's destructor tears down its whole subtree), and it optionally carries a ``bus_protocol *``. A bus with no protocol can still hold children (attached directly, one at a time, by whatever constructs it) but cannot autonomously enumerate them or match drivers against them.
+* **A driver** (``driver``) is stateless logic: ``probe()`` to claim and initialize a device, ``unbind()`` to undo that, and ``suspend()``/``resume()`` for power management. A driver instance is never itself a node in the tree — it can be bound to any number of devices, one at a time, over its lifetime.
+* **A bus protocol** (``bus_protocol``) is the bus-specific behavior a ``bus`` delegates to: ``enumerate()`` discovers a bus's children, and ``match()`` scores how well a given driver fits a given device. This is the one piece of the model every new kind of bus (ISA, a future PCI, TeachOS's own pseudo-device bus below) has to write for itself; everything else is reused as-is.
+
+.. code-block:: text
+
+ root (bus)
+ └── pseudo (bus, kernel::bus::pseudo, has a bus_protocol)
+ ├── "null" (device, publishes kernel::bus::pseudo_signature)
+ │ bound to: kernel::drivers::pseudo::null (a driver)
+ └── "zero" (device, publishes kernel::bus::pseudo_signature)
+ bound to: kernel::drivers::pseudo::zero (a driver)
+
+A device's tree name (the string passed to its constructor) is unique only within its parent bus, not globally — it is what shows up in logs and in ``kapi::devices::device_registry::find()``, not a stable identifier a filesystem or a persistent record should depend on.
+
+The Device Lifecycle
+-----------------------
+
+``kapi::devices::state`` (``device.hpp``) is a small, linear state machine, and every transition happens at one specific, findable call site:
+
+.. code-block:: cpp
+
+ enum struct state
+ {
+ uninitialized, // constructed, not yet attached to a bus
+ present, // attached to a bus, not yet matched to a driver
+ probing, // a driver's probe() is currently running
+ bound, // a driver successfully claimed this device
+ failed, // every candidate driver's probe() failed
+ removed, // detached from its bus; the object may still be kept alive elsewhere
+ };
+
+* ``uninitialized → present`` happens inside ``bus::add_child()``, immediately after the device is registered with ``device_registry`` and pushed into the parent's child list.
+* ``present → probing → (bound | failed)`` happens inside ``driver_registry``'s matching loop, described next.
+* ``(bound | failed | present) → removed`` happens inside ``bus::do_remove_child()``, which also calls the bound driver's ``unbind()`` (if any), withdraws every facet the device published (``facet_registry::withdraw_all_for()``), and unregisters the device from ``device_registry`` — all before the device object itself is necessarily destroyed. A device can stay alive past ``removed`` if something else still holds a ``shared_ptr`` to it (mirroring the reasoning in ``kapi::devices::facet_registry``'s own documentation about weak references outliving a device's tree membership).
+
+Matching and Binding
+------------------------
+
+Attaching a device to a bus does not, by itself, bind a driver to it. That is ``driver_registry``'s job (``kapi::devices::driver_registry``, a single global instance), and it runs the same algorithm from two different triggers:
+
+* ``driver_registry::add(driver)`` — a new driver was just registered; try it against every currently-unbound device in the whole tree.
+* ``driver_registry::device_attached(device)`` — a new device was just attached to a bus (called automatically from ``bus::add_child()``); try every currently-registered driver against it.
+
+Both funnel into ``try_bind()``:
+
+.. code-block:: cpp
+
+ auto driver_registry::try_bind(kstd::shared_ptr<device> const & device) -> void
+ {
+ if (!device || device->state() == state::bound) { return; } // sticky binding
+
+ auto parent = device->parent();
+ auto protocol = parent ? parent->facet<bus_protocol>() : nullptr;
+ if (!protocol) { return; } // no bus_protocol, e.g. a plain bus with no matching story
+
+ // ... collect {priority, driver} for every registered driver where protocol->match() succeeds ...
+ std::ranges::stable_sort(candidates, std::ranges::greater{}, &candidate::priority);
+
+ for (auto const & candidate : candidates)
+ {
+ device->bind_driver(candidate.driver_handle);
+ device->set_state(state::probing);
+
+ if (candidate.driver_handle->probe(*device))
+ {
+ device->set_state(state::bound);
+ return; // done — the highest-priority driver that actually probed successfully wins
+ }
+
+ device->bind_driver(kstd::weak_ptr<driver>{}); // that candidate failed; try the next
+ }
+
+ if (!candidates.empty()) { device->set_state(state::failed); }
+ }
+
+Three things worth being deliberate about, because each one is a real design decision, not an accident of implementation:
+
+* **Priority, not registration order, decides the winner.** Every driver whose ``match()`` returns a value (rather than an error) is a candidate; candidates are sorted by priority, highest first, and tried in that order. Registration order only breaks exact ties (``stable_sort``). A bus with only one plausible driver per device (ISA, the pseudo bus below) always returns a fixed priority from ``match()`` — see ISA's own ``match()`` in :doc:`/briefs/tb0003-facet-based-capability-dispatch` for why that is a legitimate degenerate case, not unfinished code.
+* **A failed ``probe()`` falls through, not fails outright.** If the highest-priority candidate's ``probe()`` returns an error, the device is unbound again and the *next* candidate is tried — a driver can lose at the probe step for a reason ``match()`` had no way to see (a resource already claimed by something else, say).
+* **Binding is sticky.** ``try_bind()`` returns immediately for a device already in ``state::bound``. A driver registered after the fact never steals an already-bound device, no matter what priority it would have scored — rebinding is not something this loop ever does on its own.
+
+``match()``'s error channel distinguishes "not a candidate" from "something actually went wrong": ``kapi::devices::driver_match_errc::no_match`` (and ``no_device_signature``) both map to ``kstd::errc::not_supported`` via their category's ``default_error_condition()``, and ``try_bind()`` only logs a warning for an error that maps to something *else* — the ordinary case of twenty unrelated drivers all declining a device produces no log spam, while a genuine probing failure inside ``match()`` itself still surfaces.
+
+A Worked Example: the Pseudo Device Bus
+-------------------------------------------
+
+TeachOS's simplest real bus, ``kernel::bus::pseudo`` (``kernel/kernel/bus/pseudo.{hpp,cpp}``), backs ``/dev/null`` and ``/dev/zero``. It has no real hardware behind it at all, which makes it a better first example to trace than ISA — there is nothing to get right beyond the device model itself.
+
+**The identification pair.** Exactly the ``*_signature``/``*_claim`` pattern from :doc:`/briefs/tb0003-facet-based-capability-dispatch`, this time matching by an arbitrary name rather than a fixed ISA identifier:
+
+.. code-block:: cpp
+
+ struct pseudo_signature
+ {
+ constexpr auto static id = kapi::capabilities::facet_id{"sig.dev.pseudo"};
+ virtual ~pseudo_signature() = default;
+ [[nodiscard]] virtual auto name() const noexcept -> std::string_view = 0;
+ };
+
+ struct pseudo_claim
+ {
+ constexpr auto static id = kapi::capabilities::facet_id{"clm.drv.pseudo"};
+ virtual ~pseudo_claim() = default;
+ [[nodiscard]] virtual auto supported_names() const noexcept -> std::span<std::string_view const> = 0;
+ };
+
+**The bus and its protocol.** ``pseudo_protocol::match()`` (an anonymous-namespace type local to ``pseudo.cpp``) checks whether the device's signature name appears in the driver's claimed names, and returns a fixed priority (``0u``) on a hit — there is never more than one candidate driver per pseudo device, so there is nothing to rank:
+
+.. code-block:: cpp
+
+ struct pseudo final : kapi::devices::bus
+ {
+ pseudo() : kapi::devices::bus{"pseudo", protocol_instance} {} // protocol_instance: a static pseudo_protocol
+ };
+
+**The device — a third way to implement a facet.** :doc:`/briefs/tb0003-facet-based-capability-dispatch` shows a facet implemented by multiple inheritance (``pit`` directly *is* an ``isa_signature``) and by composition through the facet registry (``ram_disk``'s driver-owned ``block_node``). ``kernel::devices::pseudo`` uses a third, equally valid shape: a facet implementation held as a plain **member**, with ``query_facet()`` handing out its address:
+
+.. code-block:: cpp
+
+ struct pseudo_signature final : kernel::bus::pseudo_signature
+ {
+ explicit constexpr pseudo_signature(kapi::devices::device const & device) : m_name{device.name()} {}
+ [[nodiscard]] auto name() const noexcept -> std::string_view override { return m_name; }
+ private:
+ std::string_view m_name;
+ };
+
+ struct pseudo final : kapi::devices::device
+ {
+ explicit pseudo(kstd::string name);
+ protected:
+ auto query_facet(kapi::capabilities::facet_id facet) -> void * override;
+ private:
+ pseudo_signature m_signature{*this};
+ };
+
+ auto pseudo::query_facet(kapi::capabilities::facet_id facet) -> void *
+ {
+ if (facet == kernel::bus::pseudo_signature::id) { return &m_signature; }
+ return device::query_facet(facet);
+ }
+
+This is worth choosing deliberately, not by default: multiple inheritance is right when the device type and the facet are the same concept end to end (``pit`` *is* its own ISA identity); a member is right when the facet's implementation needs its own small piece of state derived from the device (here, just the device's own name, captured once at construction) without the device type itself needing to satisfy the facet's interface through inheritance. Both forward to ``device::query_facet()`` for anything they don't recognize — the delegation rule from :doc:`/briefs/tb0003-facet-based-capability-dispatch` applies identically regardless of which shape you pick.
+
+**The driver.** ``null`` (``kernel/kernel/drivers/pseudo/null.{hpp,cpp}``) implements ``pseudo_claim`` and the two lifecycle methods every driver must provide:
+
+.. code-block:: cpp
+
+ auto null::probe(kapi::devices::device & device) -> kstd::result<void>
+ {
+ auto signature = device.facet<kernel::bus::pseudo_signature>();
+ if (!signature) { return kstd::failure(kapi::devices::driver_match_errc::no_device_signature); }
+ if (signature->name() != "null") { return kstd::failure(kapi::devices::driver_match_errc::invalid_device_signature); }
+
+ auto implementation = kstd::make_shared<null_node>(); // null_node : kapi::filesystem::character_special_file
+ auto published = kapi::devices::publish_facet<kapi::filesystem::character_special_file>(
+ device.shared_from_this(), "null", implementation.get());
+ if (!published) { return published; }
+
+ device.set_driver_data(std::move(implementation)); // keeps null_node alive for as long as the binding lasts
+ return kstd::success();
+ }
+
+ auto null::unbind(kapi::devices::device & device) -> void
+ {
+ device.set_driver_data(nullptr); // drops the driver's last reference to null_node
+ }
+
+Two things to notice, both general rules rather than quirks of this one driver:
+
+* **``probe()`` re-checks identity even though ``match()`` already checked it.** ``match()``'s job is to *rank* candidates before any one of them is committed to; ``probe()``'s job is to actually claim the device, and it is the one place a driver can fail for a reason specific to itself (here, a name mismatch would mean ``match()`` and ``probe()`` disagree — a bug worth catching defensively, distinguished from "no signature at all" by a separate ``driver_match_errc`` value). Do not assume ``probe()`` is only ever called on a device that already passed every check you might imagine — check what you depend on.
+* **``unbind()`` must undo exactly what ``probe()`` did, and nothing this driver does bypasses it.** ``null`` published a facet and stored driver data; ``unbind()`` clears the driver data (which drops the last strong reference to the published implementation) and nothing else — there is no separate facet withdrawal call here because ``bus::do_remove_child()`` already calls ``facet_registry::withdraw_all_for()`` unconditionally on removal. A driver that needs to unpublish facets *without* the device being removed from the tree (an explicit unbind requested by something other than removal) would need to call ``facet_registry::get().withdraw()`` itself — ``null`` never hits that path today, but a driver that can be unbound while its device stays attached should not assume ``unbind()`` is always followed by tree removal.
+
+**Registering the driver.** ``null`` never appears in a hand-maintained list anywhere. A small, file-local descriptor and a ``constexpr`` instance are enough:
+
+.. code-block:: cpp
+
+ namespace
+ {
+ struct descriptor final : kapi::devices::driver_descriptor
+ {
+ [[nodiscard]] auto name() const noexcept -> std::string_view override { return "dev_null"; }
+ [[nodiscard]] auto make_instance() const -> kstd::shared_ptr<kapi::devices::driver> override
+ {
+ return kstd::make_shared<null>();
+ }
+ };
+
+ [[gnu::used]]
+ constexpr auto registration = kapi::devices::kernel_driver<descriptor>{};
+ }
+
+``kernel_driver<Type>`` places a pointer to this descriptor into a dedicated linker section (``kernel_drivers``); at boot, ``kernel::drivers::init()`` walks that section, constructs one instance of every driver found in it via ``make_instance()``, and registers each with ``driver_registry::get().add()`` — the mechanism itself (why a linker section, how the section boundaries are exposed to C++, why this needs no runtime constructor to work, and how the identical idiom is reused for filesystem drivers) is covered in full in :doc:`/briefs/tb0006-compile-time-driver-self-registration` and is not repeated here. The practical rule for writing a new driver: add exactly this ``descriptor``/``kernel_driver<descriptor>`` pair (or ``platform_driver<descriptor>`` for a driver that belongs to platform rather than generic-kernel code) to your driver's ``.cpp`` file, and nothing elsewhere needs to know your driver exists.
+
+**Putting a device on the bus.** All of the above only takes effect once a device actually exists and is attached — which, for the pseudo bus, is three lines in ``kernel::devices::init()`` (``kernel/kernel/devices/init.cpp``):
+
+.. code-block:: cpp
+
+ auto init() -> void
+ {
+ auto root = kapi::devices::get_root_bus();
+
+ auto pseudo_bus = kstd::make_shared<kernel::bus::pseudo>();
+ root->add_child(pseudo_bus);
+
+ pseudo_bus->add_child(kstd::make_shared<devices::pseudo>("null"));
+ pseudo_bus->add_child(kstd::make_shared<devices::pseudo>("zero"));
+ }
+
+Each ``add_child()`` call runs the full sequence from *The Device Lifecycle* and *Matching and Binding* above: the device is registered, marked ``present``, and immediately offered to ``driver_registry::device_attached()``. Because ``kernel::drivers::init()`` (which registers ``null`` and ``zero`` via their ``kernel_driver<descriptor>`` self-registration) runs *before* ``kernel::devices::init()`` in ``kernel::main`` — a real ordering constraint, not an implementation accident, derived in full in :doc:`/briefs/tb0006-compile-time-driver-self-registration` — both drivers already exist in ``driver_registry`` by the time these two ``add_child()`` calls fire, so each pseudo device is matched and bound synchronously, inline, before ``kernel::devices::init()`` returns.
+
+Resources, Briefly
+----------------------
+
+A device can also carry a list of declarative ``kapi::devices::resource`` values (``mmio_range``, ``io_port``, ``interrupt_line``, ``dma_channel``) — assigned by whatever constructs the device, before it is attached, and requested inside ``probe()``:
+
+.. code-block:: cpp
+
+ auto port = device.request_resource<kapi::devices::resource_type::port>();
+ if (!port) { return kstd::failure(port.error()); }
+
+This keeps a driver from hard-coding an address or a port number that really belongs to whatever bus discovered the device (see the ISA PIT driver, ``arch/x86_64/arch/drivers/pit.cpp``, for a complete example). The resource type system itself — the tagged-union shape of ``resource``, how a bus assigns resources during enumeration, and its real drawbacks (no overlap detection, no automatic release on unbind) — is out of scope for this guide; see :doc:`/briefs/tb0005-device-resources` for the full treatment.
+
+Suspend and Resume, Briefly
+--------------------------------
+
+``driver::suspend()``/``resume()`` default to no-ops, so a driver that has nothing to do on a power transition needs no overrides at all. ``kapi::devices::suspend_tree()``/``resume_tree()`` (``kapi/kapi/devices/power.hpp``) walk a bus's subtree depth-first — children suspended before their parents, and a failed suspend rolls back everything already suspended in that same walk, in reverse order, before reporting the error. A driver that overrides ``suspend()`` must leave the device either fully suspended or exactly as it found it; there is no partially-suspended state this mechanism is prepared to resume from.
+
+Checklist
+------------
+
+* Every ``query_facet()`` override ends with a call to its immediate base's ``query_facet()`` for anything it doesn't recognize — see the delegation rule in :doc:`/briefs/tb0003-facet-based-capability-dispatch`.
+* ``probe()`` re-validates what ``match()`` already checked once, rather than trusting that a successful ``match()`` guarantees a successful ``probe()``.
+* ``unbind()`` undoes exactly what ``probe()`` did — released resources, withdrawn facets your driver published explicitly (tree removal withdraws the rest for you), cleared driver data.
+* A new facet type needs no changes anywhere in ``kapi::devices``; a new driver needs no changes anywhere except its own ``.cpp`` file, via ``kernel_driver<descriptor>``/``platform_driver<descriptor>``.
+* Adding new source files means updating ``CMakeLists.txt`` — easy to forget, and nothing here catches it for you at review time.