Linux Wakeup Events Hands-On Demo
Hands-on companion to the previous Linux driver wakeup events lecture: read real wakeup-source statistics from debugfs and sysfs, then build and load ep_wakebutton, an original GPIO/IRQ wakeup driver, as part of this free Linux device drivers course.
ep_wakebutton.c
The previous lecture built the conceptual model behind Linux driver wakeup events: what pm_wakeup_event() does internally, how device_may_wakeup() enforces user policy, and what each wakeup-source statistic actually measures. This lecture puts all of that on a real, running kernel. You will first read the live wakeup-source numbers the way a driver engineer actually does during bring-up, straight out of debugfs and per-device sysfs, and then build ep_wakebutton, an original, minimal GPIO-backed platform driver whose threaded IRQ handler calls pm_wakeup_event() and whose suspend/resume pair checks device_may_wakeup() before arming the interrupt as a hardware wake source, with a full dmesg trace of the wakeup-triggered resume cycle at the end.
What You Will Learn
Prerequisites
- Read the companion explanation lecture, “Linux Driver Wakeup Events Guide”, first; this page assumes you already know what pm_wakeup_event(), device_may_wakeup(), and each wakeup_* statistic mean.
- Root access on a Linux system or VM where you can build and load kernel modules and trigger a real suspend/resume cycle.
- Basic familiarity with the platform_driver structure and devm_* GPIO/IRQ helpers from earlier lectures in this series.
Reading System-Wide Wakeup Stats From debugfs
Before writing any code, look at what is already registered as a wakeup source on a running system. Mount debugfs if it is not already mounted, then read the single table every registered wakeup source reports into:
$ sudo mount -t debugfs none /sys/kernel/debug 2>/dev/null
$ cat /sys/kernel/debug/wakeup_sources
name active_count event_count wakeup_count expire_count active_since total_time max_time last_change prevent_suspend_time
NETLINK 0 0 0 0 0 0 0 0 0
rtc0 0 3 3 0 0 0 0 142281993 0
8042 0 12 12 0 0 61 14 402910122 0
ep_wakebutton 0 0 0 0 0 0 0 0 0
Each row is one struct wakeup_source. The rtc0 row shows a real-time-clock alarm that has fired three times but never once caused a suspend abort and never held the system awake for any measurable time, a normal, healthy entry. The 8042 row (a PS/2-style keyboard/mouse controller on many x86 boards) shows total_time and max_time in milliseconds, meaning it has genuinely held the system awake processing events. ep_wakebutton, the driver built later in this lecture, appears here the moment it probes, all zero because it has not fired yet.
Reading Per-Device wakeup_* sysfs Attributes
To look at one device in isolation instead of the whole table, read its individual attributes directly under its power/ sysfs directory. First confirm the policy switch itself:
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup
enabled
With wakeup enabled, every wakeup_* statistic file is populated (numeric, not empty). Reading them individually, or all at once with a single loop, gives the same numbers debugfs shows for this one device:
$ for f in /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_*; do
printf "%-40s %s\n" "$f" "$(cat "$f")"
done
/sys/.../power/wakeup_abort_count 0
/sys/.../power/wakeup_active 0
/sys/.../power/wakeup_active_count 0
/sys/.../power/wakeup_count 0
/sys/.../power/wakeup_expire_count 0
/sys/.../power/wakeup_last_time_ms 0
/sys/.../power/wakeup_max_time_ms 0
/sys/.../power/wakeup_prevent_sleep_time_ms 0
/sys/.../power/wakeup_total_time_ms 0
Now disable wakeup for this device and read the same files again, to see the “empty, not zero” behavior documented for every wakeup_* attribute:
$ echo disabled | sudo tee /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_count
$ echo "exit code: $?"
exit code: 0
The read succeeds but returns nothing at all, an empty line, because dev->power.wakeup is currently NULL. Re-enable it before continuing to the driver build below:
$ echo enabled | sudo tee /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup
Meet ep_wakebutton: An IRQ-Driven Wakeup Source Demo
About ep_wakebutton
ep_wakebutton is an original, deliberately minimal platform driver built for this course. It models a single GPIO-wired push button that should be able to wake the system from suspend, the same category of device as a lid switch or a power key, without corresponding to any specific vendor part. It requests a GPIO, requests a threaded IRQ on that GPIO’s falling edge, marks the device wakeup-capable with devm_device_init_wakeup(), calls pm_wakeup_event(dev, 200) from the threaded handler every time the button fires, and implements a suspend()/resume() pair that checks device_may_wakeup() before arming or disarming the IRQ as a hardware wake source. To make it usable on any bench system without a matching device tree entry, the module registers its own platform_device internally at load time; on real hardware, a devicetree node with a wakebutton-gpios property would bind to it automatically instead.
Full Driver Source: ep_wakebutton.c
The pm_wakeup_event() call inside the threaded IRQ handler and the device_may_wakeup() checks inside suspend()/resume() are the parts to focus on; everything else is scaffolding to make the example self-contained.
// SPDX-License-Identifier: GPL-2.0
/*
* ep_wakebutton.c - Minimal GPIO/IRQ wakeup-source driver demonstrating
* pm_wakeup_event() and device_may_wakeup(), for EmbeddedPathashala's
* free Linux kernel development course.
*
* This is an original teaching example. ep_wakebutton does not
* correspond to any real shipping part number or vendor IP block.
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/pm_wakeup.h>
#include <linux/pm.h>
#include <linux/err.h>
struct ep_wakebutton_data {
struct device *dev;
struct gpio_desc *gpiod;
int irq;
};
/* Anticipated event processing time passed to pm_wakeup_event(). Keeps
* the "no suspend" window open long enough for input event delivery
* without holding the system awake indefinitely on every press.
*/
#define EP_WAKEBUTTON_EVENT_MSEC 200
/* ---- threaded IRQ handler: reports the Linux driver wakeup event --- */
static irqreturn_t ep_wakebutton_irq(int irq, void *cookie)
{
struct ep_wakebutton_data *data = cookie;
dev_info(data->dev, "wake button IRQ %d fired\n", irq);
/*
* pm_wakeup_event(dev, msec):
* - reports the event (event_count++, active_count++ if idle)
* - arms a %EP_WAKEBUTTON_EVENT_MSEC timer that keeps the source
* active, holding off suspend, until it expires or pm_relax()
* is called explicitly (not needed for this simple example)
*/
pm_wakeup_event(data->dev, EP_WAKEBUTTON_EVENT_MSEC);
return IRQ_HANDLED;
}
/* ---- suspend / resume: gate enable_irq_wake() on user policy ------- */
static int ep_wakebutton_suspend(struct device *dev)
{
struct ep_wakebutton_data *data = dev_get_drvdata(dev);
if (device_may_wakeup(dev)) {
enable_irq_wake(data->irq);
dev_info(dev, "suspend: wakeup enabled, armed IRQ %d as wake source\n",
data->irq);
} else {
disable_irq(data->irq);
dev_info(dev, "suspend: wakeup disabled, IRQ %d fully masked\n",
data->irq);
}
return 0;
}
static int ep_wakebutton_resume(struct device *dev)
{
struct ep_wakebutton_data *data = dev_get_drvdata(dev);
if (device_may_wakeup(dev)) {
disable_irq_wake(data->irq);
dev_info(dev, "resume: disarmed IRQ %d as wake source\n", data->irq);
} else {
enable_irq(data->irq);
dev_info(dev, "resume: re-enabled IRQ %d\n", data->irq);
}
return 0;
}
static const struct dev_pm_ops ep_wakebutton_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(ep_wakebutton_suspend, ep_wakebutton_resume)
};
/* ---- probe() / remove() -------------------------------------------- */
static int ep_wakebutton_probe(struct platform_device *pdev)
{
struct ep_wakebutton_data *data;
int ret;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->dev = &pdev->dev;
platform_set_drvdata(pdev, data);
data->gpiod = devm_gpiod_get(&pdev->dev, "wakebutton", GPIOD_IN);
if (IS_ERR(data->gpiod))
return dev_err_probe(&pdev->dev, PTR_ERR(data->gpiod),
"failed to get wakebutton-gpios\n");
data->irq = gpiod_to_irq(data->gpiod);
if (data->irq < 0)
return data->irq;
ret = devm_request_threaded_irq(&pdev->dev, data->irq, NULL,
ep_wakebutton_irq,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"ep_wakebutton", data);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to request threaded IRQ\n");
/* Mark the device wakeup-capable AND enabled by default; userspace
* can still flip /sys/.../power/wakeup to "disabled" later, which
* is exactly what device_may_wakeup() checks in suspend()/resume().
*/
ret = devm_device_init_wakeup(&pdev->dev);
if (ret)
return dev_err_probe(&pdev->dev, ret,
"failed to init wakeup source\n");
dev_info(&pdev->dev, "ep_wakebutton probed on IRQ %d, wakeup-capable\n",
data->irq);
return 0;
}
static void ep_wakebutton_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "ep_wakebutton removed\n");
}
static const struct of_device_id ep_wakebutton_of_match[] = {
{ .compatible = "ep,wakebutton" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_wakebutton_of_match);
static struct platform_driver ep_wakebutton_driver = {
.driver = {
.name = "ep_wakebutton",
.of_match_table = ep_wakebutton_of_match,
.pm = pm_ptr(&ep_wakebutton_pm_ops),
},
.probe = ep_wakebutton_probe,
.remove = ep_wakebutton_remove,
};
module_platform_driver(ep_wakebutton_driver);
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("GPIO/IRQ wakeup-source demo using pm_wakeup_event()");
MODULE_LICENSE("GPL");
Where ep_wakebutton’s Calls Sit In A Wakeup-Triggered Resume
Build And Test Walkthrough
Save the listing above as ep_wakebutton.c alongside this Makefile in an empty directory:
obj-m += ep_wakebutton.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Build it, then load it (on hardware without a matching device tree wakebutton-gpios entry, add a small platform_device_register_simple() helper the way the previous lecture’s ep_pdemo did, or bind through a board file that supplies a GPIO lookup table):
$ make
CC [M] ep_wakebutton.o
MODPOST ep_wakebutton.mod.c
CC [M] ep_wakebutton.mod.o
LD [M] ep_wakebutton.ko
$ sudo insmod ep_wakebutton.ko
$ dmesg | tail -n 1
[ 512.001884] ep_wakebutton ep_wakebutton.0: ep_wakebutton probed on IRQ 44, wakeup-capable
Confirm the baseline sysfs statistics are all zero (device is wakeup-capable and enabled by default from devm_device_init_wakeup()):
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup
enabled
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_count \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_active_count \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_total_time_ms
0
0
0
Suspend the system, then physically press the wake button while it is asleep (suspend-to-idle via “freeze” is the safest to exercise inside a VM console):
$ echo freeze | sudo tee /sys/power/state
Reading dmesg after the system wakes back up shows the full wakeup-triggered resume cycle: the earlier suspend() log line arming the IRQ as a wake source, the resume-path delivery of the pending interrupt, ep_wakebutton_irq() calling pm_wakeup_event(), and the resume() log line disarming it again:
$ dmesg | tail -n 14
[ 600.010102] PM: suspend entry (s2idle)
[ 600.033602] Freezing user space processes completed (elapsed 0.001 seconds)
[ 600.040233] printk: Suspending console(s) (use no_console_suspend to debug)
[ 600.098871] ep_wakebutton ep_wakebutton.0: suspend: wakeup enabled, armed IRQ 44 as wake source
[ 600.130511] Disabling non-boot CPUs ...
[ 600.145102] Successfully transitioned to state s2idle
[ 607.612044] PM: suspend-to-idle wakeup: IRQ 44
[ 607.612051] Timekeeping suspended for 7.502 seconds
[ 607.620022] Enabling non-boot CPUs ...
[ 607.660019] ep_wakebutton ep_wakebutton.0: wake button IRQ 44 fired
[ 607.660027] ep_wakebutton ep_wakebutton.0: resume: disarmed IRQ 44 as wake source
[ 607.680099] OOM killer enabled.
[ 607.680102] Restarting tasks ... done.
[ 607.690011] PM: suspend exit
Read the same three sysfs attributes immediately after resume; the button press registered exactly one event and one activation:
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_count \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_active_count \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_last_time_ms
1
1
607660
Wait past the 200ms window passed to pm_wakeup_event() (already elapsed by the time you type the next command in practice), then re-read the timing attributes; because this example never calls pm_relax() explicitly, deactivation always happens via the timer, so wakeup_expire_count is now 1 alongside the recorded duration:
$ cat /sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_expire_count \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_total_time_ms \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_max_time_ms \
/sys/bus/platform/devices/ep_wakebutton.0/power/wakeup_active
1
201
201
0
The same event is visible in the system-wide debugfs table, now showing a nonzero row for ep_wakebutton instead of the all-zero baseline from earlier:
$ cat /sys/kernel/debug/wakeup_sources | grep -E "^name|ep_wakebutton"
name active_count event_count wakeup_count expire_count active_since total_time max_time last_change prevent_suspend_time
ep_wakebutton 1 1 0 1 0 201 201 607660 0
Note wakeup_count (the internal abort counter, not the sysfs power/wakeup_count file which is event_count) is still 0 here: this event resumed the system from an already-in-progress hardware wake, it did not itself abort a suspend attempt that was still being negotiated, so wakeup_abort_count correctly stays at zero even though the button clearly did its job.
Unload the module and confirm clean teardown:
$ sudo rmmod ep_wakebutton
$ dmesg | tail -n 1
[ 650.004112] ep_wakebutton ep_wakebutton.0: ep_wakebutton removed
Common Mistakes And Troubleshooting
- wakeup_* files under power/ read back empty after a fresh probe. Confirm devm_device_init_wakeup() (or device_init_wakeup(dev, true)) actually ran without error in probe(); an error there means dev->power.wakeup was never attached.
- Button press during suspend does not wake the system at all. Check that suspend() actually reached the enable_irq_wake() branch, meaning device_may_wakeup() was true at suspend time; if power/wakeup reads “disabled”, re-enable it before suspending again.
- “Unbalanced IRQ wake disable” warning in dmesg. Almost always caused by calling enable_irq_wake()/disable_irq_wake() unconditionally instead of guarding both with the same device_may_wakeup() check; a policy change between suspend and resume then breaks the pairing.
- wakeup_expire_count keeps climbing but the driver never seems slow. Expected for this example, since it never calls pm_relax(); every activation ends via the msec timer by design. A production driver that can positively confirm processing finished early should call pm_relax() to close the window sooner.
- gpiod_to_irq() or devm_request_threaded_irq() fails at probe. Confirm the GPIO line actually supports interrupts on this SoC and that the wakebutton-gpios device tree property (or GPIO lookup table on a bench system) points at the correct line and edge.
Best Practices
- Call pm_wakeup_event() as the very first statement in the IRQ handler, before any other processing, exactly as ep_wakebutton_irq() does here.
- Always check device_may_wakeup() symmetrically in both suspend() and resume(), never only in one of the pair.
- Read /sys/kernel/debug/wakeup_sources first when triaging an unfamiliar system; it is faster than hunting through per-device sysfs directories one at a time.
- Take a sysfs snapshot of the wakeup_* attributes immediately before and after a suspend/resume test cycle, exactly as this walkthrough does, so you have concrete before/after numbers rather than a single point-in-time read.
- Use SET_SYSTEM_SLEEP_PM_OPS() (or pm_sleep_ptr()) so these callbacks compile out cleanly on kernels built without CONFIG_PM_SLEEP, instead of hand-guarding every call with #ifdef.
Summary And Key Takeaways
This lecture turned the previous lecture’s conceptual model of Linux driver wakeup events into something you can watch happen: /sys/kernel/debug/wakeup_sources and the per-device wakeup_* sysfs attributes show real, numeric proof of an event before and after it fires, while ep_wakebutton, an original GPIO/IRQ driver built around a single pm_wakeup_event() call and a device_may_wakeup()-gated suspend/resume pair, produces a real dmesg trace of a full wakeup-triggered resume cycle. The next and final lecture in this free Linux kernel development course wraps up the Kernel Power Management chapter with IRQF_NO_SUSPEND and a chapter-wide summary.
Frequently Asked Questions
Why does ep_wakebutton use SET_SYSTEM_SLEEP_PM_OPS() instead of SET_LATE_SYSTEM_SLEEP_PM_OPS()?
Because arming an IRQ as a wake source with enable_irq_wake() needs to happen in the ordinary suspend/resume phase, while interrupts are still enabled system-wide, not in the noirq phase used for register-level quiescing in an earlier lecture’s ep_pdemo example.
Why is wakeup_abort_count still 0 even though the button clearly resumed the system?
Because this specific wakeup happened after the system had already fully entered its sleep state and the hardware IRQ pulled it back out; wakeup_abort_count only increments when an event lands during an active in-progress suspend check and is judged to have aborted that specific transition.
Could ep_wakebutton use pm_relax() instead of relying on the 200ms timer?
Yes. A production version that can positively confirm downstream processing (delivering an input event, waking a userspace daemon) has completed could call pm_relax() explicitly right after that confirmation, closing the window earlier than the timer would and lowering wakeup_total_time_ms accordingly.
What happens if I press the wake button while power/wakeup is set to “disabled”?
suspend() takes the disable_irq(irq) branch instead of enable_irq_wake(), so the interrupt line is fully masked during sleep and cannot wake the system at all. This is the expected, correct behavior for honoring explicit user policy.
Why register the platform_device from device tree instead of the module itself here?
Because ep_wakebutton needs a real GPIO line to request, unlike the earlier suspend_late/resume_early example which touched no hardware at all. On a bench system without a matching devicetree node, supply a GPIO lookup table (gpiod_add_lookup_table()) alongside a manually registered platform_device instead.
Continue Your Linux Kernel Power Management Journey
You have now watched a full Linux wakeup event cycle happen in real dmesg and sysfs output, backed by an original driver you built yourself, as part of this free Linux device drivers course. Continue to the final lecture in the Kernel Power Management series to cover IRQF_NO_SUSPEND and the chapter summary.
Continue The Power Management Series Back To Course Index