前阵子给一款电池供电的便携式计量设备做电源改版,主控选的是 PIC18F46K20,电池管理部分换成 PCA9422 这颗 PMIC。一开始我以为电源部分无非就是“LDO 加电容、MCU 起来干活”,真正调完一圈才发现,完整电源管理这件事,远比把几路电压接通要复杂得多。这篇文章把整个方案的架构、硬件互连、I2C 配置、低功耗协同、充电与电量监测,以及调试中踩过的那些真实“掉电”经历,全部捋一遍。想用 PMIC 给 8 位 MCU 做全套电源管理的朋友,可以直接参考这套思路,少走很多弯路。
1. 为什么一颗 8 位 MCU 也要认真规划电源树
很多做嵌入式的人对“电源管理”的第一反应是:MCU 内部有 LDO、有上电复位、有欠压复位,外部再接个 3.3V 稳压芯片,不是就够了吗?我第一次也是这么想的。但实际把 PIC18F46K20 接到锂电池上测试,问题一个接一个冒出来。
先说电压范围。PIC18F46K20 的 VDD 范围大概在 1.8V~3.6V,看起来锂电池从 3.0V 放到 4.2V 它都能扛住。但板子上不是只有 MCU,还有运放、LCD 背光、传感器,这些器件的工作电压各不相同。运放可能要求 3.3V 供电,传感器可能要 1.8V,背光驱动要 5V 或者 12V。如果直接从电池取电,电压从 4.2V 一路跌到 3.0V,所有模拟部分的基准都会跟着飘,ADC 采样结果根本没法用。
其次是锂电池本身的约束。锂电不能过放,单节满电 4.2V,标称 3.7V,放电到 3.0V 以下就要切保护。如果不做精确的电压监测和充电管理,电池寿命会快速衰减,甚至出现鼓包。更关键的是,电池在负载突变时电压会瞬时跌落,例如射频发射或电机启动的瞬间,电流可能到几百毫安,这时候电池内阻上的压降能达到几百毫伏。MCU 内部的 LDO 压差余量原本就不大,一旦 VDD 跌到复位阈值以下,芯片就会反复复位,表现为设备“死机”或者“莫名重启”。
还有一个容易被忽略的点:地弹和纹波。电池直接供电确实省事,但大电流瞬态会在 GND 上产生压差,模拟地、数字地、功率地如果不分开处理,ADC 的采样值会有一堆毛刺。你去查代码永远查不出问题,因为问题在电源环路里。这就是我在这个项目里决定引入专用 PMIC 的根本原因——把“充电、多路电压轨生成、电压监测、状态上报”这四件事从 MCU 身上剥离开,交给专门管电的芯片去做。
PIC18F46K20 作为一颗 8 位 MCU,本身功耗不高,深度睡眠下能做到微安级,但它的能力边界也很明显:没有充电管理,没有多路可配置输出,没有电池温度监测通道。而 PCA9422 这类 PMIC 恰好把这些功能集成了进去,通过 I2C 接口让 MCU 可以进行配置和状态读取。把这两颗芯片放一起,MCU 负责业务逻辑和低功耗策略,PMIC 负责底层电源调度,整个系统的电源架构才算完整。
补充一个选型思路:不是所有项目都需要独立 PMIC。如果你的产品用 USB 供电、电压固定 5V、板上只有一路 3.3V、没有电池、没有高精度模拟部分,那 PMIC 就是浪费。但只要你面对的是电池供电、多电压轨、有低功耗要求、还可能涉及充电管理的系统,独立 PMIC 几乎是必需品。这个判断标准,是我在多个项目里反复验证后得出的结论。
2. PCA9422 与 PIC18F46K20 的硬件互连:引脚映射、滤波和上电时序
确定了方案之后,第一件事是把 PCA9422 和 PIC18F46K20 之间的硬件连接规划清楚。PCA9422 的典型应用拓扑是:外部 USB 或适配器输入接到 VIN,锂电池接到 BAT 引脚,系统负载从 VOUT1/VOUT2 等输出轨取电。PIC18F46K20 作为主控,通过 I2C 管理 PCA9422 的寄存器。
我画出来的信号连接表大概是这样:
| 功能 | PCA9422 引脚 | PIC18F46K20 引脚 | 备注 |
|---|---|---|---|
| I2C 时钟 | SCL | RC3(MSSP1_SCL) | 需接 4.7kΩ 上拉到 VOUT1 |
| I2C 数据 | SDA | RC4(MSSP1_SDA) | 需接 4.7kΩ 上拉到 VOUT1 |
| 中断输出 | INTB | RA0(外部中断) | 低有效,需 10kΩ 上拉 |
| 电源控制 | PWRON | RE0 | MCU 控制 PMIC 开关机 |
| VOUT1 输出 | VOUT1 | MCU VDD 及板上 3.3V 负载 | 加 10µF+0.1µF 去耦 |
| VOUT2 输出 | VOUT2 | 传感器或 1.8V 逻辑 | 可以软件关断 |
| 电池正极 | VBAT | 锂电池 | 靠近 PMIC 放置大容量电容 |
这里有个细节值得多说一句:VOUT1 作为主电源轨给 MCU 供电,输出电容不能只放一颗。陶瓷电容的容值会随着直流偏置电压升高而下降,3.3V 波动还会导致容值变化,所以我会在输出端并联 10µF X5R 和 0.1µF 的高频去耦电容,同时保证走线短而粗,减少寄生电感。PIC18F46K20 的数据手册里对 VDD 上升时间有要求,虽然用 PMIC 的软启动能天然满足,但我还是建议在 VDD 引脚附近保留一个 1kΩ 串联电阻和 22µF 电容组成的“慢启动”预留位,防止换用不同体质的 PMIC 后上电斜率发生变化。
上电时序是很多人踩坑的地方。PCA9422 的 VOUT1 和 VOUT2 在使能之后并不是瞬间达到稳定值,而是有一个软启动过程。MCU 如果在这个过程还没结束时就发 I2C 命令,轻则命令丢失,重则把 I2C 总线状态搞乱。我采取的时序是:PWRON 拉高之后,等待 PMIC 的 IRQ 引脚出现“电源就绪”中断,或者直接延时 2ms~3ms,再开始初始化寄存器。如果使用中断方式,需要把 INTB 引脚配置为下降沿触发,同时在 MCU 侧把这个引脚设置为输入模式并禁用弱上拉——因为 PMIC 的 INTB 本身就是开漏输出,外部上拉已经接了,再开 MCU 的弱上拉会影响电平判断。
PCB 布局方面,PCA9422 靠近电池连接器放,PIC18F46K20 靠近 VOUT1 输出端放,充电回路和信号回路之间要保持距离。这样既减少大电流对 MCU 的干扰,也能让 I2C 走线尽量短。电源地和模拟地采用单点接地,两个地之间用 0Ω 电阻或者磁珠连接。我第一次布板时图省事,直接把所有地都焊在一起,结果充电时 ADC 读数波动非常大,后面改成单点接地才正常。
还有一点容易被忽略:PCA9422 的 I2C 地址通常由外部引脚 A0/A1 的电平决定。我把 A0 和 A1 都接地,所以 7 位地址是 0x60。如果你的板子上有多个相同型号的 PMIC,可以通过这个引脚错开地址,这也是硬件设计阶段就要考虑的事情,等固件写死地址之后再改,会非常痛苦。
3. 寄存器初始化与 I2C 通信的工程化封装
硬件连线没问题之后,软件层面最核心的工作就是通过 I2C 把 PCA9422 配置成我们想要的工作状态。这颗 PMIC 的大部分行为都映射到寄存器里:输出电压挡位、充电电流、中断使能、状态读取、软启动控制等。
我先把初始化流程写成代码。下面的代码是基于 MPLAB XC8 框架的示例,寄存器地址在不同批次的 PCA9422 上可能略有差异,正式移植时要以官方数据手册为准。
#include <xc.h> #include "i2c_sw.h" #define PCA9422_ADDR 0x60 #define REG_DEVICE_ID 0x00 #define REG_SYS_STATUS 0x01 #define REG_VOUT1_VSEL 0x02 #define REG_VOUT2_VSEL 0x03 #define REG_CHG_VLIM 0x04 #define REG_CHG_ILIM 0x05 #define REG_INT_ENABLE 0x06 #define REG_INT_STATUS 0x07 #define REG_PWR_CTRL 0x08 static uint8_t pca9422_read(uint8_t reg) { i2c_start(); i2c_write(PCA9422_ADDR << 1); i2c_write(reg); i2c_restart(); i2c_write((PCA9422_ADDR << 1) | 0x01); uint8_t val = i2c_read(NACK); i2c_stop(); return val; } static void pca9422_write(uint8_t reg, uint8_t val) { i2c_start(); i2c_write(PCA9422_ADDR << 1); i2c_write(reg); i2c_write(val); i2c_stop(); } void pca9422_init(void) { uint16_t timeout = 1000; while (timeout--); // 等待上电稳定,实际用延时函数代替 pca9422_write(REG_PWR_CTRL, 0x00); // 先关闭输出,再配置参数 pca9422_write(REG_VOUT1_VSEL, 0x26); // 设置 VOUT1 输出 3.3V pca9422_write(REG_VOUT2_VSEL, 0x1B); // 设置 VOUT2 输出 1.8V pca9422_write(REG_CHG_VLIM, 0x58); // 充电截止电压 4.2V pca9422_write(REG_CHG_ILIM, 0x50); // 充电电流 500mA pca9422_write(REG_INT_ENABLE, 0x1F); // 使能充电完成/欠压/过温等中断 pca9422_write(REG_PWR_CTRL, 0x03); // 使能 VOUT1 和 VOUT2 }这段代码看起来简单,但工程化封装的时候有几个坑。
第一,寄存器地址和值之间不是直接的电压值,而是查表编码。0x26 对应的可能是 3.3V,0x1B 对应的可能是 1.8V,具体要看数据手册里的“Output Voltage Programming Table”。如果你照抄别人的代码而没有核对表格,很可能输出一个你完全想不到的电压。我吃过这个亏,最后是拿万用表一量才发现 VOUT1 输出的是 2.8V 而不是 3.3V。
第二,I2C 读写必须做故障处理。PMIC 工作在充电回路附近,大电流切换时会产生电磁干扰,I2C 通信偶尔会丢一个字节。我在封装的读写函数里加了回读校验:写入之后立刻读同一寄存器,不一致就重试三次,三次都失败就置故障标志。这个故障标志在调试阶段可以直接打印,在生产阶段可以用来触发系统关机保护。
uint8_t pca9422_write_verify(uint8_t reg, uint8_t val) { uint8_t try; for (try = 0; try < 3; try++) { pca9422_write(reg, val); uint8_t rd = pca9422_read(reg); if (rd == val) return 0; } return 0xFF; }第三,中断处理的顺序有讲究。PCA9422 的 INTB 引脚是低有效,任何使能的中断源都会把这个引脚拉低。如果你把全部中断使能打开,但代码里没有及时读取中断状态寄存器,那么这个引脚会一直被拉低,MCU 的外部中断会不断触发,看起来就像系统卡死。正确的做法是:初始化时先读取一次中断状态寄存器,把旧事件清掉,再使能新的中断源。每次进入中断服务程序后,也要先读状态、再清标志、最后处理业务。
还有一点是关于 I2C 时钟频率的。我一开始用 400kHz 快速模式,结果充电状态下偶发通信错误。降到 100kHz 之后,问题基本消失。功耗要求不高的场景,I2C 时钟慢一点其实更稳,尤其是 PMIC 这种芯片,它的寄存器更新不追求毫秒级响应,100kHz 完全够用。
4. 睡眠-唤醒协同:PMIC 与 MCU 的功耗状态切换
做电池供电产品,低功耗是绕不开的命题。PIC18F46K20 的深度睡眠电流可以做到微安级,但你只让 MCU 睡到微安级没用——板上的 PMIC、传感器、分压电阻都在耗电。真正严苛的功耗指标,必须把 PMIC 也纳入睡眠状态机统一调度。
我的方案是把整个系统分成四个功耗状态:
- 运行态(RUN):所有电压轨正常输出,MCU 全速运行,功耗最大
- 空闲态(IDLE):CPU 停止执行指令,但外设时钟还开着,PMIC 输出不变,适合等待短时间的定时事件
- 睡眠态(SLEEP):MCU 进入深度睡眠,PMIC 关闭 VOUT2 等非必要输出轨,保留 VOUT1 给 MCU 供电,整机电流明显下降
- 关机态(OFF):PMIC 关闭所有输出轨,系统等待外部唤醒信号(如按键、充电插入)
这四个状态之间的切换由软件状态机控制。每次状态切换,MCU 都要通过 I2C 动态调整 PCA9422 的寄存器,而不是只改一个 GPIO。例如从运行态进入睡眠态时,我会执行:
void system_enter_sleep(void) { // 先把业务数据保存到 EEPROM eeprom_write(0x00, 0x5A); // 关闭外设供电轨 VOUT2 uint8_t ctrl = pca9422_read(REG_PWR_CTRL); ctrl &= ~0x02; // 清除 VOUT2 使能位 pca9422_write(REG_PWR_CTRL, ctrl); // 读取并清掉累积的中断,防止唤醒后误触发 uint8_t st = pca9422_read(REG_INT_STATUS); (void)st; // 开启 PMIC 唤醒中断源 uint8_t ie = pca9422_read(REG_INT_ENABLE); ie |= 0x10; // 使能 RTC/按键唤醒事件 pca9422_write(REG_INT_ENABLE, ie); // 进入 MCU 深度睡眠 SLEEP(); }唤醒路径要注意一个关键问题:MCU 和外设的恢复速度可能不一致。MCU 从深度睡眠醒来得很快,但 PMIC 重新使能 VOUT2 之后,1.8V 轨要软启动到稳定值,可能需要几百微秒甚至几毫秒。如果 MCU 醒来之后立刻去操作挂在 VOUT2 上的传感器,大概率读到的数据是乱的或者直接 I2C 超时。我习惯在唤醒中断里加一个固定延时,然后读 PMIC 的电源状态寄存器,确认输出轨稳定后再继续执行任务。
还有一个更隐蔽的坑:唤醒源的选择。PIC18F46K20 的深度睡眠模式下,很多外设都停止工作,能唤醒它的途径有限。如果你的唤醒源是 PMIC 的 INTB 引脚,那么 INTB 必须接到 MCU 的外部中断脚,并且这个中断脚要配置为数字输入模式,内部弱上拉要关掉。如果用的是定时器唤醒,那要确认时钟源在深度睡眠下是否还在运行。实测发现,不同时钟源在睡眠模式下的表现差异很大,有外部 32.768kHz 晶振时,唤醒时序最稳定;如果只依赖内部振荡器,唤醒后的时钟校准会影响 I2C 时序,后续读 PMIC 就可能会失败。
功耗测量这部分,我有一个建议:在硬件上预留一个 0.1Ω 的取样电阻焊盘,串在电池正极或系统总线上。测量睡眠电流时用示波器电流探头或者直接测取样电阻两端的压降,能非常直观地看到电流随时间变化的曲线。我最初就是靠这个办法发现“睡眠电流 1.2µA”的标称数据和实际 40µA 之间的巨大差异,原因是有几颗上拉电阻和 LED 指示灯没有纳入睡眠关断控制。把上拉电阻接到 VOUT2、LED 供电也接到 VOUT2,睡眠时统一关掉,整机电流瞬间降到 2µA 左右。
5. 充电控制、电量感知与整机功耗测量
PCA9422 这类带充电管理的 PMIC,内部会按照完整充电曲线工作。锂电池充电流程不是简单“恒压充到满”,而是分阶段:
- 预充电:电池电压低于某个阈值(比如 2.8V~3.0V)时,以小电流(通常是设定电流的 1/10)给电池唤醒,防止大电流冲击损坏电池
- 恒流充电:电池电压超过阈值后,以设定的充电电流(如 500mA)持续充
- 恒压充电:接近 4.2V 时切换为恒压模式,电流逐渐下降
- 截止判断:电流下降到某个阈值(如 100mA 或设定值的 1/10)时,判定充满并关断
MCU 需要做的事情,是实时读取充电状态寄存器,把充电中、充满、故障这三种状态反馈给用户。我做了两个 LED 指示灯,一个红色一个绿色,代码逻辑非常简单:
void charge_led_task(void) { uint8_t status = pca9422_read(REG_CHG_STATUS); if (status & CHG_CHARGING) { LED_RED = 1; LED_GREEN = 0; } else if (status & CHG_DONE) { LED_RED = 0; LED_GREEN = 1; } else { LED_RED = 1; LED_GREEN = 1; // 故障状态,双灯常亮 } }但这里有几个值得展开的实际问题。第一个是充电电流的设置不能闭眼填大值。很多工程师想缩短充电时间,直接把充电电流设到 800mA、1A,结果发现 PMIC 或电池烫得不行。充电电流的选择要根据电池容量、PCB 散热条件、充电线缆电阻综合考虑。我用的是 500mA,充一块 1200mAh 的电池大概需要两个半小时,虽然时间不是最短,但整板温度控制在 40°C 以下,没有热漂移问题。如果你想缩短充电时间,建议先做热测试,确认最高温度在安全范围内再往上调。
第二个是电量监测的精度问题。我没有用独立库仑计芯片,而是用 PCA9422 上报的电池电压做估算。这个方法在项目初期够用,但必须知道它的局限:电池内阻会随温度变化,电压法估算误差可能超过 20%。尤其是电池老化之后,充满电压表现正常,但放电曲线会明显提前“掉崖”。如果产品需要精确显示剩余电量,最好加一颗库仑计,或者在电池负极加采样电阻做实际电流积分。我在设计时预留了采样电阻和差分 ADC 通道的位置,后面升级不用改板子。
第三个是充电和系统供电的路径问题。很多 PMIC 的架构是“系统回路和电池回路分开”,插入 USB 时由外部输入直接给系统供电,同时给电池充电,拔掉 USB 后由电池供电。这种架构的好处是插着充时系统电压更稳,但对 PCB 布局要求高一些。充电大电流和系统负载电流会在 GND 平面上交汇,如果不做星型接地,MCU 的 ADC 采样会受到明显干扰。我碰到过一次 ADC 波动,排查半天,最后发现磁场干扰来自充电电感的位置,把电感旋转 90° 之后读数就稳了。这类问题在原理图上完全看不到,只能在 PCB 布局和实测中慢慢调。
整机功耗测量方面,我用的方法是在电池端串联一个 0.1Ω 取样电阻,用示波器观察电压波形。电流换算很简单:I = U / 0.1Ω。举例说,睡眠时示波器上看到 0.12µV 的压降,对应电流就是 1.2µA。但要注意,示波器的探头噪声本身就可能超过这个数值,所以我用的是低噪声电流探头或者高精度万用表的电流档。测量时要覆盖整个工作周期,不能只看一瞬间,因为系统会周期性唤醒、采集、再入睡,平均电流才是电池续航的关键指标。
我记得第一次测量整机平均电流时估算续航有三个月,实测却只撑了一个多月。后来才发现是 I2C 上拉电阻在睡眠模式下没断电,24 小时都在耗电;还有一个电压检测分压电阻,直接跨接在电池正极和地之间,几十微安白白流掉。把分压电阻加上 MOS 管开关、把上拉电阻挪到可关断的电源轨,平均电流才真正降到设计值。
6. 几次真实掉电问题的排查过程和结论
这部分的坑,才是完整电源管理真正考验人的地方。我选三个比较有代表性的问题来说。
第一次遇到的是“PMIC 配置总是丢”。现象:设备运行三五十分钟后突然掉电重启,重启后读 PCA9422 寄存器,发现所有配置回到默认值,MCU 必须重新初始化一次。刚开始我怀疑内部寄存器有随机翻转,于是把初始化代码里的回读校验打开,结果发现每笔写入都成功,说明不是写失败。后来测量 IRQ 引脚,掉电前它会拉低一段时间,回读状态寄存器显示“输入电压欠压”。顺着供电链路查,发现 USB 线接触电阻偏高,设备负载一上来,VIN 就被拉到阈值以下,PMIC 判定输入异常,直接关断输出。解决办法其实简单:换一根粗短线,同时在固件初始化里把欠压阈值调低一点,相当于留出电压余量。这个问题的教训是:不能只看 PMIC 引脚上的电压,要看系统在真实负载下的动态电压,很多电源问题都是“负载一拉,电压就垮”。
第二次是“休眠后无法唤醒”。表现:设备进入睡眠模式后,按唤醒按键没反应,必须拔电池重插。排查链路比较曲折。一开始怀疑 PCA9422 的 RTC 唤醒配置有问题,反复检查寄存器都没发现异常。后来测 MCU 引脚电平,发现 INTB 确实拉低了,但 MCU 就是没进中断。最后查 MCU 的休眠前代码,发现我在进入深度睡眠之前把 MSSP 外设关闭了,唤醒后的中断服务程序里又去读 PMIC 寄存器,但 I2C 模块还没重新初始化,导致读操作永远超时,中断服务死循环,系统看起来就像“醒不过来”。修复方案是封装一个power_resume_peripherals()函数,在唤醒后先把 MSSP、时钟、中断优先级重新配置好,再去访问任何 I2C 设备。这个坑提醒我:低功耗代码不只是把 MCU 睡下去,还要把“醒来后第一件事干什么”设计好,外设重初始化顺序写错,比不睡还难受。
第三次是“充电指示灯闪烁”。现象:红灯绿灯交替闪,大概 1Hz 频率,看起来像故障,但读充电状态寄存器,发现确实在充电中。我用示波器抓了电池电压波形,发现电池电压在恒流和恒压之间来回抖动,PMIC 在里面反复切换状态。究其原因,是我把充电电流设得偏高,电池在达到恒压阈值之前,电压已经因为充电电流偏大而出现了明显的上升,结果 PMIC 误判为进入恒压阶段,开始降低电流,电流一低,电池电压回落,又切回恒流,如此往复。把充电电流从 800mA 降到 500mA 后,这个问题就消失了。所以在设计充电参数时,不仅要看电池容量,还要看电池内阻、线缆压降和 PMIC 的检测精度,不同批次电池的曲线差异也会影响这个切换点。
除了这三个典型问题,还可以分享一个给所有电池项目的建议:一定要在硬件上预留充电电流和电池电压的测试点。没有测试点,示波器探头都没地方夹。我后来设计的每个电源板都预留了两个 2.54mm 的排针孔位,一个接 VBAT,一个接 GND,调试时直接夹探针,效率翻倍。
关于这套 PCA9422 加 PIC18F46K20 的方案,我最后的体会是:电源管理芯片解决的是“硬件层的秩序”,真正让系统稳定的是“软件层的调度”。初始化代码、睡眠状态机、I2C 故障处理,每一个环节都能让看似高端的 PMIC 发挥不出价值。建议做类似项目的朋友,一开始就在主板上预留取样电阻和测试点,然后把 I2C 通信封装好回读校验,最后用示波器把各种状态下的电流波形都抓一遍。做到这三件事,你的电源管理基本就稳了。