Hi6421 PMIC驱动核心解析:regmap、DVC与中断实践
简介: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, ®map_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 驱动问题时,少走几步弯路。
更多推荐

所有评论(0)