← Previous Lecture Next Lecture →
In the previous lecture we covered why hardware interrupts exist and why handlers must stay fast. In this lecture of our free Linux device drivers course, we get hands-on and write a real linux kernel interrupt handler using the modern, recommended kernel APIs. We’ll use the resource-managed devm_request_irq() function instead of the legacy request_irq(), and we’ll also look at threaded interrupt handlers, which are now the preferred approach for drivers that need to do more than a few microseconds of work.
The Interrupt Handler Function Signature
Every hardware interrupt handler in the Linux kernel follows the same function signature, regardless of the device type:
static irqreturn_t my_device_isr(int irq, void *dev_id);
The two parameters are always the same:
- irq — the IRQ number that triggered this call, useful mainly for logging or when one handler serves several lines.
- dev_id — the private pointer you supplied when you registered the handler. This is how your handler gets access to its own device context without relying on global variables.
Registering a Handler: The Modern Way
Older tutorials (and older kernel source) often show request_irq() paired with a manual free_irq() call in the driver’s remove/exit path. On kernel 6.x, the recommended approach for platform and GPIO-style drivers is the resource-managed variant, which automatically frees the IRQ when the owning device is detached — removing an entire class of “forgot to free the IRQ” bugs:
int ret;
ret = devm_request_irq(&pdev->dev, irq_number,
my_device_isr,
IRQF_TRIGGER_RISING,
"my_device_irq",
my_dev);
if (ret) {
dev_err(&pdev->dev, "failed to request IRQ %d: %d\n",
irq_number, ret);
return ret;
}
Notice there is no matching devm_free_irq() call anywhere — the device-managed (devm_) framework releases the IRQ automatically when the device is unbound, as long as you registered it against the correct struct device *.
owning device
IRQ number
your ISR function
trigger type / sharing
shown in /proc/interrupts
your private context
Understanding the irqreturn_t Return Value
Your handler must always report back whether it actually handled the interrupt. The kernel uses this to detect misbehaving hardware and spurious interrupts, especially on shared IRQ lines. There are two values you’ll use in almost every driver:
| Return Value | Meaning |
|---|---|
IRQ_HANDLED |
Your device generated this interrupt and you handled it. |
IRQ_NONE |
This interrupt was not from your device (relevant mainly on shared lines). |
IRQ_WAKE_THREAD |
Handled at the hardware level, but a threaded handler should now run to finish the work. |
Threaded Interrupt Handlers: The Preferred Pattern Today
Most real-world drivers on kernel 6.x don’t just register a single hardirq handler — they register a threaded interrupt handler using devm_request_threaded_irq(). This gives you two callback functions instead of one: a quick top-half check that runs in hardirq context, and a second function that runs in its own kernel thread, where sleeping, I2C/SPI transfers, and mutexes are all perfectly safe.
static irqreturn_t my_device_quick_check(int irq, void *dev_id)
{
struct my_device *mydev = dev_id;
if (!my_device_irq_is_mine(mydev))
return IRQ_NONE;
return IRQ_WAKE_THREAD;
}
static irqreturn_t my_device_thread_fn(int irq, void *dev_id)
{
struct my_device *mydev = dev_id;
/* Safe to sleep here: I2C reads, mutex_lock(), etc. */
my_device_process_event(mydev);
return IRQ_HANDLED;
}
ret = devm_request_threaded_irq(&pdev->dev, irq_number,
my_device_quick_check,
my_device_thread_fn,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"my_device_irq", mydev);
The IRQF_ONESHOT flag is important here — it keeps the hardware IRQ line masked until the threaded handler has finished running, which prevents the same interrupt from re-firing before your thread has had a chance to clear it on the device.
Complete Example: A GPIO Button Interrupt Driver
Here is a self-contained, original example (not taken from any book or external source) showing a minimal GPIO button driver using a threaded interrupt handler, written for a modern kernel 6.x build:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
struct button_dev {
struct gpio_desc *gpiod;
int irq;
unsigned int press_count;
};
static irqreturn_t button_thread_fn(int irq, void *dev_id)
{
struct button_dev *bdev = dev_id;
bdev->press_count++;
dev_info(NULL, "Button pressed, total presses: %u\n",
bdev->press_count);
return IRQ_HANDLED;
}
static int button_probe(struct platform_device *pdev)
{
struct button_dev *bdev;
int ret;
bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
if (!bdev)
return -ENOMEM;
bdev->gpiod = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(bdev->gpiod))
return PTR_ERR(bdev->gpiod);
bdev->irq = gpiod_to_irq(bdev->gpiod);
if (bdev->irq < 0)
return bdev->irq;
ret = devm_request_threaded_irq(&pdev->dev, bdev->irq,
NULL, button_thread_fn,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"gpio_button", bdev);
if (ret)
return ret;
platform_set_drvdata(pdev, bdev);
return 0;
}
static const struct of_device_id button_of_match[] = {
{ .compatible = "ep,gpio-button" },
{ }
};
MODULE_DEVICE_TABLE(of, button_of_match);
static struct platform_driver button_driver = {
.probe = button_probe,
.driver = {
.name = "ep_gpio_button",
.of_match_table = button_of_match,
},
};
module_platform_driver(button_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala sample GPIO button interrupt driver");
Notice the top-half callback is simply NULL here — this is a valid and common pattern when your device doesn’t need a quick hardirq-level check, and you want every press to go straight to the threaded handler.
Real-World Use Cases
- GPIO buttons and switches — exactly like the example above, common in embedded boards and IoT devices.
- I2C/SPI sensor interrupts — accelerometers and touchscreens that need to read data over a bus, which requires sleeping, so a threaded handler is mandatory.
- Network interface cards — typically pair a very fast hardirq handler with NAPI polling for high packet rates.
- UART/serial controllers — handle incoming byte data and wake up any process blocked on a read.
Common Mistakes and Troubleshooting
- Calling sleeping functions in the hardirq (top-half) callback — you’ll see a “scheduling while atomic” kernel warning. Move that code to the threaded function instead.
- Forgetting IRQF_ONESHOT with a NULL top half — this flag is required whenever you don’t provide a hardirq handler, otherwise registration will fail.
- Not checking the return value of devm_request_threaded_irq() — always propagate the error back from probe().
- Interrupt never fires — verify with
cat /proc/interruptsthat the count is registered at all, then double-check your device tree trigger type matches your flags.
Best Practices
- Prefer
devm_request_irq()/devm_request_threaded_irq()over the legacyrequest_irq()in new driver code. - Use threaded handlers whenever the device work involves bus transfers (I2C/SPI) or anything that might sleep.
- Give your IRQ a descriptive name — it shows up in
/proc/interruptsand saves debugging time later. - Always validate
dev_idand confirm the interrupt genuinely belongs to your device before touching shared resources.
Performance and Security Considerations
Performance: Threaded handlers add a small scheduling latency compared to doing everything in hardirq context, but this trade-off is almost always worth it for correctness and system responsiveness, since it keeps other interrupts from being blocked.
Security: Because dev_id is a raw pointer you control, always make sure it points to memory that stays valid for the entire lifetime the IRQ is registered — using devm_kzalloc() for your device structure, as shown in the example, ties its lifetime safely to the device itself.
Summary / Key Takeaways
- Every interrupt handler follows the signature
irqreturn_t handler(int irq, void *dev_id). - Use
devm_request_irq()ordevm_request_threaded_irq()on modern kernels instead of the legacy, manually-freed APIs. - Return
IRQ_HANDLED,IRQ_NONE, orIRQ_WAKE_THREADcorrectly — the kernel relies on this for shared IRQ detection. - Threaded handlers are the standard pattern for any device work that needs to sleep.
Conclusion
Writing a correct linux kernel interrupt handler is really about respecting the boundaries of hardirq context and choosing the right registration API for your device’s needs. With devm_request_threaded_irq() and the GPIO button example above, you now have a working template you can adapt for buttons, sensors, or any interrupt-driven peripheral on a modern embedded Linux board. This lecture is part of EmbeddedPathashala’s free Linux device drivers course — keep going to the next lecture where we look at bottom halves in more depth: tasklets, workqueues, and softirqs.
FAQ
Q1. What’s the difference between request_irq() and devm_request_irq()?
They register interrupts the same way, but devm_request_irq() ties the IRQ’s lifetime to the device, so the kernel frees it automatically on removal — you don’t need a manual free_irq() call.
Q2. When should I use a threaded interrupt handler instead of a plain one?
Whenever your interrupt processing needs to sleep — for example, reading a sensor over I2C/SPI, taking a mutex, or allocating memory with GFP_KERNEL.
Q3. Can the top-half handler in devm_request_threaded_irq() be NULL?
Yes, and it’s common when every press/event should go straight to the threaded function. You must pass IRQF_ONESHOT in that case.
Q4. What does IRQ_WAKE_THREAD actually do?
It tells the kernel that the hardirq portion is done and the registered thread function should now be woken up to complete the work.
Q5. Why did my driver fail to load with an IRQ error?
Common causes include an invalid or unmapped IRQ number, a mismatched trigger type between your flags and the device tree, or missing IRQF_ONESHOT with a NULL top-half handler.
Q6. Is it safe to allocate memory inside my ISR?
Not in the hardirq (top-half) portion. Inside a threaded handler function, sleeping allocations like GFP_KERNEL are safe.
Continue the Free Linux Device Drivers Course
Next up: Top and Bottom Halves — tasklets, workqueues, and softirqs explained.
Go to Next Lecture
2 Comments