1. 从一块电池的供电链路说起:regulator framework到底管什么
做过嵌入式Linux的人大概都遇到过这样的场景:板子上的WiFi模组时不时掉线,查了半天驱动没问题,最后发现是供电电压在负载突变时跌到了模组的最低工作门限以下。又或者,调试一颗摄像头传感器,I2C能通、寄存器能读,但图像就是出不来,折腾两天才意识到是MIPI供电域的LDO没使能。这类问题的共同点是——它们都不在"驱动逻辑"里,而在"电"上。
Linux内核的regulator framework就是专门管"电"的那一层。它要解决的问题很具体:一块SoC上通常挂着十几路甚至几十路供电,有DCDC、有LDO、有外部PMIC、有GPIO控制的负载开关,每一路的电压范围、电流能力、使能时序、父子依赖关系都不一样。如果让每个设备驱动自己去操作PMIC寄存器,代码会彻底失控——A驱动不知道B驱动已经把某路电压改了,C驱动在suspend时把D设备还要用的电给断了。regulator framework的价值就在于把这些供电资源抽象成统一的consumer/provider模型,让设备驱动用regulator_get()拿到一个句柄,用regulator_enable()、regulator_set_voltage()这样的标准接口去操作,而底层的寄存器操作、依赖管理、状态跟踪全部由framework统一协调。
这篇文章面向的是已经有一定Linux驱动基础、正在啃内核功耗子系统这块硬骨头的开发者。我会把regulator framework的通用框架从头到尾梳理一遍,包括核心数据结构之间的关系、provider和consumer两侧的注册与使用流程、设备树是怎么描述供电拓扑的、以及我在实际项目中踩过的那些坑。不会只讲API怎么调,更会讲清楚每个设计决策背后的原因——因为只有理解了"为什么这么设计",遇到问题时你才知道该往哪个方向查。
2. regulator framework的核心数据结构与它们之间的关系
2.1 三个关键结构体:regulator_dev、regulator_desc、regulator_config
理解regulator framework,第一步是把几个核心结构体的职责分清楚。很多人看源码时被struct regulator_dev、struct regulator_desc、struct regulator_config这三个名字搞晕,其实它们的定位很清晰。
struct regulator_desc是静态描述,它描述的是"这个regulator硬件本身是什么"。里面包含名字、供电类型(电压型还是电流型)、支持的电压寄存器地址、电压选择器的映射表(linear range或者table)、使能寄存器的bit位、操作模式等等。这部分信息在驱动编写时就是确定的,通常定义为static const,因为它不随运行时状态变化。
struct regulator_dev是运行时实例,它代表内核中一个活着的regulator设备。当你调用regulator_register()时,framework会根据desc创建出一个regulator_dev,把它挂到全局链表regulator_list上,同时建立sysfs节点。所有运行时的状态——当前电压、当前使能计数、当前模式、consumer列表——都记录在这里。
struct regulator_config是注册时的配置参数,它是一个临时结构体,用来把注册所需的各种信息打包传给regulator_register()。里面包含指向父设备的指针、指向desc的指针、init_data(来自设备树的初始化数据)、driver_data、以及可选的enable/disable回调(用于GPIO控制这种非标准使能方式)。
三者的关系可以这样类比:desc是产品的规格书,config是下单时填的配置单,regulator_dev是最终交付到你手上的那台设备。规格书不变,但你可以用不同的配置单下不同的单,最终得到多个独立的设备实例。
2.2 consumer侧:regulator和regulator_consumer
从使用者的角度看,设备驱动拿到的是struct regulator指针。这个结构体对consumer来说基本是个不透明的句柄,你不需要关心它内部有什么,只需要把它传给各种regulator_xxx()接口即可。
但在framework内部,struct regulator和struct regulator_dev之间是通过struct regulator_map关联的。当你调用regulator_get(dev, "vdd_core")时,framework会做这几件事:先在全局的regulator_map链表里查找有没有已经建立好的映射;如果没有,就根据设备树里consumer节点的vdd_core-supply属性找到对应的provider节点,再找到那个provider对应的regulator_dev,然后创建一条map记录,最后返回一个封装好的regulator句柄。
这里有个容易忽略的点:同一个regulator_dev可以被多个consumer共享。比如1.8V的IO电源可能同时供给WiFi、蓝牙、SD卡控制器。framework通过enable_count来管理这种共享关系——第一个consumer调用enable时真正操作硬件,后续consumer调用enable只是增加计数,只有所有consumer都disable之后才真正断电。这个机制避免了"一个驱动把别人还要用的电断了"的经典问题。
2.3 约束结构:regulation_constraints和machine_constraints
struct regulation_constraints是描述"这个regulator允许被怎么用"的约束集合。它来自设备树或board file,包含最小/最大电压、允许的电压列表、always-on标志、boot-on标志、允许的模式列表、以及各种延时参数。
这个结构体的存在非常关键。它是一道安全闸门——当consumer调用regulator_set_voltage()请求一个电压时,framework会先检查这个请求是否落在constraints允许的范围内,如果超出范围会直接拒绝并返回错误。我在一个项目里就遇到过因为设备树里min_uV写得太高,导致驱动请求1.2V时被拒绝,查了半天才发现是constraints的问题。
约束的另一个重要作用是初始化。系统启动时,framework会根据constraints里的regulator-boot-on、regulator-always-on、regulator-initial-mode等属性,在注册阶段就把regulator配置到正确的状态。这样即使没有consumer显式使能,关键的电源域也能保持在工作状态。
2.4 数据结构之间的关系图景
把这些结构体串起来看:设备树描述硬件拓扑,解析后生成init_data;init_data加上desc和config一起传给regulator_register(),创建出regulator_dev;consumer通过regulator_get()建立map,拿到regulator句柄;所有操作经过framework的约束检查和状态管理,最终通过desc里定义的回调或寄存器操作落到硬件上。
理解了这个数据流,再看源码时就不会迷路。每个函数在做什么、操作的是哪个结构体、为什么要经过这一层,都会变得清晰。
3. Provider侧:一个regulator驱动是怎么注册进内核的
3.1 从设备树匹配到probe函数被调用
写一个regulator provider驱动,起点和普通platform驱动一样:定义of_match_table,在probe函数里做注册。以一颗常见的PMIC为例,设备树里会有这样的节点:
pmic: pmic@58 { compatible = "vendor,pmic-x"; reg = <0x58>; regulators { compatible = "vendor,pmic-x-regulators"; vdd_core: buck1 { regulator-name = "vdd_core"; regulator-min-microvolt = <800000>; regulator-max-microvolt = <1300000>; regulator-boot-on; }; vdd_io: ldo1 { regulator-name = "vdd_io"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; regulator-always-on; }; }; };驱动probe时,先做常规的I2C初始化和寄存器配置,然后调用devm_regulator_register()为每一路regulator注册。注意这里用的是devm_版本,好处是设备卸载时自动清理,不用手写unregister。
3.2 regulator_desc的填充要点
填充desc时有几个字段特别容易出错,我逐个说。
of_match字段用于把设备树里的regulator子节点和desc对应起来。如果你的PMIC有多路regulator,通常定义一个desc数组,每路的of_match不同,framework会根据设备树节点的compatible或regulator-name来匹配。
vsel_reg和vsel_mask定义电压选择寄存器和掩码。如果电压是线性映射的,还要填min_uV、uV_step、n_voltages,framework会用公式voltage = min_uV + selector * uV_step来计算。如果是查表式的,就填volt_table数组。这里有个坑:n_voltages必须和实际可选择的数量严格一致,多一个少一个都会导致set_voltage时选到错误的selector。
enable_reg和enable_mask定义使能控制。有些PMIC的使能是"写1使能",有些是"写0使能",通过enable_val和disable_val来指定。如果使能是通过GPIO控制的,那就不填这些寄存器字段,而是在config里提供ena_gpiod。
ops字段指向你的操作函数集,至少要实现enable、disable、is_enabled、set_voltage、get_voltage这几个。如果硬件支持,还可以实现set_mode、get_mode、set_current_limit等。
3.3 regulator_config的组装与注册
config的组装相对直接,但有几个细节值得注意:
struct regulator_config config = { }; config.dev = &pdev->dev; config.of_node = np; /* 当前regulator子节点的of_node */ config.init_data = of_get_regulator_init_data(&pdev->dev, np, &desc); config.driver_data = priv; config.ena_gpiod = gpiod; /* 如果是GPIO使能 */of_get_regulator_init_data()这个调用很关键,它负责从设备树节点解析出constraints。如果你忘了调它,init_data就是NULL,那么设备树里写的min/max电压、always-on这些约束全部失效,regulator会以默认状态注册,后续consumer请求电压时可能被意外拒绝。
注册时用devm_regulator_register(&pdev->dev, &desc, &config),返回值用IS_ERR()检查。注册成功后,sysfs下会出现/sys/class/regulator/regulator.N/目录,里面有name、microvolts、state等属性,调试时非常有用。
3.4 一个容易忽视的点:注册顺序与依赖
如果PMIC本身也需要供电(比如它的IO口需要1.8V),那它的regulator注册就依赖于上一级regulator已经就绪。设备树的probe顺序由framework根据依赖关系自动处理,但前提是你在设备树里正确描述了vin-supply属性。如果漏写,可能出现PMIC驱动先probe、但它的供电还没建立,导致寄存器读写失败。
我在一个项目里遇到过PMIC probe随机失败的问题,最后发现是它的vin-supply指向的regulator在另一个I2C总线上,而那条总线的驱动加载顺序不确定。解决办法是在设备树里显式声明依赖,让framework保证顺序。
4. Consumer侧:设备驱动如何正确申请和使用regulator
4.1 regulator_get的两种形式和选择依据
consumer获取regulator句柄有两个接口:regulator_get(dev, id)和devm_regulator_get(dev, id)。后者是推荐用法,因为它绑定了设备生命周期,驱动卸载时自动释放,避免忘记调用regulator_put()导致的内存泄漏。
id参数是供电的名字,framework会用它去设备树里找对应的<id>-supply属性。比如你传"vdd_core",它就会找consumer节点下的vdd_core-supply = <&vdd_core_reg>。如果传"vdd",就找vdd-supply。
这里有个实用技巧:如果设备只有一个供电,可以用regulator_get(dev, NULL)或者传"vdd",framework会尝试找任何可用的supply。但我不推荐这种做法,因为一旦设备后续增加了第二路供电,代码就会出问题。显式命名永远更安全。
4.2 enable/disable的计数机制与常见误用
前面提到enable_count的共享机制,这里展开说几个实际使用中的注意点。
第一,enable和disable必须配对。如果你在probe里enable了,在remove里忘了disable,那路电就永远关不掉。用devm_regulator_get配合devm_regulator_enable(部分内核版本支持)可以缓解,但更稳妥的做法还是自己管理好配对。
第二,不要在中断上下文里调用enable/disable。这些操作可能涉及I2C/SPI通信,会睡眠。如果确实需要在中断里控制电源,用工作队列或者regulator_set_voltage的异步版本(如果硬件支持)。
第三,disable之后电压设置会丢失。有些regulator在disable时会掉电,重新enable后电压回到默认值。如果你的设备对电压有特定要求,每次enable之后都要重新set_voltage,或者用regulator-always-on保持常开。
4.3 set_voltage的返回值与错误处理
regulator_set_voltage()返回0表示成功,负数表示失败。常见的错误码有-EINVAL(请求超出constraints范围)、-ENOSYS(provider没实现set_voltage)、-EACCES(被约束禁止)。
实际项目中,我建议对set_voltage的返回值做检查,但不要因为失败就panic。有些场景下电压设置失败是可以降级处理的——比如性能模式切换时电压调不上去,可以退回到保守频率继续跑。当然,如果是核心电压设置失败,那确实该报错。
还有一个细节:regulator_set_voltage()设置的是电压范围,不是精确值。如果你传min=1200000, max=1200000,framework会尽量选最接近的。但如果硬件只支持100mV步进,实际可能是1200000或1300000。要获取实际设置的电压,得调用regulator_get_voltage()。
4.4 设备树中consumer节点的写法
consumer侧的设备树描述很简单,就是在设备节点下加一行supply属性:
&i2c1 { wifi: wifi@10 { compatible = "vendor,wifi-chip"; reg = <0x10>; vdd_supply: vdd-supply = <&vdd_io>; vdd_core-supply = <&vdd_core>; }; };注意属性名的格式是<name>-supply,name就是驱动里regulator_get的id。如果写错了名字,regulator_get会返回-EPROBE_DEFER或者错误指针,设备probe失败。
另外,如果consumer和provider在同一个设备树里但跨了总线,要确保phandle引用正确。我见过有人把<&vdd_io>写成了<&vdd_io_reg>,结果phandle找不到,probe一直defer。
5. 设备树中的供电拓扑描述:从简单到复杂
5.1 基本属性一览与含义
设备树里regulator相关的属性不少,我整理一个常用表格:
| 属性名 | 作用 | 典型值 |
|---|---|---|
| regulator-name | regulator名字,sysfs显示用 | "vdd_core" |
| regulator-min-microvolt | 允许的最小电压 | 800000 |
| regulator-max-microvolt | 允许的最大电压 | 1300000 |
| regulator-always-on | 常开,不允许disable | 空 |
| regulator-boot-on | 启动时使能 | 空 |
| regulator-initial-mode | 初始工作模式 | 1 (FAST) |
| regulator-allowed-modes | 允许的模式列表 | <1 2> |
| vin-supply | 上级供电 | <&parent_reg> |
| regulator-ramp-delay | 电压爬升延时(us) | 1000 |
这些属性在of_get_regulator_init_data()里被解析,填充到constraints结构体。如果某个属性没写,就用默认值——比如min/max不写的话,framework认为不限制电压范围,但这通常不是你想要的结果。
5.2 父子regulator的级联描述
复杂系统里regulator是有层级的。比如一颗PMIC的DCDC输出3.3V,这个3.3V又供给另一颗LDO,LDO输出1.8V给WiFi。设备树里要这样描述:
dcdc1: dcdc1 { regulator-name = "dcdc1_3v3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; }; ldo1: ldo1 { regulator-name = "ldo1_1v8"; regulator-min-microvolt = <1800000>; regulator-max-microvolt = <1800000>; vin-supply = <&dcdc1>; };vin-supply建立了父子关系。framework在enable ldo1时,会先确保dcdc1已经enable。这个机制叫"supply aliasing",它保证了级联供电的正确时序。
但这里有个坑:如果vin-supply指向的regulator没有正确注册,子regulator的enable会失败。而且错误信息往往不直观,可能只报一个-EPROBE_DEFER。排查时可以用/sys/kernel/debug/regulator/regulator_summary查看整个供电树的状态。
5.3 用regulator-summary调试供电拓扑
regulator_summary是调试regulator问题最有力的工具。挂载debugfs后,cat /sys/kernel/debug/regulator/regulator_summary会输出一张表,列出所有regulator的名字、当前电压、enable计数、以及每个consumer的使用情况。
我遇到供电问题时,第一步永远是看这个summary。它能快速告诉你:某个regulator是不是没使能、电压是不是不对、哪个consumer持有引用没释放。有一次调试一个suspend后无法唤醒的问题,就是通过summary发现某个regulator的enable_count在suspend后没有归零,导致系统无法进入低功耗状态。
5.4 常见设备树错误与排查方法
设备树写错是regulator问题的高发区。我总结几个典型错误:
第一,phandle引用错误。<&vdd_io>写成了<&vdd_io_reg>,或者引用了还没定义的label。这种错误在编译dtb时不一定报,但运行时probe会defer。
第二,属性名拼写错误。regulator-min-microvolt写成了regulator-min-voltage,framework解析不到,constraints里min就是0,导致set_voltage时行为异常。
第三,电压范围写反。min写得比max大,framework会拒绝注册或者行为不可预测。
第四,always-on和consumer disable冲突。如果regulator标了always-on,consumer调用disable不会真正断电,但enable_count会变化,可能造成状态混乱。
排查这些问题,除了看regulator_summary,还可以用of_node相关的debugfs节点查看设备树解析结果。另外,内核启动日志里如果有regulator相关的warning,一定要重视,那通常是配置有问题的信号。
6. 实际项目中踩过的坑与排查思路
6.1 电压设置成功但设备不工作:查实际输出电压
有一次调试一颗传感器,驱动里set_voltage(1800000)返回成功,但传感器就是没反应。用万用表量实际电压,发现只有1.2V。查了半天,发现是硬件上这颗LDO的输出被另一路regulator通过分压电阻影响了,而软件层面set_voltage只是写了PMIC寄存器,实际输出被外部电路拉低。
这个案例的教训是:软件层面的set_voltage成功不代表硬件输出正确。排查供电问题时,软件状态和硬件实测要结合看。regulator_summary告诉你软件认为的电压,万用表告诉你实际电压,两者不一致时就要查硬件。
6.2 probe defer导致的启动缓慢
系统启动时如果大量设备报-EPROBE_DEFER,启动时间会显著变长。regulator依赖是defer的常见原因之一。比如WiFi驱动probe时调regulator_get,但PMIC驱动还没probe完,就返回defer,WiFi驱动被放到deferred probe链表,等PMIC就绪后重试。
如果defer次数太多,可以检查设备树的依赖描述是否完整。有时候是因为某个regulator的vin-supply没写,导致framework无法确定正确的probe顺序,只能反复重试。
6.3 suspend/resume中的regulator状态管理
suspend时,framework会根据constraints决定哪些regulator要关闭。如果某个regulator标了always-on,它不会被关;如果没标,且没有consumer持有引用,就会被disable。
这里容易出的问题是:consumer在suspend回调里没有正确释放regulator引用,导致regulator无法关闭,系统功耗降不下来。排查方法是在suspend前后对比regulator_summary,看哪些regulator的enable_count没有归零。
另一个问题是resume时regulator的恢复顺序。如果consumer的resume回调在regulator恢复之前执行,它可能访问到还没上电的设备。解决办法是在consumer的resume里重新调用regulator_enable,或者用regulator-always-on保证电源常开。
6.4 用debugfs和sysfs快速定位问题
除了regulator_summary,还有几个有用的调试节点:
/sys/class/regulator/regulator.N/下有name、state、microvolts、num_users等属性,可以快速查看单个regulator的状态。
/sys/kernel/debug/regulator/下除了summary,还有regulator_always_on等节点,可以查看哪些regulator被标记为常开。
如果内核编译时开了CONFIG_REGULATOR_DEBUG,还会有更详细的调试信息输出到dmesg。排查复杂问题时,打开这个选项能省不少时间。
7. 从框架设计看regulator子系统的扩展性
7.1 为什么用provider/consumer模型而不是直接操作寄存器
这个设计决策背后是关注点分离的思想。provider驱动只关心"如何操作这颗PMIC的寄存器",consumer驱动只关心"我需要多少伏的电"。中间的协调——谁先谁后、能不能改、改了影响谁——全部由framework处理。
如果不用这个模型,每个consumer都要自己处理PMIC寄存器,代码重复不说,还容易出现竞争和依赖混乱。provider/consumer模型把这些问题集中到一处解决,虽然增加了框架的复杂度,但换来了整个系统的可维护性。
7.2 约束机制如何防止系统级错误
constraints的存在,本质上是给regulator加了一层"策略"。硬件能力(desc)和系统需求(constraints)分离,使得同一颗PMIC在不同板子上可以用不同的约束配置,而不需要改驱动代码。
这个机制还能防止一类严重错误:consumer请求一个硬件不支持或者系统不允许的电压。比如某个regulator在硬件上支持3.3V,但板子上它只接了1.8V的设备,constraints里把max限制在1.8V,就能防止误操作烧毁设备。
7.3 与其他功耗子系统的协作关系
regulator framework不是孤立的,它和OPP(Operating Performance Points)、cpufreq、genpd(Generic Power Domain)等子系统都有交互。
OPP框架在切换频率时,会通过regulator_set_voltage调整核心电压,这就是DVFS(动态电压频率调整)的基础。cpufreq驱动在调频时,往往需要同步调压,这个协调就是通过regulator接口完成的。
genpd在关闭一个电源域时,会disable该域内的regulator。如果regulator和genpd的层级描述不一致,可能出现电源域关了但regulator还开着,或者反过来。
理解这些协作关系,有助于在调试复杂功耗问题时,知道该从哪个子系统入手排查。
8. 写在最后:一些个人经验
regulator framework的代码量不小,但核心逻辑其实就围绕"注册-映射-约束-操作"这四个环节。初学时容易被各种结构体和API淹没,我的建议是先抓住一条主线:从设备树解析开始,到provider注册,再到consumer使用,最后到硬件操作,把这条链路走通,剩下的细节都是在这条主线上挂载的。
调试regulator问题,regulator_summary是第一工具,dmesg是第二工具,万用表是第三工具。三者结合,大部分问题都能定位。我见过不少人只盯着代码看,忽略了实际电压测量,结果在软件层面绕了很久。
最后说一个习惯:每次改设备树的regulator配置后,一定要重新看一遍regulator_summary,确认电压、使能状态、consumer列表都符合预期。这个习惯帮我避免了很多"改了配置但没生效"的低级错误。供电这东西,软件说对了不算数,硬件真正输出了才算数。