Power Domains and genpd
Grouping devices under shared power rails: the generic power domain framework
The problem: shared power rails
On a desktop or server CPU, each core can individually gate its power. On an embedded SoC it is rarely that simple. A typical mobile SoC might power the GPU block, the display controller, and the memory interface for that display all from a single PMIC rail. Cutting that rail to save power requires that every device on it be quiescent first. If even one device is still doing DMA, powering off the rail corrupts data.
SoC power topology (simplified)
VDD_GPU rail
├── GPU core
├── GPU MMU
└── GPU L2 cache
VDD_DISPLAY rail
├── Display controller (DPU)
├── DSI host
└── MIPI PHY
Each rail: single switch, all devices must be idle before it can be cut.
Before genpd, each SoC vendor duplicated logic to track device idleness and sequence rail power-off. The Generic Power Domain (genpd) framework, introduced in Linux 3.1 (2011), provides that infrastructure once and for all.
struct generic_pm_domain
The central structure (include/linux/pm_domain.h):
/* include/linux/pm_domain.h — abridged; the real struct has ~30 fields */
struct generic_pm_domain {
struct device dev; /* domain's own struct device */
struct dev_pm_domain domain; /* embedded; exposes dev_pm_ops */
struct list_head gpd_list_node;/* link in global gpd_list */
const char *name;
enum gpd_status status; /* GENPD_STATE_ON or GENPD_STATE_OFF */
unsigned int device_count;
unsigned int device_id; /* unique device id */
unsigned int suspended_count; /* system-suspend device counter, NOT runtime PM */
unsigned int prepared_count;
unsigned int performance_state; /* aggregated max performance state */
struct dev_power_governor *gov; /* NULL if no governor was registered */
/* callbacks */
int (*power_off)(struct generic_pm_domain *domain);
int (*power_on) (struct generic_pm_domain *domain);
struct list_head dev_list; /* struct generic_pm_domain_data per dev */
struct list_head parent_links; /* links where this domain is the parent (i.e. its subdomains) */
struct list_head child_links; /* links where this domain is the child (i.e. its parent domains) */
/* governor timing data lives in genpd->gd (struct genpd_governor_data) */
};
status reflects whether the rail is currently powered:
| Value | Meaning |
|---|---|
GENPD_STATE_ON |
Rail on; at least one device is active |
GENPD_STATE_OFF |
Rail off; all devices are runtime-suspended |
Registering a power domain
/* platform code or SoC driver */
#include <linux/pm_domain.h>
static struct generic_pm_domain gpu_domain = {
.name = "gpu-domain",
.power_off = gpu_domain_power_off, /* cut the VDD_GPU rail */
.power_on = gpu_domain_power_on, /* assert the VDD_GPU rail */
};
static int soc_pm_init(void)
{
/* Register with genpd core; third argument: is the domain initially off?
* NULL for the governor argument means NO governor at all, not "use
* the default governor" — genpd_power_off() then skips power_down_ok()
* entirely and always falls back to the shallowest power state. Pass
* &simple_qos_governor explicitly to get the QoS-aware governor. */
pm_genpd_init(&gpu_domain, &simple_qos_governor, false);
return 0;
}
pm_genpd_init() links the domain into the global gpd_list and populates genpd->domain.ops (a struct dev_pm_ops) with genpd's own runtime-PM and system-sleep callbacks (genpd_runtime_suspend(), genpd_runtime_resume(), genpd_suspend_noirq(), and so on). Every device later added to this domain gets dev->pm_domain pointed at &genpd->domain via dev_pm_domain_set(), so those callbacks run instead of the device's bus-level ones.
Adding devices to a domain
/* Called during device probe, typically from OF glue or platform code */
int ret = pm_genpd_add_device(&gpu_domain, &gpu_dev->dev);
if (ret)
dev_err(&gpu_dev->dev, "failed to add to power domain: %d\n", ret);
After this call the Runtime PM lifecycle of gpu_dev is managed by the domain. When gpu_dev calls pm_runtime_put(), the genpd layer intercepts runtime_suspend and checks whether this was the last active device. If so, it invokes gpu_domain.power_off().
On ARM SoCs using Device Tree, the binding power-domains = <&pd_gpu> in the DTS node causes the driver core to call dev_pm_domain_attach() during probe, which in turn calls genpd_dev_pm_attach() to parse the phandle and perform the attachment automatically — no explicit driver call is needed. (of_genpd_add_device() is a separate, lower-level API for platform code that already has an of_phandle_args and wants to add a device manually.) The &pd_gpu phandle refers to a power domain provider registered with of_genpd_add_provider_simple() or of_genpd_add_provider_onecell().
Domain dependencies
Some rails have dependencies — a sub-rail cannot be on unless its parent rail is also on. genpd models this with subdomain links:
/*
* Declare: "display_dsi_domain" cannot be powered unless
* "display_core_domain" is already powered.
*/
ret = pm_genpd_add_subdomain(&display_core_domain, &display_dsi_domain);
Internally each domain's parent_links list holds the links to its own subdomains (the domains it is parent of), while child_links holds the links to the domains it is itself a subdomain of. During power-on, the parent is powered first; during power-off, the child is powered off first.
Dependency graph:
display_core_domain (parent)
└── display_dsi_domain (child)
└── mipi_phy_domain (child of child)
Power-on order: display_core → display_dsi → mipi_phy
Power-off order: mipi_phy → display_dsi → display_core
Runtime PM integration
genpd integrates transparently with Runtime PM. Note that suspended_count
is a system-suspend counter (see its comment in struct
generic_pm_domain above) — it plays no part in this runtime-PM decision.
The real check, in genpd_power_off(), walks dev_list and asks Runtime
PM's own per-device status:
pm_runtime_put(dev) [device driver]
│
▼
genpd_runtime_suspend() [genpd framework, registered as the
│ device's dev_pm_ops.runtime_suspend]
▼
genpd_power_off(domain)
for each dev in domain->dev_list:
if (!pm_runtime_suspended(dev))
not_suspended++
if (not_suspended > 1 || (not_suspended == 1 && !one_dev_on))
return /* still devices active — stay on */
if (genpd->gov && genpd->gov->power_down_ok)
if (!power_down_ok()) return /* governor vetoes the power-off */
if (!genpd->gov)
genpd->state_idx = 0 /* no governor: hardcode shallowest state */
domain->power_off() [platform callback → cuts rail]
On resume the direction reverses:
pm_runtime_get_sync(dev) [device driver]
│
▼
genpd runtime_resume hook
if (domain->status == GENPD_STATE_OFF)
domain->power_on() [platform callback → asserts rail]
wait for power_on to settle (gpd_timing_data latency)
dev->runtime_resume() [device driver callback]
struct gpd_timing_data
Each device can carry timing metadata used by the genpd governor to decide whether it is worth powering the domain off for a predicted idle period:
/* include/linux/pm_domain.h */
struct gpd_timing_data {
s64 suspend_latency_ns; /* time from last device suspend to rail off */
s64 resume_latency_ns; /* time from power_on() call to device ready */
s64 effective_constraint_ns;
ktime_t next_wakeup;
bool constraint_changed;
bool cached_suspend_ok;
};
The simple_qos_governor's actual comparison (in __default_power_down_ok(), drivers/pmdomain/governor.c) is not this per-device timing sum against a predicted idle time. Instead, for the domain power state being considered it computes off_on_time_ns = state.power_off_latency_ns + state.power_on_latency_ns (from struct genpd_power_state, the domain's own timing table) and compares that against each device's effective_constraint_ns — the QoS-derived maximum time that device may be left unavailable. If any device's constraint is less than or equal to off_on_time_ns, the power-off is rejected (the round trip would violate that device's latency budget); otherwise the smallest surviving constraint becomes the domain's max_off_time_ns budget for staying off.
Drivers can supply per-state timing via the domain-idle-state Device Tree node using entry-latency-us and exit-latency-us properties (in microseconds; the kernel scales to nanoseconds internally).
genpd governors
Three governors ship in-tree (drivers/pmdomain/governor.c):
simple_qos_governor: the general-purpose governor most genpd backends register explicitly. It powers off the domain when all devices are suspended and the shortest QoS-constrained off-time across those devices (and subdomains) still exceeds the domain's power-off + power-on latency. It uses effective_constraint_ns, computed from the resume-latency QoS constraints attached to devices. It is not selected automatically — a domain registered with a NULL governor gets no governor at all.
pm_domain_always_on_gov: never powers off the domain; used for domains that must stay on (e.g., always-on power rails).
pm_domain_cpu_gov (built only under CONFIG_CPU_IDLE): used for CPU/cluster power domains (GENPD_FLAG_CPU_DOMAIN); picks the deepest idle state whose residency and latency fit the predicted idle window and any CPU PM QoS constraints.
The governor is selected as the second argument to pm_genpd_init(), which simply stores it in genpd->gov — there is no fallback to a default. Passing NULL means the domain has no governor at all: genpd_power_off() then skips power_down_ok() entirely and hardcodes genpd->state_idx = 0 (the shallowest state) whenever it decides to power off. If the domain has more than one power state and no governor was given, pm_genpd_init() even logs pr_warn("%s: no governor for states\n", ...). To get the QoS-aware behavior described above, pass &simple_qos_governor explicitly.
Debugging with debugfs
The genpd core creates one directory per domain under /sys/kernel/debug/pm_genpd/:
# List all registered power domains
ls /sys/kernel/debug/pm_genpd/
# pm_genpd_summary gpu-domain display_core display_dsi ...
# Global summary of all domains and their devices:
cat /sys/kernel/debug/pm_genpd/pm_genpd_summary
# domain status children performance
# /device runtime status managed by
# ------------------------------------------------------------------------------
# gpu-domain on 0
# /devices/platform/soc/gpu active 0 SW
# display_core off-0 0
# /devices/platform/soc/display suspended 0 SW
# Per-domain directories contain: current_state, sub_domains,
# idle_states, active_time, total_idle_time, devices
cat /sys/kernel/debug/pm_genpd/gpu-domain/current_state
The global pm_genpd_summary file is synthesized by summary_show() in drivers/pmdomain/core.c, which walks the global gpd_list and calls genpd_summary_one() per domain. For each domain it prints the domain name, its on/off-<state_idx> status, and aggregated performance_state, followed by one line per attached device giving that device's runtime-PM status via rtpm_status_str() (active/suspending/suspended/resuming/unsupported/error), its own performance state, and an SW/HW indicator from mode_status_str() showing whether that device's power state is currently managed by software or left to hardware autonomy.
Real-world implementations
Several production genpd backends illustrate the framework:
Qualcomm GDSC (Global Distributed Switch Controller) — drivers/clk/qcom/gdsc.c. Each GDSC controls one power domain. The gdsc_enable() / gdsc_disable() functions register directly as a generic_pm_domain's power_on/power_off callbacks; they toggle the GDSC's collapse bit (via gdsc_toggle_logic()) and poll the hardware for the domain to settle before returning.
Samsung Exynos power domains — drivers/pmdomain/samsung/exynos-pm-domains.c. Gates power by writing to a LOCAL_PWR_CFG register and polling a status register at offset +0x4 for the PMU's acknowledgement.
Rockchip power domains — drivers/pmdomain/rockchip/pm-domains.c. Uses a power domain controller peripheral accessed via regmap. Subdomain dependencies are declared in Device Tree; the driver calls pm_genpd_add_subdomain() based on DT topology.
All three follow the same pattern: allocate struct generic_pm_domain, fill power_on/power_off callbacks, call pm_genpd_init(), and register as a DT provider.
Further reading
Kernel source
- include/linux/pm_domain.h —
struct generic_pm_domain,struct gpd_timing_data, and thepm_genpd_*/of_genpd_*API - drivers/pmdomain/core.c — genpd core:
pm_genpd_init(),genpd_add_subdomain(),genpd_dev_pm_attach(), debugfs summary - drivers/pmdomain/governor.c —
simple_qos_governorandpm_domain_always_on_gov - drivers/clk/qcom/gdsc.c — Qualcomm GDSC genpd backend:
gdsc_enable()/gdsc_disable() - Documentation/devicetree/bindings/power/power-domain.yaml — DT binding spec for
power-domainsand#power-domain-cells
Related pages
- Runtime PM — usage counting, autosuspend, dev_pm_ops; genpd's runtime_suspend/resume hooks build on this
- System Suspend — system-wide sleep; genpd participates in the freeze/suspend sequence
- Device Tree — phandle parsing, the mechanism the
power-domainsDT binding relies on
LWN articles
- PM / Domains: Support for generic I/O PM domains — Rafael J. Wysocki's original genpd patchset (2011) explaining the shared-power-resource and parent/child domain model this page describes
External
- Documentation/driver-api/pm/devices.rst — Device Power Management Domains — upstream explanation of
dev->pm_domain,struct dev_pm_domain, and why power domain callbacks take precedence over subsystem callbacks