all
Stage 08

Platform Drivers

Write Linux platform drivers — the platform_driver and platform_device model, Device Tree binding and matching, probe/remove lifecycle, resource management with devm_* APIs, and real-world GPIO/register examples.

8 min read
51320 chars

Basic: Platform Device Model {:.gc-basic}

Basic

A platform device is a hardware peripheral that sits on a memory-mapped bus with no self-discovery mechanism — SoC-internal UARTs, GPIO controllers, I2C masters, timers, and custom IP blocks. Unlike PCI or USB, the kernel cannot probe the bus to find these devices; they must be described explicitly, either in the Device Tree (ARM/RISC-V) or in board files (legacy x86 ACPI / non-DT systems).

The driver side is a platform_driver, and the device side is a platform_device. The kernel’s driver core matches them by comparing compatible strings (DT) or name strings (non-DT).

#include <linux/module.h>
#include <linux/platform_device.h>

/* Describe which devices this driver handles */
static const struct of_device_id mydrv_of_match[] = {
    { .compatible = "vendor,mydevice-v1" },
    { .compatible = "vendor,mydevice-v2" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);

static int mydrv_probe(struct platform_device *pdev);
static int mydrv_remove(struct platform_device *pdev);

static struct platform_driver mydrv_driver = {
    .probe  = mydrv_probe,
    .remove = mydrv_remove,
    .driver = {
        .name           = "mydrv",
        .of_match_table = mydrv_of_match,
        .owner          = THIS_MODULE,
    },
};

/* Expands to module_init/module_exit that call platform_driver_register/unregister */
module_platform_driver(mydrv_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example platform driver");

The module_platform_driver() macro is a convenience that replaces writing explicit module_init / module_exit functions that call platform_driver_register / platform_driver_unregister.


Device Tree Binding {:.gc-basic}

Basic

The Device Tree Source (DTS) file describes hardware topology. The kernel parses the compiled DTB at boot and creates platform_device objects for every DT node whose compatible string matches a registered driver.

/* arch/arm64/boot/dts/vendor/board.dts */

/ {
    soc {
        #address-cells = <1>;
        #size-cells = <1>;

        mydevice: mydevice@40010000 {
            compatible = "vendor,mydevice-v1";
            reg = <0x40010000 0x1000>;   /* base address, size */
            interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&ccu CLK_MYDEV>;
            clock-names = "aclk";
            resets = <&rst RST_MYDEV>;
            vendor,fifo-depth = <64>;
            status = "okay";
        };
    };
};

Reading DT properties in the probe function

#include <linux/of.h>
#include <linux/of_device.h>

static int mydrv_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    u32 fifo_depth;
    const char *label;
    int ret;

    /* Read a u32 property */
    ret = of_property_read_u32(np, "vendor,fifo-depth", &fifo_depth);
    if (ret) {
        dev_warn(&pdev->dev, "fifo-depth not set, using default 32\n");
        fifo_depth = 32;
    }

    /* Read a string property */
    ret = of_property_read_string(np, "label", &label);

    /* Check a boolean property */
    if (of_property_read_bool(np, "vendor,use-dma"))
        dev_info(&pdev->dev, "DMA mode enabled\n");

    dev_info(&pdev->dev, "probed: fifo_depth=%u\n", fifo_depth);
    return 0;
}

DT matching table

of_match_table entry:         DT node compatible:
"vendor,mydevice-v1"   <-->   compatible = "vendor,mydevice-v1";

The MODULE_DEVICE_TABLE(of, ...) macro embeds the match table in the .ko so modprobe can autoload the driver when the DT node is found.


Probe and Remove Lifecycle {:.gc-mid}

Intermediate

probe() is called by the driver core when a matching device is found. It must initialise hardware, allocate resources, and register any sub-devices. If anything fails, it must clean up and return a negative error code.

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/clk.h>
#include <linux/interrupt.h>

struct mydrv_priv {
    void __iomem    *base;
    struct clk      *clk;
    int              irq;
    struct device   *dev;
};

#define REG_CTRL   0x00
#define REG_STATUS 0x04
#define REG_DATA   0x08

static irqreturn_t mydrv_irq_handler(int irq, void *data)
{
    struct mydrv_priv *priv = data;
    u32 status = readl(priv->base + REG_STATUS);

    if (!(status & BIT(0)))
        return IRQ_NONE;

    /* Clear interrupt by writing 1 */
    writel(BIT(0), priv->base + REG_STATUS);
    dev_dbg(priv->dev, "interrupt: status=0x%08x\n", status);
    return IRQ_HANDLED;
}

static int mydrv_probe(struct platform_device *pdev)
{
    struct mydrv_priv *priv;
    struct resource *res;
    int ret;

    /* Allocate private data — freed automatically on driver detach */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->dev = &pdev->dev;

    /* Map MMIO region */
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* Get and enable clock */
    priv->clk = devm_clk_get(&pdev->dev, "aclk");
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);

    ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;

    /* Request IRQ */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    ret = devm_request_irq(&pdev->dev, priv->irq, mydrv_irq_handler,
                            IRQF_SHARED, dev_name(&pdev->dev), priv);
    if (ret)
        return ret;

    /* Store private data for later retrieval in remove/ioctl/etc */
    platform_set_drvdata(pdev, priv);

    /* Initialise hardware */
    writel(BIT(0), priv->base + REG_CTRL);   /* enable device */

    dev_info(&pdev->dev, "probed successfully, base=%p irq=%d\n",
             priv->base, priv->irq);
    return 0;
}

static int mydrv_remove(struct platform_device *pdev)
{
    struct mydrv_priv *priv = platform_get_drvdata(pdev);

    /* Disable hardware */
    writel(0, priv->base + REG_CTRL);

    /* devm_* resources are freed automatically */
    clk_disable_unprepare(priv->clk);

    dev_info(&pdev->dev, "removed\n");
    return 0;
}

Error handling: goto pattern for non-devm resources

static int mydrv_probe(struct platform_device *pdev)
{
    int ret;

    ret = clk_prepare_enable(priv->clk);  /* not devm — must undo manually */
    if (ret)
        goto err_clk;

    ret = register_something();
    if (ret)
        goto err_register;

    return 0;

err_register:
    clk_disable_unprepare(priv->clk);
err_clk:
    return ret;
}

Resource Management: devm_* APIs {:.gc-mid}

Intermediate

devm_* (device-managed) functions register a cleanup action tied to the struct device. When the driver is unbound — either by remove() completing or by probe returning an error — the kernel automatically calls all registered cleanup actions in reverse order. This eliminates error-path cleanup bugs.

Common devm_ functions*

Function What it manages
devm_kzalloc(dev, size, flags) kfree() on detach
devm_ioremap_resource(dev, res) iounmap() + release_mem_region()
devm_platform_ioremap_resource(pdev, index) Same, fetches resource by index
devm_request_irq(dev, irq, handler, ...) free_irq() on detach
devm_clk_get(dev, id) clk_put() on detach
devm_gpio_request_one(dev, gpio, flags, label) gpio_free() on detach
devm_regulator_get(dev, id) regulator_put() on detach
devm_iio_device_alloc(dev, size) iio_device_free() on detach

Getting resources from platform_device

/* MMIO resource by index */
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
    return -ENODEV;

void __iomem *base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
    return PTR_ERR(base);

/* IRQ by index */
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
    return irq;

/* Named resource */
struct resource *uart_res = platform_get_resource_byname(pdev,
                                IORESOURCE_MEM, "uart-regs");

MMIO register access

/* Always use readl/writel for MMIO — never dereference __iomem pointer directly */
u32 val = readl(base + REG_STATUS);
val |= BIT(4);
writel(val, base + REG_CTRL);

/* Relaxed variants (no memory barrier) for non-ordering-sensitive registers */
u32 v = readl_relaxed(base + REG_FIFO_DATA);
writel_relaxed(v, base + REG_FIFO_DATA);

Advanced: Power Management {:.gc-adv}

Advanced

Platform drivers participate in system suspend/resume and runtime PM through dev_pm_ops.

#include <linux/pm.h>
#include <linux/pm_runtime.h>

static int mydrv_suspend(struct device *dev)
{
    struct mydrv_priv *priv = dev_get_drvdata(dev);
    writel(0, priv->base + REG_CTRL);          /* disable hardware */
    clk_disable_unprepare(priv->clk);
    dev_info(dev, "suspended\n");
    return 0;
}

static int mydrv_resume(struct device *dev)
{
    struct mydrv_priv *priv = dev_get_drvdata(dev);
    int ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;
    writel(BIT(0), priv->base + REG_CTRL);     /* re-enable hardware */
    dev_info(dev, "resumed\n");
    return 0;
}

/* Runtime PM: called when device is idle for too long */
static int mydrv_runtime_suspend(struct device *dev)
{
    struct mydrv_priv *priv = dev_get_drvdata(dev);
    clk_disable(priv->clk);
    return 0;
}

static int mydrv_runtime_resume(struct device *dev)
{
    struct mydrv_priv *priv = dev_get_drvdata(dev);
    return clk_enable(priv->clk);
}

static const struct dev_pm_ops mydrv_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(mydrv_suspend, mydrv_resume)
    SET_RUNTIME_PM_OPS(mydrv_runtime_suspend, mydrv_runtime_resume, NULL)
};

/* In platform_driver.driver: */
// .pm = &mydrv_pm_ops,

Enabling runtime PM in probe

pm_runtime_enable(&pdev->dev);
pm_runtime_set_active(&pdev->dev);

/* Before accessing registers, increment usage count */
ret = pm_runtime_get_sync(&pdev->dev);
if (ret < 0)
    return ret;
/* ... do work ... */
pm_runtime_put_autosuspend(&pdev->dev);

Interview Q&A {:.gc-iq}

Interview Q&A

Q1: What is the format of a DT compatible string, and why does the format matter?

The canonical format is "vendor,device-version" — a vendor prefix (matching the vendor in Documentation/devicetree/bindings/vendor-prefixes.yaml) followed by a device name and optional version. The format matters because: (1) it prevents collisions between vendors, (2) of_match_table does exact string matching, and (3) a device can list multiple compatible strings in order of specificity so a generic driver can bind if a specific one is absent.

Q2: Why prefer devm_* APIs over their non-managed counterparts?

If probe() returns an error partway through, all devm_* resources allocated so far are automatically released in reverse order. Without devm_*, you need careful goto unwinding that is error-prone and easy to get wrong when adding new resources. devm_* also ensures that resources in remove() are freed even if the function is accidentally omitted.

Q3: What happens when probe() returns -EPROBE_DEFER?

The driver core moves the device to a deferred probe list and retries the probe after other drivers have finished loading. This is used when a dependency (a clock provider, regulator, GPIO controller, pinctrl) has not yet registered. When the dependency is later registered and calls driver_deferred_probe_trigger(), deferred probes are retried. Returning -EPROBE_DEFER is the correct way to handle “resource not ready yet” in probe.

Q4: What is the difference between a platform driver and a PCI driver?

A PCI driver uses pci_register_driver and matches on PCI vendor/device IDs read from hardware configuration space. PCI is self-describing — the kernel scans the bus and discovers devices automatically. A platform driver matches devices described in the Device Tree or board files. There is no bus scan; the driver core creates platform_device objects from DT nodes at boot.

Q5: How does DT matching work step by step?

  1. The kernel parses the DTB and creates a platform_device for each node with a compatible property and status = "okay".
  2. The device’s of_node contains the compatible string list.
  3. When a platform_driver is registered, the driver core calls of_match_device() which iterates the driver’s of_match_table and compares each entry against every compatible string in the device’s DT node.
  4. On a match, probe() is called with the platform_device *.

Q6: What is the difference between ioremap and devm_ioremap_resource?

ioremap(phys, size) maps a physical address range to kernel virtual space but does not request the memory region from the kernel’s resource tree. devm_ioremap_resource calls request_mem_region first (which records the region in /proc/iomem and prevents another driver from claiming it) and then calls devm_ioremap. It also associates cleanup with the device, so the unmap and region release happen automatically.


References {:.gc-ref}

References

Resource Link
Linux Device Model documentation https://www.kernel.org/doc/html/latest/driver-api/driver-model/index.html
Device Tree specification https://www.devicetree.org/specifications/
kernel.org: DT bindings documentation https://www.kernel.org/doc/html/latest/devicetree/bindings/index.html
kernel.org: devm_* managed device resources https://www.kernel.org/doc/html/latest/driver-api/driver-model/devres.html
kernel.org: Runtime Power Management https://www.kernel.org/doc/html/latest/power/runtime_pm.html
platform_device and platform_driver overview https://www.kernel.org/doc/html/latest/driver-api/driver-model/platform.html
Deferred probe documentation https://www.kernel.org/doc/html/latest/driver-api/device_link.html