Chapter 43: Interrupts¶
What:
request_irq, the top-half / bottom-half split, and the four standard bottom halves, softirqs, tasklets, work queues, and threaded IRQs. By the end you’ll have a driver that owns a hardware IRQ, acknowledges it in nanoseconds in the top half, and processes the event without blocking the rest of the kernel. IRQ: interrupt request, the signal path that tells the CPU or interrupt controller that hardware needs service.Why: interrupts are how hardware tells the kernel something happened: data arrived, DMA finished, a button was pressed, a timer expired. Get the IRQ-handler design wrong and you hit one of two failures: missed interrupts (handler too slow or wrong polarity) or IRQ storms (handler does not acknowledge, hardware re-asserts continuously, system hangs). The rules below give you the right design every time. MCU bridge: Think of DMA like the MCU DMA controller you used for UART or SPI, but with cache coherency, scatter-gather descriptors, and kernel ownership rules added. DMA: Direct Memory Access. Hardware moves data to or from memory without the CPU copying each byte.
Focus: the IRQ contract is “fast, atomic, and minimal.” Your top-half runs with interrupts disabled, in atomic context (no sleeping, no
kmalloc(GFP_KERNEL), nocopy_to_user). Anything that takes more than a few microseconds must be deferred to a bottom half. Once you accept this constraint, the API choices below follow naturally.
43.1 How the i.MX6ULL gets an interrupt to your code¶
The chain, end to end:
hardware peripheral (e.g., GPIO pin transition)
│
▼
peripheral IRQ output line
│
▼
GIC (Generic Interrupt Controller — Cortex-A IRQ multiplexer)
│ assigns IRQ number (e.g., 99)
▼
CPU's IRQ exception vector
│
▼
kernel's gic_handle_irq → generic_handle_irq(virq)
│
▼
IRQ domain mapping (DT-based) → your handler
│
▼
your_irq_handler(irq, dev_id) — runs with IRQ off
Two things to notice:
IRQ numbers in DT and
request_irqare virtual. The DT lineinterrupts = <0 99 IRQ_TYPE_LEVEL_HIGH>carries the GIC hardware number. At boot, the kernel maps it to a virtual IRQ (a virq). Yourrequest_irquses this virq. You usually do not see the mapping happen, the framework hands you the virq.GIC is the multiplexer. The CPU has one IRQ line. The GIC has up to ~160 inputs (i.MX6ULL specific) and figures out which is firing. The kernel’s GIC driver demultiplexes and routes to your handler.
43.2 The top half¶
Your top half is the function the kernel calls when the IRQ fires. The contract:
It runs in atomic context. No sleeping. No
kmalloc(GFP_KERNEL). Nomutex_lock. Nocopy_to_user. NoprintkwithKERN_INFO(well,printkworks but is rate-limited. Minimize).It runs with that IRQ disabled. The GIC won’t re-fire the same IRQ on the same CPU until you return. (Other CPUs can see it. That’s how SMP works.)
It runs with kernel preemption off. No context switch until you return.
It returns
IRQ_HANDLEDif it processed the IRQ,IRQ_NONEif not (used in shared-IRQ scenarios, “this wasn’t mine”).It must acknowledge the hardware. Otherwise the IRQ line stays asserted and the IRQ fires again immediately ⇒ IRQ storm ⇒ system hang.
A canonical top-half:
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
u32 status;
/* 1. Read status; figure out what fired */
status = readl(dev->base + STATUS_REG);
if (!(status & MY_IRQ_FLAG))
return IRQ_NONE; /* not for us — shared IRQ */
/* 2. Acknowledge the hardware (write-1-clear pattern) */
writel(MY_IRQ_FLAG, dev->base + STATUS_REG);
/* 3. Capture minimal data; defer the rest */
dev->raw_value = readl(dev->base + DATA_REG);
dev->irq_count++;
/* 4. Wake the bottom half (workqueue, tasklet, or wait queue) */
queue_work(dev->wq, &dev->work);
return IRQ_HANDLED;
}
Five lines of real work. Read status, ack, snapshot, defer, return. Under 1 µs on i.MX6ULL.
If your top-half is doing anything more than this, parsing protocol bytes, looking up tables, doing math, it’s too long. Move it to a bottom half.
43.3 Requesting an IRQ¶
#include <linux/interrupt.h>
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
void free_irq(unsigned int irq, void *dev);
For platform drivers, the irq number comes from the DT via platform_get_irq():
int virq = platform_get_irq(pdev, 0);
if (virq < 0)
return virq;
err = devm_request_irq(&pdev->dev, virq, my_irq_handler,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"myhw", priv);
The devm_request_irq variant auto-frees on driver-unbind. Always prefer it.
Flags¶
Flag |
Meaning |
|---|---|
|
Edge-triggered, rising edge |
|
Edge-triggered, falling edge |
|
Level-triggered, active high |
|
Level-triggered, active low |
|
Multiple handlers may share this IRQ line |
|
Don’t re-enable IRQ until threaded handler completes |
|
Force top-half-only (don’t run as kernel thread) |
The trigger flag is usually omitted for platform drivers because the DT specifies it (in the interrupts property’s third cell). The kernel’s IRQ subsystem reads from DT.
43.4 Bottom halves, four choices¶
Top half done. Now you need to do the real work outside the atomic constraints. Four options, in order of “easiest” to “most flexible”:
1. Threaded IRQ, the modern default¶
request_threaded_irq is the cleanest pattern: the kernel calls your primary (top-half) function with IRQs off, then schedules your threaded function as a kernel thread that runs with normal kernel context, can sleep, take mutexes, do copy_to_user, everything.
static irqreturn_t my_primary(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* Read status, ack hw — atomic context */
writel(...);
return IRQ_WAKE_THREAD;
}
static irqreturn_t my_threaded(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
/* Process at leisure — full kernel context */
mutex_lock(&dev->lock);
/* ... */
mutex_unlock(&dev->lock);
wake_up(&dev->wq);
return IRQ_HANDLED;
}
/* In probe: */
err = devm_request_threaded_irq(&pdev->dev, virq,
my_primary, my_threaded,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"myhw", priv);
The IRQF_ONESHOT flag is important: it keeps the IRQ masked from when the primary returns IRQ_WAKE_THREAD until the threaded handler completes. Without it, the IRQ could re-fire and re-schedule before you’ve finished processing.
You can pass NULL for the primary, in which case the kernel installs a default that just returns IRQ_WAKE_THREAD. Then your threaded handler is the only thing you wrote.
Use threaded IRQ for ~80% of new driver code. It’s the cleanest model.
2. Work queues, explicit deferral¶
A work queue is a kernel thread that processes a queue of work items. Schedule a work item from your top-half. The work runs later in a normal kernel thread.
#include <linux/workqueue.h>
static struct workqueue_struct *my_wq;
static struct work_struct my_work;
static void my_work_fn(struct work_struct *w)
{
/* Runs in process context. Can sleep. */
struct my_dev *dev = container_of(w, struct my_dev, work);
/* Process accumulated data */
}
static irqreturn_t my_irq(int irq, void *dev_id)
{
/* Top half */
queue_work(my_wq, &my_work);
return IRQ_HANDLED;
}
/* In probe */
my_wq = alloc_workqueue("myhw", WQ_UNBOUND, 0);
INIT_WORK(&my_work, my_work_fn);
For most drivers, the shared system workqueue is fine and you don’t need to allocate your own:
INIT_WORK(&my_work, my_work_fn);
/* In IRQ: */
schedule_work(&my_work);
When to use work queues over threaded IRQs:
You want to coalesce multiple IRQs into one bottom-half execution (work is idempotent. Queueing it again is a no-op if already queued).
You don’t want a dedicated kthread per IRQ.
You’re piggy-backing on existing workqueue infrastructure.
3. Tasklets, legacy¶
A tasklet runs in softirq context (atomic, can’t sleep, but with all IRQs enabled). It’s faster to schedule than a workqueue but lives in atomic context.
#include <linux/interrupt.h>
/* Modern form (since v5.9): DECLARE_TASKLET(name, fn) where fn takes (struct tasklet_struct *).
* DECLARE_TASKLET_OLD is the backward-compat macro for the legacy (unsigned long) callback shown
* here. New drivers should prefer DECLARE_TASKLET — but better still, prefer workqueues
* (tasklets are being phased out across the tree). */
static DECLARE_TASKLET_OLD(my_tasklet, my_tasklet_fn);
static void my_tasklet_fn(unsigned long data)
{
/* Softirq context. Can't sleep. */
}
static irqreturn_t my_irq(int irq, void *dev_id)
{
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
Tasklets are discouraged in new code. The kernel is migrating away from them, they’re an obstacle for PREEMPT_RT (real-time kernel. Ch 52A). Use threaded IRQs unless you have a strong reason for atomic-context bottom-half processing.
PREEMPT_RT: the Linux real-time patch set that makes more kernel paths preemptible and reduces latency.
4. Softirqs, kernel-internal only¶
Softirqs are the lowest-level deferred mechanism. Used internally for networking, timers, and block-I/O completion. Driver authors don’t write softirqs. We use tasklets/workqueues/threaded IRQs which are built on top of softirq machinery.
Picking among them, table¶
Bottom half |
Context |
Can sleep? |
Coalesces? |
Scheduling cost |
When to use |
|---|---|---|---|---|---|
Threaded IRQ |
Process (kthread) |
Yes |
No |
~10 µs (wake kthread) |
Default modern choice |
Workqueue (shared) |
Process (kthread) |
Yes |
Yes (if queued) |
~10 µs |
Coalescing multi-IRQ |
Workqueue (dedicated) |
Process (kthread) |
Yes |
Yes |
~10 µs |
Want isolation |
Tasklet |
Softirq |
No |
Yes |
< 1 µs |
Legacy; latency-sensitive |
43.5 GPIO interrupts, the everyday case¶
The most common reason embedded drivers want IRQs is to react to a GPIO transition (button press, sensor data-ready, etc.). The mechanics:
MCU bridge: Think of Linux GPIO like the same pin set/reset block you used on STM32, but accessed through a kernel subsystem that owns numbering, direction, interrupts, and user-space exposure. GPIO: General-Purpose Input/Output, a pin controlled as a digital input, output, or interrupt source.
DT side¶
my_button {
compatible = "linuxlearn,button";
interrupt-parent = <&gpio4>;
interrupts = <14 IRQ_TYPE_EDGE_FALLING>;
button-gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
};
The interrupts property names the GPIO bank (via interrupt-parent) and the pin within that bank. IRQ_TYPE_EDGE_FALLING says trigger on the high-to-low transition.
Driver side¶
static int button_probe(struct platform_device *pdev)
{
struct gpio_desc *gpio;
int virq, err;
gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(gpio))
return PTR_ERR(gpio);
virq = gpiod_to_irq(gpio);
if (virq < 0)
return virq;
err = devm_request_threaded_irq(&pdev->dev, virq, NULL, button_thread,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"button", priv);
return err;
}
static irqreturn_t button_thread(int irq, void *dev_id)
{
/* Button pressed; do something in process context */
pr_info("button: pressed\n");
return IRQ_HANDLED;
}
Two new things:
gpiod_to_irqconverts a GPIO descriptor into a virq we can pass torequest_irq. This is the bridge.NULL primary handler in
request_threaded_irq, when you don’t have any atomic-context work to do, pass NULL and the kernel installs a default that just returnsIRQ_WAKE_THREAD.
That’s it. The threaded handler runs whenever the button is pressed. Sleep, mutex, copy_to_user, all fine.
43.8 Lab¶
Privilege boundary: $ means normal user. # or sudo means root and can change host or target state. After a privileged command, verify the expected device, service, or file appears before continuing. Roll back by undoing the config change or stopping the service you just enabled.
Write a GPIO-button driver. Use the user button on the Point Atom MINI. Threaded IRQ, prints to dmesg on press.
Measure IRQ latency. Use ftrace to time from a button press (capture in the GPIO bank IRQ) to your threaded handler running. Compare against running the same work in a tasklet.
Add bouncing handling. Add a 20 ms debounce: ignore further IRQs that fire within 20 ms of the previous (use
jiffiesandtime_after). Test by pressing and holding. Observe a single event in dmesg.Force an IRQ storm. Set the trigger type to LEVEL but don’t acknowledge the hardware. Observe
/proc/interruptscount exploding. Recover withrmmod. (Note: do this on a non-critical system. If you don’t recover quickly, the kernel may detect the storm and disable the IRQ.)Convert from threaded to workqueue. Rewrite #1 using
schedule_workfrom a non-threadedrequest_irq. Compare code complexity. Observe equivalent behavior.Shared-IRQ experiment. On a real shared IRQ (or fake one), register two handlers and verify the kernel calls both. Confirm
IRQ_NONEis the right return when your device didn’t fire.
43.9 Pitfalls¶
Sleeping in a top-half.
kmalloc(GFP_KERNEL),mutex_lock,copy_to_user, all forbidden.CONFIG_DEBUG_ATOMIC_SLEEP=ycatches at the call site. UseGFP_ATOMICif you really must allocate from IRQ context. Otherwise pre-allocate.Forgetting to ack. Level-triggered IRQ + no acknowledge = continuous re-fire = lockup. The kernel will eventually detect the storm and disable the IRQ, printing “spurious IRQ disabled” to dmesg.
Wrong trigger polarity in DT. Symptom: IRQ never fires. Always cross-check the device’s datasheet against the DT’s
IRQ_TYPE_*value.Returning
IRQ_NONEfrom a non-shared handler. The kernel treats this as a spurious IRQ. After enough of these, the IRQ is disabled.Calling
request_irqthenfree_irqwith mismatcheddevpointers.free_irqis keyed on the cookie. Mismatch ⇒ silently fails to free the right handler.Not using
IRQF_ONESHOTwith threaded IRQs. The hardware can re-fire while the thread is still running, causing a queue of pending threaded calls. Almost always you wantIRQF_ONESHOT.Wrong context for memory allocation. In an IRQ handler,
kmalloc(GFP_KERNEL)may sleep waiting for memory reclaim. UseGFP_ATOMICin top-halves and bottom-halves running in softirq context. Top tip: pre-allocate at probe time so you never alloc in IRQ context.Forgetting
dev_idparameter.request_irqtakes a cookie. You can pass a pointer to your private state. Don’t pass NULL even if you don’t need it, the kernel won’t free the handler later, because the cookie is part of the identity forfree_irq.Not handling shared IRQ correctly. If you register with
IRQF_SHAREDbut always returnIRQ_HANDLED, other handlers on the same line are starved. Inspect your hardware status before claiming the IRQ.
43.10 Going deeper¶
Documentation/core-api/genericirq.rst: the kernel’s generic IRQ framework.Documentation/devicetree/bindings/interrupt-controller/: the binding for declaring IRQ controllers in DT.Documentation/locking/lockdep-design.rst: IRQ-context vs process-context locking rules (important once you have shared state).drivers/gpio/gpio-mxc.c: the i.MX GPIO driver. Shows how a chained IRQ controller works: the GPIO bank handler demuxes 32 pin events into per-pin virqs.Documentation/PCI/MSI-HOWTO.rst: PCI Message Signaled Interrupts. Different mechanism, same API on the receiving side.drivers/spi/spi-imx.c: a real i.MX driver using threaded IRQ + DMA.
End of foundation chapters (Ch 36–43). You now have the full kernel-module driver vocabulary: load/unload, chardev, hot-plug, platform binding, locking, blocking I/O, and interrupts. The chapters that follow (44–51 + insertions) take this vocabulary and apply it to specific subsystems: GPIO, input, I²C, SPI, PWM/RTC, IIO, regmap, DMA, network, sound, LCD/DRM. Each chapter follows the same pattern, the subsystem provides a registration API, you fill in callbacks, the framework handles the rest. MCU bridge: Think of Linux PWM like an MCU timer output channel, except the driver exposes period, duty cycle, polarity, and enable state through a subsystem. MCU bridge: Think of regmap like a typed wrapper around your read_reg() and write_reg() helpers, with caching, locking, and bus differences handled centrally. PWM: Pulse-Width Modulation, a timer output whose duty cycle controls average power or encodes timing. IIO: Industrial I/O, Linux’s subsystem for sensors, ADCs, DACs, and buffered sampled data. regmap: a kernel helper that wraps register reads and writes over I2C, SPI, or MMIO.
Next chapter: Chapter 44: GPIO subsystem. The
gpiod_*API and how character drivers integrate with the GPIO framework.