First driver on real silicon with a real build system. Platform driver probed via Device Tree overlay — GPIO23 interrupt with workqueue bottom half, atomic counter exposed via /dev node, built and deployed through a custom Yocto meta layer.
Lab 07 touched real hardware but still used a simple GPIO API with no Device Tree, no bus model, no build system. Real production drivers don't work that way. This lab introduces the platform driver model — where the kernel matches a driver to hardware via the Device Tree — and wraps the entire thing in a Yocto meta layer. This is the workflow used by every embedded Linux camera driver in the field.
| Parameter | Value |
|---|---|
| Target board | Raspberry Pi 3 (BCM2837) |
| GPIO | GPIO23 (Pin 16) |
| Interrupt trigger | Falling edge (IRQF_TRIGGER_FALLING) |
| Pull configuration | Pull-up via DT overlay (brcm,pull = 2) |
| Build system | Yocto Kirkstone — custom meta-sanath layer |
| Host | Dell Latitude 5320, Ubuntu 22.04 |
A platform driver manages devices connected directly to the SoC — not through a bus like I2C or SPI. The kernel matches a platform driver to a device using the compatible string in the Device Tree. When the compatible string in a DT node matches a registered driver's of_match_table, the kernel calls that driver's probe() function.
This is fundamentally different from earlier labs which had no hardware representation. The platform driver represents real hardware — a GPIO pin on the BCM2837.
static const struct of_device_id sanath_irq_of_match[] = {
{ .compatible = "sanath-irq-lab", },
{ }
};
MODULE_DEVICE_TABLE(of, sanath_irq_of_match);
static struct platform_driver sanath_irq_driver = {
.probe = sanath_irq_probe,
.remove = sanath_irq_remove,
.driver = {
.name = "sanath-irq-lab",
.of_match_table = sanath_irq_of_match,
},
};
module_platform_driver(sanath_irq_driver);
The overlay does two things: configures GPIO23 as input with pull-up, and creates the platform device node with the matching compatible string and interrupt binding.
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2837";
fragment@0 {
target = <&gpio>;
__overlay__ {
sanath_irq_pins: sanath-irq-pins {
brcm,pins = <23>;
brcm,function = <0>; /* input */
brcm,pull = <2>; /* pull-up */
};
};
};
fragment@1 {
target-path = "/";
__overlay__ {
sanath_irq_lab: sanath-irq-lab {
compatible = "sanath-irq-lab";
pinctrl-names = "default";
pinctrl-0 = <&sanath_irq_pins>;
interrupt-parent = <&gpio>;
interrupts = <23 2>; /* GPIO23, falling edge */
status = "okay";
};
};
};
};
The compatible string "sanath-irq-lab" in the overlay matches the driver's of_match_table — this is the link that causes probe() to be called when the overlay is loaded.
devm_ prefixed APIs tie resource lifetime to the device. When remove() is called, all devm_ allocated resources are freed automatically by the kernel — no manual cleanup needed.
static int sanath_irq_probe(struct platform_device *pdev)
{
struct sanath_irq_data *data;
int ret;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
init_waitqueue_head(&data->wq);
data->irq = platform_get_irq(pdev, 0); /* reads IRQ from DT */
if (data->irq < 0)
return data->irq;
ret = devm_request_irq(&pdev->dev, data->irq, sanath_irq_handler,
IRQF_TRIGGER_FALLING, "sanath-irq-lab", data);
if (ret)
return ret;
platform_set_drvdata(pdev, data);
dev_info(&pdev->dev, "sanath-irq-lab probed successfully\n");
return 0;
}
| API | What it does |
|---|---|
| platform_get_irq(pdev, 0) | Reads IRQ number from DT node — no hardcoding |
| devm_kzalloc() | Allocates zeroed memory — freed automatically on remove() |
| devm_request_irq() | Requests IRQ — released automatically on remove() |
| platform_set_drvdata() | Attaches per-device struct to the device |
Edge detection is done entirely in hardware by the BCM2835 GPIO controller — the CPU never sees the voltage. The controller has dedicated registers:
GPIO23 voltage drops 3.3V → 0V
→ BCM2835 writes GPFEN register (Falling Edge Detect Enable) for GPIO23
→ Sets status bit in GPEDS register
→ Raises interrupt line to GIC (Generic Interrupt Controller)
→ GIC interrupts the CPU
→ CPU saves state, enters your handler
When IRQF_TRIGGER_FALLING is passed to request_irq(), the kernel's pinctrl-bcm2835 driver writes to GPFEN for GPIO23. The CPU only sees the interrupt signal — all detection is done before the handler runs.
static irqreturn_t sanath_irq_handler(int irq, void *dev_id)
{
struct sanath_irq_data *data = dev_id;
ktime_t now = ktime_get();
/* Reject edges within 50ms of last valid interrupt */
if (ktime_to_ms(ktime_sub(now, data->last_irq)) < 50)
return IRQ_HANDLED;
data->last_irq = now;
schedule_work(&data->work); /* all real work in workqueue */
return IRQ_HANDLED;
}
ktime_get() is used instead of jiffies because on RPi3 (HZ=250) one jiffy is 4ms — too coarse for bounce windows of 1–5ms. ktime_get() gives nanosecond resolution and is safe in hardIRQ context. Sleeping calls like msleep() are forbidden in the handler — the CPU cannot be scheduled away during interrupt handling.
During testing, interrupts fired even before the jumper wire touched GND. Investigation revealed three distinct causes of spurious GPIO interrupts:
| Cause | Pull-up fixes it? | SW debounce fixes it? | HW filter fixes it? |
|---|---|---|---|
| Floating pin (no pull) | ✓ Yes | Partially | Partially |
| Antenna / external EMI | Partially | Partially | ✓ Yes |
| Contact bounce | ✗ No | ✓ Yes | ✓ Yes |
In this lab the pull-up was already configured via DT overlay (brcm,pull = <2>), ruling out a floating pin. The actual cause was the jumper wire acting as an antenna — picking up EMI from the RPi3's switching regulators and USB power lines.
# More reliable than dmesg — pr_info can be rate-limited under high IRQ load
cat /proc/interrupts | grep sanath
184: 420 466 458 435 pinctrl-bcm2835 23 Edge sanath-irq-lab
Each column is the interrupt count on one CPU core. The BCM2837 is quad-core, so four columns appear.
The driver, DT overlay, and Makefile are packaged as a Yocto recipe in a custom meta-sanath layer on top of Kirkstone. The recipe handles cross-compilation and deployment to the RPi3 image automatically.
# Prevent Yocto from deleting work directory during active development
RM_WORK_EXCLUDE += "sanath-dtbo"
# Remove once the recipe is stable
INHERIT += "rm_work" in Yocto's default config deletes tmp/work/ after every successful build to save ~30GB of disk space. Adding a recipe to RM_WORK_EXCLUDE preserves its work directory for debugging.
The IRQ handler must never sleep and must complete as fast as possible — it interrupts the CPU mid-execution. All real work is deferred to a workqueue which runs in process context where sleeping, memory allocation, and longer operations are safe.
INIT_WORK is used instead of DECLARE_WORK because the work_struct is embedded inside the per-device struct. Each device instance gets its own work item. container_of recovers the parent struct inside the work function.
struct sanath_irq_data {
int irq;
atomic_t counter;
unsigned long last_irq;
struct work_struct work; /* embedded — one per device instance */
};
/* in probe() — initialise once */
INIT_WORK(&data->work, sanath_irq_work);
/* IRQ handler — top half */
static irqreturn_t sanath_irq_handler(int irq, void *dev_id)
{
struct sanath_irq_data *data = dev_id;
unsigned long now = jiffies;
if (now - data->last_irq < msecs_to_jiffies(50))
return IRQ_HANDLED; /* debounce */
data->last_irq = now;
schedule_work(&data->work); /* defer everything else */
return IRQ_HANDLED;
}
/* work function — bottom half, process context */
static void sanath_irq_work(struct work_struct *work)
{
struct sanath_irq_data *data =
container_of(work, struct sanath_irq_data, work);
atomic_inc(&data->counter);
pr_info("sanath-irq-lab: IRQ fired, counter = %d\n",
atomic_read(&data->counter));
}
| Mechanism | Context | Can sleep? | Use when |
|---|---|---|---|
| IRQ handler (top half) | hardIRQ atomic | Never | Minimal — save timestamp, schedule work |
| Workqueue (bottom half) | Process | Yes | All real work — counter, logging, wake queue |
| Tasklet | softIRQ atomic | Never | Low latency, no sleeping needed (deprecated) |
The counter is incremented in the work function and read in etx_read(). A mutex would technically work since both run in process context — but atomic_t is the correct choice for a single integer because it is race-free by design, has no locking overhead, and works in any context including atomic.
/* wrong — unsigned long with no protection */
data->counter++;
/* correct — atomic, race-free, works in any context */
atomic_inc(&data->counter);
unsigned long val = atomic_read(&data->counter);
| atomic_t | mutex | spinlock | |
|---|---|---|---|
| Use for | Single integer | Multiple variables, process context only | Data touched from IRQ context |
| Can sleep? | N/A | Yes | No |
| Works in IRQ? | Yes | No | Yes |
A character device exposes the IRQ counter via read(). The module registers both the platform driver and the char device from module_init() — module_platform_driver() macro cannot be used when the module also manages a char device.
static ssize_t etx_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
char kbuf[32];
int kbuf_len;
if (*off > 0)
return 0; /* EOF — prevent cat from looping forever */
if (!g_data)
return -ENODEV;
kbuf_len = snprintf(kbuf, sizeof(kbuf), "%d\n",
atomic_read(&g_data->counter));
if (copy_to_user(buf, kbuf, kbuf_len))
return -EFAULT;
*off += kbuf_len;
return kbuf_len;
}
The *off > 0 check is critical — without it, cat calls read() in a loop forever because it never sees EOF (return 0). After returning data, *off is advanced by bytes returned. On the next call *off > 0 triggers and returns 0, signalling EOF.
# Verify
cat /dev/sanath_queue
5
Without cancel_work_sync, a use-after-free is possible: rmmod triggers remove(), devm_ frees sanath_irq_data, then the work function runs and accesses freed memory — kernel oops.
cancel_work_sync(&data->work) blocks until any pending or in-flight work completes before remove() continues. The data struct is guaranteed valid for the entire duration of the work function.
static int sanath_irq_remove(struct platform_device *pdev)
{
struct sanath_irq_data *data = platform_get_drvdata(pdev);
cancel_work_sync(&data->work); /* wait for in-flight work */
dev_info(&pdev->dev, "sanath-irq-lab removed\n");
return 0; /* devm_ handles free_irq and kfree automatically */
}
devm_request_irq() means free_irq() is called automatically — no manual cleanup needed in remove() beyond cancel_work_sync.