Linux No-Suspend IRQ Code Example
A hands-on companion to the IRQF_NO_SUSPEND explanation lecture — a working request_irq() example, the dmesg trail you should expect during suspend/resume, and a worked anti-pattern showing exactly why IRQF_SHARED plus IRQF_NO_SUSPEND breaks.
Lecture 22 of 22
Hands-On Example
Final PM Lecture
This is the code companion to our IRQF_NO_SUSPEND explanation lecture, and the final hands-on example in this free Linux device drivers course power management series. Rather than re-explain the theory, this lecture walks through one complete, original driver — ep_wdt_kick — that has a real, defensible reason to request an interrupt with IRQF_NO_SUSPEND, shows the request_irq() call and the dmesg output you should see with the handler still firing mid-suspend, and then deliberately breaks it with the classic IRQF_SHARED plus IRQF_NO_SUSPEND anti-pattern so you can recognize it instantly in review.
What You Will Learn
Prerequisites
Read the IRQF_NO_SUSPEND explanation lecture first — it covers the suspend_device_irqs() internals, the no_suspend_depth counter, and the IRQF_NO_SUSPEND vs enable_irq_wake() distinction that this example assumes you already understand. You should also have completed the Driver Wakeup Events lecture and, ideally, the rest of this free Linux kernel development course power management series, since the example driver below assumes familiarity with request_irq(), basic dev_pm_ops, and platform driver probe/remove structure.
Scenario: An Always-On Watchdog-Kick Interrupt
Consider a board with a small, always-powered timer block separate from the main SoC power domain. Its only job is to fire a periodic interrupt — say every two seconds — that the kernel uses to service (“kick”) a hardware watchdog timer. On this platform, suspend-to-idle keeps the SoC clocked well enough that this timer block keeps ticking, but if nothing pets the watchdog for too long during a long suspend-to-idle period, the watchdog fires and resets the board mid-sleep. This is a legitimate, narrow case for IRQF_NO_SUSPEND: the interrupt is not a device we want to wake the system, it is infrastructure that must keep running specifically because the system is asleep for an extended time.
The driver below, ep_wdt_kick, models this. All identifiers are original and prefixed ep_.
Correct Usage: request_irq() with IRQF_NO_SUSPEND
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/watchdog.h>
#include <linux/of.h>
struct ep_wdt_kick_dev {
struct platform_device *pdev;
void __iomem *regs;
int irq;
unsigned int kick_count;
};
/*
* ep_wdt_kick_isr - fires roughly every 2 seconds from an always-on
* timer block, independent of the main SoC power domain.
*
* This handler MUST keep running through the entire suspend-resume
* cycle (including the noirq window) or the hardware watchdog behind
* it will time out and reset the board while the system is asleep.
*/
static irqreturn_t ep_wdt_kick_isr(int irq, void *dev_id)
{
struct ep_wdt_kick_dev *wdev = dev_id;
/* Service the hardware watchdog register directly. */
writel(0x1ACE, wdev->regs + 0x04);
wdev->kick_count++;
return IRQ_HANDLED;
}
static int ep_wdt_kick_probe(struct platform_device *pdev)
{
struct ep_wdt_kick_dev *wdev;
int ret;
wdev = devm_kzalloc(&pdev->dev, sizeof(*wdev), GFP_KERNEL);
if (!wdev)
return -ENOMEM;
wdev->pdev = pdev;
wdev->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(wdev->regs))
return PTR_ERR(wdev->regs);
wdev->irq = platform_get_irq(pdev, 0);
if (wdev->irq < 0)
return wdev->irq;
/*
* IRQF_NO_SUSPEND: this line is NOT shared (see the anti-pattern
* section below for why that matters), and it belongs to hardware
* that stays powered while the SoC is in suspend-to-idle. Without
* this flag, suspend_device_irqs() would disable the line and the
* watchdog would go unserviced during noirq.
*/
ret = devm_request_irq(&pdev->dev, wdev->irq, ep_wdt_kick_isr,
IRQF_NO_SUSPEND, "ep-wdt-kick", wdev);
if (ret) {
dev_err(&pdev->dev, "failed to request kick irq: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, wdev);
dev_info(&pdev->dev, "ep_wdt_kick: watchdog-kick irq %d armed with IRQF_NO_SUSPEND\n",
wdev->irq);
return 0;
}
static const struct of_device_id ep_wdt_kick_ids[] = {
{ .compatible = "ep,wdt-kick-irq" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_wdt_kick_ids);
static struct platform_driver ep_wdt_kick_driver = {
.driver = {
.name = "ep_wdt_kick",
.of_match_table = ep_wdt_kick_ids,
},
.probe = ep_wdt_kick_probe,
};
module_platform_driver(ep_wdt_kick_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala example: IRQF_NO_SUSPEND watchdog-kick IRQ");
Two design choices here matter as much as the flag itself: the IRQ line is dedicated to this device (no IRQF_SHARED), and the handler does the absolute minimum — one register write and a counter increment, no locking that could contend with a suspended subsystem, no sleeping calls. That is exactly the discipline an IRQF_NO_SUSPEND handler needs, because it can run at points in the suspend/resume sequence where most kernel services are not in a normal, schedulable state.
Expected dmesg During Suspend and Resume
With ep_wdt_kick loaded, triggering a suspend-to-idle cycle (echo freeze > /sys/power/state) and watching dmesg -w shows the handler continuing to log kicks straight through the noirq window, unlike every other device’s interrupts which go silent:
[ 812.104112] PM: suspend entry (s2idle)
[ 812.108871] Filesystems sync: 0.004 seconds
[ 812.121340] Freezing user space processes
[ 812.124903] Freezing remaining freezable tasks
[ 812.129557] printk: Suspending console(s)
[ 812.140021] ep_wdt_kick ep-wdt-kick.0: entering suspend_late, no PM ops needed for kick irq
[ 812.151203] PM: suspend-to-idle: noirq phase, entering
[ 812.151890] ep_wdt_kick: kick #4113 (post-noirq, IRQF_NO_SUSPEND handler active)
[ 812.512004] ep_wdt_kick: kick #4114 (post-noirq, IRQF_NO_SUSPEND handler active)
[ 813.011998] ep_wdt_kick: kick #4115 (post-noirq, IRQF_NO_SUSPEND handler active)
[ ... ] (kicks continue every ~2s for the entire s2idle period)
[ 940.203441] ep_wdt_kick: kick #4177 (post-noirq, IRQF_NO_SUSPEND handler active)
[ 940.301120] PM: resume from suspend-to-idle, noirq phase exiting
[ 940.318774] printk: Resuming console(s)
[ 940.402215] PM: suspend exit
Notice there is no “disabling IRQ” or “re-enabling IRQ” message for ep-wdt-kick anywhere in that trail — because suspend_device_irqs() never touched the descriptor in the first place. That absence of any suspend/resume bookkeeping for this specific IRQ is the expected, correct signature of IRQF_NO_SUSPEND working as intended. You can confirm the flag took effect on a running system with:
# cat /proc/interrupts | grep ep-wdt-kick
47: 41402 0 GPIO 3 Level ep-wdt-kick
# grep -A2 "ep-wdt-kick" /proc/interrupts
Anti-Pattern: IRQF_SHARED + IRQF_NO_SUSPEND
Now the mistake. Suppose a second device, an ordinary I2C-based environmental sensor called ep_env_sensor, happens to share the same physical GPIO interrupt line as the watchdog-kick timer on this board (a plausible layout on a pin-constrained SoC). If the always-on driver author reaches for IRQF_SHARED so both drivers can register on that pin, the request looks innocent but is wrong:
/* ANTI-PATTERN — do not do this */
static int ep_wdt_kick_probe_BAD(struct platform_device *pdev)
{
struct ep_wdt_kick_dev *wdev;
int ret;
/* ... same setup as before ... */
/*
* WRONG: IRQF_SHARED combined with IRQF_NO_SUSPEND. This line is
* also used by ep_env_sensor, a regular I2C sensor that DOES get
* suspended (its I2C bus and regulator are powered off in its own
* ->suspend() callback).
*/
ret = devm_request_irq(&pdev->dev, wdev->irq, ep_wdt_kick_isr,
IRQF_SHARED | IRQF_NO_SUSPEND,
"ep-wdt-kick", wdev);
if (ret)
return ret;
return 0;
}
/* Meanwhile, ep_env_sensor registers its OWN handler on the same irq: */
static int ep_env_sensor_probe(struct platform_device *pdev)
{
struct ep_env_sensor_dev *sdev;
int ret;
/* ... */
ret = devm_request_irq(&pdev->dev, sdev->irq, ep_env_sensor_isr,
IRQF_SHARED, "ep-env-sensor", sdev);
return ret;
}
Because desc->no_suspend_depth is a property of the whole IRQ descriptor and not of one handler, the moment ep_wdt_kick‘s action sets IRQF_NO_SUSPEND on that shared line, suspend_device_irqs() skips the entire descriptor — including ep_env_sensor_isr, even though ep_env_sensor never asked for IRQF_NO_SUSPEND itself and its device is genuinely, correctly suspended. If the shared GPIO line toggles during the noirq window (which it will, since the watchdog-kick timer keeps pulling it every two seconds), ep_env_sensor_isr runs too, and tries to talk to an I2C bus and a regulator that ep_env_sensor‘s own ->suspend() callback has already powered down.
Expected dmesg With the Anti-Pattern (Failure Signature)
[ 812.151890] PM: suspend-to-idle: noirq phase, entering
[ 812.152011] ep_wdt_kick: kick #4113 (post-noirq, IRQF_NO_SUSPEND handler active)
[ 812.152034] ep_env_sensor ep-env-sensor.0: irq 47 fired on suspended device
[ 812.152201] i2c-ep-bus0: controller access attempted while runtime suspended
[ 812.152550] WARNING: CPU: 1 PID: 0 at drivers/i2c/i2c-core-base.c:1123 __i2c_transfer+0x1a4/0x220
[ 812.152901] ------------[ cut here ]------------
[ 812.153044] Call Trace:
[ 812.153050] ep_env_sensor_isr+0x2c/0x80 [ep_env_sensor]
[ 812.153061] __handle_irq_event_percpu+0x48/0x140
[ 812.153070] handle_irq_event+0x38/0x90
[ 813.011998] ep_wdt_kick: kick #4115 (post-noirq, IRQF_NO_SUSPEND handler active)
[ 813.012211] ep_env_sensor ep-env-sensor.0: irq 47 fired on suspended device
[ 813.012390] i2c-ep-bus0: controller access attempted while runtime suspended
Depending on the platform this either produces a warning splat like the one above (if the I2C core is defensive enough to catch the access), a silent bus timeout that adds seconds of delay to every remaining suspend cycle, or — if the regulator supplying the sensor was fully cut and the bus controller itself has no power at all — a hard hang that requires a cold reboot. All three outcomes trace back to the same root cause: one handler’s IRQF_NO_SUSPEND kept the whole shared line alive for a handler that was never supposed to run.
The fix is not a clever workaround, it is a structural one: give the always-on interrupt its own dedicated line if the hardware allows it, or, if sharing is genuinely unavoidable, redesign ep_env_sensor to meet the strict IRQF_COND_SUSPEND contract (distinguish spurious triggers from real events, call enable_irq_wake(), and report wakeup itself) — covered in the explanation lecture. Simply removing IRQF_SHARED and moving the two devices to separate physical IRQ lines is almost always the simpler and safer fix.
Common Mistakes and Troubleshooting
- Sharing a line without checking who else is on it. Before adding IRQF_NO_SUSPEND, grep the board’s device tree / ACPI tables and driver tree for every other consumer of that same physical IRQ.
- Reading “no suspend/resume log lines” as a bug. For a correctly flagged IRQF_NO_SUSPEND IRQ, the absence of disable/enable messages in dmesg is expected — the descriptor was never touched.
- Blocking calls inside the handler. Sleeping, taking mutexes that other suspending code might hold, or issuing I2C/SPI transactions from an IRQF_NO_SUSPEND hard-IRQ handler is asking for the exact kind of hang shown in the anti-pattern trace.
- Assuming devm_request_irq() behaves differently from request_irq() here. It does not — both accept the same flags word and install the same action; devm_ only adds automatic cleanup on driver detach.
- Forgetting to re-test after adding a second consumer to the same line. A driver can be correct in isolation and become the anti-pattern the moment a second device is wired to the same physical IRQ later in the board’s life.
Best Practices
- Keep an IRQF_NO_SUSPEND handler minimal: register writes and counters only, no bus transactions, no memory allocation, no blocking.
- Give always-on interrupts a dedicated, non-shared line whenever the board layout permits it — this makes the anti-pattern structurally impossible rather than merely “avoided by convention.”
- Document in the driver’s probe function, right next to the request_irq() call, exactly why IRQF_NO_SUSPEND is required — future maintainers (including yourself) will thank you.
- Verify behavior on real hardware with a full suspend/resume cycle and dmesg -w, not just code review — the failure mode in the anti-pattern only appears when the shared line actually toggles during noirq.
- If sharing is truly unavoidable, treat IRQF_COND_SUSPEND as a checklist, not a shortcut: spurious-vs-genuine detection, enable_irq_wake(), and pm_system_wakeup() are all mandatory together.
Summary and Key Takeaways
The ep_wdt_kick example shows the one legitimate shape of an IRQF_NO_SUSPEND user: a dedicated, non-shared, always-on interrupt line with a short, non-blocking handler and a clearly documented reason. Its dmesg signature during suspend is the total absence of any disable/enable bookkeeping for that IRQ, because suspend_device_irqs() skips it entirely. The anti-pattern — adding IRQF_SHARED alongside IRQF_NO_SUSPEND — breaks that guarantee for every other handler on the same physical line, including handlers belonging to devices that suspended correctly, and the resulting bus-access-on-suspended-device failures are exactly the class of bug the kernel’s own suspend-and-interrupts documentation warns about. This closes out the hands-on portion of our free Linux kernel development course power management series: combine Runtime PM, System Sleep dev_pm_ops, CPU Idle/CPUFreq/Thermal tuning, and precisely scoped wakeup/IRQF_NO_SUSPEND handling, and you have a driver that behaves correctly across the entire power lifecycle of the device.
Frequently Asked Questions
When is it justified to request an IRQ with IRQF_NO_SUSPEND in a real driver?
When the interrupt belongs to hardware that stays powered and must keep functioning specifically during suspend — timer/watchdog-kick style interrupts, IPIs, or a leaf handler behind a chained controller that must keep dispatching. If you cannot name the specific consequence of the handler not running during noirq, it does not need the flag.
How do I verify my IRQF_NO_SUSPEND handler is really still firing during suspend?
Add a rate-limited log line or counter increment inside the handler, trigger a suspend-to-idle cycle with echo freeze > /sys/power/state, and watch dmesg -w. You should see the handler’s output continue uninterrupted through the noirq phase, with no disable/enable messages for that IRQ.
Why does the anti-pattern example crash or hang the system?
Because IRQF_NO_SUSPEND applies to the whole shared IRQ descriptor. When the line fires during noirq, every handler on it runs, including the handler for a device that was correctly suspended and whose bus or regulator is now powered off, leading to bus timeouts, warning splats, or hard hangs.
Can devm_request_irq() be used with IRQF_NO_SUSPEND?
Yes. devm_request_irq() and request_irq() accept the same flags argument and install the same action on the interrupt descriptor; devm_ only adds automatic teardown when the driver detaches.
Should the handler registered with IRQF_NO_SUSPEND avoid sleeping/blocking calls?
Yes, strongly. The handler can run during parts of the suspend/resume sequence, such as around CPU hotplug for suspend, where blocking or contending for locks held by suspending code can stall the whole system’s suspend or resume.
What tool shows which IRQs are marked IRQF_NO_SUSPEND on a running system?
There is no single sysfs flag exposing this directly, but /proc/interrupts shows which IRQs remain active (their counters keep incrementing) during a suspend-to-idle cycle, which is the practical way to confirm the flag’s effect alongside dmesg and driver source inspection.
Power Management Series Complete — Onward to PCI Device Drivers
That’s the last hands-on example in the Linux Kernel Power Management series of our free Linux kernel development course. Next up: PCI device drivers — enumeration, BARs, and MSI/MSI-X interrupts, continuing this free embedded systems course into a whole new bus.
Start PCI Device Drivers Back to IRQF_NO_SUSPEND Explanation