Linux Sleep Callback Driver Example-Free Linux Device Drivers Course

Linux Sleep Callback Driver Example

Linux Sleep Callback Driver Example

Hands-on companion to the previous system sleep callbacks lecture: build and load ep_keypad_demo, an original platform driver wired with DEFINE_SIMPLE_DEV_PM_OPS, as part of this free Linux kernel development course.

1 Driver
ep_keypad_demo.c
DEFINE_SIMPLE_DEV_PM_OPS
Lecture 8 of 11

The previous lecture worked through the six Linux system sleep callbacks and the macros that populate struct dev_pm_ops with them. This lecture makes that concrete: you will build ep_keypad_demo, an original, minimal platform driver modeling a matrix keypad scan controller, whose entire teaching purpose is to make a single Linux system sleep callback pair, suspend and resume, visible as real dmesg output across an actual suspend/resume cycle. The driver keeps one small piece of configuration state, a scan-rate and debounce control value, that must survive being power-cycled and get reprogrammed back into hardware on the way up, exactly the kind of save/restore logic a real input controller driver would need.

What You Will Learn

Structuring a platform driver’s suspend/resume pair around DEFINE_SIMPLE_DEV_PM_OPS Wiring pm_sleep_ptr() into struct platform_driver’s .pm field correctly Saving and restoring a device configuration register across a real power cycle Triggering a suspend/resume cycle and reading the resulting dmesg sequence Recognizing where SIMPLE-style sleep callbacks sit relative to noirq and late phases

Prerequisites

  • Read the companion explanation lecture, “Linux System Sleep PM Callbacks”, first. This page assumes you already know what the six generic sleep callbacks are and what DEFINE_SIMPLE_DEV_PM_OPS actually expands to.
  • Root access on a Linux system or VM where you can build and load kernel modules and trigger a real suspend (a VM with suspend-to-idle support, or a physical board, both work).
  • Basic familiarity with the platform_driver structure (probe, remove, of_device_id) from earlier lectures in this series.

Meet ep_keypad_demo: A Suspend/Resume Callback Demo

About ep_keypad_demo

ep_keypad_demo is an original, deliberately minimal platform driver built for this course, modeling one control register of a fictional matrix keypad scan controller. It owns no real hardware, an original software field named scan_ctrl stands in for a register that packs a scan-rate value, a debounce value, and an IRQ-enable bit, so the entire example is about the timing and correctness of the Linux system sleep callback pair, not about any particular chip. It implements exactly one suspend/resume function pair, wired into struct dev_pm_ops using the current DEFINE_SIMPLE_DEV_PM_OPS() macro covered in the previous lecture, and logs a dev_info() line at every meaningful step so the full save/quiesce/restore/reprogram sequence is visible directly in dmesg. 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.

Full Driver Source: ep_keypad_demo.c

The suspend/resume pair and the DEFINE_SIMPLE_DEV_PM_OPS() macro that wires them into struct dev_pm_ops are highlighted in the walkthrough after the listing.

// SPDX-License-Identifier: GPL-2.0
/*
 * ep_keypad_demo.c - Minimal platform driver demonstrating the suspend/
 * resume Linux system sleep callback pair via DEFINE_SIMPLE_DEV_PM_OPS,
 * for EmbeddedPathashala's free Linux kernel development course.
 *
 * This is an original teaching example. ep_keypad_demo does not
 * correspond to any real shipping part number or vendor IP block.
 */

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/err.h>
#include <linux/bitops.h>

/* ---- Fictional control-register layout (software-only) ---------- */
#define EP_KEYPAD_SCAN_RATE_MS(x)   ((x) & 0xff)
#define EP_KEYPAD_DEBOUNCE_MS(x)    (((x) & 0xff) << 8)
#define EP_KEYPAD_IRQ_EN            BIT(16)

struct ep_keypad_demo {
	struct device *dev;
	u32 scan_ctrl; /* software-held config; hardware register is fictional */
};

/*
 * ep_keypad_program_hw() stands in for a real hardware write.  On real
 * silicon this would be a single regmap_write(kp->regmap,
 * EP_KEYPAD_REG_CTRL, kp->scan_ctrl) call.  ep_keypad_demo owns no
 * physical registers, so it only logs what would be written, which is
 * enough to observe the suspend/resume ordering in dmesg.
 */
static void ep_keypad_program_hw(struct ep_keypad_demo *kp)
{
	dev_info(kp->dev, "programming scan_ctrl=0x%08x into hardware\n",
		 kp->scan_ctrl);
}

/* ---- suspend / resume pair ---------------------------------------
 * Wired via DEFINE_SIMPLE_DEV_PM_OPS() below, so the same two
 * functions also cover .freeze/.thaw and .poweroff/.restore: correct
 * here because this driver's only job on any sleep path is "quiesce,
 * then reprogram from the software-held scan_ctrl value."
 */

static int ep_keypad_demo_suspend(struct device *dev)
{
	struct ep_keypad_demo *kp = dev_get_drvdata(dev);

	dev_info(dev, "suspend: scan_ctrl=0x%08x already held in software, no register read needed\n",
		 kp->scan_ctrl);
	dev_info(dev, "suspend: disabling keypad scanning, device now quiesced\n");
	return 0;
}

static int ep_keypad_demo_resume(struct device *dev)
{
	struct ep_keypad_demo *kp = dev_get_drvdata(dev);

	dev_info(dev, "resume: power restored, reprogramming controller from software state\n");
	ep_keypad_program_hw(kp);
	dev_info(dev, "resume: keypad scanning re-armed\n");
	return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(ep_keypad_demo_pm_ops,
				 ep_keypad_demo_suspend,
				 ep_keypad_demo_resume);

/* ---- probe() / remove() ----------------------------------------- */

static int ep_keypad_demo_probe(struct platform_device *pdev)
{
	struct ep_keypad_demo *kp;

	kp = devm_kzalloc(&pdev->dev, sizeof(*kp), GFP_KERNEL);
	if (!kp)
		return -ENOMEM;

	kp->dev = &pdev->dev;
	kp->scan_ctrl = EP_KEYPAD_SCAN_RATE_MS(20) |
			EP_KEYPAD_DEBOUNCE_MS(15) |
			EP_KEYPAD_IRQ_EN;
	platform_set_drvdata(pdev, kp);

	ep_keypad_program_hw(kp);
	dev_info(&pdev->dev, "ep_keypad_demo probed, scan_ctrl=0x%08x, ready to observe suspend/resume\n",
		 kp->scan_ctrl);
	return 0;
}

static void ep_keypad_demo_remove(struct platform_device *pdev)
{
	dev_info(&pdev->dev, "ep_keypad_demo removed\n");
}

static const struct of_device_id ep_keypad_demo_of_match[] = {
	{ .compatible = "ep,keypad-demo" },
	{ }
};
MODULE_DEVICE_TABLE(of, ep_keypad_demo_of_match);

static struct platform_driver ep_keypad_demo_driver = {
	.driver = {
		.name           = "ep_keypad_demo",
		.of_match_table = ep_keypad_demo_of_match,
		.pm             = pm_sleep_ptr(&ep_keypad_demo_pm_ops),
	},
	.probe  = ep_keypad_demo_probe,
	.remove = ep_keypad_demo_remove,
};

/*
 * No device tree entry is required on a bench system: this module
 * registers its own platform_device by name at load time, and unwinds
 * it cleanly on unload.
 */
static struct platform_device *ep_keypad_demo_pdev;

static int __init ep_keypad_demo_init(void)
{
	int ret;

	ret = platform_driver_register(&ep_keypad_demo_driver);
	if (ret)
		return ret;

	ep_keypad_demo_pdev = platform_device_register_simple("ep_keypad_demo", -1, NULL, 0);
	if (IS_ERR(ep_keypad_demo_pdev)) {
		platform_driver_unregister(&ep_keypad_demo_driver);
		return PTR_ERR(ep_keypad_demo_pdev);
	}

	return 0;
}
module_init(ep_keypad_demo_init);

static void __exit ep_keypad_demo_exit(void)
{
	platform_device_unregister(ep_keypad_demo_pdev);
	platform_driver_unregister(&ep_keypad_demo_driver);
}
module_exit(ep_keypad_demo_exit);

MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Minimal platform driver demonstrating DEFINE_SIMPLE_DEV_PM_OPS suspend/resume");
MODULE_LICENSE("GPL");

Two details in this listing are worth calling out explicitly, both direct consequences of the previous lecture’s macro research. First, .pm = pm_sleep_ptr(&ep_keypad_demo_pm_ops), not a bare pointer, this is what actually lets the entire ep_keypad_demo_pm_ops struct and its two functions be dropped from the kernel image on a CONFIG_PM_SLEEP=n build. Second, DEFINE_SIMPLE_DEV_PM_OPS() declares the const struct dev_pm_ops itself, so there is no separate struct declaration line the way there would be with the older SET_SYSTEM_SLEEP_PM_OPS() macro.

Where ep_keypad_demo Sits In The Suspend/Resume Sequence

prepare→runs for every device (ep_keypad_demo has no .prepare, so nothing logs here)
suspend→ep_keypad_demo_suspend() runs, logs scan_ctrl and quiesces
suspend_late, suspend_noirq→run for every device (DEFINE_SIMPLE_DEV_PM_OPS fills only the standard-phase fields, so nothing logs here)
[ system asleep / wakeup event ]
resume_noirq, resume_early→run for every device (nothing logs here, same reason)
resume→ep_keypad_demo_resume() runs, reprograms hardware, logs restored state
complete→runs for every device (ep_keypad_demo has no .complete, so nothing logs here)

Because DEFINE_SIMPLE_DEV_PM_OPS() also fills .freeze/.thaw and .poweroff/.restore with the same two functions, hibernating this exact driver would print the identical suspend log line during both the freeze and poweroff sub-transitions, and the identical resume log line during restore, without any additional code. That is the entire value of the SIMPLE macro family: one correct pair of functions, three sleep paths covered.

Build And Test Walkthrough

Save the listing above as ep_keypad_demo.c alongside this Makefile in an empty directory:

obj-m += ep_keypad_demo.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 against your running kernel’s headers:

$ make
  CC [M]  ep_keypad_demo.o
  MODPOST ep_keypad_demo.mod.c
  CC [M]  ep_keypad_demo.mod.o
  LD [M]  ep_keypad_demo.ko

Load it. Because the module registers its own platform_device internally, probe() runs immediately with no device tree entry needed:

$ sudo insmod ep_keypad_demo.ko
$ dmesg | tail -n 2
[  212.301004] ep_keypad_demo ep_keypad_demo.0: programming scan_ctrl=0x000116f4 into hardware
[  212.301011] ep_keypad_demo ep_keypad_demo.0: ep_keypad_demo probed, scan_ctrl=0x000116f4, ready to observe suspend/resume

Trigger a real suspend/resume cycle (suspend-to-idle is the safest to test inside a VM; use mem instead of freeze on hardware that supports suspend-to-RAM):

$ cat /sys/power/state
freeze mem
$ echo freeze | sudo tee /sys/power/state

Reading dmesg after the system wakes back up (a timed wakeup, a key press, or any configured wakeup source will bring it back) shows ep_keypad_demo’s four log lines sitting exactly where the .suspend and .resume phases belong in the callback chain, wrapped by the higher-level suspend stages:

$ dmesg | tail -n 20
[  260.010102] PM: suspend entry (s2idle)
[  260.030044] Filesystems sync: 0.010 seconds
[  260.032511] Freezing user space processes
[  260.033602] Freezing user space processes completed (elapsed 0.001 seconds)
[  260.033610] Freezing remaining freezable tasks
[  260.034201] Freezing remaining freezable tasks completed (elapsed 0.001 seconds)
[  260.036221] ep_keypad_demo ep_keypad_demo.0: suspend: scan_ctrl=0x000116f4 already held in software, no register read needed
[  260.036229] ep_keypad_demo ep_keypad_demo.0: suspend: disabling keypad scanning, device now quiesced
[  260.040233] printk: Suspending console(s) (use no_console_suspend to debug)
[  260.098871] ACPI: EC: interrupt blocked
[  260.130511] Disabling non-boot CPUs ...
[  260.145102] Successfully transitioned to state s2idle
[  260.160044] Timekeeping suspended for 5.001 seconds
[  260.170022] Enabling non-boot CPUs ...
[  260.210044] printk: Resuming console(s)
[  260.220071] ep_keypad_demo ep_keypad_demo.0: resume: power restored, reprogramming controller from software state
[  260.220078] ep_keypad_demo ep_keypad_demo.0: programming scan_ctrl=0x000116f4 into hardware
[  260.220084] ep_keypad_demo ep_keypad_demo.0: resume: keypad scanning re-armed
[  260.230011] OOM killer enabled.
[  260.230014] Restarting tasks ... done.
[  260.240009] PM: suspend exit

Notice that ep_keypad_demo’s suspend lines appear right after task freezing completes but before console suspension, in the plain .suspend phase, not suspend_late or suspend_noirq, and its resume lines appear after console resume but before task restart, the plain .resume phase. This is the correct location for a driver wired with DEFINE_SIMPLE_DEV_PM_OPS(), which only populates the standard-phase fields.

Unload the module and confirm clean teardown:

$ sudo rmmod ep_keypad_demo
$ dmesg | tail -n 1
[  310.004112] ep_keypad_demo ep_keypad_demo.0: ep_keypad_demo removed

Common Mistakes And Troubleshooting

  • ep_keypad_demo’s dev_info() lines never show up in dmesg after suspend. Check the module actually loaded (lsmod | grep ep_keypad_demo) and that /sys/power/state accepts the state you wrote; some virtualized environments only support “freeze” (suspend-to-idle), not “mem”.
  • Forgetting pm_sleep_ptr() and assigning .pm = &ep_keypad_demo_pm_ops directly. It will still work functionally on a CONFIG_PM_SLEEP=y kernel, but defeats the entire purpose of DEFINE_SIMPLE_DEV_PM_OPS on a CONFIG_PM_SLEEP=n build, since the compiler can no longer prove the struct is unreachable and strip it.
  • Expecting suspend_late or suspend_noirq log lines from this driver. DEFINE_SIMPLE_DEV_PM_OPS() only fills the standard-phase and hibernation-phase fields, never the _late or _noirq ones; that would require SET_LATE_SYSTEM_SLEEP_PM_OPS() or SET_NOIRQ_SYSTEM_SLEEP_PM_OPS() instead, covered in the previous lecture’s decision table.
  • Assuming scan_ctrl needs to be re-read from hardware during suspend(). Because the software copy is the only copy that matters here (the fictional hardware never diverges from what was last written), there is nothing to read back; a real device with hardware-autonomous state changes might genuinely need a register read at this point.
  • Confusing pm_ptr() with pm_sleep_ptr(). pm_ptr() gates on CONFIG_PM and is meant for structs mixing runtime and sleep ops; pm_sleep_ptr(), used here, gates specifically on CONFIG_PM_SLEEP and is correct for a sleep-only struct like ep_keypad_demo_pm_ops.

Best Practices

  • Use DEFINE_SIMPLE_DEV_PM_OPS() plus pm_sleep_ptr() together, as shown here, rather than mixing the old SET_SYSTEM_SLEEP_PM_OPS() struct-declaration style with the new assignment style.
  • Log a dev_info() line at the start and end of every Linux system sleep callback during bring-up on new hardware; it is the fastest way to confirm the suspend/resume sequence is hitting the phase you expect, exactly as ep_keypad_demo does here.
  • Keep configuration state that must survive a sleep cycle in a plain software field on your driver’s private struct, and treat “reprogram hardware from that field” as the single source of truth on resume, rather than trusting any register to have retained its value.
  • Prefer testing suspend/resume flows with suspend-to-idle (freeze) in a VM first; it exercises the full device-level callback chain without requiring real platform firmware support for deeper sleep states.
  • Only reach for SET_LATE_SYSTEM_SLEEP_PM_OPS() or SET_NOIRQ_SYSTEM_SLEEP_PM_OPS() once you have a concrete reason (a genuinely shared IRQ line, or logic that must run after runtime PM is disabled); DEFINE_SIMPLE_DEV_PM_OPS() is the right default otherwise.

Summary And Key Takeaways

This lecture turned the previous lecture’s conceptual coverage of the Linux system sleep callbacks into something observable: ep_keypad_demo, an original driver wired with the current DEFINE_SIMPLE_DEV_PM_OPS() macro and pm_sleep_ptr(), makes exactly one suspend/resume pair show up as real dmesg lines at precisely the phase the previous lecture’s diagrams predicted, with a software-held scan_ctrl value standing in for real register state that must survive a power cycle and get reprogrammed on the way back up. The next lecture in this free Linux kernel development course continues the power management series with wakeup sources, how a device requests that the system leave a sleep state it is currently in.

Frequently Asked Questions

Why does ep_keypad_demo only implement suspend/resume and not the noirq or late phases?

To isolate exactly the two standard-phase Linux system sleep callbacks that DEFINE_SIMPLE_DEV_PM_OPS() actually populates, so their position in dmesg is unambiguous. A real keypad controller sharing an interrupt line with other devices might additionally need suspend_noirq/resume_noirq.

Why register the platform_device from inside the module instead of using a device tree entry?

So the example works on any bench Linux system or VM without needing to modify and reflash a device tree blob. On real hardware, the .of_match_table entry (compatible = “ep,keypad-demo”) would bind automatically to a matching devicetree node instead.

What does DEFINE_SIMPLE_DEV_PM_OPS() actually expand to?

It declares a const struct dev_pm_ops with the given name and fills .suspend/.resume plus .freeze/.thaw and .poweroff/.restore, all from the same two functions, using pm_sleep_ptr() internally so CONFIG_PM_SLEEP=n builds compile the references away cleanly.

Would this driver behave any differently during hibernation than during suspend-to-idle?

Not with the current code, since DEFINE_SIMPLE_DEV_PM_OPS() points .freeze/.poweroff at the same suspend function and .thaw/.restore at the same resume function. A driver whose hardware genuinely needs different behavior for hibernation would need to fill those fields separately instead of using the SIMPLE macro.

What happens if ep_keypad_demo_suspend() returned a nonzero error code?

The PM core would abort the suspend transition and unwind by resuming the devices that were already suspended, the same way an error from any other suspend-phase callback does.

Continue Your Linux Kernel Power Management Journey

You have now watched a full Linux system sleep callback cycle happen in real dmesg output, backed by an original driver you built yourself, as part of this free Linux kernel development course. Continue to the next lecture in the Kernel Power Management series to cover wakeup sources.

Continue The Power Management Series Back To Course Index

Leave a Reply

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