Drivers & device trees

How the kernel learns about hardware and exposes it to programs.

10 min readChapter 32 of 50Intermediate → advanced
In this chapter

Before you start#

Required earlier lessons: C/C++ for source readers, Memory and system calls, Concurrency and object lifetime, and GKI and kernel builds. You need to know what a pointer, callback, process virtual address, kernel, and device lifetime mean. No device modification or driver loading is part of this lesson.

Learning goals#

You will distinguish a device-tree description from driver code; follow one pinned example from DT binding through match, probe, resource setup, and cleanup; explain common hardware terms without confusing bus/physical addresses with process virtual addresses; and explain what devm_* cleanup does and does not guarantee.

Practice#

Driver, subsystem, and device description

A device driver is kernel code that controls a hardware or virtual device. A driver should normally connect to the Linux subsystem for that device type: for example, input devices publish key/touch events through the input subsystem. Android userspace services/HALs can build higher-level features on top; they are not the kernel driver. A bus is a way devices are connected/discovered (platform, I2C, SPI, PCI, USB); it is also a parent context for resources and address interpretation.

A device tree (DT) is structured data describing hardware the kernel cannot discover by itself. Human-edited DTS is compiled to DTB; DTBO is an overlay that may be combined with a base tree (AOSP device-tree overlays). DT data describes a board; it is not executable driver code. The bootloader passes the selected tree to the kernel (Linux DT usage model). A binding defines which properties a compatible device may have and what they mean. The compatible string helps the kernel match a node to driver support (same page).

Plain-language hardware terms:

  • A bus address is an address meaningful to a device/controller on a particular bus. A reg property commonly describes an address range, interpreted using the parent node's #address-cells and #size-cells (DT specification, basics). It is not a userspace virtual pointer, and must not be copied across unrelated boards.
  • An interrupt (IRQ) is a notification that an event needs attention. interrupts is interpreted through the relevant interrupt-controller binding; trigger type and numbering are controller-specific.
  • A clock provides a timing signal; a regulator supplies/manages electrical power; pinctrl selects/configures physical pins; DMA (direct memory access) lets a device transfer data to/from memory under system-controlled mappings. These are real hardware wiring/configuration contracts, not decorative properties.
  • The CPU's virtual address is what a process pointer uses. A device register's physical/bus address is different; drivers use kernel resource APIs to map/access registers safely. Do not dereference a reg value as a C pointer.

A DTS can compile yet be wrong: syntax/schema validity cannot prove that a board's pin, voltage, clock, interrupt wiring, or address is correct. Never test guessed hardware values on a phone.

Selected verified example: GPIO keys

From device tree to driver
  1. gpio-keys.yaml binding

    Describes compatible values and button properties. Device-tree data is not driver code.

  2. gpio_keys_of_match

    The driver match table includes compatible = gpio-keys.

  3. gpio_keys_probe

    Reads configuration, allocates state, and sets up GPIO/IRQ and deferred work.

  4. Input subsystem registration

    Publishes the device interface after setup; cleanup must account for asynchronous users.

Source map. Pinned GPIO-keys reading path, not a tested board. A binding/schema example cannot establish real pin, voltage, IRQ, or bus wiring.
Quiesce before releasing state
  1. Device active

    Interrupts, timers, and delayed work may reference driver state.

  2. Quiesce asynchronous users

    Cancel timers and synchronously cancel delayed work through the explicit cleanup action.

  3. Managed cleanup releases resources

    devm helpers run registered cleanup; they do not invent rules for arbitrary callbacks or references.

Source map. A lifecycle invariant illustrated by gpio_keys_quiesce_key and its devm_add_action registration. Exact release order and error paths require the surrounding pinned code.

Rather than “pick any driver,” use this fixed example from ACK commit 2f0d864181b9acf801e9b51c345e11b6156b767a:

  1. Binding: Documentation/devicetree/bindings/input/gpio-keys.yaml declares the compatible values gpio-keys and gpio-keys-polled. Its example has a gpio-keys parent and child button nodes; properties describe GPIO/interrupt and Linux input key code. This is schema/example evidence, not a particular phone's wiring.
  2. Match: drivers/input/keyboard/gpio_keys.c has gpio_keys_of_match[] with .compatible = "gpio-keys"; the platform driver's .of_match_table points at that table (around line 1056).
  3. Probe: gpio_keys_probe() (around line 802) reads platform data or DT child nodes, allocates driver/input state with devm_*, sets input capabilities, sets up each key, then calls input_register_device(). probe means “the driver matched a device; initialize it now.” A negative return is a normal initialization failure, not permission to leave half-initialized live callbacks.
  4. Resources and async work: button setup requests GPIO/IRQ resources and initializes delayed work/timers. The interrupt path can defer debouncing/reporting to delayed work. The driver is therefore a useful concrete example of why an interrupt callback and slower work have different contexts.
  5. Cleanup and error unwind: gpio_keys_quiesce_key() (around line 128) cancels timers and calls cancel_delayed_work_sync() for work. Setup registers it through devm_add_action() (around line 658), so managed cleanup invokes this explicit quiesce action. A probe error unwinds registered devres actions/resources; device detach after unregister also releases managed resources. platform_driver_unregister() appears in the module exit path (around line 1068).

This path is checked against the pinned source and binding, not inferred from a current upstream branch. In the driver, devm_kzalloc/devm_input_allocate_device tie allocations to device teardown (Linux devres describes managed resources released on driver detach); devm_add_action is used for the special timer/work shutdown. The driver still has to define ordering and synchronization: the managed helper does not invent a cancellation rule, stop arbitrary callbacks, cancel every async mechanism automatically, protect raw references held elsewhere, or make the hardware description correct. Managed lifetime is a cleanup mechanism, not a universal safety proof.

The important sequence is:

text
match → probe allocates/registers → interrupts/work may access state
       → prevent new work → synchronously cancel/finish pending work
       → release remaining resources/state

On any setup error, already-acquired resources must be released and no callback may continue using freed state. Driver authors arrange failure paths and devres ordering carefully; never assume “devm means cleanup is solved.”

Source-browser worksheet

Open only the pinned URLs above. Browser find (Ctrl/Cmd-F) locates symbols; verify the commit in the URL before recording a result. Do not build, load a module, edit a DTB, or access hardware.

TraceExact thing to findRecord what it proves
DT → bindingIn gpio-keys.yaml, the root compatible and one child exampleWhich schema describes the example; which properties belong to the parent vs child.
Binding → matchIn gpio_keys.c, gpio_keys_of_matchThe compatible string and the driver's match table.
Match → probe.probe = gpio_keys_probe and gpio_keys_probeWhich function runs after a match and which subsystem it registers with.
Async → cleanupINIT_DELAYED_WORK, gpio_keys_quiesce_key, cancel_delayed_work_sync, and devm_add_actionHow async state is quiesced before managed state is released.
Driver unregistergpio_keys_exit / platform_driver_unregisterThe driver unregister path; distinguish it from per-device probe cleanup.

Artifact: draw a six-node arrow chain (binding example → compatible → match table → probe → input subsystem + async worker → cleanup/unregister). Under each arrow cite a source path/symbol and write one “does not prove” note. Expected: the generic binding example does not prove any named board has that wiring; devm_* does not itself prove arbitrary callbacks have stopped. If source access fails, mark the artifact incomplete rather than substituting unpinned code.

Advanced reading: character device and user buffer

Some drivers expose a character device, a byte-stream/command-style interface using a struct file_operations table (.open, .read, .write, .ioctl, .release). It is not the right interface for every device; input, sensors, storage, networking, and graphics have established subsystem interfaces. A custom ioctl can become a permanent ABI. User-provided arguments are untrusted; kernel code must validate sizes/values and use user-copy helpers rather than dereference a userspace pointer directly.

Preserved source-reading example: below is an abbreviated excerpt from ACK drivers/char/mem.c at the pinned commit. The ... are omitted source, not compilable C; the excerpt shows the relevant loop/table shape.

c
static ssize_t read_zero(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
        size_t cleared = 0;
        while (count) {
                size_t chunk = min_t(size_t, count, PAGE_SIZE);
                size_t left;
                left = clear_user(buf + cleared, chunk);
                if (unlikely(left)) {
                        cleared += (chunk - left);
                        if (!cleared)
                                return -EFAULT;
                        break;
                }
                cleared += chunk;
                ...
        }
        ...
}

static const struct file_operations zero_fops = {
        .llseek = zero_lseek,
        .write = write_zero,
        .read_iter = read_iter_zero,
        .read = read_zero,
        .write_iter = write_iter_zero,
        .mmap = mmap_zero,
        ...
};

char __user *buf is a userspace address, not a normal kernel pointer. clear_user is a fault-aware helper; it reports bytes it could not clear. If the first chunk fails the callback returns -EFAULT; after partial progress it can return the completed byte count. PAGE_SIZE bounds each chunk, not the entire read. zero_fops is a dispatch table, not evidence by itself of registration, permissions, or complete device lifetime. /dev/zero is a special core character device, not a platform-driver template.

The next chapter, Driver I/O, interrupts, and DMA, expands the hardware terms introduced above into a transfer lifecycle: fixed-width ioctl contracts, partial user-copy results, overflow-safe sizes, CPU versus DMA addresses, cache/order rules, interrupts, deferred completion, and teardown while a device can still write. The distinction matters because stopping a CPU callback does not itself stop a DMA engine.

Knowledge check#

Answer before reading the key.

  1. The gpio-keys binding compiles, but the driver never matches. Which link in the verified chain would you inspect first, and what exact source evidence is relevant?
  2. GPIO-key work is queued when device removal starts. Why is freeing state before cancel_delayed_work_sync() unsafe?
  3. Does devm_kzalloc automatically cancel a work item that contains a pointer to the allocation?
  4. A DT reg value is 0x1000. Is it safe to cast this to a userspace pointer? Why not?
  5. The /dev/zero callback returns 20 for a 40-byte request. Must it be an error?

Answers#

  1. Compare the DT node's compatible with gpio_keys_of_match[], then verify .of_match_table is connected to the platform driver. The pinned binding and match-table source prove these links; they do not prove board wiring.
  2. The callback may already be running or queued and could dereference freed state. Stop future work and synchronously wait for current/pending work before releasing state.
  3. No. devm_kzalloc releases memory at device teardown; async users still need a correct cancellation/reference protocol. This driver registers an explicit quiesce action.
  4. No. reg is a bus/device resource address interpreted in DT parent context; it is not a process virtual address. Drivers use resource APIs and correct mappings.
  5. Not necessarily. A read can be short; a partial user-copy may report completed bytes. The caller must use the returned count and continue if appropriate.

Checkpoint#

  • Ready: your chain identifies binding, match, probe, subsystem registration, and cleanup separately; you can state why teardown waits for callbacks.
  • Revise: you describe DT as executable code, treat reg as a pointer, or say devm cancels all asynchronous work; revisit the selected example and lifetime sequence.
  • Not yet evidenced: your worksheet lacks cited match/probe/cleanup symbols or conflates driver unregister with a per-device callback. Complete the table using the pinned source before proceeding.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.