1. regulator framework 在功耗子系统里到底管什么
先把场景摆出来。你拿到一块嵌入式板子,SoC 是常见的 ARM64 平台,外设一堆:eMMC、WiFi 模组、摄像头、音频编解码器、屏幕背光。这些外设各自需要不同的供电电压,有的要 1.8V,有的要 3.3V,有的还要在运行时动态调压省电。如果每个驱动都自己写一套 GPIO 拉高拉低、I2C 写 PMIC 寄存器的代码,那内核里会乱成一锅粥,而且电压域之间的依赖关系根本没法统一管理。
regulator framework 就是干这件事的。它是 Linux 内核功耗子系统里的一个通用框架,负责抽象"供电"这个动作。你可以把它理解成内核里的"电源调度中心":谁需要电、需要多少伏、什么时候开、什么时候关、能不能调压、多个设备共享一路电时怎么协调,全部由它统一管。
关键词里的regulator framework、功耗子系统、通用框架,其实指向的是同一件事——内核如何用一套标准化的模型来描述和管理电压调节器。它属于内核驱动框架层,向上给各种设备驱动提供regulator_get、regulator_enable、regulator_set_voltage这类 API,向下对接具体的 PMIC 驱动(比如常见的 RK8xx、AXP 系列、MAX 系列等)。
为什么要有这层抽象?因为电源管理是嵌入式系统里最容易出问题、也最容易被忽视的部分。电压给错了,轻则外设不工作,重则烧芯片。而 regulator framework 把"电压域"这个概念做成了内核对象,让依赖关系、开关顺序、电压约束都能被描述和校验。
这篇文章适合谁看?如果你正在做嵌入式 Linux 驱动开发,或者在看内核源码时被regulator相关的结构体和回调绕晕,又或者你只是想知道"我板子上的电到底是怎么被管起来的",那这篇梳理就是给你准备的。我会从框架的核心数据结构讲起,一路走到实际驱动里怎么用、怎么调、怎么排错。
2. 从设备树到内核对象:regulator 的抽象模型
2.1 三个核心结构体的分工
regulator framework 的代码主要在drivers/regulator/目录下,核心头文件是include/linux/regulator/driver.h和consumer.h。理解这个框架,先要抓住三个结构体,它们分别对应"描述能力""描述实例""描述使用者"。
第一个是struct regulator_desc,它描述的是一个 regulator 的静态能力。比如这个 regulator 支持哪些电压档位、有几个可选电压、能不能通过 I2C 调压、有没有 enable 引脚、工作模式有哪些。这个结构体通常在具体 PMIC 驱动里定义,是硬件能力的直接映射。
第二个是struct regulator_dev,它是 regulator 在内核里的实例。一个regulator_desc可以对应多个regulator_dev,比如同一颗 PMIC 上有好几路输出,每路都是独立的 regulator 设备。regulator_dev会注册到 regulator 核心层,拥有自己的设备节点、约束条件和状态。
第三个是struct regulator,这是消费者视角的句柄。设备驱动调用regulator_get()拿到的就是这个东西。它代表"某个设备正在使用某路电"这个关系。一个 regulator 可以被多个消费者共享,框架会记录引用计数。
这三者的关系可以这样类比:regulator_desc是电器的规格书,regulator_dev是实际装好的那台电器,regulator是你手里的遥控器。规格书只有一份,但同型号电器可以有很多台,遥控器也可以有好几个。
2.2 设备树是怎么把硬件描述进来的
现代内核里,regulator 的硬件信息基本都从设备树来。一个典型的 PMIC 节点长这样:
pmic: pmic@20 { compatible = "rockchip,rk808"; reg = <0x20>; regulators { vdd_cpu: DCDC_REG1 { regulator-name = "vdd_cpu"; regulator-min-microvolt = <750000>; regulator-max-microvolt = <1350000>; regulator-always-on; regulator-boot-on; }; vdd_logic: DCDC_REG2 { regulator-name = "vdd_logic"; regulator-min-microvolt = <800000>; regulator-max-microvolt = <1200000>; }; }; };这里每个子节点都会被解析成一个regulator_dev。regulator-min-microvolt和regulator-max-microvolt是硬约束,框架会保证任何调压请求都落在这个区间内。regulator-always-on表示这路电不允许被关闭,regulator-boot-on表示启动阶段就要打开。
设备树解析的入口在drivers/regulator/of_regulator.c,核心函数是of_get_regulator_init_data()。它会把设备树里的约束条件读出来,填进struct regulation_constraints,然后交给核心层做校验。
注意:
regulator-min-microvolt和regulator-max-microvolt不是随便填的。填窄了,驱动想调压会被拒绝;填宽了,框架失去了保护作用。正确做法是查 PMIC 数据手册,按实际支持的输出范围填。
2.3 约束条件:框架的"安全护栏"
struct regulation_constraints是 regulator framework 里最容易被低估的部分。它不只是记录电压范围,还包含一大堆约束标志:
| 约束字段 | 含义 | 典型使用场景 |
|---|---|---|
| min_uV / max_uV | 电压上下限 | 所有 regulator 必填 |
| always_on | 不允许关闭 | CPU 核心供电 |
| boot_on | 启动时打开 | 关键外设供电 |
| valid_ops_mask | 允许的操作 | 限制可调压/可开关 |
| valid_modes_mask | 允许的工作模式 | 省电模式选择 |
| initial_mode | 初始工作模式 | 启动时的模式 |
| apply_uV | 启动时立即设压 | 需要精确初始电压 |
这些约束在 regulator 注册时由核心层校验。如果驱动请求的操作不在valid_ops_mask里,regulator_set_voltage()会直接返回错误,而不是悄悄失败。这个设计很关键——它把"硬件不支持"和"驱动写错了"区分开了。
我在实际项目里遇到过一次典型问题:某路 regulator 在设备树里没写valid_ops_mask,结果驱动调用regulator_set_mode()一直返回-EINVAL。查了半天才发现是约束没放开。后来在设备树里补上对应的模式掩码就好了。这个坑的教训是:约束不是可选项,它决定了框架允许你做什么。
3. 消费者 API 的使用逻辑与常见误用
3.1 regulator_get 到底做了什么
设备驱动要用电,第一步是拿句柄:
struct regulator *reg; reg = regulator_get(dev, "vdd"); if (IS_ERR(reg)) { return PTR_ERR(reg); }regulator_get()的第一个参数是消费者设备,第二个是供电名称。这个名称会和设备树里 consumer 节点的*-supply属性匹配。比如设备树里写vdd-supply = <&vdd_cpu>;,那驱动里就用"vdd"去 get。
框架内部会做几件事:查找对应的regulator_dev、创建或复用regulator句柄、增加引用计数、检查约束是否允许该消费者使用。如果找不到匹配的 regulator,会返回-EPROBE_DEFER,这是很常见的延迟探测信号,说明 regulator 还没注册好,驱动应该稍后重试。
这里有个容易踩的坑:regulator_get()返回的句柄必须配对regulator_put()。很多驱动在 probe 失败路径上忘了 put,导致引用计数泄漏。虽然单次泄漏不一定立刻出问题,但在频繁热插拔或模块反复加载的场景下,计数会越来越乱。
3.2 enable/disable 与电压设置的顺序
拿到句柄后,典型的使用流程是:
ret = regulator_set_voltage(reg, 1800000, 1800000); if (ret) goto err_put; ret = regulator_enable(reg); if (ret) goto err_put;注意顺序:先设压,再使能。原因是很多 PMIC 在输出已经打开的情况下调压,会产生瞬态波动,可能触发外设复位。先设好目标电压再打开,输出从一开始就是稳定的。
但也不是绝对的。如果这路电已经被别的消费者打开了,你再去 set_voltage 就是在改一个正在工作的电压域,这时候框架会检查所有消费者的约束,确保新电压对所有人都合法。如果有一个消费者要求最低 1.9V,你设 1.8V 就会被拒绝。
regulator_enable()和regulator_disable()是引用计数的。两个驱动都 enable 同一路电,只有两个都 disable 之后,实际输出才会关。这个机制保证了共享供电的安全性。
3.3 电压设置的两个版本:set_voltage 与 set_voltage_sel
框架提供两套调压 API:
regulator_set_voltage(reg, min_uV, max_uV):用微伏为单位,框架自己选最接近的档位。regulator_set_voltage_sel(reg, selector):直接指定档位索引。
大多数驱动用前者就够了。后者一般用在需要精确控制档位的场景,比如配合regulator_list_voltage()做电压表遍历。
regulator_set_voltage()的内部逻辑值得说一下:它会把请求的 min/max 和当前约束、其他消费者的约束做交集,然后调用具体驱动的set_voltage回调。回调返回实际选中的档位,框架再读回实际电压。如果驱动不支持连续调压,框架会在支持的离散档位里找最接近的。
提示:调压后建议用
regulator_get_voltage()读回实际值确认。有些 PMIC 的档位是离散的,你请求 1.8V,实际可能落在 1.79V 或 1.81V,读回能避免"以为设对了其实没对"的问题。
3.4 常见误用清单
我在 review 驱动代码时见过不少 regulator 的误用,整理成表格方便对照:
| 误用方式 | 后果 | 正确做法 |
|---|---|---|
| get 后不 put | 引用计数泄漏 | 所有失败路径都要 put |
| 先 enable 再 set_voltage | 瞬态波动 | 先设压再使能 |
| 忽略 set_voltage 返回值 | 电压没设上但继续跑 | 检查返回值并处理 |
| 在原子上下文调 regulator API | 可能睡眠导致崩溃 | 确认 API 是否可原子调用 |
| 多个驱动争抢同一路电不协调 | 电压冲突 | 用约束和引用计数协调 |
其中"原子上下文"这条特别隐蔽。regulator_set_voltage()可能触发 I2C 通信,而 I2C 传输会睡眠。如果你在中断处理函数里调用它,系统直接挂。框架里只有部分 API 标注了regulator_can_sleep(),用之前最好确认一下。
4. 驱动侧:怎么把一颗 PMIC 接进框架
4.1 注册流程的骨架
写一个 regulator 驱动,核心工作是填regulator_desc和regulator_config,然后调devm_regulator_register()。骨架大概是这样:
static const struct regulator_ops my_reg_ops = { .enable = my_reg_enable, .disable = my_reg_disable, .is_enabled = my_reg_is_enabled, .set_voltage_sel = my_reg_set_voltage_sel, .get_voltage_sel = my_reg_get_voltage_sel, .list_voltage = my_reg_list_voltage, }; static const struct regulator_desc my_reg_desc = { .name = "my_reg", .of_match = my_reg_of_match, .ops = &my_reg_ops, .type = REGULATOR_VOLTAGE, .owner = THIS_MODULE, .n_voltages = ARRAY_SIZE(my_voltages), .volt_table = my_voltages, .enable_reg = MY_ENABLE_REG, .enable_mask = MY_ENABLE_BIT, }; static int my_pmic_probe(struct i2c_client *client) { struct regulator_config config = { }; struct regulator_dev *rdev; config.dev = &client->dev; config.of_node = client->dev.of_node; config.regmap = my_regmap; rdev = devm_regulator_register(&client->dev, &my_reg_desc, &config); if (IS_ERR(rdev)) return PTR_ERR(rdev); return 0; }regulator_ops是驱动能力的核心。框架不会要求你实现所有回调,但至少要有enable/disable或者set_voltage_sel/get_voltage_sel中的一组,否则这路 regulator 没有实际意义。
4.2 volt_table 与线性映射的选择
电压档位有两种描述方式。一种是volt_table,直接给一个电压数组:
static const unsigned int my_voltages[] = { 900000, 1000000, 1100000, 1200000, };另一种是线性映射,用min_uV、uV_step、n_voltages描述:
.min_uV = 800000, .uV_step = 25000, .n_voltages = 33,选哪种取决于硬件。如果 PMIC 的档位是均匀步进的,线性映射更省内存;如果档位是跳跃的、非线性的,就必须用volt_table。
这里有个细节:n_voltages必须和实际档位数一致。填多了,框架会允许选择不存在的档位,写进寄存器就是未定义行为;填少了,可用的电压范围被白白缩小。我一般会写个编译期断言或者运行时校验,确保n_voltages和表长度匹配。
4.3 regmap 在 regulator 驱动里的角色
现代 regulator 驱动几乎都基于 regmap。原因是 PMIC 通常同时提供 regulator、GPIO、RTC、ADC 等多种功能,大家共享同一套寄存器访问逻辑。regmap 把 I2C/SPI 读写抽象成统一的regmap_read()/regmap_write(),还自带缓存和锁。
在regulator_config里传入config.regmap,框架在调用你的set_voltage_sel等回调时,你可以直接用这个 regmap 操作寄存器。好处是寄存器访问的并发安全由 regmap 保证,你不用自己加锁。
但要注意:regmap 的缓存策略会影响 regulator 状态读取。如果开了寄存器缓存,regmap_read()可能读到缓存值而不是硬件真实值。对于is_enabled这种需要反映真实状态的回调,要么关掉对应寄存器的缓存,要么用regmap_read的强制刷新版本。
4.4 一个容易忽略的点:of_match 与驱动匹配
regulator_desc里的of_match字段决定了设备树节点怎么和这个 regulator 描述匹配。如果填错,设备树里的配置就应用不到对应的 regulator 上,表现是约束全部丢失,框架用默认值。
我见过一次因为of_match写成了另一个型号,导致某路电的always_on没生效,系统休眠时这路电被关掉,唤醒后外设直接失联。排查时用cat /sys/kernel/debug/regulator/regulator_summary才看出来约束是空的。这个 debugfs 节点是排查 regulator 问题的第一站,强烈建议记住。
5. 排查 regulator 问题的完整链路
5.1 先看 regulator_summary
遇到供电相关的问题,第一步永远是:
cat /sys/kernel/debug/regulator/regulator_summary这个输出会列出系统里所有 regulator 的名字、当前电压、开关状态、使用者、约束范围。典型输出片段:
regulator use open bypass voltage current min max ----------------------------------------------------------------------- vdd_cpu 2 2 0 1000mV 0mV 750mV 1350mV vdd_logic 1 1 0 900mV 0mV 800mV 1200mVuse是引用计数,open是硬件实际状态。如果use是 0 但open是 1,说明有驱动 enable 了没 disable,或者always_on在起作用。如果use大于 0 但电压是 0mV,说明调压没成功。
5.2 电压设不上去的三层排查
电压设不上去是最常见的问题,我一般按三层排查:
第一层:约束是否允许。检查设备树的regulator-min-microvolt/regulator-max-microvolt是否覆盖了你请求的值。如果请求 1.8V 但 max 只有 1.5V,框架直接拒绝。
第二层:驱动回调是否实现。如果regulator_ops里没有set_voltage_sel,框架会返回-EINVAL。用regulator_summary看不到回调信息,但可以看驱动源码确认。
第三层:硬件是否响应。如果前两层都没问题,但读回的电压不变,那可能是 I2C 通信失败、寄存器地址写错、或者 PMIC 处于某种保护状态。这时候要抓 I2C 波形或者读 PMIC 的状态寄存器。
5.3 一个真实的排查案例
之前有个项目,摄像头模组偶尔初始化失败。日志里能看到regulator_set_voltage返回 0,但摄像头就是不出图。用regulator_summary看,电压显示正常。
后来用示波器抓供电引脚,发现使能瞬间有一个明显的跌落,从 1.8V 掉到 1.5V 左右再恢复。原因是这路电的负载电容偏小,而摄像头模组上电瞬间电流冲击大。regulator_set_voltage返回成功只代表寄存器写成功了,不代表实际输出在负载下稳定。
解决办法是在设备树里给这路 regulator 加上regulator-ramp-delay,让框架知道这路电的爬升速度,从而在使能时留出足够的稳定时间。另外在硬件上加了滤波电容。这个问题说明:软件层面的成功不等于硬件层面的稳定,涉及模拟信号的问题最终要回到硬件去验证。
5.4 用 ftrace 跟踪 regulator 调用
如果问题出在调用时序上,比如某个驱动在错误的时机 enable 了 regulator,可以用 ftrace:
cd /sys/kernel/debug/tracing echo function > current_tracer echo regulator_enable > set_ftrace_filter echo 1 > tracing_on # 触发场景 cat trace这样能看到regulator_enable被哪些函数调用、调用顺序如何。对于分析"谁先谁后"的问题特别有效。
6. 多路供电与依赖关系处理
6.1 父子 regulator 的级联
实际硬件里,regulator 经常是级联的。比如一颗 PMIC 输出 3.3V,这个 3.3V 又供给另一颗 LDO,LDO 再输出 1.2V 给 CPU。这种关系在设备树里用vin-supply描述:
ldo1: LDO_REG1 { regulator-name = "vdd_cpu"; vin-supply = <&vcc3v3>; regulator-min-microvolt = <800000>; regulator-max-microvolt = <1200000>; };框架会识别这种依赖。当你 enable 子 regulator 时,它会先确保父 regulator 已经打开。disable 时则相反,先关子再关父。这个顺序如果搞反,会出现子级还有电但父级已经断开的诡异状态。
6.2 共享供电的协调
多个设备共用一路电的情况很常见。比如 eMMC 和 SD 卡槽可能共用 3.3V。这时候每个设备驱动各自regulator_get和regulator_enable,框架用引用计数保证只要有一个用户,电就不会断。
但共享供电有个隐患:电压约束是取交集的。如果 eMMC 要求 3.3V,SD 卡要求 3.0V 到 3.6V,那实际可调范围是 3.3V 到 3.3V。如果某个驱动想把电压降到 3.0V 省电,会被拒绝,因为 eMMC 不允许。
处理这种问题,要么在硬件上分开供电,要么在软件上协调各驱动的电压需求。我倾向于在设备树里就把约束写清楚,让框架在启动阶段就暴露冲突,而不是等到运行时才发现。
6.3 suspend/resume 中的 regulator 状态
系统休眠时,regulator 的状态管理是个大话题。always_on的 regulator 在休眠时保持输出,其他的可能被关闭。但关闭顺序和恢复顺序必须和设备的 suspend/resume 顺序匹配。
框架提供了regulator_suspend()和regulator_resume(),但实际行为取决于具体驱动和约束。如果某个设备在 resume 时需要供电,但它的 regulator 还没恢复,就会失败。解决办法通常是在设备树里给关键 regulator 加regulator-boot-on和regulator-always-on,或者调整设备的 suspend 优先级。
注意:休眠唤醒问题往往在实验室复现不出来,因为实验室环境温度稳定、电源干净。到了现场,温度变化和电源波动会让时序问题暴露。所以涉及 regulator 的休眠配置,建议留足余量,不要卡着时序边界设计。
7. 调试接口与日常维护建议
7.1 debugfs 里的 regulator 信息
除了regulator_summary,debugfs 下还有几个有用的节点:
/sys/kernel/debug/regulator/regulator_summary:全局概览。- 每个 regulator 在
/sys/kernel/debug/regulator/<name>/下有独立目录,可以看更详细的状态。
这些节点依赖CONFIG_DEBUG_FS和CONFIG_REGULATOR_DEBUG。生产固件通常关掉 debugfs 省空间,但调试固件一定要开。
7.2 sysfs 接口的有限使用
regulator 在 sysfs 下暴露的接口很有限,主要是/sys/class/regulator/下的只读信息。不像 GPIO 那样有完整的用户空间控制接口。这是有意为之——电压管理太危险,不适合让用户空间随意操作。
如果确实需要在用户空间调压(比如功耗测试),一般通过特定的测试驱动或者 debugfs 接口,而不是直接操作 sysfs。
7.3 日常维护的几个习惯
第一,每次改设备树的 regulator 配置,都要重新看一遍 regulator_summary。确认约束生效、电压正确、引用计数合理。
第二,驱动里所有 regulator API 的返回值都要检查。我见过太多regulator_enable()返回值被忽略,结果设备没上电却继续初始化,最后在别的地方崩掉,排查起来绕一大圈。
第三,probe 失败路径上的 regulator_put 和 regulator_disable 要成对。用devm_系列 API 可以省掉一部分手动清理,但不是所有场景都能用 devm。
第四,保留一份硬件供电框图。软件里的 regulator 树应该和硬件框图一一对应。如果对不上,要么是设备树写错了,要么是硬件设计有隐藏的供电关系。这份框图在排查跨模块供电问题时价值极高。
7.4 关于 regulator 与整体功耗的关系
最后说一个容易被忽视的点:regulator 本身也是功耗大户。LDO 的效率通常只有 60% 到 80%,剩下的都变成热。如果一路 LDO 长期给大电流负载供电,它的静态电流和压差损耗会显著影响系统续航。
所以在做低功耗设计时,不只是让设备进休眠,还要考虑 regulator 的工作模式。很多 PMIC 支持 PFM(脉冲频率调制)和 PWM(脉冲宽度调制)两种模式,轻载时 PFM 效率更高。框架里的regulator_set_mode()就是干这个的,配合regulator-mode设备树属性可以在启动时设定。
但模式切换不是没有代价的。PFM 模式下输出电压纹波更大,对噪声敏感的模拟电路可能不接受。所以模式选择要结合负载特性,不能一味追求效率。
我在实际项目里的体会是:regulator 这块,软件能做的优化有限,大头在硬件选型和供电架构设计。但软件至少要做到不添乱——约束写对、时序正确、该关的关掉。把这几件事做好,系统功耗和稳定性都会有明显改善。