Device Tree Interrupt Handling Guide- Free Linux Device Drivers Course

PREV_LEC | NEXT_LEC

Device Tree Interrupt Handling Guide

Free Linux Kernel Development Course — Device Tree Chapter, Lecture 7

Kernel 6.x Ready
Original Driver Code
Hands-On Lab

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 embedded systems course
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-controller and #interrupt-cells declare on the controller node
  • What interrupt-parent and interrupts declare on the consumer node, and how an ARM GIC interrupt specifier is structured cell by cell
  • How to go from a device tree interrupts entry to a live Linux IRQ number with platform_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-hub style 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.

Interrupt Controller And Consumer Relationship
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
Threaded Interrupt Handler Flow
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 number
  • devm_request_threaded_irq() with IRQF_ONESHOT is 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.

PREV_LEC | NEXT_LEC

Leave a Reply

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