Device Tree Interrupt Handling Guide
Free Linux Kernel Development Course — Device Tree Chapter, Lecture 7
Device tree interrupt handling connects two very different pieces of hardware description: the controller that owns a set of IRQ lines, and the consumer device that needs one of those lines routed to it. In this lecture of our free linux kernel development course we unpack the four device tree properties behind every interrupt binding — interrupt-controller, #interrupt-cells, interrupt-parent, and interrupts — decode a real GIC interrupt specifier cell by cell, and then write an original driver that requests and services an interrupt the modern way.
Free Embedded Linux Course Keywords
free linux development course
free linux device drivers course
free linux kernel development course
device tree interrupt handling
What You Will Learn
- The difference between the interrupt controller side and the interrupt consumer side of a device tree binding
- What
interrupt-controllerand#interrupt-cellsdeclare on the controller node - What
interrupt-parentandinterruptsdeclare on the consumer node, and how an ARM GIC interrupt specifier is structured cell by cell - How to go from a device tree
interruptsentry to a live Linux IRQ number withplatform_get_irq() - How to register a modern threaded interrupt handler with
devm_request_threaded_irq()in an original driver,ep_irq_demo
Prerequisites
- The previous lecture on device tree named resources, since this lecture reuses the same
ep,sensor-hubstyle node - Basic familiarity with what a hardware interrupt line is
- A working ARM64 or Raspberry Pi based Linux kernel 6.x build environment with device tree overlay support
Two Sides Of Every Device Tree Interrupt Binding
Device tree interrupt handling always involves two roles. The controller is the hardware block that physically owns a bank of IRQ lines and routes them to the CPU — on most modern ARM boards this is a Generic Interrupt Controller, or GIC. The consumer is any device node that generates one or more of those interrupts, such as our sensor hub from the previous lecture.
| Role | Node Property | Meaning |
|---|---|---|
| Controller | interrupt-controller |
Empty boolean property marking this node as an IRQ source router |
| Controller | #interrupt-cells |
How many cells this controller needs to fully describe one interrupt |
| Consumer | interrupt-parent |
Phandle pointing to the controller node this consumer is wired to |
| Consumer | interrupts |
One or more interrupt specifiers, each #interrupt-cells cells long |
If a consumer node omits interrupt-parent, it inherits the property from its parent node in the tree. This is very common on real boards — the root node or the SoC node usually declares the GIC as the default interrupt parent once, and individual peripherals simply omit the property.
Decoding A GIC Interrupt Specifier
On an ARM GIC, #interrupt-cells is 3, so every entry in a consumer’s interrupts property is exactly three cells. Take the specifier we used for the sensor hub’s “alarm” line in the previous lecture:
interrupts = <0 85 4>;
What Each Cell Means
| Cell | Value Here | Meaning |
|---|---|---|
| 1st — interrupt type | 0 | 0 = Shared Peripheral Interrupt (SPI), routable to any core. 1 = Private Peripheral Interrupt (PPI), tied to one core |
| 2nd — interrupt number | 85 | Hardware IRQ number within the selected type; its valid range depends on whether it is SPI or PPI |
| 3rd — trigger flags | 4 | Sense/trigger type such as level-high, level-low, edge-rising, edge-falling, defined in the kernel’s irq.h constants |
You will rarely need to memorize the numeric trigger flag values by hand — device tree source files normally include the GIC or IRQ header that defines symbolic names such as IRQ_TYPE_LEVEL_HIGH, so the source stays readable.
From Device Tree Entry To Linux IRQ Number
A driver never touches the raw interrupts cells directly. Instead, the kernel’s interrupt mapping code (driven by the controller’s own driver) turns each entry into a plain Linux IRQ number, which the driver fetches with platform_get_irq() or, when named interrupts are used, platform_get_irq_byname() as covered in the previous lecture.
int irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
The second argument is the index into the consumer’s interrupts list, exactly the same ordering risk we solved with interrupt-names in the previous lecture. For a node with only one interrupt, index 0 is fine as-is.
Requesting An Interrupt The Modern Way
Once you have a Linux IRQ number, the classic call to wire up a handler is request_irq(). On kernel 6.x, two changes matter for new driver code:
- Prefer the managed variant
devm_request_irq()so the interrupt is automatically freed when the device detaches, instead of requiring manual cleanup in a remove path - Prefer
devm_request_threaded_irq()whenever the handler needs to do any non-trivial work. It splits the handler into a fast primary function that runs in hard-IRQ context and a threaded function that runs in a normal kernel thread, keeping interrupt-context work to an absolute minimum
| Stage | Runs In | Typical Work |
|---|---|---|
| Primary handler | Hard IRQ context | Acknowledge hardware, decide IRQ_WAKE_THREAD vs IRQ_HANDLED |
| Threaded handler | Dedicated kernel thread | Register reads, mutex-protected data updates, longer processing |
Hands-On Driver: ep_irq_demo
Let us write an original driver that binds to the same ep,sensor-hub node from the previous lecture, but this time focuses purely on interrupt handling with a modern threaded IRQ handler for the alarm line.
// ep_irq_demo.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/mutex.h>
struct ep_irq_demo_priv {
int alarm_irq;
struct mutex lock;
unsigned int alarm_count;
};
/* Fast primary handler: hard IRQ context, keep it short */
static irqreturn_t ep_irq_demo_primary(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
/* Threaded handler: normal kernel thread context, safe to sleep */
static irqreturn_t ep_irq_demo_threaded(int irq, void *dev_id)
{
struct ep_irq_demo_priv *priv = dev_id;
mutex_lock(&priv->lock);
priv->alarm_count++;
dev_info(NULL, "ep_irq_demo: alarm count now %u\n", priv->alarm_count);
mutex_unlock(&priv->lock);
return IRQ_HANDLED;
}
static int ep_irq_demo_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct ep_irq_demo_priv *priv;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
mutex_init(&priv->lock);
priv->alarm_irq = platform_get_irq_byname(pdev, "alarm");
if (priv->alarm_irq < 0)
return priv->alarm_irq;
ret = devm_request_threaded_irq(dev, priv->alarm_irq,
ep_irq_demo_primary,
ep_irq_demo_threaded,
IRQF_ONESHOT,
"ep_irq_demo-alarm", priv);
if (ret)
return dev_err_probe(dev, ret, "failed to request alarm irq\n");
platform_set_drvdata(pdev, priv);
dev_info(dev, "ep_irq_demo: alarm irq %d ready\n", priv->alarm_irq);
return 0;
}
static void ep_irq_demo_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "ep_irq_demo: removed\n");
}
static const struct of_device_id ep_irq_demo_of_match[] = {
{ .compatible = "ep,sensor-hub" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_irq_demo_of_match);
static struct platform_driver ep_irq_demo_driver = {
.probe = ep_irq_demo_probe,
.remove = ep_irq_demo_remove,
.driver = {
.name = "ep_irq_demo",
.of_match_table = ep_irq_demo_of_match,
},
};
module_platform_driver(ep_irq_demo_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Interrupt handling demo driver for free linux kernel course");
IRQF_ONESHOT is required whenever a primary handler has no hardware-level acknowledge step of its own and simply wakes the threaded handler — it keeps the IRQ line masked until the thread finishes, preventing a second interrupt from firing before the first one has been fully processed.
Building And Running The Driver
Reuse the same overlay from the previous lecture (the ep,sensor-hub node already defines the alarm named interrupt) and build this module the same way:
cat > Makefile << 'EOF'
obj-m += ep_irq_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
EOF
make
sudo dtoverlay ep-sensor-hub
sudo insmod ep_irq_demo.ko
dmesg | tail -n 3
Expected output right after loading:
ep_irq_demo ep_sensor_hub: ep_irq_demo: alarm irq 190 ready
If your hardware or a simple GPIO-driven test fixture pulses the alarm line, every pulse prints an incrementing counter line:
ep_irq_demo: alarm count now 1
ep_irq_demo: alarm count now 2
Common Mistakes And Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
| Doing heavy work directly in a non-threaded primary handler | System-wide latency spikes, possible “scheduling while atomic” if it sleeps | Move real work into a threaded handler with devm_request_threaded_irq() |
| Forgetting IRQF_ONESHOT with a NULL-equivalent primary handler | Interrupt storm or handler re-entered before thread completes | Always set IRQF_ONESHOT when the primary handler only returns IRQ_WAKE_THREAD |
| Mismatched #interrupt-cells count in a custom interrupt-controller node | Overlay fails to apply or interrupt never fires | Match the exact cell count the controller binding documentation specifies |
Best Practices
- Always check
platform_get_irq()/platform_get_irq_byname()return values before passing them to a request call - Default to
devm_request_threaded_irq()for any handler doing more than a quick register acknowledge - Use
dev_err_probe()for consistent, deferred-probe-aware error reporting in the interrupt request path - Keep any shared state touched by a threaded handler protected by a mutex, not a spinlock, since threaded handlers run in process context and are allowed to sleep
Performance And Security Considerations
A well designed interrupt handler minimizes work in hard-IRQ context because it runs with local interrupts disabled and blocks other interrupts on the same priority level. Threaded IRQ handlers exist specifically to move that work back into a schedulable context, trading a small amount of dispatch latency for a system that stays responsive under interrupt load. From a security standpoint, an interrupt handler should never trust an interrupt count or payload as authenticated input from a peripheral without validating it, since a compromised or malfunctioning device could flood a driver with spurious interrupts.
Summary And Key Takeaways
- Every device tree interrupt binding has a controller side (
interrupt-controller,#interrupt-cells) and a consumer side (interrupt-parent,interrupts) - An ARM GIC interrupt specifier is three cells: type, number, and trigger flags
platform_get_irq()/platform_get_irq_byname()convert a device tree entry into a usable Linux IRQ numberdevm_request_threaded_irq()withIRQF_ONESHOTis the modern pattern for any handler that needs to do real work
Conclusion
Device tree interrupt handling ties directly back into everything else this device tree chapter has covered — a consumer node’s interrupts property is just another resource, decoded through the same controller/consumer relationship pattern you now understand for registers, clocks, and DMA channels. With named resources and interrupt handling both covered, the next lectures in this free linux kernel development course move on to Chapter 11’s kernel memory management topics referenced throughout this chapter, including a closer look at how ioremap() actually works under the hood.
Frequently Asked Questions
What does the interrupt-controller property actually declare?
It is an empty boolean property placed on a node to mark that node as a source of routable interrupt lines, distinguishing it from a normal consumer device node.
Why is #interrupt-cells needed at all?
Different interrupt controllers need different amounts of information to fully describe one interrupt. #interrupt-cells tells every consumer bound to that controller exactly how many cells each of their interrupts entries must contain.
What happens if a consumer node omits interrupt-parent?
The node inherits the interrupt-parent value from its nearest ancestor in the device tree, which is why most peripheral nodes on a real board never need to set it explicitly.
What is the difference between an SPI and a PPI on a GIC?
A Shared Peripheral Interrupt can be routed by the GIC to any CPU core, while a Private Peripheral Interrupt is tied to one specific core and typically used for per-core timers or similar hardware.
Why use devm_request_threaded_irq instead of request_irq?
It automatically frees the interrupt when the device is removed, and it lets you separate a fast hard-IRQ context handler from a threaded handler that is allowed to sleep and do heavier work.
When is IRQF_ONESHOT required?
It is required whenever your primary handler does not itself acknowledge the hardware and simply returns IRQ_WAKE_THREAD, so the line stays masked until the threaded handler finishes.
How do I get the Linux IRQ number for a named interrupt?
Call platform_get_irq_byname() with the platform device and the exact string from interrupt-names in the device tree node.
Continue The Free Linux Kernel Development Course
Explore more free embedded systems course lectures covering device drivers, kernel memory management, and synchronization.
