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?
- The kernel parses the DTB and creates a
platform_devicefor each node with acompatibleproperty andstatus = "okay". - The device’s
of_nodecontains the compatible string list. - When a
platform_driveris registered, the driver core callsof_match_device()which iterates the driver’sof_match_tableand compares each entry against every compatible string in the device’s DT node. - On a match,
probe()is called with theplatform_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 |