前阵子给某便携数据终端项目做电源部分,主控是 PIC32MZ2048EFM100,电源管理芯片用了 PCA9422。整套系统从锂电池充电、多路电压输出、ADC 电压电流监测到低功耗切换,最后都压在这两颗芯片上。这篇文章把这段完整经历梳理了一遍:为什么选它们、硬件怎么接、寄存器怎么配、低功耗策略怎么调,以及调试时踩过的一堆坑。如果你也在做带电池的嵌入式设备,这份过程记录可以直接当一份参考样板。
1. 方案选型:为什么偏偏是这两颗芯片
1.1 PCA9422 一颗芯片吞下充电和供电的活
以前做便携设备电源,常规做法是充电管理找一颗充电 IC,DC-DC 降压再单独选几颗 LDO 或 Buck,电压电流采样还要外接分压电阻和 ADC。整套下来 BOM 表长一截,PCB 面积也多出一块,最关键的是上电时序、充电状态机这些完全要靠硬件额外搭电路或者靠主控硬扛,调试起来非常痛苦。
PCA9422 这颗 PMIC 把最占地方的几个模块都收了进去。充电管理、多路降压输出、寄存器可配置的电压档位、ADC 采集,还有用于状态上报的中断输出,基本覆盖了电池供电产品对电源系统的绝大多数需求。我在这个项目里用它的两路可配置输出,一路稳定输出 3.3V 给 MCU 和数字外设,另一路输出 1.8V 供传感器和逻辑电平转换,电流容量和动态响应对当前外设负载来说都够用。
选 PMIC 而不是分立方案,还有一层考虑是上电时序。PCA9422 各路输出可以按寄存器配置的顺序依次启动,省掉了以前用分立德器件时必须单独做 Power Sequence 的麻烦。只要在主控上电后尽快通过 I2C 把配置刷进去,整机的电源行为就是稳定可预期的。
1.2 PIC32MZ2048EFM100 在这里的角色是电源管家
主控这边选 PIC32MZ2048EFM100 不是因为它的运算能力,而是因为它做电源管理这套活儿非常顺手。这颗 MCU 的 2MB Flash 足够把整套 PMIC 配置表、校准曲线、异常日志都放进去,不担心空间紧张。100 脚封装可用的 I2C、GPIO、外部中断管脚很充足,跑一个 PMIC 控制任务加业务逻辑,完全不会出现引脚不够用的情况。
更实际的一点是它的外设资源方便做电源状态监控。I2C 可以连 PMIC 的寄存器接口,外部中断管脚接 PMIC 的中断输出,这样电池电压跌落、充电完成、温度异常这些事件可以实时触发 MCU 处理,不用靠轮询硬扛。低功耗模式下 MCU 能把大部分时钟关掉,只留关键唤醒源,正好配合 PMIC 一起做整机的低功耗策略。
1.3 这套组合适合什么项目
按这套方案的设计目标,它最适合中等复杂度的电池供电产品:要么是带屏幕的手持设备,要么是需要长时间待机、偶尔执行任务的采集终端。这类设备对电源系统的要求很明确,充电管理要有,多路电压输出要有,还要有电量显示和低压保护,而且对 PCB 面积和 BOM 成本比较敏感。
如果项目是超大功率设备,或者对电池计量精度有很高要求,那就得考虑专业 BMS 芯片,PCA9422 这类 PMIC 不会是最优解。同理,如果追求极致的元器件成本,把功能拆成分立方案反而更便宜,PMIC 的集成优势主要体现在面积、时序可靠性和开发效率上。
2. 硬件设计:电源路径与引脚连接实操
2.1 先把电源架构画清楚
拿到原理图前,我的习惯是先画一张简单的电源拓扑图。这套系统的电源路径是这样的:USB 或者适配器的 5V 输入接 PCA9422 的输入脚,单节锂电池接到电池端,两路 Buck 输出分别进不同电压域。PCA9422 内部会有输入路径管理,优先保证系统负载供电,多余的能量再给电池充电。
画拓扑时最容易忽略的是负载的分组。我把负载严格分成两类:一类是 MCU、存储这类一直要 3.3V 的常供电域;另一类是传感器、射频前端这类可以随时关断的受控供电域。这样做的好处是低功耗模式下可以通过寄存器直接关掉第二路输出,不用额外加负载开关,省了一颗 MOSFET 和控制引脚。
电流路径上,电池充放电回路要走短,尽量加宽覆铜,充电电流到 1A 以上时铜箔电阻造成的压降就开始有影响了。采样电阻如果放在电池负极回路,PCB 走线要采用开尔文接法,单独拉两根细线到 ADC 检测脚,避免把功率电流的压降算进电池电压里。
2.2 关键引脚连接参考
下面是我在原理图里的连接方式,给你做参考。具体管脚编号在不同 PCB 布局下会有差异,接之前一定以你手里那颗芯片的数据手册管脚图和勘误表为准。
| 信号 | 连接对象 | 说明 |
|---|---|---|
| VBUS | 5V USB/适配器输入 | 输入路径检测和充电供电来源 |
| VBAT | 锂电池正极 | 电池端,尽量短粗走线 |
| SCL/SDA | MCU 的 I2C1 | 上拉电阻 4.7kΩ 到 3.3V |
| INT | MCU 外部中断引脚 | 触发电源事件处理 |
| BUCK1_OUT | 3.3V 域 | MCU 主供电 |
| BUCK2_OUT | 1.8V 域 | 传感器等外设供电 |
| NTC | 电池热敏电阻 | 充电温度保护 |
I2C 上拉电阻的位置值得单独说。上拉电阻尽量靠近 PCA9422 一侧放,走线不要经过高噪声区域,比如电感和开关节点附近。我在这块板上就把上拉电阻从默认的 I2C 总线末端挪到了 PMIC 附近,总线上出现的振铃明显变小,通信也稳定很多。
2.3 布局布线的几条硬经验
这类集成 PMIC 的板子,布局布线直接决定调试要不要加班。第一是去耦电容要贴着芯片的电源脚放,电容放到距离引脚 3mm 以外的位置,高频噪声就会明显变差。第二是输出电压的反馈检测线要从输出电容两端单独走线,尽量避开电感下方的地层,否则负载瞬态时反馈会被污染。
电感下方的铜皮处理也很关键。开关节点那面尽量不铺铜,可以有效减少寄生电容和辐射。但芯片底部的地焊盘一定要接实,这既是散热通道,也是降低开关噪声回流路径的重要手段。散热焊盘下方多打过孔到地层,整机在连续充电加高负载工作时,芯片温度能差出十几度。
最后是地平面的划分,我的策略是功率地、信号地、模拟地分开铺,然后在芯片焊盘下方单点汇合。ADC 采样电池电压的地参考线单独走,不要让它和充电功率地的压降混在一起,否则电量监测读出来的电压会跟着充电电流波动,这个问题在手工飞线调试时特别容易暴露。
3. 软件实操:I2C 通信与寄存器配置
3.1 初始化 I2C 外设
PCA9422 和 PIC32MZ2048EFM100 之间的通信接口是 I2C。PIC32MZ 的 I2C 外设初始化其实很直接,关键是把波特率算对。下面这段是按外设时钟 50MHz 配置 400kHz 总线的示例,具体的外设时钟来源要跟你的时钟树配置对上。
#define PBCLK 50000000UL #define I2C_CLK 400000UL #define I2C_BRG ((PBCLK / (2UL * I2C_CLK)) - 1UL) void i2c1_init(void) { I2C1CONbits.ON = 0; I2C1BRG = I2C_BRG; I2C1CONbits.DISSLW = 0; // 允许时钟延展 I2C1CONbits.SMEN = 1; // 开启 SMBus 输入电平 I2C1CONbits.ON = 1; }注意 DISSLW 这一项,PCA9422 在内部状态切换时可能会拉低 SCL 做时钟延展,如果 MCU 这侧不支持时钟延展,读取过程会直接算错或卡住。我建议保持允许时钟延展,并且在后端读写函数里加上超时保护,宁可超时返回错误,也不能让任务挂在 I2C 上。
400kHz 的速率对这颗 PMIC 完全够用。每次启动重新配置所有寄存器,总数据量不到几十字节,耗时可以忽略。没有必要跑 1MHz 而牺牲稳定性。
3.2 寄存器读写的基础函数
I2C 读写 PMIC 是整套软件的地基。我通常会封装两个基础函数,后面所有配置都在这之上做。下面这段是带超时保护的读寄存器函数,写寄存器函数结构类似,区别在末尾发停止位前不需要回读。
#define PMIC_I2C_ADDR 0x53 // 7-bit 地址,按你板上的地址脚电平设置 uint8_t pmic_read_reg(uint8_t reg) { uint8_t val = 0; i2c1_start(); if (!i2c1_write(PMIC_I2C_ADDR << 1 | 0)) goto out; if (!i2c1_write(reg)) goto out; i2c1_restart(); if (!i2c1_write(PMIC_I2C_ADDR << 1 | 1)) goto out; if (!i2c1_read(&val, 0)) goto out; out: i2c1_stop(); return val; } void pmic_write_reg(uint8_t reg, uint8_t val) { i2c1_start(); i2c1_write(PMIC_I2C_ADDR << 1 | 0); i2c1_write(reg); i2c1_write(val); i2c1_stop(); }实际项目里,我强烈建议在 pmic_write_reg 后再调用一次 pmic_read_reg 做回读校验。PCA9422 的寄存器写入偶尔会被总线噪声干扰,回读发现不对就再写一次,最多重试三次。这个习惯帮我挡住过好几次上电随机配置失效的问题,代码里多三行,调试时少掉不少头发。
3.3 芯片启动配置流程
PCA9422 上电后默认状态不一定满足你的系统需求,所以主控起来后的第一件事就是完整刷一遍配置。我把初始化拆成固定顺序,每一步的回读校验都在,顺序不能乱:
- 读设备 ID 寄存器,确认 I2C 通信正常。
- 配置充电电压和充电电流,同时设置输入限流。
- 配置两路 Buck 输出电压。
- 设置中断掩码,打开需要上报的事件。
- 回读所有配置值,与设定值比对。
下面是我这个项目里的初始化代码片段,寄存器地址和取值请以你手里的 PCA9422 数据手册为准,不同批次可能存在差异,这里的写法只是提供格式参考。
#define REG_DEVICE_ID 0x00 #define REG_CHG_VOLT 0x02 #define REG_CHG_CURR 0x03 #define REG_INPUT_LIMIT 0x04 #define REG_BUCK1_VOUT 0x10 #define REG_BUCK2_VOUT 0x11 #define REG_INT_MASK 0x40 int pmic_init(void) { uint8_t id; uint8_t val; id = pmic_read_reg(REG_DEVICE_ID); if (id != PMIC_DEVICE_ID_EXPECT) { return -1; } pmic_write_reg(REG_CHG_VOLT, 0x2C); // 恒压 4.2V pmic_write_reg(REG_CHG_CURR, 0x40); // 电流挡位按手册取值 pmic_write_reg(REG_INPUT_LIMIT, 0x30); // 输入限流 pmic_write_reg(REG_BUCK1_VOUT, 0x18); // 3.3V 档 pmic_write_reg(REG_BUCK2_VOUT, 0x0C); // 1.8V 档 pmic_write_reg(REG_INT_MASK, 0x00); // 全开中断 val = pmic_read_reg(REG_BUCK1_VOUT); if (val != 0x18) return -1; return 0; }配置表的数值建议抽出来放到常量数组里,方便后续根据产品版本调整。我习惯把整张配置表放在 Flash 里,每次启动完整加载,这样万一需要现场升级电源参数,只要更新固件就行,不用改硬件。
3.4 充电状态读取与电量监测
PCA9422 的电量信息我主要靠它的状态寄存器和 ADC 通道拿。状态寄存器里的充电中、已充满、检测到输入、温度异常这几个 bit,是电源状态机的基础信号。ADC 通道则用于实时读取电池电压和输入电压。
读取电池电压时有两点要注意。第一是 ADC 结果往往是一个满量程比例值,要换算成真实电压,必须知道分压比例和参考电压。第二是电池电压在充电时会比实际开路电压偏高,如果产品要做电量百分比显示,最好在停止充电或者负载稳定后再采样,否则计算出的电量会来回跳。
我在项目里采取的做法是每隔固定时间读一次电压,同时记录充电状态。如果充电中,就按充电状态做修正;如果处于放电状态,则直接用电压估算。用状态信息辅助判断后,电量显示稳定了很多。更精细的库仑计方案当然更准,但成本和应用复杂度会明显提高,这套方案里没必要。
4. 系统级电源策略:低功耗与动态调压
4.1 动态调压的实际收益
PIC32MZ2048EFM100 的好处是控制系统完全可编程,PMIC 的电压输出也能随时调,这让动态电源管理成为可能。设备在工作状态、待机状态、深度睡眠状态下,对电压和电流的需求完全不同:
| 工作状态 | 主控电压 | 传感器电压 | 备注 |
|---|---|---|---|
| 全速工作 | 3.3V | 1.8V | 全功能运行 |
| 轻载待机 | 3.3V | 1.8V | 关外设,降低主频 |
| 深度睡眠 | 2.8V | 关闭 | MCU 降频降压运行 |
把一个本来跑在 3.3V 的模块降到 2.8V,功耗下降是明显的,因为数字电路的动态功耗跟电压的平方成正比。这个优化在 PMIC 上实现起来就是写一个寄存器的事,但在没有可调电源的方案里只能干瞪眼。实测轻载场景下这一项省出来的电流相当可观,对于延长电池续航很有价值。
4.2 低功耗切换的完整操作顺序
从工作状态进深度睡眠,不能一上来就关输出。直接砍电压或关路,外设可能进入未定义状态,醒来后第一批 I2C 读写就会出问题。我最后定下来的顺序是:
- 先把会唤醒的中断源配好,防止睡死。
- 关闭传感器供电域,也就是第二路输出,让外围彻底断电。
- 把主控电压降到低电压档位。
- 等输出稳定后,MCU 再关掉外设时钟,执行休眠指令。
- 唤醒时进入中断服务程序,先把 PMIC 配置回工作状态,再恢复外设供电。
这个顺序的意义在于让所有电压切换都发生在系统还清醒的时候。如果把 PMIC 切到低电压之后才关外设时钟,那段时间里的总线操作可能因为电压匹配问题偶发错误,表现为系统时不时死机但很难复现。
4.3 用中断驱动的电源事件状态机
我在软件里把电源管理做成了一个状态机,外部事件都通过 PMIC 中断引脚触发。PCA9422 的中断输出接到 PIC32MZ2048EFM100 的外部中断引脚,芯片内部检测到充电完成、输入拔掉、电池低压时就会主动拉这个脚,主控从休眠中醒来后读取中断状态寄存器,再跳转到对应处理分支。
这样设计最直接的好处是省电,主控不用为了检查电池电压而周期性地醒来。坏处是对中断服务程序的可靠性要求更高,中断里读 I2C 要非常小心,宁可先置标志位再在主循环处理,也不要在一段非常长的中断函数里做复杂逻辑。我在调试早期犯过这个错,中断函数里做数据缓冲处理,结果把主循环的任务饿死了,整机表现就是偶尔卡顿。
现在的中断服务程序只做两件事:读取并清除 PMIC 中断状态,把对应事件标志位置位。真正的电压切换、充电策略调整都放到主循环的状态机里去执行。这样代码逻辑清晰,也不会漏事件。
5. 常见问题与排查技巧实录
5.1 I2C 读不到寄存器,波形看着像有时序问题
这种情况我先怀疑三件事:地址对不对、上拉有没有、电源起来没有。PMIC 如果本身供电没完全起来,I2C 端口可能没激活,表现就是 NACK 或总线一直忙。用示波器抓 SCL 和 SDA,能很直观地区分是总线被拉死还是从机没响应。
我踩过比较隐蔽的坑是时钟延展导致的读取超时。之前把 DISSLW 关掉测试过,结果在 PMIC 内部状态切换时偶发卡死,后来打开时钟延展支持并加了超时保护,这个问题再没出现过。建议写一个 I2C 总线恢复函数,检测到超时就把总线拉几位释放,然后重新初始化,这样比复位整机温和得多。
5.2 充电电流一直上不去
充电电流上不去别急着怀疑 PMIC 坏了。先看输入限流寄存器,如果输入限流设得比充电电流还小,实际充电电流会被限制住。再看是否处于预充电阶段,电池电压过低时 PMIC 会以小电流预充,这时候你设置的大电流不生效是正常的。
温度检测也是一个隐藏因素。电池 NTC 没接好或者阻值不匹配,PMIC 可能进入温度保护状态,直接禁止大电流充电。我调试时发现充电电流诡异波动,最后是一颗 NTC 分压电阻的精度问题导致的,换成 1% 精度就好了。
5.3 输出纹波大、电感啸叫
纹波大最常见的原因是输出电容容量不够或者放得离电感太远。我碰到过一次电感啸叫,是电感饱和电流选小了,负载瞬态时电感进入饱和,噪声顺着地层传开,贴近板子都能听到声音。换了一颗饱和电流余量大一倍的电感后,啸叫立刻消失。
还有一点容易被忽略,PMIC 的环路补偿通常是根据推荐输出电容范围设计的。如果为了省钱把输出电容降到推荐值以下,环路相位余量不足,动态响应会变差甚至振荡。开关电源的电容选取规则是宁多勿少,优先级高于省那几分钱。
5.4 PMIC 配置上电就丢,系统行为随机
PCA9422 这类 PMIC 的寄存器大部分是易失性的,重新上电后就回到默认值。如果主控还没来得及配置,外设就可能处于错误电压下。我在项目里把所有默认配置写成了常量表,放在主控 Flash 中,PMIC 初始化函数第一件事就是刷这张表,并且加回读校验。
回读校验在这里不是形式主义。配置写失败一次,系统可能整机都会异常,而且这种异常非常难查。我实测过在强干扰环境下寄存器写入偶发出错,加了写后回读加重试之后,再没有出现过隐蔽配置错误。
5.5 调试问题速查表
| 现象 | 可能原因 | 排查办法 |
|---|---|---|
| I2C 无应答 | 上拉电阻缺失、地址错误 | 量总线波形,确认 ADDR 脚电平 |
| 充电电流偏低 | 输入限流设置过小 | 读寄存器比对配置 |
| 电池电压跳动 | 采样线受功率电流干扰 | 改开尔文走线,避开充电回路 |
| 纹波偏大 | 输出电容不足或位置差 | 增加 X7R 电容,贴引脚放置 |
| 偶尔死机 | 中断函数处理过重 | 中断里只置标志位 |
| 低功耗失效 | 唤醒源没配好 | 检查 PMIC 中断是否真正产生 |
这块板子最终跑稳定之后,我最大的体会是 PMIC 和 MCU 这套组合,真正的难点不在硬件的龙飞凤舞,而在于把电源状态当成一个持续存在的事件源去管理。无论是上电初始化、充电状态切换,还是低功耗唤醒,所有动作都要有清晰的时序和校验逻辑,不能抱着"写个寄存器应该没问题"的心态。PCA9422 把硬件集成度拉高了,PIC32MZ2048EFM100 又把软件可控性给足了,剩下要做的就是把每次电源切换都当作一次正式的状态迁移来对待。调试过程中我养成的习惯是:每次改完电源参数,一定跑一遍回读校验,每次进低功耗前,一定确认唤醒链路完整。如果你也在做类似的电池供电产品,建议最开始就把这几条习惯写进代码规范里,后面能少熬很多个晚上。