1. 功耗子系统里的“供电管家”:regulator framework 到底管什么
搞嵌入式 Linux 的兄弟大概率都遇到过这种场景:板子跑起来,某个外设时好时坏,示波器一量供电电压在抖;或者系统进 suspend 之后电流死活降不下去,查了半天发现是某路 LDO 没关。这类问题的根子,十有八九落在电源管理这块。而 Linux 内核里专门负责“给各个外设分配、开关、调节供电”的那套机制,就是regulator framework,中文一般叫“稳压器框架”或者“调压器子系统”。
先把定位说清楚。regulator 在硬件上指的是那些能输出稳定电压/电流的器件,比如 PMIC 里的各路 LDO、BUCK(DCDC),外挂的独立稳压芯片,甚至某些 GPIO 控制的简单电源开关。内核里的 regulator framework 就是给这些硬件做一层统一抽象,让驱动开发者不用关心底下到底是 I2C 挂的 PMIC 还是 SPI 挂的独立芯片,统一用regulator_get、regulator_enable、regulator_set_voltage这套 API 就能把电管起来。
它解决的核心痛点有三个。第一是资源归属混乱:一块板子上往往十几路电源,谁给谁供电、什么时候能关,如果没有统一登记,很容易出现 A 驱动把电关了结果 B 驱动还在用的惨案。第二是电压/电流的精细控制:CPU 调频要动态调 core 电压(DVFS),摄像头要按分辨率切供电档位,这些都需要框架提供标准接口。第三是省电:系统空闲时把没用的 regulator 关掉,是待机功耗优化的重头戏。
这篇文章适合谁看?如果你在写字符设备驱动、平台驱动,需要给自己的硬件申请电源;或者你在做 BSP 移植,要往设备树里写 regulator 节点;又或者你在排查“某个外设上电时序不对”“suspend 后漏电”这类问题,那这篇梳理就是给你准备的。我会按“整体设计思路 → 核心数据结构与 API → 设备树与驱动实操 → 常见坑排查”这条线走一遍,尽量把框架背后的“为什么这么设计”讲透,而不是只罗列函数原型。
需要提前说明的是,regulator framework 的代码量不小,drivers/regulator/目录下核心文件加上各厂商驱动轻松过万行。我这里聚焦的是通用框架层(core.c、内部 consumer/provider 机制、设备树解析),厂商私有驱动的实现细节只做必要提及。另外不同内核版本(4.x、5.x、6.x)在细节上有差异,我以 5.10 到 6.x 这条主线为主,遇到版本差异会单独点出来。
2. 框架整体设计与核心思路拆解
2.1 为什么要有 provider 和 consumer 两层抽象
理解 regulator framework,最关键的是抓住provider(供应方)和consumer(使用方)这对概念。这是整个框架的骨架,想通了这一层,后面所有 API 都能对上号。
Provider 就是稳压器本身的驱动,比如drivers/regulator/rk808-regulator.c、max77620-regulator.c这类,它负责描述“我这几路电源能输出什么范围的电压、支持哪些工作模式、怎么通过寄存器去设置”。Consumer 则是使用电源的外设驱动,比如某个 SD 卡控制器、某个 sensor 驱动,它关心的是“我要一路 3.3V、最大 200mA 的电,给我”。
这么分层的好处非常直接:解耦。外设驱动不需要知道供电芯片是哪个型号、挂在哪条总线上,它只跟框架打交道;供电芯片驱动也不需要知道自己的电被谁用了,它只负责响应框架转发过来的请求。中间这层框架还顺便做了引用计数、状态跟踪、约束检查这些脏活累活。
我打个生活化的比方。Provider 就像小区里的配电房,它知道自己能供多少电、有几路闸;Consumer 就像各家住户,住户不需要知道电从哪个电厂来的,只要打开开关有电就行;而 regulator framework 就是物业,负责登记谁家用了哪路电、什么时候能拉闸、电压不稳了找谁。没有物业,住户和配电房直接对接,一旦住户多了、需求变了,管理就彻底乱套。
2.2 核心数据结构:从 regulator_dev 到 regulator
框架内部有几个关键结构体,理清它们的关系,看代码就不会迷路。
struct regulator_dev是 provider 侧的核心,一个实例代表一路物理稳压器输出。它由 provider 驱动通过devm_regulator_register注册进来,里面挂着描述符regulator_desc、约束regulation_constraints、当前状态、操作函数集regulator_ops等。
struct regulator是 consumer 侧拿到的句柄,regulator_get返回的就是它。注意它和regulator_dev不是一回事:一个regulator_dev可以被多个 consumer 共享,每个 consumer 拿到的是自己的regulator句柄,框架靠这个句柄做引用计数。这就像配电房只有一路 3.3V,但可以同时供三个住户,每个住户手里有自己的“用电凭证”。
struct regulator_desc描述的是“这一路电源的静态属性”:名字、供电类型(电压还是电流)、支持的电压档位表(volt_table或min_uV/max_uV/uV_step)、寄存器地址、使能方式等。provider 驱动基本就是填这个结构体。
struct regulation_constraints描述的是“这一路电源被允许怎么用”:电压上下限、能不能改、启动时是否使能、suspend 时的状态、always-on 标志等。它主要来自设备树,是板级配置的落点。
struct regulator_ops是操作函数集,provider 驱动实现它,框架调用它。核心回调包括enable、disable、is_enabled、set_voltage、get_voltage、set_current_limit、set_mode等。不是每个都要实现,框架会根据regulator_desc里声明的能力去判断。
2.3 引用计数与状态机:为什么关电不能想关就关
这是框架里最容易被忽视、但出问题最多的部分。每个regulator句柄都有enable_count,regulator_enable加一,regulator_disable减一,只有减到零框架才真正去调 provider 的disable。这个设计是为了解决共享电源的问题:三个驱动都用同一路电,谁都不能因为自己不用了就把它关了。
状态机方面,框架维护了regulator_dev的state,包括电压、电流、模式、使能状态。每次操作前框架会做约束检查,比如你请求 1.8V 但约束里写死了 3.3V,框架会拒绝并打印警告。这个检查在调试阶段特别有用,能帮你快速定位“为什么我设的电压没生效”。
提示:
enable_count是 per-consumer 的,不是全局的。也就是说 A 驱动 enable 两次、B 驱动 enable 一次,底层实际只 enable 一次,但计数是 2 和 1。排查“电关不掉”时,先看是不是有驱动 enable 了没 disable。
2.4 与功耗子系统其他模块的协作关系
regulator framework 不是孤立的,它和几个模块有明确分工。和clock framework是并列关系,一个管电、一个管时钟,很多外设上电时序要求“先供电再给时钟”,这个顺序得驱动自己保证。和PM domain / genpd是配合关系,genpd 管的是整个电源域的开关,regulator 管的是具体某路电,粒度不同。和OPP(Operating Performance Points)关系密切,CPU DVFS 调频时,OPP 表里会关联对应的电压,最终通过 regulator 去设置。
理解这层协作关系,能帮你在排查“调频时电压没跟上”“suspend 时某路电没关”这类跨模块问题时,快速定位到该看哪个子系统。
3. 核心 API 与实操要点详解
3.1 Consumer 侧 API:外设驱动怎么申请和用电
Consumer 侧最常用的几个 API,我按使用顺序列一下,并说清楚每个的坑。
获取句柄用regulator_get(dev, id)或devm_regulator_get(dev, id)。强烈建议用devm_版本,它会在设备卸载时自动释放,省得你手动regulator_put漏掉。id是设备树里定义的电源名字,比如"vdd"、"avdd"。如果设备树里没配,regulator_get会返回一个 dummy regulator(除非内核配置了严格模式),这时候 enable/set_voltage 都是空操作,不会报错——这个特性在早期调试阶段很坑,会让你误以为电配好了其实根本没配。
设置电压用regulator_set_voltage(regulator, min_uV, max_uV)。注意它传的是范围,框架会在范围内选一个最接近的值。如果你想要精确值,min 和 max 传一样。返回值要检查,失败通常是约束不允许或者硬件不支持。
使能用regulator_enable/regulator_disable。前面说了引用计数,这里补充一个实操点:enable 失败一定要处理,常见原因是上一级电源没开、或者硬件本身有问题。我见过有驱动 enable 返回值直接忽略,结果后面访问寄存器全返回 0xFFFFFFFF,查了半天才发现是电没上。
还有几个进阶 API:regulator_set_load设置负载电流(影响 LDO 的功耗模式选择)、regulator_set_mode设置工作模式(如 fast/normal/idle)、regulator_get_voltage读当前电压。这些在功耗敏感场景会用到。
3.2 Provider 侧注册流程:写一个稳压器驱动要做什么
Provider 驱动的骨架其实很固定,我按步骤拆一下。
第一步,定义regulator_desc数组,每一路电源一个。关键字段:name(必须唯一)、of_match(设备树匹配名)、type(REGULATOR_VOLTAGE 或 REGULATOR_CURRENT)、owner(THIS_MODULE)、ops(操作函数集)、n_voltages和volt_table(或者min_uV/uV_step)、enable_reg/enable_mask(如果使能是寄存器控制)、vsel_reg/vsel_mask(如果调压是寄存器控制)。
第二步,实现regulator_ops。最基础的是enable、disable、is_enabled、list_voltage、set_voltage(或set_voltage_sel)、get_voltage(或get_voltage_sel)。如果硬件支持,再实现set_mode、get_mode、set_current_limit等。
第三步,在 probe 里解析设备树、初始化硬件、然后对每一路调用devm_regulator_register(dev, &desc[i], &config)。config里带of_node和regmap(如果用 regmap 访问寄存器)。
第四步,处理of_match_table,让设备树能匹配上。
这里有个经验点:list_voltage和set_voltage_sel的索引必须对齐。框架用 selector(索引)来操作电压,list_voltage(dev, selector)返回该索引对应的电压,set_voltage_sel(dev, selector)把硬件设到该索引。如果你用volt_table,框架会自动处理;如果用min_uV/uV_step线性计算,要保证list_voltage和硬件实际档位一致,否则会出现“设了 1.8V 实际出来 1.9V”的问题。
3.3 设备树里的 regulator 节点怎么写
设备树是板级配置的核心,regulator 节点写不对,驱动再对也白搭。我拿一个典型的 PMIC 举例说明结构。
Provider 侧节点挂在 PMIC 的 I2C 节点下,比如:
&i2c1 { pmic: pmic@20 { compatible = "vendor,pmic"; reg = <0x20>; regulators { compatible = "simple-bus"; vdd_cpu: buck1 { regulator-name = "vdd_cpu"; regulator-min-microvolt = <800000>; regulator-max-microvolt = <1300000>; regulator-ramp-delay = <10000>; regulator-always-on; }; vdd_io: ldo1 { regulator-name = "vdd_io"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <3300000>; regulator-boot-on; }; }; }; };Consumer 侧引用:
&sdmmc { vmmc-supply = <&vdd_io>; vqmmc-supply = <&vdd_io>; };几个关键属性解释一下。regulator-min/max-microvolt是约束,框架会据此检查 consumer 的请求。regulator-always-on表示这路电永远不关,适合给 CPU、DDR 这种核心供电。regulator-boot-on表示 bootloader 已经开了,内核别去动它,适合那些“关了系统就挂”的电。regulator-ramp-delay是电压爬升时间(微伏每毫秒或微秒,看具体 binding),DVFS 调压时框架会用它做延时,设不对会导致调频时电压没稳定就切频率,系统直接崩。
注意:
regulator-always-on和regulator-boot-on语义不同。前者是“内核保证它一直开”,后者是“内核启动时它已经是开的,别乱关”。搞混了会出现“明明写了 always-on 结果 suspend 后还是关了”的情况,因为 suspend 路径有单独的约束。
3.4 约束检查与调试接口
框架提供了 sysfs 调试接口,在/sys/class/regulator/下,每一路电源一个目录,里面有name、state、microvolts、num_users等文件。num_users就是当前 enable 计数,排查“谁在用这路电”时非常有用。
/sys/kernel/debug/regulator/下还有更详细的信息(需要开 debugfs),包括每路电源的约束、当前状态、consumer 列表。我排查电源问题时,第一步就是cat /sys/kernel/debug/regulator/regulator_summary,一眼能看出哪路电开着、电压多少、谁在用。
约束检查失败时,内核会打印类似regulator: failed to set voltage的警告,带上具体数值。看到这种 log,先确认设备树里的 min/max 范围是否覆盖了驱动请求的值,再看 provider 的volt_table是否支持该档位。
4. 完整实操流程与关键环节实现
4.1 从零梳理一块板子的供电拓扑
拿到一块新板子,别急着写代码,先把供电拓扑画出来。我的做法是:翻原理图,找出所有稳压器输出,标出每路电压、最大电流、给哪些外设供电、有没有 GPIO 使能脚、有没有 I2C 控制。这一步花半小时,能省后面几天的调试。
举个我实际遇到的例子。一块板子上有个 sensor,原理图上它的供电标着“1.8V”,但没标来源。我顺着网络标号查,发现它接在一个 load switch 上,load switch 的输入又来自 PMIC 的 LDO3。而 LDO3 在设备树里被配成了regulator-always-on,但 load switch 的使能脚是个 GPIO,默认拉低。结果就是 sensor 一直没电,驱动 probe 失败。这种“两级供电”的拓扑,光看设备树是看不出来的,必须对着原理图理。
理清拓扑后,把每一路电源在设备树里的节点规划好:哪些是 provider 直接输出的,哪些需要 GPIO 控制的 fixed regulator,哪些是 always-on 的核心电。
4.2 用 fixed-regulator 处理 GPIO 控制的电源
不是所有电源都由 PMIC 控制,很多板子上有简单的 GPIO 使能电源,比如 USB VBUS、某些外设的独立供电。这种用regulator-fixed驱动处理最合适。
设备树写法:
vdd_sensor: regulator-sensor { compatible = "regulator-fixed"; regulator-name = "vdd_sensor"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; gpio = <&gpio1 12 GPIO_ACTIVE_HIGH>; enable-active-high; startup-delay-us = <1000>; off-on-delay-us = <5000>; };gpio指定使能脚,enable-active-high表示高电平使能(如果是低电平使能就不写或写enable-active-low)。startup-delay-us是上电后等待稳定的时间,off-on-delay-us是关电到再开电之间的最小间隔——这个参数很多人忽略,但某些器件要求断电后必须等一段时间才能重新上电,否则会闩锁。我踩过一次坑:一个 sensor 快速开关电后直接不响应,加了off-on-delay-us就好了。
fixed regulator 的 consumer 用法和普通 regulator 完全一样,regulator_get拿到句柄后 enable/disable 即可。它的好处是不用写驱动代码,纯设备树配置。
4.3 上电时序与 DVFS 调压的实操细节
上电时序是嵌入式电源管理里最容易出问题的地方。很多芯片要求“core 电先上,IO 电后上”,或者“某路电必须在时钟之前稳定”。框架本身不保证顺序,顺序得靠驱动或设备树里的依赖关系来保证。
一种做法是在 consumer 驱动里显式控制顺序:先regulator_get并 enable 第一路,延时,再 enable 第二路。另一种做法是利用设备树的*-supply属性建立依赖,框架在解析时会按依赖顺序处理。但要注意,*-supply主要影响的是 regulator 之间的父子关系(比如 LDO 的输入来自 BUCK),不直接控制 consumer 的上电顺序。
DVFS 调压这块,核心是regulator_set_voltage的调用时机。CPU 调频的标准流程是:先升压再升频,先降频再降压。这个顺序不能反,反了会在高频低压下跑,直接死机。内核的 cpufreq 框架和 OPP 框架已经处理了这个顺序,但如果你自己写调频逻辑,一定要遵守。
regulator-ramp-delay这个参数在 DVFS 里至关重要。它告诉框架电压从 A 变到 B 需要多久,框架会在这段时间内等待(或轮询),确保电压稳定后才返回。设小了,电压还没到位就切频率,系统不稳;设大了,调频延迟增加,性能受影响。我的经验是:查 datasheet 里的 ramp rate,取典型值,然后实测调频时的电压波形,用示波器确认稳定时间,再微调。
4.4 suspend/resume 中的 regulator 行为
系统进 suspend 时,regulator 的处理分几种情况。标了regulator-always-on的,保持开启。标了regulator-boot-on但没标 always-on 的,框架会根据约束决定是否关闭。其他没特殊标记的,如果 enable_count 为零,会被关闭。
这里有个大坑:suspend 时关闭的电源,resume 后不一定会自动恢复。框架会保存 suspend 前的状态,resume 时恢复,但如果你的驱动在 suspend 回调里手动关了电,resume 回调里忘了开,就会出现“resume 后外设不工作”。我的做法是:在驱动的 suspend 回调里尽量不动 regulator,让框架统一管理;如果必须动,resume 里一定要对称恢复。
另外,regulator_set_suspend_voltage可以设置 suspend 时的电压,适合那些 suspend 时需要降压省电的场景。但这个 API 用得不多,多数情况保持默认即可。
5. 常见问题与排查技巧实录
5.1 regulator 申请失败与 dummy regulator 陷阱
最常见的报错是regulator_get返回错误指针,或者返回了一个 dummy regulator。前者通常是设备树里*-supply没配、名字写错、或者 provider 还没注册。后者是内核配置了CONFIG_REGULATOR_DUMMY且没找到对应电源,框架给个假的让你继续跑。
dummy regulator 的坑在于:它让regulator_enable返回成功,但实际什么都没做。如果你的驱动依赖电真的上了才能访问寄存器,就会在后面莫名其妙地失败。排查方法:看启动 log 里有没有dummy字样,或者cat /sys/kernel/debug/regulator/regulator_summary看有没有dummy条目。
要禁用 dummy 行为,可以关掉CONFIG_REGULATOR_DUMMY,或者在设备树里确保每个*-supply都能找到对应 provider。生产固件建议关掉 dummy,让问题在开发阶段就暴露。
5.2 电压设置不生效的几种原因
“我明明设了 1.8V,读回来还是 3.3V”——这个问题我遇到过至少三种原因。
第一种,约束不允许。设备树里regulator-min/max-microvolt范围没覆盖 1.8V,框架直接拒绝,但有些驱动不检查返回值,看起来就像没生效。查 log 能看到not allowed by constraints的警告。
第二种,selector 对不上。provider 的list_voltage返回的档位和硬件实际档位不一致,框架算出的 selector 设下去,硬件出来的是另一个电压。这种要对着 datasheet 的电压表逐档核对。
第三种,硬件本身不支持动态调压。有些 LDO 只能通过外部电阻设定电压,寄存器改不了。这种在regulator_desc里就不该声明set_voltage能力,否则框架以为能调,实际调了个寂寞。
5.3 电源关不掉与漏电排查
“系统 suspend 后电流还有几十毫安”——这是待机功耗优化的经典问题。排查思路:先看regulator_summary,找出 suspend 后还开着的电源,逐个确认是否应该开。
常见原因有几个。一是某个驱动 enable 了没 disable,num_users不为零。二是regulator-always-on标多了,把本该关的电标成了常开。三是硬件上有漏电通路,比如某个 GPIO 状态不对导致电流倒灌。四是 suspend 顺序问题,某个外设在电源关闭前还在工作,拉高了电流。
我的排查顺序是:先在 suspend 前 dump 一次regulator_summary,suspend 后再 dump 一次(如果系统还能唤醒),对比差异。然后逐个关掉可疑电源做二分测试,定位到具体哪路电。最后结合原理图确认这路电该不该关。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| regulator_get 返回错误 | 设备树 supply 未配或名字错 | 检查*-supply属性和 provider 的regulator-name |
| enable 成功但外设不工作 | dummy regulator 或上一级电源未开 | 查regulator_summary,确认电压和 num_users |
| 设电压无效 | 约束不允许或 selector 不匹配 | 查 log 警告,核对 volt_table |
| suspend 后漏电 | 电源未关或 always-on 标多 | 对比 suspend 前后 regulator_summary |
| 调频时死机 | ramp-delay 太小或调压顺序反 | 示波器看电压波形,确认先升压后升频 |
| 快速开关电后器件不响应 | 缺少 off-on-delay | 加off-on-delay-us属性 |
5.5 几个我踩过的坑和独家技巧
第一个坑:regulator_bulkAPI 的批量操作。当你要同时操作多路电源时,regulator_bulk_get、regulator_bulk_enable能省不少代码。但要注意,bulk enable 是逐个 enable 的,如果中间某路失败,前面已经 enable 的不会自动回滚,需要自己处理。我一般会在失败时手动 disable 已成功的那些。
第二个坑:设备树里 regulator 节点的compatible别乱写。有些 PMIC 的 regulator 子节点用simple-bus作为 compatible,有些用厂商自定义的。写错了 provider 驱动匹配不上,整路子节点都不注册。这个错误很隐蔽,因为不会报错,只是电源“消失”了。
第三个技巧:用regulator_get_optional处理可选电源。有些外设的某路电是可选的(比如某些封装才有),用regulator_get会返回错误导致 probe 失败。regulator_get_optional在找不到时返回 NULL 而不是错误,驱动里判断一下即可。这个 API 在处理兼容多种硬件版本时特别有用。
第四个技巧:调试时打开CONFIG_REGULATOR_DEBUG。它会打印每次 enable/disable/set_voltage 的调用者和参数,排查“谁在动我的电”时一目了然。生产固件记得关掉,log 量很大。
6. 写在最后的一点个人体会
regulator framework 这套东西,刚接触时容易被一堆结构体和 API 绕晕,但抓住 provider/consumer 这条主线,再理解引用计数和约束检查这两个机制,剩下的就是查手册填参数的事。我自己的经验是,与其死磕 core.c 的源码,不如先拿一块现成的板子,把regulator_summary看懂,把设备树里的每个 regulator 节点和原理图对上号,理解会快得多。
真正花时间的从来不是框架本身,而是硬件那些“不成文的规定”——上电时序、ramp rate、off-on delay、哪路电必须常开。这些 datasheet 里往往写得含糊,得靠实测和踩坑积累。所以每次做新板子,我都会先花时间把供电拓扑和时序要求整理成一张表,贴在工位上,写驱动和调 suspend 的时候随时对照,比事后 debug 省事太多。
后面如果要做更细的功耗优化,可以顺着 regulator 往下挖 OPP 和 genpd,再结合 cpuidle 和 cpufreq,那是一条完整的待机功耗优化链路。regulator 是这条链路的底座,底座稳了,上面的优化才有意义。