拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux电源域通用框架GENPD:核心机制、注册流程与调试实战

1. 电源域框架到底在解决什么问题先说说我为什么专门花时间梳理电源域这块。做过嵌入式 Linux 功耗优化的朋友应该都有体会一块 SoC 上挂着几十个外设模块显示、摄像头、音频、PCIe、USB、GPU 各自有独立的供电开关和时钟门控如果每个驱动都自己写一套寄存器操作去开关电源代码会乱成一锅粥而且模块之间的依赖关系根本没法管理——比如显示控制器依赖 PLLPLL 又依赖某个父电源域关错顺序直接死机。电源域通用框架就是来解决这个问题的。它把 SoC 内部按供电区域划分成一个个域每个域可以独立开关域之间形成父子层级关系内核通过统一的 API 来管理这些域的开关、引用计数和依赖传播。GENPDGeneric Power Domain就是 Linux 内核里这套通用电源域框架的实现代码主要在drivers/base/power/domain.c和include/linux/pm_domain.h这两个文件里。这篇文章适合谁看如果你正在做 ARM/ARM64 平台的功耗优化或者要给一个新 SoC 写电源域驱动又或者你调试时遇到某个设备 suspend 之后再也唤不醒这类问题那这篇梳理应该能帮你少走弯路。我会从框架的设计思路讲起把数据结构、注册流程、开关时序、引用计数、依赖传播这些核心机制拆开揉碎最后给出实际调试中踩过的坑和排查方法。需要说明的是GENPD 的代码在不同内核版本之间有差异我下面讲的内容主要基于 5.x 到 6.x 这一段的实现核心机制是稳定的但具体函数名和字段可能随版本微调你对照自己手上的源码看就行。2. 框架整体设计与核心数据结构拆解2.1 为什么要有通用电源域在 GENPD 出现之前各平台都有自己的电源管理实现比如三星的 exynos 有一套TI 的 OMAP 有一套每家 API 都不一样。驱动想跨平台复用几乎不可能。GENPD 的核心价值在于抽象出一套与平台无关的接口让设备驱动只需要调用dev_pm_domain_attach()、pm_runtime_get_sync()这类标准接口底层的寄存器操作由各平台的 GENPD provider 去实现。这个设计思路和时钟框架 CCF、稳压器框架 regulator 是一脉相承的内核提供框架平台提供 provider驱动只面向框架编程。理解了这一点后面所有的机制都好理解了。2.2 三个核心结构体GENPD 的数据结构不复杂抓住三个就够generic_pm_domain是核心代表一个电源域。关键字段包括name域的名字调试时靠它认人power_on/power_off平台实现的实际开关回调返回耗时微秒attach_dev/detach_dev设备加入/离开域时的回调dev_list挂在这个域下的设备链表parent/parent_links父域指针和父域连接child_links子域连接链表state_idx/states支持的电源状态数组govgovernor决定用哪个状态device域本身也是一个 device可以挂在父域上gpd_list是全局链表所有注册的电源域都挂在这里遍历查找靠它。genpd_power_state描述一个电源状态比如POWER_ON、RETENTION、OFF每个状态有自己的power_off_latency_ns、power_on_latency_ns、residency_nsgovernor 就是根据这些延迟和驻留时间来决定进哪个状态的。2.3 域之间的层级关系域不是孤立的它们形成一棵树。比如一个典型的移动 SoCVDD_CORE (顶层) ├── VDD_GPU ├── VDD_DISP │ ├── VDD_MIPI │ └── VDD_DSI └── VDD_AUDIO子域要开父域必须先开父域要关所有子域必须先关。这个约束是框架自动保证的不需要驱动操心。实现上通过parent_links和child_links两个链表维护开关时递归处理。注意域的层级关系是在注册时通过pm_genpd_add_subdomain()建立的不是自动从设备树推断的。很多新手以为设备树的power-domains属性会自动建层级其实那只是把设备关联到域域之间的父子关系得平台代码显式添加。2.4 引用计数机制这是最容易出问题的地方。每个域有一个device结构域本身的开关通过这个 device 的 runtime PM 引用计数来控制。设备加入域时会通过genpd的attach_dev建立关联设备 runtime resume 时会给域的 device 加引用suspend 时减引用。当引用计数归零域才可能被关闭。这个设计的好处是多个设备共享一个域时只要还有一个设备在用域就不会关。坏处是如果某个驱动忘了pm_runtime_put()域就永远关不掉功耗下不去。我调试过的一个案子就是摄像头驱动在 error path 里漏了一次 put导致整个 VDD_CAM 域常开待机功耗高了 30mA。3. 电源域的注册与设备绑定实操3.1 平台侧如何注册一个电源域平台代码注册电源域的标准流程大致是这样static struct generic_pm_domain my_pd { .name my_power_domain, .power_on my_pd_power_on, .power_off my_pd_power_off, .attach_dev my_pd_attach_dev, .detach_dev my_pd_detach_dev, .flags GENPD_FLAG_ALWAYS_ON, // 按需 }; static int __init my_pd_init(void) { int ret; ret pm_genpd_init(my_pd, NULL, false); if (ret) return ret; ret of_genpd_add_provider_simple(np, my_pd); if (ret) pm_genpd_remove(my_pd); return ret; }pm_genpd_init()的第三个参数是is_off表示初始状态是否关闭。如果传false域初始是开的传true表示初始关闭第一次被引用时才开。of_genpd_add_provider_simple()把域注册成设备树 provider这样设备树里写power-domains my_pd的设备就能自动绑定到这个域。3.2 设备树里的写法设备树侧provider 节点需要#power-domain-cellsmy_pd: power-domain0 { compatible vendor,my-pd; #power-domain-cells 0; /* 或者 1 如果域有多个实例 */ }; camera: camera12340000 { compatible vendor,camera; power-domains my_pd; /* 多个域可以这样写 */ /* power-domains pd_a, pd_b; */ };#power-domain-cells 0表示这个域没有额外参数设备引用时直接my_pd。如果是1引用时要带一个索引比如my_pd 2表示用第 2 个域实例。这个设计是为了支持一个 provider 管理多个域的情况。3.3 设备绑定的时机设备绑定到域发生在驱动 probe 之前由genpd_dev_pm_attach()完成。具体路径是设备注册时really_probe()会调用dev_pm_domain_attach()后者遍历设备的power-domains属性找到对应的 provider调用 provider 的attach_dev回调。这里有个关键点绑定必须在设备 runtime resume 之前完成。如果驱动在 probe 里就访问硬件寄存器而此时域还没开就会挂。所以平台代码要保证attach_dev回调里把域打开或者域初始状态就是开的。实操心得我见过不少平台把attach_dev写成空函数然后指望设备树里的power-domains自动开域。这是错的。attach_dev必须显式调用pm_runtime_get_sync()或者直接操作寄存器把域打开否则设备 probe 时访问寄存器会触发总线错误。3.4 多域绑定的顺序问题一个设备可以绑定多个域比如一个显示控制器同时依赖 VDD_DISP 和 VDD_MIPI。设备树里写display: display12340000 { power-domains pd_disp, pd_mipi; };绑定时按顺序处理先绑pd_disp再绑pd_mipi。如果两个域有父子关系顺序就很重要——必须先绑父域再绑子域否则子域的attach_dev可能在父域还没开的时候执行导致失败。框架本身不检查这个顺序得平台代码保证。我的做法是在设备树里按层级从外到内排列父域写前面。4. 开关时序、引用计数与依赖传播机制4.1 域打开的实际流程当设备调用pm_runtime_get_sync()时如果域当前是关的会触发genpd_power_on()。这个函数做的事情按顺序是检查域是否已经开开了直接返回递归打开所有父域genpd_power_on对 parent 调用调用平台power_on回调实际操作寄存器更新域状态为 on唤醒等待队列上的任务注意第 2 步父域是先于子域打开的。这个顺序不能反因为子域的供电可能依赖父域的 PLL 或参考电压。4.2 域关闭的实际流程关闭流程更复杂因为要处理引用计数和子域检查域的 device 引用计数非零直接返回检查所有子域是否都已关闭有开着的子域就返回调用平台power_off回调更新状态为 off递归尝试关闭父域如果父域引用计数也归零第 5 步是向上传播子域关了父域可能也没人用了就顺便关掉。这个传播是自动的不需要驱动干预。4.3 引用计数的加减时机引用计数的加减发生在设备 runtime PM 的 resume/suspend 路径上。具体来说设备pm_runtime_get_sync()→ 域 device 的pm_runtime_get_sync()设备pm_runtime_put()→ 域 device 的pm_runtime_put()但这里有个细节设备加入域时genpd会把设备的dev-pm_domain指向一个dev_pm_domain结构这个结构里的runtime_resume和runtime_suspend回调会去操作域的引用计数。所以设备驱动调用标准的pm_runtime_*接口框架自动帮你管域的引用。踩坑记录有一次调试发现某个域关不掉用cat /sys/kernel/debug/pm_genpd/pm_genpd_summary看到域的active计数是 1但所有设备都 suspend 了。最后查出来是一个驱动在 probe 失败路径里调了pm_runtime_get_sync()但没配对put。这种问题用 debugfs 一看引用计数就能定位。4.4 依赖传播与性能状态除了简单的开关GENPD 还支持性能状态performance state通过dev_pm_genpd_set_performance_state()设置。比如一个域支持多个电压档位设备可以请求某个档位框架会取所有设备请求的最大值然后设置到域上。这个机制在 GPU、显示这类需要动态调频调压的场景很有用。实现上域维护一个performance_state字段每次设备请求时重新计算最大值如果变了就调用genpd_set_performance_state()通知平台。依赖传播还体现在GENPD_FLAG_*标志上常用的几个标志含义使用场景GENPD_FLAG_ALWAYS_ON域常开不参与关闭核心电源域GENPD_FLAG_IRQ_SAFE可在中断上下文开关中断控制器相关域GENPD_FLAG_ACTIVE_WAKEUP域活跃时阻止系统 suspend需要保持唤醒的域GENPD_FLAG_PM_CLK域开关时自动管理时钟时钟和电源绑定的域GENPD_FLAG_PM_CLK这个特别实用它让域在开关时自动调用clk_prepare_enable()和clk_disable_unprepare()省得平台代码自己写。但要注意用了这个标志后域关联的时钟必须在设备树里通过clocks属性声明否则框架找不到时钟。5. 常见问题排查与调试技巧实录5.1 域关不掉的排查思路这是最高频的问题。排查步骤我一般这样走第一步看 debugfscat /sys/kernel/debug/pm_genpd/pm_genpd_summary输出会列出所有域的状态、引用计数、子域、设备。重点看active计数和device列表。第二步如果计数非零找出是哪个设备没 put。可以用pm_runtime的 debugfscat /sys/kernel/debug/pm_runtime/status或者直接看设备的runtime_status和usage_count。第三步如果计数为零但域还是开的检查子域。父域只有在所有子域都关的情况下才能关。用 summary 看子域的active计数。第四步如果都正常但域还是关不掉可能是GENPD_FLAG_ALWAYS_ON标志设了或者 governor 选择了不关的状态。5.2 设备 probe 时访问寄存器挂掉这个问题的根因通常是域没开就访问硬件。排查确认设备树里power-domains属性写对了确认 provider 的attach_dev回调里开了域确认域的初始状态pm_genpd_init的is_off参数如果attach_dev是空的域初始又是关的那 probe 时访问寄存器必挂。解决办法是在attach_dev里调pm_runtime_get_sync(genpd-dev)或者把is_off传false。5.3 suspend/resume 顺序错乱系统 suspend 时设备的 suspend 顺序和 resume 顺序是相反的。如果域之间有依赖顺序错了会导致 resume 时访问未上电的硬件。GENPD 框架本身会处理域的 suspend/resume 顺序但设备驱动如果自己实现了suspend/resume回调要注意不要在回调里访问依赖其他域的硬件。我的经验是驱动回调里只做软件状态保存硬件操作交给 runtime PM 的runtime_suspend/runtime_resume。5.4 常见问题速查表现象可能原因排查方法域常开功耗高引用计数泄漏看 pm_genpd_summary 的 active 计数probe 时总线错误域未开检查 attach_dev 和 is_offresume 后设备不工作域未恢复看 resume 顺序和域状态域关不掉子域未关看子域 active 计数性能状态不生效governor 未设置检查 states 数组和 gov时钟未使能缺 PM_CLK 标志加 GENPD_FLAG_PM_CLK5.5 几个独家避坑技巧技巧一在power_on和power_off回调里加pr_debug打印时间戳配合ftrace看开关时序。我调试一个显示闪烁问题时就是靠这个发现域开关和 vsync 中断的竞争。技巧二用GENPD_FLAG_IRQ_SAFE要谨慎。这个标志允许在中断上下文开关域但平台回调里如果有msleep之类的睡眠操作就会崩。确认回调是原子的再加这个标志。技巧三多域绑定时如果某个域是可选的用dev_pm_domain_attach_by_name()按名字绑定比按索引绑定更健壮。设备树里用power-domain-names属性给每个域起名。技巧四调试域依赖问题时把CONFIG_PM_GENPD_DEBUG打开会有更详细的日志。这个选项在 menuconfig 里藏得比较深在Device Drivers - Generic Driver Options - Power Management下面。6. 从框架到实战的几点个人体会梳理完 GENPD 这套框架我最大的感受是它的设计哲学是框架管机制平台管策略。框架负责引用计数、依赖传播、状态机这些通用逻辑平台只需要实现几个回调。这种分工让驱动代码可以跨平台复用但也意味着平台代码的质量直接决定功耗表现。实际项目中我见过太多因为attach_dev写错、引用计数泄漏、域层级建错导致的功耗问题。这些问题用 debugfs 基本都能定位关键是要养成先看 summary 再看代码的习惯。另外域的层级关系最好在平台代码里画个图注册时对照着写比事后调试省事得多。后续如果要做更细的功耗优化可以研究一下 governor 的实现GENPD 默认用的是simple_qosgovernor它根据延迟和驻留时间选状态。如果平台有特殊需求可以自己实现 governor通过pm_genpd_init()的gov参数传进去。这块内容比较多有机会再单独写一篇。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门