What are Device Trees vs Platform Data-Free Linux Device Drivers Course

PREV_LEC NEXT_LEC

Device Trees vs Platform Data

How the Linux kernel discovers non-discoverable SoC hardware and matches it to the right driver, with a working of_match_table example.

Device Tree Basics
Platform Data (Legacy)
of_match_table Matching

PCI and USB devices are polite: plug one in and it announces its own vendor and product IDs over the bus. Most of the hardware blocks soldered onto an embedded SoC — a UART, an I2C controller, an LED driver — are not nearly so polite. Nothing on the silicon tells the kernel “I exist, here’s my address.” Something has to describe that hardware to the kernel from the outside. This lecture, part of our free linux kernel development course, covers the two mechanisms Linux has used to do that — device trees and platform data — and shows exactly how a driver matches itself to a piece of described hardware and gets its probe function called. It’s essential material for anyone working through this free linux device drivers course on real embedded boards rather than a PC.

What You Will Learn

Why SoC hardware needs external description Device tree syntax and compatible strings Platform data and struct platform_device of_device_id and of_match_table Writing a probe() function Modern platform_driver conventions

Prerequisites

This builds directly on the previous lecture in this free embedded linux course, “Building and Loading Kernel Modules” — you should already be comfortable writing a Makefile for an out-of-tree module and loading it with insmod or modprobe. No device tree or platform-driver experience is assumed here.

Why Hardware Description Is Needed at All

The Linux driver model works on a matching principle: a driver registers itself with a subsystem along with an identifier for the hardware it supports, and the kernel calls that driver’s probe function whenever it finds hardware with a matching identifier. For PCI and USB, that identifier comes from the hardware itself, read over the bus at enumeration time. For hardware wired directly onto an SoC — often called “platform devices” in kernel terminology, regardless of whether device tree is used — there is no bus to query. The identifier and the hardware’s resources (register addresses, IRQ numbers) have to be supplied from outside: either as data compiled directly into the kernel image, or as a separate description the kernel parses at boot.

Two Sources of Hardware Description, One Matching Path
Device Tree (.dtb) Platform Data (C structs) parsed by bootloader/kernel compiled into the kernel image | | v v struct device (with struct platform_device an of_node attached) (name + resources) \ / \ / v v Linux Driver Core (bus/platform) | matches “compatible” string / device name against a driver’s of_match_table | v driver->probe(struct platform_device *pdev)

Device Trees: Describing Hardware as Data

A device tree is a plain-text description (a .dts file, compiled by dtc into a binary .dtb) of the hardware on a board — every peripheral’s register range, interrupt line, clocks, and any board-specific configuration, expressed as a tree of nodes. The bootloader hands the compiled blob to the kernel at boot, and the kernel walks it to build up its internal device model before any driver runs.

Every node that a driver can bind to carries a compatible property — one or more strings identifying exactly what the hardware is, most specific first. Here’s a representative I2C-attached sensor node, written fresh for this lecture rather than lifted from any existing board file:

// Fragment of a board .dts file
&i2c1 {
	status = "okay";

	ep_tempsensor: sensor@48 {
		compatible = "example,ep-tempsensor";
		reg = <0x48>;
		interrupt-parent = <&gpio1>;
		interrupts = <14 2>;   /* GPIO line 14, falling edge */
	};
};

The compatible string is the entire matching key — reg here is the I2C address rather than a memory address, since this device sits behind an I2C controller node rather than directly on the system bus. Memory-mapped peripherals instead use reg = <address size> pairs directly under the SoC’s root bus, matching how the earlier lecture on Common Clock and PCI framework device tree nodes were structured.

Device tree’s biggest practical advantage is that the same kernel binary works across many boards — swap the .dtb the bootloader loads, and the same drivers bind to whatever hardware that board actually has, with no kernel rebuild required.

Platform Data: The Legacy, Compiled-In Alternative

Before device tree became the default on ARM (and on architectures that still don’t use it), the same information was supplied as plain C structures compiled directly into board-support code: a struct platform_device giving the hardware a name and a list of struct resource entries describing its memory ranges and interrupt numbers.

// Legacy-style platform data (board support file)
static struct resource ep_tempsensor_resources[] = {
	[0] = {
		.start = 0x48000000,
		.end   = 0x48000000 + SZ_4K - 1,
		.flags = IORESOURCE_MEM,
	},
	[1] = {
		.start = 42,
		.end   = 42,
		.flags = IORESOURCE_IRQ,
	},
};

static struct platform_device ep_tempsensor_device = {
	.name          = "ep-tempsensor",
	.id            = -1,
	.num_resources = ARRAY_SIZE(ep_tempsensor_resources),
	.resource      = ep_tempsensor_resources,
};

Registering it happens once, at board init time — normally deep inside arch-specific setup code you would rarely touch on a modern board:

platform_device_register(&ep_tempsensor_device);

The matching key here is the plain string in .name, compared against a driver’s declared name, instead of a device tree compatible string. Mainline Linux has moved almost entirely away from this style — new SoC support is expected to use device tree — but you will still meet platform data on older BSPs, some x86 platform drivers, and anywhere a board’s hardware genuinely can’t be described any other way. Understanding it matters for maintaining and porting legacy code even on a modern kernel.

Comparing the Two Approaches

AspectDevice TreePlatform Data
Where hardware is describedExternal .dtb, parsed at bootC structs compiled into the kernel image
Matching keycompatible string.name string
Same kernel, different boardsYes — swap the .dtb onlyNo — requires a kernel rebuild
Current statusDefault on ARM/ARM64/RISC-V and most modern platformsLegacy; still used on some x86 and older BSPs

Writing a Driver That Matches Either One

A well-written platform driver doesn’t need to know or care which mechanism supplied its hardware description — it declares an of_device_id table for device tree matching, and the driver core takes care of calling probe() exactly the same way regardless of which path found the match. Here is an original demo driver, ep_tempsensor, built as a misc device that just proves the matching and probe mechanics work; it deliberately skips real sensor I/O to keep the focus on driver binding:

// ep_tempsensor.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

static ssize_t ep_ts_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
	static const char msg[] = "ep_tempsensor: 25000 (millidegree C, fake)\n";
	return simple_read_from_buffer(buf, len, off, msg, sizeof(msg) - 1);
}

static const struct file_operations ep_ts_fops = {
	.owner = THIS_MODULE,
	.read  = ep_ts_read,
};

static struct miscdevice ep_ts_miscdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name  = "ep_tempsensor",
	.fops  = &ep_ts_fops,
};

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

	ret = misc_register(&ep_ts_miscdev);
	if (ret)
		return ret;

	dev_info(&pdev->dev, "ep_tempsensor: probed via %s\n",
		 pdev->dev.of_node ? "device tree" : "platform data");
	return 0;
}

static void ep_tempsensor_remove(struct platform_device *pdev)
{
	misc_deregister(&ep_ts_miscdev);
}

static const struct of_device_id ep_tempsensor_of_match[] = {
	{ .compatible = "example,ep-tempsensor" },
	{ }
};
MODULE_DEVICE_TABLE(of, ep_tempsensor_of_match);

static struct platform_driver ep_tempsensor_driver = {
	.probe  = ep_tempsensor_probe,
	.remove = ep_tempsensor_remove,
	.driver = {
		.name           = "ep-tempsensor",
		.of_match_table = ep_tempsensor_of_match,
	},
};
module_platform_driver(ep_tempsensor_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Device tree / platform data matching demo");

A few things worth calling out. of_device_id.compatible matches the device tree node’s compatible string exactly; MODULE_DEVICE_TABLE(of, ...) is what exports that match table into the module’s alias information, which is exactly what lets udev and modprobe auto-load this driver the moment matching hardware is described — the same alias mechanism covered in the previous lecture. driver.name doubles as the platform-data matching key for the legacy path, so this one driver structure genuinely serves both mechanisms. And note the remove() callback returns void, not int — recent kernels changed this signature because a driver can’t meaningfully fail to be removed, so don’t copy an older tutorial’s int-returning remove() onto a current kernel.

module_platform_driver() is a convenience macro that expands to the usual module_init()/module_exit() pair, registering and unregistering the driver — you get correct init/exit boilerplate for a platform driver in a single line instead of writing it by hand.

$ make
$ sudo insmod ep_tempsensor.ko
$ dmesg | tail -n 1
[ 3021.884471] ep_tempsensor: probed via device tree

$ cat /dev/ep_tempsensor
ep_tempsensor: 25000 (millidegree C, fake)

$ sudo rmmod ep_tempsensor

On a board without a matching device tree node, this same .ko simply never probes — nothing calls ep_tempsensor_probe() until the driver core finds a compatible or .name match, which is exactly the “no policy, mechanism only” philosophy this course keeps coming back to.

Common Mistakes and Troubleshooting

  • Driver loads but never probes — check the device tree node’s compatible string against of_device_id.compatible character for character; a single typo silently prevents matching.
  • Forgetting MODULE_DEVICE_TABLE(of, ...) — the driver will still probe if loaded manually, but udev/modprobe auto-loading on device tree match won’t work, since the alias never gets exported.
  • Copying an int-returning remove() from an older book or tutorial — this now produces a compiler warning (and eventually an error) on current kernels; use the modern void signature.
  • Assuming reg is always a memory address — under a bus node like I2C or SPI, reg means the device’s address on that bus, not a physical memory range.

Best Practices

  • Write new drivers against device tree by default; only reach for platform-data-style registration when maintaining or porting genuinely legacy board support.
  • Always pair an of_device_id table with MODULE_DEVICE_TABLE(of, ...) so auto-loading and module aliasing work correctly.
  • Keep compatible strings vendor-prefixed and specific ("vendor,exact-part") rather than generic, to avoid a driver mismatching against unrelated hardware.
  • Check pdev->dev.of_node in probe() only for diagnostics, as shown above — don’t branch core driver logic on which mechanism supplied the description, or you’ve defeated the point of the shared driver model.

Summary

Non-discoverable SoC hardware needs its resources described from outside the driver, either through a device tree parsed at boot or through legacy platform data compiled directly into the kernel image. Both funnel into the same driver core matching mechanism: a compatible or name string gets compared against a driver’s declared match table, and a match triggers that driver’s probe() function with a fully populated struct platform_device. Writing the driver against of_device_id and MODULE_DEVICE_TABLE(of, ...), as shown in the ep_tempsensor example, gets you correct matching, auto-loading through udev, and forward compatibility with current kernel conventions like the void-returning remove() callback.

FAQ

Why doesn’t my platform driver’s probe function ever get called?

Almost always a mismatch between the device tree node’s compatible property and the driver’s of_device_id.compatible string. Compare them character by character — a missing vendor prefix or typo is enough to prevent matching.

Is platform data deprecated?

It’s legacy rather than formally deprecated. New ARM/ARM64/RISC-V board support is expected to use device tree, but platform data still appears in some x86 platform drivers and older board support packages that haven’t been converted.

What does MODULE_DEVICE_TABLE actually do?

It exports the driver’s match table into the module’s metadata so that userspace tools like depmod and udev can build an alias database, enabling automatic module loading when matching hardware is described — without it, the driver only loads if you insmod/modprobe it manually.

Why does my remove() callback give a compiler warning on a recent kernel?

The platform_driver.remove callback signature changed to return void in recent kernel versions, since a driver can’t meaningfully refuse removal. Older tutorials showing an int-returning remove() are out of date for current kernels.

Can one driver support both device tree and platform data?

Yes — declare both an of_match_table (for the compatible string) and a driver.name (for the legacy name-based match), and write probe() without branching on which mechanism supplied the device, as shown in this lecture’s example.

What does the reg property mean in a device tree I2C node?

Under a bus node such as i2c or spi, reg is the device’s address on that bus (for example an I2C 7-bit address), not a physical memory range. Under a memory-mapped SoC bus node, reg instead gives an address and size pair.

Continue the Device Drivers Series

Next in this free linux device drivers course: linking a driver’s file operations to real hardware access patterns once probe() has run.

Continue to Next Lecture Back to Course Index
PREV_LEC NEXT_LEC

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *