简介:PMIC(电源管理芯片)是嵌入式系统中承担多路电压转换、供电时序和功耗管理的关键器件,其Linux驱动稳定性直接影响整机运行。在内核开发中,regmap框架将I2C/SPI等总线协议封装为统一寄存器访问接口,结合缓存、位域修改和锁机制,大幅降低PMIC驱动实现成本。借助MFD子设备机制,可在单个PMIC主驱动下拆分出regulator、中断等子功能模块,提升代码复用性与可维护性。以Hi6421为例,驱动源码的核心在于probe阶段的regmap初始化、DVC动态电压控制以及中断上报链路设计,这些经验同样适用于RK808、AXP20x等同类PMIC。掌握这套方法,有助于解决调压失效、中断风暴、寄存器读错等常见工程问题。

1. 把 Hi6421 驱动代码当黑匣子是最大的误区

拿到 hi6421-pmic-core.rar 这类 PMIC 驱动源码时,多数人的第一反应是搜数据手册、查寄存器表、对着 PDF 一行行对代码。但真正拆过海思平台电源方案的人会告诉你一个反直觉的结论: Hi6421 驱动的核心难点不在 PMIC 芯片本身,而在 regmap 框架和中断子系统 。这份压缩包里那份 hi6421-pmic-core.c 源码,本质上是 Linux 内核 regmap API 与具体硬件之间的粘合层,读懂了它,你就能把同一套思路平移到 RK808、AXP20x 等一大票使用 regmap 框架的 PMIC 驱动上。它适合两类人:一类是在海思平台做 BSP 移植、被 [ 2.345678] hi6421-pmic: probe fail 逼到翻内核日志的驱动工程师;另一类是准备从零写一个 PMIC 驱动的入门者——这份代码就是最好的模板。下面我按"文件怎么组织、通信怎么建立、核心函数怎么写、坑在哪、怎么验证"的顺序,把这份资源拆开讲透。

2. 先看资源内部结构: hi6421-pmic-core.c 到底在驱动里承担什么角色

2.1 压缩包里的三个文件层级

hi6421-pmic-core.rar 解压后,你面对的其实是一个最小化的驱动骨架。它和你在内核源码树 drivers/mfd/ 目录下看到的 hi6421-pmic-core.c 基本同源,但去掉了大量与具体开发板绑定的代码。文件构成分三层:

文件/目录 作用 对应内核子系统
hi6421-pmic-core.c MFD 核心驱动,负责 probe 设备、注册 regmap、初始化中断 MFD(多功能设备)
hi6421-regulator.c (若有) 电压调节器驱动,实现 DVC 调压和 regulator 框架对接 Regulator
hi6421_pdf 文档 芯片手册/驱动说明,含寄存器地址和位域定义 硬件参考

第一层最容易搞混。很多人以为 PMIC 驱动写 VS 的是怎么调电压、怎么控制负载开关,但实际上 hi6421-pmic-core.c 做的事情非常收敛:申请 I2C 资源、填充 regmap_config、注册 regmap 实例、挂中断。真正的调压逻辑在 regulator 驱动里,这个文件里顶多留了几个 static int hi6421_core_reg_read/write 之类的封装函数。

2.2 驱动在 probe 阶段的三件套:ioremap、regmap、irq

把 hi6421-pmic-core.c 的 probe 函数拆开看,几乎所有 PMIC 驱动都是这个套路:

static int hi6421_pmic_probe(struct i2c_client *client,
                             const struct i2c_device_id *id)
{
    struct hi6421_pmic *pmic;
    struct regmap_config regmap_config = {
        .reg_bits = 8,          /* 寄存器地址宽度:8bit */
        .val_bits = 8,          /* 寄存器数据宽度:8bit */
        .max_register = HI6421_REG_MAX,  /* 最大寄存器地址,枚举定义 */
    };

    pmic = devm_kzalloc(&client->dev, sizeof(*pmic), GFP_KERNEL);
    if (!pmic)
        return -ENOMEM;

    pmic->regmap = devm_regmap_init_i2c(client, &regmap_config);
    if (IS_ERR(pmic->regmap)) {
        dev_err(&client->dev, "failed to init regmap: %ld\n",
                PTR_ERR(pmic->regmap));
        return PTR_ERR(pmic->regmap);
    }

    /* 若存在设备树子节点,则创建 MFD 子设备 */
    devm_mfd_add_devices(&client->dev, client->irq,
                         hi6421_devs, ARRAY_SIZE(hi6421_devs),
                         NULL, 0, NULL);
    return 0;
}

这段代码有两个参数值得你改的时候格外谨慎。 reg_bits 和 val_bits 必须与硬件实际地址/数据宽度严格一致,Hi6421 是标准的 8bit 地址 + 8bit 数据,但部分 PMIC 是 16bit 寄存器(如 MAX77686),填错之后 regmap 读写会全部错位,表现就是"寄存器地址对了,数据永远是 0xFF"。 max_register 必须在初始化前就确定,它直接影响 regmap 内存缓存区的大小,填大了浪费内存,填小了访问高位寄存器时会触发 regmap 的 range check 直接报错。 devm_mfd_add_devices 这个函数会读取设备树里 PMIC 节点下挂的子节点,为每个子节点创建对应的 platform device,触发 regulator、led 等子驱动的 probe。所以你在设备树里写的 compatible = "hisilicon,hi6421-regulator" 必须和子驱动里的 of_device_id 完全匹配,否则子设备一个都 probe 不起来,但内核日志不会给你任何明确报错。

2.3 为什么 MFD 拆分驱动是 PMIC 的标配

我见过不少初学者试图把所有调压逻辑塞进一个文件,结果一个 1000 行的 .c 文件里 struct i2c_client、struct regmap、struct regulator_dev 的指针满天飞。Hi6421 这份驱动遵循的 MFD 拆分思路才是大规模落地时的正解: 每个子设备一个驱动,各自独立管理生命周期,互不干扰 。比如 regulator 子驱动挂掉,最多影响调压功能,不会让整个 PMIC 的 I2C 总线瘫痪。你在设备树绑定时也应该遵循这个边界——不要试图在 hi6421-pmic-core.c 里直接调 regulator_set_voltage 。

3. I2C 通信层:regmap 框架怎么把读写信令交给 Hi6421

3.1 为什么选择 regmap 而不是裸写 i2c_transfer

直接看 hi6421-pmic-core.c 的核心,你会发现驱动里几乎找不到 i2c_master_send 或 i2c_transfer 这类函数。这不是作者偷懒,而是 Hi6421 这类 PMIC 的寄存器访问有两个硬性要求,逼迫你走 regmap:一是寄存器数量多但位域离散,比如一个字节里 bit0~bit3 是一个电压档位、bit4~bit5 是使能位,裸操作需要反复 read-modify-write;二是内核的 regulator 框架在调压时会频繁调用 regmap_update_bits ,这个函数要求你预先注册好 regmap 实例。regmap 帮你做掉了两件事: 缓存 (避免每次读操作都走 I2C 总线)和 位域修改 (传入 mask 和 value,自动完成读改写原子操作)。

3.2 一套可复用的 regmap 初始化模板

下面这个代码块是我在多个 PMIC 驱动里验证过的写法,直接对着 Hi6421 抄问题不大:

static const struct regmap_range hi6421_readable_ranges[] = {
    regmap_reg_range(HI6421_REG_CTRL, HI6421_REG_CTRL),
    regmap_reg_range(HI6421_REG_IRQ1, HI6421_REG_IRQ3),
};

static const struct regmap_access_table hi6421_readable_table = {
    .yes_ranges = hi6421_readable_ranges,
    .n_yes_ranges = ARRAY_SIZE(hi6421_readable_ranges),
};

static const struct regmap_config hi6421_regmap_config = {
    .reg_bits = 8,
    .val_bits = 8,
    .max_register = HI6421_REG_FAULT,
    .cache_type = REGCACHE_RBTREE,    /* 使用红黑树缓存寄存器值 */
    .readable_reg = regmap_readable_check,   /* 读取权限检查回调 */
    .volatile_reg = regmap_volatile_check,   /* 哪些寄存器被标记为 volatile */
};

逻辑说明: readable_ranges 定义了哪些寄存器允许被读取,通过 regmap_access_table 绑定到 config。interrupt 状态寄存器必须设为 volatile,否则 regmap 缓存会让中断状态寄存器永远保持第一次读到的值——后续中断永远读不出来。 .cache_type = REGCACHE_RBTREE 适合寄存器读写频繁但大多数是重复读写的场景;如果 PMIC 支持睡眠保持(某些寄存器的配置在 I2C 断线后仍不丢失),rbtree 缓存能显著减少 I2C 总线负载。注意 volatile_reg 的判断函数不能漏掉 IRQ 状态和 IRQ mask 寄存器,否则你会遇到"读一次中断状态后第二次读永远是同一个值"的诡异现象。

参数变更建议:如果你的 PMIC 有 100 个以上寄存器且大多数不需要频繁回读,把 cache_type 改成 REGCACHE_RBTREE 没问题;但如果你遇到"寄存器被硬件自动清零(如中断标志位写 1 清 0)"的情况,务必把这个寄存器在 volatile_reg 回调里返回 true,否则 regmap 框架会把它当成普通缓存寄存器,读到的是上一次写入的残留值。 max_register 的值可以从 PDF 手册里查"Register Address Map"章节的最高地址,宁大勿小,但别超过 0xFF 以免浪费。

3.3 设备树里的 I2C 节点绑定必须注意的时钟频率

PMIC 挂在 I2C 总线上时,设备树节点里的 clock-frequency 参数经常被忽略,而这恰恰是 Hi6421 通信故障的常见源头:

&i2c3 {
    status = "okay";
    clock-frequency = <400000>;   /* Hi6421 支持标准模式和快速模式 */
    hi6421: pmic@61 {
        compatible = "hisilicon,hi6421";
        reg = <0x61>;              /* 7bit I2C 地址,PDF 手册的 8bit 地址右移一位 */
        interrupt-parent = <&gpio28>;
        interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
    };
};

注意 reg = <0x61> 的取值:Hi6421 手册上写的从机地址如果是 0xC2(8bit 含读写位),那设备树里 7bit 地址就是 0xC2 >> 1 = 0x61 。这个问题几乎每三个 PMIC 移植项目里就有一个翻车的,现象是 i2c 探测能发现设备,但 regmap_read 返回的数据全是乱码或总线错误。 clock-frequency = <400000> 是快速模式的合法上限,如果从设备树注释删掉这个属性,I2C 子系统默认走 100kHz 标准模式,不会出故障但性能打折;但如果误设成 1MHz(部分主控支持超频模式),Hi6421 的 I2C 时序参数跟不上,会出现偶发性的 NACK 和总线 stall。

4. 核心机制逐个拆:DVC 调压、中断上报、睡眠控制的实现逻辑

4.1 DVC 动态电压控制:regulator 框架如何调用到 smp_rmb

Hi6421 的 DVC(Dynamic Voltage Control)是这颗 PMIC 在嵌入式领域站稳脚跟的核心卖点。它支持软件控制输出电压逐级切换,而不需要外部电阻分压网络。在 hi6421-pmic-core.c 系里,虽然具体调压函数在 regulator 子驱动中,但核心 regmap 接口必须预留 regmap_update_bits 的快速通道。驱动代码长这样:

static int hi6421_dvc_set_voltage(struct regulator_dev *rdev,
                                  int min_uV, int max_uV)
{
    struct hi6421_regulator *info = rdev_get_drvdata(rdev);
    unsigned int sel = 0;
    int ret;

    /* 查找电压档位表中第一个 >= min_uV 的档位 */
    sel = find_voltage_index(info->vol_table, min_uV);
    if (sel == info->vol_count - 1 &&
        info->vol_table[sel] < min_uV) {
        return -EINVAL;
    }

    /* 写入电压选择寄存器,DVC_EN 位置 1 后硬件平滑切换 */
    ret = regmap_update_bits(info->regmap,
                             info->dvc_reg,
                             DVC_SEL_MASK | DVC_EN,
                             sel << DVC_SEL_SHIFT | DVC_EN);
    if (ret)
        return ret;

    /* 等待 PMIC 完成内部 settling,典型 30~50us */
    usleep_range(30, 50);
    return 0;
}

逻辑说明:这段代码先查表,找到最接近目标电压的档位编号,然后通过 regmap_update_bits 一次性把档位和 DVC 使能位写入寄存器。 usleep_range(30, 50) 是必须的,Hi6421 的 DVC 切换时机有硬件延迟,如果立即回读电压寄存器,可能读到的是中间跳变过程中的随机值。 .vol_table 这个数组来自 PDF 手册里的"Output Voltage Selection Table",你需要手动把它抄到驱动里。

参数说明: DVC_SEL_SHIFT 的值取决于寄存器里档位字段的起始 bit 位置,通常在 0 或 4; DVC_SEL_MASK = 0xF 还是 0x1F 取决于档位总数(16 档用 4 bit,32 档用 5 bit)。 regmap_update_bits 这个函数在并发调压时天然安全,因为 regmap 内部有锁,但如果你在中断上下文里调用调压函数,要改用 regmap_update_bits_unlocked 并自行加锁。

4.2 中断上报链路:从硬件事件到内核通知

Hi6421 的中断设计在驱动里是个典型:多个中断源(过压、欠压、过温)共享一个 INT 引脚,驱动必须读状态寄存器才能知道具体是什么事件。 hi6421-pmic-core.c 的中断注册代码一般长这样:

static const struct regmap_irq hi6421_irqs[] = {
    { .reg_offset = 0, .mask = BIT(0), .type = REGMAP_IRQ_REG }, /* PWRON */
    { .reg_offset = 0, .mask = BIT(1), .type = REGMAP_IRQ_REG }, /* OVP */
    { .reg_offset = 1, .mask = BIT(0), .type = REGMAP_IRQ_REG }, /* OTMP */
};

static const struct regmap_irq_chip hi6421_irq_chip = {
    .name = "hi6421",
    .status_base = HI6421_IRQ1_REG,
    .mask_base = HI6421_IRQ_MASK1_REG,
    .ack_base = HI6421_IRQ1_REG,   /* 写 1 清中断 */
    .num_irqs = ARRAY_SIZE(hi6421_irqs),
    .irqs = hi6421_irqs,
};

这里面最容易写错的是 ack_base 。Hi6421 的很多中断状态寄存器是 write-1-to-clear 语义,也就是说读出来的值代表事件,写 1 到对应位才清除。如果你把 ack_base 设成另一个寄存器地址,会出现"中断来了,读完状态也清了,但中断引脚一直拉低"的假死现象——因为硬件状态没被真正确认掉。 regmap_irq_chip 框架会帮你在 handle_nested_irq 里自动完成读状态、调用子中断 handler、写 ack 的完整流程。

4.3 睡眠控制:REGUALTOR_SUSPEND 状态的硬件实现

低功耗模式的实现是不可忽略的重头戏。Hi6421 支持将某个 regulator 在系统 suspend 时自动切换到一个预设电压或直接关闭,驱动里通过 regulator framework 的 regulator_suspend_voltage 实现:

static int hi6421_set_suspend_voltage(struct regulator_dev *rdev, int uV)
{
    struct hi6421_regulator *info = rdev_get_drvdata(rdev);
    unsigned int sel = 0;
    int ret;

    sel = find_voltage_index(info->vol_table, uV);
    if (sel < 0)
        return -EINVAL;

    /* 写入睡眠电压寄存器,硬件在 suspend 时自动加载 */
    ret = regmap_update_bits(info->regmap,
                             info->suspend_reg,
                             SUSPEND_SEL_MASK,
                             sel << SUSPEND_SEL_SHIFT);
    return ret;
}

这里有个隐藏的坑:很多 PMIC 的睡眠电压寄存器与正常电压寄存器是同一个地址,但需要先将某个"睡眠使能"位写 1 才能令其生效。如果只写了睡眠电压不置使能位,suspend 时电压纹丝不动。你在对这份驱动的时候,一定要翻开 PDF 手册确认 SUSPEND_EN 位的位置,不要想当然认为 regmap 里地址对了就行。此外,set_suspend_voltage 回调里不能调用 usleep_range ,因为 suspend 流程在时间约束上很紧,sleep 会让整个休眠流程超时。

5. 常见问题与排查:寄存器读错、中断风暴、调压失效的五个翻车点

5.1 I2C 读回的数据全是 0xFF

现象: cat /sys/kernel/debug/regmap/0-0061/registers 里大部分寄存器值是 0xFF,且杂散无规律。

原因:绝大多数情况是 val_bits 或 reg_bits 配置错误。如果芯片是 16bit 数据但驱动写了 8bit,regmap 只能取到低字节。另一个高发原因是 I2C 地址在设备树里填错,7bit 地址和 8bit 地址混用(详见 3.3 节)。

解决:先查 dmesg | grep hi6421 确认 probe 是否成功;然后用 i2cdetect -y 3 扫描总线确认设备地址。如果地址对但数据全 0xFF,检查 max_register 是否把受保护的寄存器范围暴露出来,部分 PMIC 的读取权限是有限制的,越权读会返回 0xFF 而不是 NACK。注意: i2cdetect 显示设备存在但 regmap 全 FF,绝大多数是 reg_bits/val_bits 不匹配,其次是设备树里 reg 地址被 I2C 控制器的地址移位逻辑二次偏移。

5.2 中断注册后 5 秒内触发一次"虚假"中断

现象: cat /proc/interrupts 里 hi6421 中断计数不断增长,但实际 PMIC 并没有任何负载变化。

原因:Hi6421 的某些中断状态寄存器位在出厂时就有默认值(比如 PWRON 键按住状态),如果驱动没有在 init 阶段主动清一次中断标志,这些残留标志会在中断使能后立即触发一次中断风暴。

解决:在 regmap_irq_chip 注册之前主动读一遍所有状态寄存器并写 1 清除:

static void hi6421_clear_irq_status(struct regmap *regmap)
{
    unsigned int val;
    int i;

    for (i = 0; i < HI6421_IRQ_REG_COUNT; i++) {
        regmap_read(regmap, HI6421_IRQ1_REG + i, &val);
        regmap_write(regmap, HI6421_IRQ1_REG + i, val & 0xFF);
    }
}

还有一个低概率原因:GPIO 中断触发类型配错了。Hi6421 INT 引脚是低电平有效,设备树里必须用 IRQ_TYPE_LEVEL_LOW ,如果配成 IRQ_TYPE_EDGE_FALLING ,在电平持续拉低期间会反复进入中断。检查 /proc/interrupts 里中断号对应的 trigger 类型,改成 level low 后故障消失。

5.3 regulator 调压后电压纹丝不动

现象:调用 regulator_set_voltage 成功返回,但用万用表量输出电压没变化,或者变化幅度远小于设定值。

原因:多半是电压档位表不对。Hi6421 的某些输出电压档位是线性步进的,但有些档位之间存在"空洞"(保留位,不允许写入)。如果 find_voltage_index 返回的档位恰好落在保留区,硬件会忽略写入。

解决:把 PDF 手册的电压表完整抄进驱动,注意区分"选择码"和"实际电压值"——有时候直接写选择码(0、1、2…)比写电压值更可靠。多看几眼 .vol_table 数组里最后一个元素是不是 HI6421_VOLT_MAX 或注册了特殊标记,那份 hi6421_pdf 文档里有完整的档位对应关系表。我曾在 RK808 上因为少抄了一个 1.8V 的档位,导致系统在 1.8V 和 2.8V 之间反复切换死机。

5.4 cannot halt the core 或启动阶段 PMIC probe 卡死

现象:内核启动到 hi6421_pmic_probe 时整个系统挂起,或者卡在 wait_for_completion 。

原因:PMIC 的 I2C 控制器在启动时如果还没完成 reset, devm_regmap_init_i2c 会尝试发传输,但此时 I2C 控制器时钟没起来,传输永远不返回。

解决:在设备树 PMIC 节点中加入 status = "okay" 的同时,确认 I2C 控制器节点里 clocks 属性已被正确填充。另一个常见诱因是 PMIC 子设备里的中断 handler 在 probe 阶段就触发了调用 mutex_lock 的路径,但此时 regmap 的锁还没有被初始化,造成死锁。解决方案是把中断请求放到 probe 的最后一步——从 devm_request_threaded_irq 移到 regmap_irq_chip 注册之后并显式调用 enable_irq 。

5.5 休眠唤醒后 PMIC 寄存器状态丢失

现象:系统从 suspend 唤醒后,regulator 输出电压全部变成默认值 0V,或某个 rail 直接没输出。

原因:PMIC 的某些寄存器在上电后只能写入一次(OTP 保护),或者 suspend 期间 PMIC 内部 power domain 被关闭,寄存器内容丢失。

解决:在 resume 回调里做一次完整的寄存器恢复。推荐做法是借助 regmap 的 regcache_sync :只要初始化时 cache_type 设为 REGCACHE_RBTREE ,suspend 期间缓存不会丢,resume 后调用 regcache_sync(pmic->regmap) 一次性把所有缓存值回写。注意顺序:必须在 I2C 控制器恢复之后、regulator 子驱动 resume 之前执行,所以放在 MFD 核心驱动的 resume 回调里是最保险的。

6. 验证清单:拿到这包代码后,我建议你这样自测

下载这份资源后,至少要做三轮验证才能说"我读懂了这份驱动"。第一轮是编译层面,把 hi6421-pmic-core.c 放进内核树的 drivers/mfd/ 目录,确认它能通过 make drivers/mfd/ 编译,主要检查 hi6421_pmic_probe 里引用的 struct 是否与当前内核版本的 MFD API 兼容——不同内核版本的 devm_mfd_add_devices 参数发生过变化(从 5 个参数变为 7 个参数),这会直接导致编译报错。第二轮是注册层面,用 i2cdetect 在总线上扫描出 0x61 地址后,通过 debugfs 的 regmap 接口做闭环读写:

# 在目标板上执行
cat /sys/kernel/debug/regmap/0-0061/registers
# 读 IRQ 状态寄存器,确认能读到硬件值
i2cget -y 3 0x61 0x0b
# 在 DVC 电压寄存器上做一次安全写入测试
i2cset -y 3 0x61 0x1a 0x08
i2cget -y 3 0x61 0x1a

跑完这组命令,你至少能判断 regmap 配置是否正确、I2C 通路是否畅通。第三轮是功能验证,如果板子上有可调电源接入 PMIC 的某个 DVC 通道,用 regulator_set_voltage 做一次从 1.2V 到 1.3V 的切换,同时用示波器抓 DVC 切换波形,确认没有毛刺和跳变。没有示波器的话,可以在驱动里加一个 pr_info 打印目标电压和实际读回值,至少确认寄存器内容是对的。如果这一步发现电压切换需要很久(超过 100us),检查 usleep_range 的参数是否过保守。

你在实际项目里不可能只在实验室环境跑通就完事。把那套 hi6421_pdf 手册里的寄存器位域表格打印出来放桌上,每改一个 hi6421-pmic-core.c 里的宏定义,就在手册上标记一次变更时间和原因。PMIC 驱动的 bug 特征就是"不动则不坏,一动必出妖",大多数线上故障都发生在改了一个看似无关的位域定义之后。从那以后我每次提交 PMIC 驱动改动,都会强制在 commit message 里写明"改动寄存器的地址来源、位域偏移、验证方式"三要素,少了任何一项就打回重写。这个习惯救过我至少三次——比如说 5.3 里的电压档位空洞,就是靠这条规矩查出来的。希望这份拆解能帮你在下次遇到 PMIC 驱动问题时,少走几步弯路。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

海思 HiSpark 开发者社区是上海海思为开发者搭建的内容和交流平台,依托完备芯片解决方案、专业开发环境、开源/开放的软件资源与社区技术内容,构建完整服务生态开发者的闭环体系。

更多推荐