3. regulator框架:电压调节器核心API、regulator设备驱动、动态电压频率调整(DVFS)基础

好,咱们今天来聊聊regulator框架。说实话,这个框架在Linux内核里属于那种「平时不太显眼,但一旦出问题就让你抓狂」的模块。我最早接触它是在做一款平板电脑的功耗优化时,发现CPU降频后电压没跟着降,板子直接热到烫手——嗯,从那以后我就把regulator框架好好啃了一遍。

3.1 为什么需要regulator框架?

你想想看,一个SoC上挂着多少路电源?CPU核心、GPU、DDR、PLL、IO……每一路都需要不同的电压。有的要1.1V,有的要1.8V,有的还要动态调整。如果让每个驱动自己去操作PMIC寄存器,那不乱套了?

regulator框架干的事,说白了就是:统一管理电压调节器。它提供了一套标准API,让消费者驱动(比如CPUfreq、GPU驱动)可以优雅地申请、设置、释放电压,而不需要关心底层是PMIC还是DC-DC转换器。

核心思想: regulator框架把电压调节器抽象为「 regulator 设备」,消费者通过句柄(struct regulator *)来操作,底层硬件差异由驱动开发者搞定。

3.2 核心数据结构

先看几个关键结构体,我习惯把它们记成「三件套」:

结构体 作用 谁实现
struct regulator_dev 代表一个物理regulator设备 驱动开发者创建
struct regulator_ops 操作函数集(设置电压、使能等) 驱动开发者实现
struct regulator 消费者看到的句柄 内核分配,消费者使用

这里有个容易混淆的点:regulator_dev 是给驱动用的,regulator 是给消费者用的。我见过有新手把这两个搞混,结果传参传错了,内核直接oops。

3.3 核心API:消费者视角

作为驱动开发者,你更多时候是消费者——比如你要写一个GPU驱动,需要动态调压。那你会用到这些API:

3.3.1 获取与释放

struct regulator *regulator_get(struct device *dev, const char *id);
void regulator_put(struct regulator *regulator);

注意这个 id 参数,它对应设备树里的 regulator-name 属性。我踩过坑:设备树里名字写的是 cpu-vdd,代码里传的是 cpu_vdd,下划线和中划线不匹配,结果 regulator_get 返回NULL。查了半天才发现是这种低级错误。

3.3.2 电压设置

int regulator_set_voltage(struct regulator *regulator, int min_uV, int max_uV);
int regulator_get_voltage(struct regulator *regulator);

这里有个设计哲学:你告诉内核一个「电压范围」,而不是一个精确值。为什么?因为regulator硬件可能只能以步进方式调整(比如12.5mV一步),内核会在范围内选一个最合适的值。我个人习惯把min和max设成一样,除非你明确允许一定波动。

3.3.3 使能与禁用

int regulator_enable(struct regulator *regulator);
int regulator_disable(struct regulator *regulator);
int regulator_is_enabled(struct regulator *regulator);

使能操作是有引用计数的。你调一次 enable,引用计数+1;调一次 disable,引用计数-1。只有计数降到0时,硬件才会真正关掉。这个机制是为了防止多个消费者共享一路电源时,一个消费者关了导致其他消费者掉电。

小技巧: 调试时可以用 regulator_is_enabled 检查当前状态。我曾经在suspend/resume流程里发现regulator没关,就是用这个API定位到的。

3.4 regulator设备驱动:如何编写一个regulator驱动

好,现在换个角色——你要写一个regulator驱动,让其他模块能用你的PMIC。核心步骤就三步:

3.4.1 实现 struct regulator_ops

这是最关键的。你至少要实现这几个回调:

static struct regulator_ops my_regulator_ops = {
    .enable      = my_reg_enable,
    .disable     = my_reg_disable,
    .is_enabled  = my_reg_is_enabled,
    .set_voltage = my_reg_set_voltage,
    .get_voltage = my_reg_get_voltage,
};

每个回调里操作你的硬件寄存器。比如 set_voltage 可能是写I2C给PMIC发命令。我记得第一次写时,忘了在 set_voltage 里做范围检查,结果上层传了个超出硬件能力的电压值,硬件直接没反应。后来我加了一段:

if (min_uV < my_reg->min_uV || max_uV > my_reg->max_uV)
    return -EINVAL;

3.4.2 定义regulator描述符

static struct regulator_desc my_reg_desc = {
    .name           = "my-vdd",
    .id             = 0,
    .type           = REGULATOR_VOLTAGE,
    .ops            = &my_regulator_ops,
    .n_voltages     = 64,
    .min_uV         = 800000,
    .uV_step        = 12500,
    .vsel_reg       = 0x10,
    .vsel_mask      = 0x3f,
};

这里 n_voltagesmin_uVuV_step 定义了电压表。比如800mV起步,每步12.5mV,共64档,最高就是800000 + 63*12500 = 1.5875V。内核会用这个表来做电压映射。

3.4.3 注册regulator设备

struct regulator_dev *rdev;
rdev = devm_regulator_register(&pdev->dev, &my_reg_desc, &config);

devm_ 版本的好处是,驱动卸载时内核自动帮你清理。我早期用非devm版本,漏了 regulator_unregister,导致模块卸载后重新加载时注册失败——嗯,血的教训。

3.5 DVFS基础:动态电压频率调整

DVFS,说白了就是「跑得快时给高电压,跑得慢时给低电压」。为什么电压要和频率联动?因为CMOS电路的功耗公式:P ∝ C × V² × f。电压降一点,功耗是平方级下降。但频率高了,电压不够,电路就跑不动了。

在Linux里,DVFS通常由 cpufreq 框架驱动,它通过regulator API来调压。典型的流程是:

  1. 用户或内核策略决定要升频
  2. cpufreq驱动先调高电压(通过regulator_set_voltage)
  3. 等电压稳定后,再提高PLL频率
  4. 降频时顺序相反:先降频,再降压

这个顺序很重要。我曾经在项目里把顺序搞反了——先升频再升压,结果CPU在电压到位前就跑了高频,直接死机。后来我在代码里加了 udelay 等待电压稳定,才解决。

注意: 电压切换是有延迟的。PMIC的爬坡速率(slew rate)通常在几mV/µs到几十mV/µs之间。如果你的频率切换很快,但电压跟不上,系统就会不稳定。我建议在切换频率前,用 regulator_set_voltage_time 查询需要的等待时间。

3.6 设备树中的regulator配置

实际项目中,regulator的配置大多在设备树里完成。一个典型例子:

cpu_vdd: regulator@0 {
    compatible = "my-pmic,regulator";
    regulator-name = "cpu-vdd";
    regulator-min-microvolt = <800000>;
    regulator-max-microvolt = <1587500>;
    regulator-always-on;
    regulator-boot-on;
};

然后消费者节点引用它:

cpu0: cpu@0 {
    cpu-supply = <&cpu_vdd>;
    operating-points = <
        1000000 1100000
        1400000 1200000
        1800000 1300000
    >;
};

这里 operating-points 定义了频率-电压对。内核的cpufreq驱动会解析它,并在调频时自动调用regulator API。嗯,这套机制用起来确实省心。

3.7 调试与常见问题

最后分享几个调试技巧:

  • 查看当前电压: cat /sys/class/regulator/regulator.X/microvolts
  • 查看使能状态: cat /sys/class/regulator/regulator.X/state
  • 打开regulator调试日志:drivers/regulator/core.c 里加 #define DEBUG,或者动态调整 debugfs 级别

常见问题我列一下:

现象 可能原因 解决
regulator_get返回NULL 设备树名称不匹配 检查regulator-name和id参数
电压设置失败 超出硬件范围 检查min_uV/max_uV和n_voltages
系统在调频时死机 电压切换时序不对 先升压再升频,加延迟等待
regulator无法关闭 引用计数不为0 检查是否有其他消费者

好了,regulator框架的核心内容就这些。说白了,它就是一个「电压中间层」,让上层驱动不用关心底层硬件细节。下一章我们会深入DVFS的具体实现,包括如何与cpufreq、devfreq框架配合。到时候再聊。