Learn how to write Platform Devices and Drivers from scratch using Device Tree, I2C, interrupts, and testing, explained clearly for beginners.
If you are stepping into Linux device driver development, sooner or later you will hear one phrase again and again: Platform Devices and Drivers.
They sound complex at first, but once you understand the flow, they are actually one of the cleanest and most structured ways to write Linux drivers for embedded systems.
In this article, we will go from zero to a working platform driver.
We will start with the basic idea, then move step by step through implementation, device tree integration, I2C communication, interrupt handling, and finally testing and debugging.
What Is Platform Devices and Drivers?
Platform Devices and Drivers are a Linux kernel mechanism used to describe and manage on-board hardware that is not discoverable dynamically.
Unlike USB or PCI devices, platform devices do not announce themselves.
They are usually fixed on the board, soldered directly to the SoC or connected via internal buses like I2C or SPI.
Examples include:
- GPIO controllers
- I2C sensors
- RTC chips
- Watchdog timers
- Custom hardware blocks
Linux needs a way to:
- Describe this hardware
- Match it with the correct driver
- Initialize it safely during boot
That is exactly what platform devices and drivers do.
Why Platform Devices and Drivers Matter in Embedded Linux
If you work with embedded boards, you will almost always use Platform Devices and Drivers because:
- Hardware is board-specific
- There is no auto-detection
- Device Tree is used to describe hardware
- Drivers must bind cleanly during boot
This model separates hardware description (Device Tree) from driver logic (kernel module), which makes systems easier to maintain and port.
High-Level Architecture
Before writing any code, let’s understand the flow:
- You buy an I2C chip (for example, a temperature sensor)
- You connect it to your board
- You describe it in the Device Tree
- Linux creates a platform device
- Your platform driver registers itself
- Kernel matches device and driver
probe()function runs- Driver initializes hardware
- Device becomes usable from user space
Once you understand this flow, everything else fits naturally.
From Purchase to Product: Real-World Scenario
Let’s make this real.
Imagine you purchased an I2C temperature sensor that:
- Works over I2C
- Has an interrupt pin
- Is connected to your SoC
Your goal:
- Write a Linux driver
- Handle I2C communication
- Handle interrupts
- Expose data to user space
We will build this using Platform Devices and Drivers.
Step 1: Understand the Hardware
Before touching the kernel:
- Read the datasheet
- Note the I2C address
- Register map
- Interrupt behavior
- Voltage levels
Example details:
- I2C address:
0x48 - Interrupt active low
- Data register at
0x00
Never skip this step. Drivers written without understanding hardware always fail.
Step 2: Device Tree Basics
In modern Linux, Device Tree describes platform devices.
The Device Tree tells the kernel:
- Where the device is
- How it is connected
- What driver should bind to it
Example Device Tree Node
i2c1 {
status = "okay";
temp_sensor@48 {
compatible = "vendor,temp-sensor";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
};
};
Important properties:
compatible→ used for driver matchingreg→ I2C addressinterrupts→ interrupt configuration
This node becomes a platform device internally.
Step 3: Platform Driver Structure
A platform driver is a kernel module that registers itself using platform_driver.
Basic Skeleton
static int temp_probe(struct platform_device *pdev)
{
return 0;
}
static int temp_remove(struct platform_device *pdev)
{
return 0;
}
static const struct of_device_id temp_of_match[] = {
{ .compatible = "vendor,temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, temp_of_match);
static struct platform_driver temp_driver = {
.probe = temp_probe,
.remove = temp_remove,
.driver = {
.name = "temp_sensor",
.of_match_table = temp_of_match,
},
};
module_platform_driver(temp_driver);
This is the heart of Platform Devices and Drivers.
Step 4: Driver and Device Matching
Matching happens using the compatible string.
- Device Tree provides
compatible = "vendor,temp-sensor" - Driver declares the same string
- Kernel binds them automatically
If probe() is not called:
- Check
compatiblestring - Check Device Tree compilation
- Check kernel logs
Step 5: Accessing Device Tree Data
Inside probe(), you can read properties from Device Tree.
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
This lets you:
- Read GPIO numbers
- Read custom properties
- Configure behavior per board
This is why platform drivers are flexible.
Step 6: Integrating I2C in Platform Drivers
Many beginners get confused here.
Yes, the device is on I2C, but the binding still happens via platform devices when using Device Tree.
You usually:
- Get the I2C adapter
- Create an I2C client
- Communicate using I2C APIs
Example
struct i2c_client *client;
client = i2c_new_dummy_device(adapter, 0x48);
Now you can read registers:
i2c_smbus_read_byte_data(client, 0x00);
This approach keeps hardware description separate from communication logic.
Step 7: Interrupt Handling
Interrupts are critical for real devices.
Get IRQ Number
int irq = platform_get_irq(pdev, 0);
Register Interrupt Handler
request_irq(irq, temp_irq_handler,
IRQF_TRIGGER_FALLING,
"temp_irq", dev);
Interrupt Handler
static irqreturn_t temp_irq_handler(int irq, void *dev_id)
{
// Read status register
return IRQ_HANDLED;
}
Always keep interrupt handlers short and fast.
Step 8: Exposing Data to User Space
A driver is useless if user space cannot talk to it.
Common approaches:
- sysfs
- character device
- IIO subsystem (for sensors)
Simple sysfs Example
static ssize_t temp_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
return sprintf(buf, "25\n");
}
DEVICE_ATTR_RO(temp);
This creates:
/sys/devices/.../temp
Step 9: Error Handling and Cleanup
Good platform drivers always clean up.
In remove():
- Free IRQ
- Unregister I2C client
- Free memory
free_irq(irq, dev);
Never assume probe() always succeeds.
Step 10: Building and Testing
Compile the Driver
- Add to kernel tree or as module
- Enable in
menuconfig
Load Module
insmod temp_driver.ko
Check Logs
dmesg | grep temp
Verify Device Tree
ls /proc/device-tree
Testing is where most learning happens.
Common Mistakes Beginners Make
- Wrong
compatiblestring - Forgetting to enable I2C bus
- Interrupt polarity mismatch
- Blocking code in IRQ handler
- Ignoring return values
Every one of these will cost you hours if you ignore basics.
How to Write Platform Devices and Drivers
// SPDX-License-Identifier: GPL-2.0
/*
* Production-ready Platform Driver example
* - Device Tree based
* - I2C sensor
* - Interrupt handling
* - sysfs interface
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#include <linux/pm.h>
#include <linux/sysfs.h>
/* ---------- Driver Private Data ---------- */
struct temp_dev {
struct device *dev;
struct i2c_client *client;
int irq;
int temperature;
struct mutex lock;
};
/* ---------- I2C Read Helper ---------- */
static int temp_sensor_read(struct temp_dev *tdev)
{
int ret;
ret = i2c_smbus_read_byte_data(tdev->client, 0x00);
if (ret < 0)
dev_err(tdev->dev, "I2C read failed\n");
return ret;
}
/* ---------- Interrupt Handler ---------- */
static irqreturn_t temp_irq_handler(int irq, void *data)
{
struct temp_dev *tdev = data;
int temp;
mutex_lock(&tdev->lock);
temp = temp_sensor_read(tdev);
if (temp >= 0)
tdev->temperature = temp;
mutex_unlock(&tdev->lock);
dev_info(tdev->dev, "Interrupt: temperature=%d\n",
tdev->temperature);
return IRQ_HANDLED;
}
/* ---------- sysfs Interface ---------- */
static ssize_t temperature_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct temp_dev *tdev = dev_get_drvdata(dev);
int temp;
mutex_lock(&tdev->lock);
temp = tdev->temperature;
mutex_unlock(&tdev->lock);
return sysfs_emit(buf, "%d\n", temp);
}
static DEVICE_ATTR_RO(temperature);
static struct attribute *temp_attrs[] = {
&dev_attr_temperature.attr,
NULL,
};
static const struct attribute_group temp_attr_group = {
.attrs = temp_attrs,
};
/* ---------- Probe Function ---------- */
static int temp_probe(struct platform_device *pdev)
{
struct temp_dev *tdev;
struct i2c_adapter *adapter;
struct device *dev = &pdev->dev;
int ret;
dev_info(dev, "Probing temperature sensor\n");
tdev = devm_kzalloc(dev, sizeof(*tdev), GFP_KERNEL);
if (!tdev)
return -ENOMEM;
tdev->dev = dev;
mutex_init(&tdev->lock);
platform_set_drvdata(pdev, tdev);
/* Get I2C adapter (from DT bus number) */
adapter = i2c_get_adapter(1);
if (!adapter) {
dev_err(dev, "Failed to get I2C adapter\n");
return -ENODEV;
}
/* Create I2C client */
tdev->client = i2c_new_dummy_device(adapter, 0x48);
i2c_put_adapter(adapter);
if (IS_ERR(tdev->client)) {
dev_err(dev, "Failed to create I2C client\n");
return PTR_ERR(tdev->client);
}
/* Read initial value */
ret = temp_sensor_read(tdev);
if (ret >= 0)
tdev->temperature = ret;
/* Get IRQ from Device Tree */
tdev->irq = platform_get_irq(pdev, 0);
if (tdev->irq < 0) {
dev_err(dev, "Failed to get IRQ\n");
return tdev->irq;
}
ret = devm_request_irq(dev,
tdev->irq,
temp_irq_handler,
IRQF_TRIGGER_FALLING,
dev_name(dev),
tdev);
if (ret) {
dev_err(dev, "Failed to request IRQ\n");
return ret;
}
/* Create sysfs group */
ret = sysfs_create_group(&dev->kobj, &temp_attr_group);
if (ret) {
dev_err(dev, "Failed to create sysfs group\n");
return ret;
}
dev_info(dev, "Temperature sensor driver loaded\n");
return 0;
}
/* ---------- Remove Function ---------- */
static int temp_remove(struct platform_device *pdev)
{
struct temp_dev *tdev = platform_get_drvdata(pdev);
sysfs_remove_group(&pdev->dev.kobj, &temp_attr_group);
if (tdev->client)
i2c_unregister_device(tdev->client);
dev_info(&pdev->dev, "Temperature sensor removed\n");
return 0;
}
/* ---------- Power Management ---------- */
#ifdef CONFIG_PM_SLEEP
static int temp_suspend(struct device *dev)
{
dev_info(dev, "Suspending temperature sensor\n");
return 0;
}
static int temp_resume(struct device *dev)
{
dev_info(dev, "Resuming temperature sensor\n");
return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(temp_pm_ops,
temp_suspend,
temp_resume);
/* ---------- Device Tree Match ---------- */
static const struct of_device_id temp_of_match[] = {
{ .compatible = "demo,temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, temp_of_match);
/* ---------- Platform Driver ---------- */
static struct platform_driver temp_driver = {
.probe = temp_probe,
.remove = temp_remove,
.driver = {
.name = "temp_sensor",
.of_match_table = temp_of_match,
.pm = &temp_pm_ops,
},
};
module_platform_driver(temp_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Nishant Singh");
MODULE_DESCRIPTION("Production-ready Platform Driver with I2C and Interrupt");
Device Tree
&i2c1 {
status = "okay";
temp_sensor@48 {
compatible = "demo,temp-sensor";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
};
};
User Space Test
cat /sys/devices/platform/temp_sensor/temperature
Why Platform Devices and Drivers Scale Well
Once you master Platform Devices and Drivers, you can:
- Port drivers across boards
- Reuse Device Tree files
- Support multiple variants
- Keep drivers clean and readable
This is why the Linux kernel strongly encourages this model.
FAQs : Platform Devices and Drivers.
1. What are Platform Devices and Drivers in Linux?
Platform Devices and Drivers are a Linux kernel mechanism used for non-discoverable hardware. These are devices that are permanently attached to the board, like I2C sensors, GPIO controllers, RTCs, and on-chip peripherals. Since such hardware cannot announce itself, Linux relies on the Device Tree to describe it and platform drivers to manage it.
2. Why do we need Platform Drivers when using Device Tree?
Device Tree only describes hardware.
Platform drivers contain the actual code that talks to the hardware. The kernel uses the compatible string in the Device Tree to match a platform device with the correct platform driver and then calls the driver’s probe() function.
3. What is the role of the probe() function?
The probe() function is where everything starts. It is called when the kernel successfully matches a platform device with its driver. In probe(), you:
- Allocate memory
- Read Device Tree properties
- Initialize hardware
- Register interrupts
- Create sysfs or character devices
If probe() fails, the device will not work.
4. How does Device Tree match a Platform Driver?
Matching is done using the compatible string.
The Device Tree node provides a compatible property, and the platform driver provides an of_device_id table with the same string. If they match exactly, the kernel binds them together.
5. Can Platform Drivers be used for I2C or SPI devices?
Yes. This is very common.
Even though the device communicates over I2C or SPI, the binding still happens through Platform Devices and Drivers when using Device Tree. Inside the platform driver, you create or access an I2C or SPI client to communicate with the hardware.
6. What is the difference between a Platform Driver and an I2C Driver?
An I2C driver is tied directly to the I2C subsystem and is usually auto-created by the I2C core.
A Platform Driver is more generic and is often used when:
- You need board-specific logic
- The device uses multiple resources (IRQ, GPIO, regulators)
- You want tighter control over initialization
Both approaches are valid and used in production.
7. How are interrupts handled in Platform Drivers?
Interrupts are described in the Device Tree using the interrupts property.
Inside the driver, you retrieve the IRQ number using platform_get_irq() and register a handler using request_irq() or devm_request_irq(). The interrupt handler should be fast and should avoid sleeping or heavy processing.
8. What is devm_* and why should it be used?
devm_* APIs automatically free resources when the device is removed.
They reduce memory leaks and simplify error handling. In production drivers, devm_kzalloc(), devm_request_irq(), and similar APIs are strongly recommended.
9. How do Platform Drivers expose data to user space?
Common methods include:
- sysfs attributes
- character devices
- standard kernel subsystems like IIO, input, or hwmon
For simple data like sensor readings, sysfs is often enough. For high-performance or streaming data, character devices or IIO are better choices.
10. What are the most common reasons probe() is not called?
The most common reasons are:
- Mismatch in
compatiblestring - Device Tree node not compiled or loaded
- Driver not enabled in kernel configuration
- Incorrect Device Tree hierarchy (wrong bus node)
Checking dmesg usually points you in the right direction.
11. Are Platform Drivers used in real production kernels?
Absolutely.
Most SoC drivers, board-level drivers, and embedded peripherals in the Linux kernel are implemented using Platform Devices and Drivers. This model scales well and is heavily used by vendors and upstream kernel developers.
12. When should I avoid using Platform Devices and Drivers?
You should avoid platform drivers if:
- The hardware is self-discoverable (USB, PCI)
- The kernel already provides a dedicated subsystem driver
- You are writing a pure user-space driver
In those cases, platform drivers would add unnecessary complexity.
You can also read : Linked List Coding Questions
Mr. Raj Kumar is a highly experienced Technical Content Engineer with 7 years of dedicated expertise in the intricate field of embedded systems. At Embedded Prep, Raj is at the forefront of creating and curating high-quality technical content designed to educate and empower aspiring and seasoned professionals in the embedded domain.
Throughout his career, Raj has honed a unique skill set that bridges the gap between deep technical understanding and effective communication. His work encompasses a wide range of educational materials, including in-depth tutorials, practical guides, course modules, and insightful articles focused on embedded hardware and software solutions. He possesses a strong grasp of embedded architectures, microcontrollers, real-time operating systems (RTOS), firmware development, and various communication protocols relevant to the embedded industry.
Raj is adept at collaborating closely with subject matter experts, engineers, and instructional designers to ensure the accuracy, completeness, and pedagogical effectiveness of the content. His meticulous attention to detail and commitment to clarity are instrumental in transforming complex embedded concepts into easily digestible and engaging learning experiences. At Embedded Prep, he plays a crucial role in building a robust knowledge base that helps learners master the complexities of embedded technologies.
