ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux电源域GENPD框架:从设备树绑定到Runtime PM的功耗管理实战

Linux电源域GENPD框架:从设备树绑定到Runtime PM的功耗管理实战 1. 从一次功耗异常排查说起为什么需要电源域框架去年冬天调一块车载中控板子的时候遇到一个很典型的问题设备在熄屏待机状态下整机静态电流比预期高了将近 40mA。用电流探头逐路量下去发现有一颗摄像头的 MIPI 供电轨在待机时并没有真正断电而系统日志里明明显示摄像头驱动已经进入了 runtime suspend。这个现象逼着我把 Linux 功耗子系统里电源域这一层从头到尾捋了一遍——因为问题最终不在驱动而在电源域之间的依赖关系没有被正确描述导致内核不敢真正关掉那条供电轨。这件事让我意识到很多人学 Linux 功耗子系统注意力都放在 cpufreq、cpuidle、runtime PM 这些明星模块上却忽略了底下那层真正决定谁能不能关、什么时候关的通用框架——电源域Power Domain。它不像调频调压那样有直观的功耗数字反馈但一旦描述错了轻则多耗几毫安重则上电时序错乱直接起不来。这篇就围绕 Linux 内核里的电源域通用框架做一次系统梳理重点讲清楚 GENPDGeneric Power Domain这套通用抽象是怎么设计的、它和 runtime PM 是怎么咬合的、设备树里那些power-domains属性到底怎么被解析、以及实际调试时该盯哪些点。适合已经写过驱动、但对功耗子系统只停留在会用pm_runtime_get_sync层面的嵌入式工程师也适合正在啃内核源码、想把 PM 这块串起来的人。看完你应该能自己判断一个设备的电源域该怎么描述、为什么这么描述、出问题时从哪查。2. GENPD 到底抽象了什么把电源开关变成一个有状态的设备2.1 电源域的本质一组共享供电的设备的集合先抛开代码讲清楚概念。SoC 内部通常会把芯片划分成若干个供电区域每个区域可以独立地上电、下电、调压。比如一块典型的应用处理器可能有 CPU 域、GPU 域、显示域、视频编解码域、外设域等等。同一个域里的所有模块共享同一路电源要么一起开要么一起关——这就是电源域最朴素的含义。那为什么内核要专门搞一套框架来管它因为一起开关这件事远比想象中复杂。一个域能不能关取决于三件事域内所有设备是不是都允许关都进入了低功耗状态、这个域有没有被别的域依赖比如显示域依赖视频域的输出、以及关掉之后恢复时上电时序对不对。如果每个 SoC 厂商各写各的驱动就没法通用了。GENPD 的价值就在于它把电源域抽象成一个内核对象用统一的接口管理它的开关、状态、依赖和与设备的绑定关系。2.2 三个核心数据结构generic_pm_domain、gpd_list 与 dev_pm_domain落到代码层面GENPD 的核心是struct generic_pm_domain定义在include/linux/pm_domain.h。这个结构体里几个字段最关键name域的名字调试时靠它认人。power_on/power_off真正干活的回调由 SoC 的电源域驱动实现负责操作寄存器把这一路电开起来或关掉。flags一堆行为标志比如GENPD_FLAG_ALWAYS_ON常开永不关、GENPD_FLAG_PM_CLK域开关时自动管理时钟、GENPD_FLAG_IRQ_SAFE允许在中断上下文里开关等。dev_list挂在当前域下的设备链表。parent/child域之间的父子依赖关系这是处理级联电源域的关键。states和state_count支持多个电源状态比如 retention、off配合power_on/power_off或set_performance_state使用。所有注册进内核的电源域都挂在全局链表gpd_list上用互斥锁gpd_list_lock保护。设备要跟域绑定靠的是struct dev_pm_domain它作为struct device里的一个成员pm_domain存在。当设备的dev-pm_domain被设置成某个 GENPD 时这个设备后续所有的 runtime PM 操作都会先经过 GENPD 的调度而不是直接调用驱动自己的runtime_suspend。2.3 为什么用域而不是设备来管电源这里有个设计哲学值得说清楚。如果按设备粒度管电源每个设备自己控制自己的供电听起来更精细但实际做不到——因为物理上供电是成组共享的。GENPD 选择以域为粒度本质上是尊重硬件事实。它把设备想不想关和域能不能关这两件事解耦设备通过 runtime PM 表达自己的意愿GENPD 汇总所有设备的意愿再结合依赖关系决定域的实际状态。这种分层是理解整个框架的钥匙。3. 设备与电源域的绑定设备树里的 power-domains 是怎么生效的3.1 设备树描述与解析时机设备树里一个设备要声明自己属于哪个电源域写法是这样的camera: camera1a0000 { compatible vendor,camera; reg 0x1a0000 0x1000; power-domains pd_cam; clocks clk_cam; };power-domains属性里是一个 phandle 数组指向对应的电源域节点。解析发生在设备探测阶段具体是of_genpd_add_device()这条路径。内核在of_pm_domain相关代码里遍历设备的power-domains属性找到对应的generic_pm_domain然后调用pm_genpd_add_device()把设备挂到域的dev_list上同时设置dev-pm_domain。这里有个容易踩的点解析顺序依赖 probe 顺序。如果电源域驱动还没注册设备的power-domains就解析不到绑定会失败。所以电源域驱动通常要放在比较早的初始化阶段或者用-EPROBE_DEFER机制让设备探测延后重试。我在实际项目里见过因为电源域驱动和摄像头驱动 probe 顺序颠倒导致摄像头一直 probe 失败、日志里只报一个含糊的-517的情况。3.2 多级电源域与 parent/child 关系真实 SoC 里电源域往往是嵌套的。比如显示子系统域下面挂着MIPI DSI 域和DP 域显示域又是外设电源域的子域。设备树里通过域的power-domains属性域节点自己也可以有来表达这种层级内核解析后形成parent/child链表。级联关系带来的核心问题是子域要关父域不一定能关父域要关所有子域必须先关。GENPD 用genpd_power_off()里的递归逻辑处理这件事——关一个域之前先检查它的子域是否都处于关闭状态以及它自己域内的设备是否都 suspend 了。这个递归如果描述错了就会出现父域关了但子域还开着的诡异状态表现为某些外设莫名其妙掉电。3.3 一个真实的绑定错误案例回到开头那个 40mA 的问题。根因是摄像头模组的供电其实来自两个域MIPI PHY 在一个域sensor 核心在另一个域但设备树里只写了 sensor 核心那个域。结果待机时sensor 核心域关了MIPI PHY 域因为没有设备声明依赖它而一直开着。修复方式是在设备树里补上第二个power-domains引用让两个域都跟摄像头设备绑定。改完之后静态电流降到了预期值。这个案例说明设备树里的电源域描述必须和硬件供电拓扑严格对应少写一个都会漏电。4. GENPD 与 Runtime PM 的咬合机制谁在什么时候决定关电4.1 调用链从 pm_runtime_put 到 power_off这是整个框架最核心、也最容易讲糊涂的地方。我把它拆成一条清晰的链路。驱动调用pm_runtime_put_sync(dev)时runtime PM 核心会递减设备的引用计数。当计数归零核心准备调用设备的runtime_suspend。但如果这个设备绑定了 GENPD事情会拐个弯GENPD 在设备注册时会替换掉设备的 PM domain 回调dev-pm_domain-ops指向 GENPD 自己的genpd_runtime_suspend。于是流程变成pm_runtime_put_sync→ 引用计数归零。runtime PM 核心调用dev-pm_domain-ops-runtime_suspend即genpd_runtime_suspend。genpd_runtime_suspend先调用设备驱动真正的runtime_suspend通过genpd-dev_ops.stop或保存的原始回调让设备自己进入低功耗。设备 suspend 成功后GENPD 检查这个域内所有设备是否都已 suspend。如果都 suspend 了且没有子域还开着GENPD 调用genpd_power_off()最终执行 SoC 注册的power_off回调真正断电。注意第 3 步和第 5 步的区别设备自己的 suspend 和域的断电是两回事。设备 suspend 只是我准备好了域断电才是电真的没了。很多初学者以为调了pm_runtime_put电就断了其实中间还隔着 GENPD 的汇总判断。4.2 引用计数与最后一个设备语义GENPD 内部对每个域维护了一个使用计数。每当域内一个设备从 active 变 suspend计数递减反之递增。只有当计数归零域才允许断电。这个语义叫最后一个设备离开才关灯跟办公室最后一个走的人关灯是一个道理。这里有个隐蔽的坑如果某个设备的 runtime PM 没使能或者引用计数一直不为零整个域就永远关不掉。我调试过一个案子某个传感器的驱动在 probe 里调了pm_runtime_get_sync却忘了在 remove 或错误路径里 put导致引用计数永远大于零整个传感器域待机时无法断电。这种 bug 不会报错只会默默耗电非常难查。排查手段是打开pm_runtime的 debugfs 统计看每个设备的usage_count。4.3 系统级 suspend 时 GENPD 的角色除了 runtime PM系统进入 suspend-to-RAM 时 GENPD 也参与。系统级 suspend 会遍历所有域按父子顺序依次关闭。这里的关键是顺序必须先关子域再关父域恢复时反过来。GENPD 通过genpd_suspend_noirq等回调配合GENPD_FLAG_*标志来保证顺序。如果设备树里父子关系描述反了系统 suspend 时可能出现父域先断电、子域寄存器访问失败的情况表现为 suspend 卡死或恢复后外设异常。5. 调试电源域问题的实战路径从现象到根因5.1 先看 debugfs别急着改代码GENPD 在 debugfs 里暴露了非常丰富的信息路径通常是/sys/kernel/debug/pm_genpd/。里面有每个域的状态、当前电源状态、域内设备列表、使用计数等。遇到功耗问题第一步永远是cat /sys/kernel/debug/pm_genpd/*/state cat /sys/kernel/debug/pm_genpd/*/devicesstate会告诉你域当前是 on 还是 offdevices列出域内所有设备。如果待机时某个域还是 on先看它域内哪个设备的usage_count不为零或者哪个子域还开着。这一步能解决八成的问题比盲目读代码快得多。5.2 常见现象与对应根因对照我把实际踩过的坑整理成一张表方便对照排查现象可能根因排查手段待机电流偏高某域不关域内设备引用计数不为零查 debugfs 的 usage_count设备 probe 失败报 -517电源域驱动未就绪绑定失败查 probe 顺序确认 defer系统 suspend 卡死父子域顺序描述错误查设备树 power-domains 层级恢复后外设寄存器异常域断电时未保存/恢复上下文检查域的 save/restore 回调域反复开关功耗反而升高设备频繁 runtime resume/suspend查 runtime PM 调用频率5.3 打开 PM 调试开关的正确姿势内核配置里要打开CONFIG_PM_DEBUG、CONFIG_PM_ADVANCED_DEBUG、CONFIG_PM_GENERIC_DOMAINS_DEBUG才能看到完整的 debugfs 节点。另外CONFIG_PM_TRACE可以记录每次电源状态切换配合pm_trace能还原出完整的开关时序。我习惯在调功耗问题时先开这几个开关把一次待机过程的域状态变化全打出来往往一眼就能看出哪个域该关没关。注意CONFIG_PM_TRACE会往 RTC 寄存器里写数据某些平台上可能和 RTC 功能冲突调试完记得关掉。6. 自己动手给一个虚拟设备挂上电源域6.1 最小可运行的 GENPD 注册示例光看理论不够我写一个最小的电源域驱动骨架帮你把注册流程跑通。假设我们有一个虚拟的传感器域#include linux/pm_domain.h #include linux/platform_device.h static int sensor_pd_power_on(struct generic_pm_domain *domain) { /* 实际操作寄存器把这一路电打开 */ pr_info(sensor pd: power on\n); return 0; } static int sensor_pd_power_off(struct generic_pm_domain *domain) { pr_info(sensor pd: power off\n); return 0; } static struct generic_pm_domain sensor_pd { .name sensor_pd, .power_on sensor_pd_power_on, .power_off sensor_pd_power_off, .flags GENPD_FLAG_PM_CLK, }; static int __init sensor_pd_init(void) { int ret; ret pm_genpd_init(sensor_pd, NULL, false); if (ret) return ret; ret of_genpd_add_provider_simple( of_find_node_by_path(/sensor-pd), sensor_pd); if (ret) pm_genpd_remove(sensor_pd); return ret; }pm_genpd_init的第三个参数是is_off表示初始状态是否已断电。of_genpd_add_provider_simple把域注册成设备树 provider这样设备树里的power-domains sensor_pd才能解析到它。6.2 设备侧怎么配合设备驱动这边只要设备树里写了power-domains绑定是自动完成的驱动本身不需要额外调用 GENPD 接口。驱动要做的只有一件事正确使用 runtime PM。在 probe 里pm_runtime_enable在需要用电时pm_runtime_get_sync用完pm_runtime_put。剩下的交给 GENPD。static int sensor_probe(struct platform_device *pdev) { pm_runtime_enable(pdev-dev); pm_runtime_set_autosuspend_delay(pdev-dev, 200); pm_runtime_use_autosuspend(pdev-dev); /* ... */ return 0; }autosuspend延迟设成 200ms 是个经验值太短会导致频繁开关域反而增加功耗太长则待机响应慢。具体值要结合设备实际使用频率调。6.3 验证绑定是否成功注册完跑起来先确认域出现在 debugfs 里ls /sys/kernel/debug/pm_genpd/ cat /sys/kernel/debug/pm_genpd/sensor_pd/devices如果devices里能看到你的设备说明绑定成功。然后手动触发一次 runtime suspend观察power_off回调有没有被调用。这一步跑通整个 GENPD 链路就算打通了。7. 几个容易忽略的细节与经验之谈7.1 GENPD_FLAG_PM_CLK 的自动时钟管理很多 SoC 的电源域开关必须配合时钟开关顺序还不能错——通常要先开时钟再上电下电后再关时钟。GENPD_FLAG_PM_CLK让 GENPD 自动帮你处理这个顺序域内设备的时钟会在域上电时自动 enable、下电时自动 disable。省事但前提是设备树里设备的clocks属性写对了。我见过时钟属性漏写导致域上电后设备访问挂死的案例排查时一度以为是电源问题其实是时钟没开。7.2 常开域的代价GENPD_FLAG_ALWAYS_ON用起来很爽——标上它这个域就永不关闭省得处理各种依赖。但代价是待机功耗下不去。我的建议是除非这个域确实必须常开比如包含唤醒源控制器否则不要图省事标 ALWAYS_ON。曾经有个项目为了快速出功能把三个域都标了 ALWAYS_ON结果待机功耗超标后期返工逐个拆依赖花的时间比一开始就写对多得多。7.3 域状态与性能状态的区分GENPD 除了开关还支持性能状态performance state通过dev_pm_genpd_set_performance_state设置。这主要用于那些不能简单开关、而是需要调电压/频率的域比如 GPU 域。它和开关是两个维度一个域可以处于 on 状态但性能状态为最低。理解这个区分对调 GPU、显示这类域的功耗很关键。7.4 中断上下文里的域操作有些域需要在中断上下文里快速开关比如某些低延迟外设这时要用GENPD_FLAG_IRQ_SAFE。但要注意标了这个标志后GENPD 内部不能睡眠power_on/power_off回调里也不能用可能睡眠的函数。我踩过一次坑在标了 IRQ_SAFE 的域回调里调了regulator_set_voltage可能睡眠结果偶发调度异常。后来改成用regulator_set_voltage_atomic这类原子接口才稳。8. 把电源域框架串起来看它在整个功耗子系统里的位置梳理到这里可以把 GENPD 放回 Linux 功耗子系统的全局里看。最上层是 cpuidle 和 cpufreq管 CPU 的休眠和调频中间是 runtime PM 和系统 suspend管设备的运行时和系统级低功耗最底下就是 GENPD 和具体的时钟、稳压器、电源管理 IC 驱动管电到底给不给。GENPD 是承上启下的那一层向上给 runtime PM 提供域级的决策依据向下调用 SoC 具体的电源操作。理解了这一层很多之前觉得零散的知识点就串起来了。比如为什么设备 suspend 了电还没断因为域里还有别的设备、为什么系统 suspend 要按父子顺序因为域有层级、为什么设备树里电源域写错会漏电因为域汇总判断依赖设备声明。这些问题的答案都指向同一个核心GENPD 是一个以域为粒度、汇总设备意愿、尊重硬件依赖的电源决策层。我个人在实际项目里的体会是电源域这块最花时间的不是写代码而是把硬件供电拓扑准确地翻译成设备树描述。硬件手册里一张供电框图对应到设备树里就是一堆power-domains引用和层级关系任何一处对不上都会在待机功耗或者上下电时序上以某种隐蔽的方式暴露出来。所以我的习惯是拿到新板子先把供电框图抄一遍逐个域对照设备树核对这个前置工作做扎实了后面调功耗会省掉大量返工。
返回列表