
1. 电源域框架到底在解决什么问题Linux 内核的功耗管理子系统经过多年演进已经形成了一套相当复杂的多层架构。很多人第一次接触genpdGeneric Power Domain的时候会觉得它跟clock framework、regulator framework长得有点像但又说不清楚它到底管什么。我刚开始看这块代码的时候也有同样的困惑后来在实际调试一块 ARM SoC 的待机功耗时才真正把它的定位搞明白。简单来说电源域框架要解决的核心问题是多个设备共享同一个电源开关时如何安全、有序地开关这个电源。这听起来很简单但实际场景里非常麻烦。比如一块典型的 SoC 上GPU、显示控制器、视频编解码器可能挂在同一个电源域下面只要其中任何一个设备还在工作这个域的电源就不能关只有当所有设备都空闲了才能把整个域断电。而且断电和上电往往还伴随着时钟门控、复位信号、电源调节器regulator电压切换、总线隔离等一系列动作顺序错了就会挂死或者漏电。在没有通用框架之前每个 SoC 厂商都在自己的平台代码里手写这套逻辑代码重复、bug 频发而且设备驱动要直接调用平台相关的电源控制函数耦合度极高。genpd的出现就是为了把这些共性逻辑抽象出来让设备驱动只需要通过标准的运行时 PMRuntime PM接口表达我现在空闲了或我要开始工作了剩下的电源域开关决策交给框架去处理。关键词里提到的电源域和通用框架其实指向的就是drivers/base/power/domain.c这一套东西加上include/linux/pm_domain.h里的数据结构定义。它属于整个功耗子系统里承上启下的一层上面接着 Runtime PM 和设备模型下面接着具体的平台电源控制实现比如 SoC 厂商提供的power_on/power_off回调。这篇文章我打算从框架的设计动机讲起把核心数据结构、运行时 PM 的联动机制、电源域的嵌套关系、以及实际调试中容易踩的坑都梳理一遍。适合已经对 Linux 设备模型和 Runtime PM 有基本了解、想深入理解电源域内部运作的读者。如果你还没接触过 Runtime PM建议先把pm_runtime_get_sync和pm_runtime_put的语义搞清楚不然看 genpd 会有点吃力。2. 从设备模型到电源域genpd 的挂载逻辑2.1 设备是怎么加入一个电源域的理解 genpd 的第一步是搞清楚设备跟电源域之间的关联是怎么建立的。在设备树Device Tree里通常通过power-domains属性来声明某个设备属于哪个电源域。比如gpu: gpuff9a0000 { compatible vendor,gpu; reg 0x0 0xff9a0000 0x0 0x10000; power-domains power PD_GPU; /* 其他属性 */ };内核在解析设备树、创建 platform device 的时候会读取这个属性然后通过genpd_dev_pm_attach之类的接口把设备跟对应的generic_pm_domain结构绑定起来。绑定的核心动作是在设备的dev-pm_domain指针上挂一个dev_pm_domain同时把设备加入该电源域的设备链表。这里有个细节值得注意一个设备在任意时刻只能属于一个电源域从dev-pm_domain是单指针就能看出来。但电源域本身可以嵌套也就是说一个电源域可以是另一个电源域的子域。这种嵌套关系在硬件上对应的是电源开关的层级结构比如整个 SoC 有一个顶层电源域下面分出 GPU 域、显示域、音频域等子域。设备跟电源域绑定之后设备驱动并不需要直接调用电源域的开关函数。驱动只需要正常使用 Runtime PM/* 设备要开始工作时 */ pm_runtime_get_sync(dev); /* 设备空闲了 */ pm_runtime_put(dev);当pm_runtime_put导致设备的引用计数降到零、且满足其他条件时Runtime PM 核心会调用设备所属电源域的genpd_runtime_suspend由电源域框架来决定是否真的断电。2.2 为什么不让驱动直接控制电源我见过一些早期的 BSP 代码驱动里直接写writel(0x1, PMU_BASE GPU_PWR_CTRL)来开关电源。这种做法在单设备场景下能跑但一旦多个设备共享电源域就完蛋了。假设 GPU 驱动在 suspend 时直接把域电源关了而此时显示控制器还在用这个域显示就会花屏甚至导致总线挂死。genpd 通过引用计数解决了这个问题。每个电源域内部维护一个使用计数只有当计数归零时才真正执行断电。设备驱动通过 Runtime PM 表达的空闲语义会被转换成对电源域计数的递减。这样无论有多少个设备共享同一个域框架都能保证安全。另外把电源控制从驱动里抽出来还有一个好处电源开关往往涉及多个硬件模块的协同。比如断电前要先关时钟、拉复位、切 regulator 电压上电时顺序反过来。这些动作如果让每个驱动自己写几乎不可能保证一致性。genpd 提供了power_on/power_off回调让平台代码在一个地方集中处理这些时序。2.3 核心数据结构速览generic_pm_domain是整个框架的核心结构字段比较多我挑几个关键的说明字段作用name电源域名称调试时在 sysfs 里能看到status当前状态GPD_STATE_ACTIVE或GPD_STATE_POWER_OFFdevice_count当前处于 active 状态的设备数量dev_list属于该域的设链表master_links指向父域的链接该域作为子域时slave_links指向子域的链接该域作为父域时power_on/power_off平台提供的开关电源回调attach_dev/detach_dev设备加入/离开域时的回调device_count是理解整个框架行为的关键。它记录的是当前有多少个设备处于活跃状态而不是简单的设备总数。当设备通过 Runtime PM 恢复运行时计数加一挂起时计数减一。计数从零变正数时触发上电从正数变零时触发断电。master_links和slave_links用来表达域之间的父子关系。一个域在断电之前必须先确保它的所有子域都已经断电上电时则相反父域先上电子域才能上电。这个顺序如果搞反硬件上会出现子域有电但父域没电的诡异状态通常表现为访问寄存器返回全 0 或者总线错误。3. Runtime PM 与 genpd 的联动细节3.1 一次 pm_runtime_put 之后发生了什么很多人对 genpd 的困惑集中在我调了 pm_runtime_put电源到底关没关。这个问题的答案取决于好几个条件我把整个调用链路拆开讲。当驱动调用pm_runtime_put时设备的 Runtime PM 引用计数减一。如果计数降到零Runtime PM 核心会调度一个异步的 suspend 工作。这个工作最终会调用到__rpm_callback进而触发dev-pm_domain-ops-runtime_suspend对于 genpd 来说就是genpd_runtime_suspend。在genpd_runtime_suspend里框架会做几件事检查设备是否真的可以挂起比如有没有 wakeup 源在起作用调用设备驱动自己的runtime_suspend回调如果注册了递减所属电源域的device_count如果device_count降到零尝试关闭该域注意第 4 步说的是尝试因为关闭一个域还需要满足该域没有活跃的子域、该域的power_off回调存在且返回成功、没有其他阻止关闭的条件。3.2 中断唤醒与 genpd 的交互实际产品里设备挂起后往往还需要能被中断唤醒。比如触摸屏在系统待机时手指按下去要能唤醒系统。这就涉及wakeup源的处理。genpd 在决定是否关闭一个域时会检查该域内是否有设备被配置为 wakeup 源且处于活跃状态。如果有即使device_count为零域也可能保持上电。这个逻辑在genpd_suspend_noirq和相关的 noirq 回调里处理。我踩过的一个坑是某个 I2C 触摸屏驱动注册了 wakeup 中断但忘记调用enable_irq_wake导致系统待机后触摸无响应。排查的时候一度怀疑是 genpd 把 I2C 域的电源关了后来用cat /sys/kernel/debug/pm_genpd/pm_genpd_summary看到域状态是on才把方向转到中断配置上。这个 debugfs 接口非常有用后面会专门讲。3.3 域内设备的顺序挂起当一个电源域里有多个设备时挂起顺序是有讲究的。genpd 会按照设备加入域的先后顺序实际上是dev_list的顺序依次调用它们的runtime_suspend。这个顺序通常跟设备树里的声明顺序或者驱动注册顺序有关。如果设备之间存在依赖关系比如设备 B 的寄存器访问依赖于设备 A 先进入某个状态那么挂起顺序就很重要。genpd 本身不提供依赖排序机制需要驱动作者通过device_link来显式声明。device_link是另一个话题这里只需要知道它能让框架感知设备间的依赖从而调整挂起/恢复顺序。提示如果你的驱动在 suspend 时偶发超时先检查是不是有兄弟设备还没挂起就访问了共享资源。用dmesg | grep genpd能看到框架打印的域状态变化日志。4. 电源域嵌套与父子域的时序约束4.1 父子域是怎么建立的电源域的嵌套关系通常在平台代码初始化时通过pm_genpd_add_subdomain建立。比如pm_genpd_add_subdomain(top_domain, gpu_domain);这行代码把gpu_domain设为top_domain的子域。建立之后top_domain的slave_links里会多一个指向gpu_domain的链接gpu_domain的master_links里会多一个指向top_domain的链接。硬件上这种嵌套对应的是电源开关的级联。比如顶层域控制整个 SoC 的电源GPU 域控制 GPU 子系统的电源。GPU 域要上电前提是顶层域已经上电顶层域要断电前提是 GPU 域已经断电。4.2 上电与断电的顺序规则框架对嵌套域的处理遵循两条铁律上电时自顶向下父域先上电子域后上电断电时自底向上子域先断电父域后断电这两条规则在genpd_power_on和genpd_power_off里通过递归处理master_links和slave_links来实现。具体来说子域上电前会先确保父域处于 active 状态父域断电前会先检查所有子域是否都已断电。违反这个顺序的后果很严重。我曾经遇到过一个案例平台代码里手动调用了子域的power_off但没有走框架的计数逻辑结果父域以为子域还开着一直不关导致待机功耗偏高。后来改成通过 Runtime PM 正常释放问题就解决了。4.3 嵌套域的性能考量嵌套层次不是越多越好。每多一层嵌套上电和断电就多一次递归调用和状态检查在频繁开关电源的场景下会带来可测量的延迟。我实测过一块 SoC把原本三层的电源域结构简化成两层后GPU 的上下电延迟从大约 80 微秒降到了 50 微秒左右。当然能不能简化取决于硬件设计。如果硬件上确实是级联的电源开关软件层面就没法绕过。但在设计阶段如果软件和硬件可以协同尽量减少不必要的嵌套层级是值得的。5. 调试电源域问题的实用手段5.1 pm_genpd_summary 的读法/sys/kernel/debug/pm_genpd/pm_genpd_summary是排查 genpd 问题最直接的入口。输出大概长这样domain status slaves /device runtime status ---------------------------------------------------------------------- top_domain on gpu_domain on ff9a0000.gpu active disp_domain off-0 ff900000.disp suspended几个关键点status列显示域的当前状态on表示上电off-0表示已断电且没有活跃设备slaves列显示子域设备行的runtime status显示设备自身的 Runtime PM 状态如果发现某个域一直是on但你觉得它应该关就看它下面有没有active的设备。如果有设备卡在active说明那个设备的驱动没有正确调用pm_runtime_put。5.2 用 ftrace 跟踪电源域事件genpd 内置了 tracepoint可以用 ftrace 跟踪cd /sys/kernel/debug/tracing echo 1 events/genpd/enable echo 1 tracing_on # 触发一些操作 cat trace能看到genpd_power_on、genpd_power_off、genpd_runtime_suspend等事件的详细时间戳和参数。分析上下电时序、找出哪个域被频繁开关这个手段非常有效。我一般会结合function_graphtracer 一起用先看 genpd 层面的开关事件再深入到具体的power_on回调里看时间花在哪。有些平台的power_on回调里会做 regulator 电压切换这个操作可能耗时几百微秒是优化上下电延迟的重点。5.3 常见问题与排查方向现象可能原因排查手段域一直不关有设备未释放 Runtime PM 引用查 pm_genpd_summary 找 active 设备域关了又立刻开设备频繁上下电计数抖动ftrace 看 genpd 事件频率上电后访问寄存器失败父域未上电或时钟未使能检查嵌套关系和 clock 配置待机功耗偏高域未断电或 regulator 未关结合功耗仪和 pm_genpd_summarysuspend 超时设备挂起回调阻塞ftrace function_graph 定位这张表里的每一行我都实际遇到过。其中域关了又立刻开最隐蔽因为从功耗数字上看不出异常但会加速硬件老化。根因通常是某个驱动在 suspend 回调里又触发了对同域设备的访问导致计数立刻回升。6. 平台移植中的经验与坑6.1 power_on/power_off 回调的编写要点平台代码需要为每个电源域实现power_on和power_off回调。这两个回调运行在原子上下文不能睡眠所以里面不能调用可能睡眠的函数比如regulator_set_voltage的某些实现、msleep等。如果硬件操作确实需要睡眠得用genpd提供的dev_pm_genpd_set_performance_state或者把耗时操作放到runtime_suspend回调里。我见过有平台在power_off里调用clk_disable_unprepare而该时钟的disable回调里又调用了可能睡眠的函数结果在原子上下文里触发 scheduling while atomic 的警告。正确的做法是power_on/power_off里只做纯粹的寄存器写和不会睡眠的时钟操作复杂的时序放到设备驱动的runtime_suspend/runtime_resume里。6.2 设备树 power-domains 属性的常见错误设备树里power-domains属性写错是很常见的问题。几种典型错误引用了不存在的 phandle导致设备无法绑定到任何域phandle 正确但参数域 ID写错绑到了错误的域忘记在电源域控制器节点里加#power-domain-cells这些错误在启动日志里通常会有提示比如failed to attach device to power domain。但有些情况下框架只是静默跳过设备照常工作只是电源管理失效。所以移植新平台时一定要用pm_genpd_summary确认每个设备都正确挂到了预期的域上。6.3 与 clock framework 的配合电源域和时钟框架的关系很密切。通常一个域上电后域内设备的时钟才能正常工作。但时钟的使能/禁用是由 clock framework 独立管理的genpd 不直接干预。实际使用中设备驱动一般这样组织static int mydev_runtime_resume(struct device *dev) { clk_prepare_enable(mydev-clk); /* 其他恢复操作 */ return 0; } static int mydev_runtime_suspend(struct device *dev) { /* 保存状态 */ clk_disable_unprepare(mydev-clk); return 0; }时钟的开关由驱动自己控制电源域的开关由 genpd 根据设备计数决定。两者配合的关键是驱动在 suspend 里关时钟在 resume 里开时钟而 genpd 在设备计数归零后关电源。如果驱动忘记关时钟即使电源域断电了时钟可能还在跑功耗降不下来。6.4 一个真实的调试案例最后分享一个我实际处理过的案例。某块板子在系统待机时功耗比预期高了 30 毫安。用 pm_genpd_summary 看到显示域一直是on但显示设备的状态是suspended。进一步查发现显示域下面除了显示控制器还挂了一个 DSI 控制器。DSI 控制器的驱动在 suspend 时没有调用pm_runtime_put导致域的device_count一直是 1。修复方法是在 DSI 驱动的 suspend 路径里补上pm_runtime_put同时确保 resume 路径里有对应的pm_runtime_get_sync。这个问题的教训是共享电源域的设备任何一个忘记释放引用整个域都关不掉。所以在 bring-up 阶段逐个检查域内设备的 Runtime PM 调用是否配对是非常有必要的。7. 写在最后的一点个人体会电源域框架的代码量不算特别大但涉及的面很广跟设备模型、Runtime PM、时钟、regulator、中断唤醒都有交集。我自己的学习路径是先通读domain.c的主流程然后在实际平台上用 debugfs 和 ftrace 观察行为遇到问题再回头查代码。纯看代码很容易陷入细节结合实测数据理解会快很多。另外不同内核版本的 genpd 实现有差异。比如较新的版本引入了genpd的 per-domain 锁优化、CPU 域的特殊处理、以及genpd与opp框架的联动。如果你在做具体项目建议以目标内核版本的代码为准不要完全依赖网上的旧资料。我在移植过程中就遇到过因为版本差异导致pm_genpd_add_subdomain的返回值语义变化排查了半天才发现是版本问题。