做电池供电的嵌入式设备,电源管理从来不是"接一个稳压芯片、加一个充电模块"那么简单。尤其是手头同时有PCA9422这样的电源管理芯片和PIC18F87J50这样的主控时,两者怎么分工、怎么接线、怎么在运行时调整策略,往往直接决定整机的续航和可靠性。这篇文章把我完成一套完整电源管理设计的全过程拆开讲:为什么选这对组合、硬件连接有哪些细节、I2C 配置顺序怎么排、状态机怎么写,以及量产调试时最容易踩的三个坑。如果你正在用分离式充电管理芯片加单片机做便携设备,这篇里的思路、代码和参数可以直接作为参照。
1. 为什么锁定 PCA9422 + PIC18F87J50,而不是用分立方案
1.1 分立电源方案的先天不足
很多项目一开始都是先选一颗充电管理 IC,再挂一颗 LDO,再从 MCU 里抽一个 ADC 通道去采样电池电压,最后拼出一块"电源板"。这种方案不是不能跑,而是一旦整机既要充电、又要放电、还要低功耗待机,需要协调的东西就会越堆越多。充电 IC 只管充电回路,LDO 只管输出电压,电压检测要 MCU 自己轮询,关机逻辑要自己搭 MOS 电路。出了问题,先得判断是哪颗芯片在闹脾气,调试链路被拉得很长。
我做过对比实验:用分立方案时,想实现"插入适配器后自动切换供电并开始充电"这个功能,软件里要写两套独立的 ADC 采样,一套盯 VBUS,一套盯 BAT,还要处理上电抖动和切换瞬间的毛刺。稍不留神,系统就会在适配器拔出的一瞬间出现一次短暂掉电复位。换成带电源路径管理的 PMIC 之后,这个切换由芯片内部自动完成,软件只需要等中断通知。
1.2 PCA9422 和 PIC18F87J50 各自的分内之事
这套组合的分工逻辑很清晰:PCA9422 负责"功率层",PIC18F87J50 负责"策略层"。
PCA9422 这类 PMIC 把充电管理、适配器与电池之间的路径切换、SYS 稳压输出、电流电压采样、多种保护逻辑都集成在内部。它不是一个单纯的充电 IC,而是一个可以随时通过 I2C 改写配置的电源中枢。比如充电电流可以动态调整,电池欠压保护点可以配置,内部 ADC 可以告诉我当前电池电压和系统电流。这些信息如果让 MCU 自己采样,至少要占用两个 ADC 通道和一堆分压电阻,而且很难做到电源状态切换瞬间的实时性。
PIC18F87J50 则是一个非常适合当主控的 8 位芯片:3.3V 工作、有 MSSP 模块可以走 I2C 硬件协议、IO 数量充足、休眠功耗可控。它在这里不需要干重活,只需要定时读一读 PMIC 的状态寄存器,根据运行场景写寄存器,处理 PMIC 发来的中断事件。把功率执行和策略调度分开之后,硬件保护有 PMIC 兜底,策略逻辑由 MCU 统一调度,两边各自专注。
1.3 一套配置、多状态输出的收益
用一张表能把这套组合和分立方案的区别说清楚:
| 对比维度 | 分立式方案 | PCA9422 + PIC18F87J50 |
|---|---|---|
| BOM 数量 | 充电 IC、LDO、保护 IC、运放等一堆 | PMIC 加主控,外围以电容电阻为主 |
| 运行时可调 | 充电电流和输出电压基本固定 | 通过 I2C 动态调整电流、电压、保护点 |
| 事件上报 | 需要 MCU 轮询 ADC 判断 | PMIC 中断脚主动上报事件 |
| 低功耗手段 | 靠多颗芯片使能脚分头控制 | 一条指令进入 Ship Mode |
| 调试复杂度 | 多芯片交叉排查 | 先验证通信,再验证策略 |
对我来说最值钱的一条是"事件上报"。PCA9422 的 INT 脚挂在 PIC18F87J50 的外部中断上,适配器插入、拔出、充电完成、电池低电压这些事件都会触发中断,MCU 不用每隔几十毫秒去查寄存器。这在休眠状态下尤其重要——我可以在睡梦中被电源事件唤醒处理,而不是靠定时器醒来查一下再睡回去。
2. 硬件连接拆解:把 PCA9422 和 PIC18F87J50 接在一起
2.1 引脚级连接关系
先说最常见的连接方式,具体引脚以手里那颗芯片的封装为准:
| PCA9422 功能引脚 | PIC18F87J50 引脚 | 说明 |
|---|---|---|
| VBUS | 适配器 / USB 5V 输入 | PMIC 的输入供电 |
| BAT | 锂电池正极 | 单节锂电直接接入 |
| SYS | PIC 的 VDD 电源轨 | PMIC 稳压输出 |
| SDA | RC4 | I2C 数据线 |
| SCL | RC3 | I2C 时钟线 |
| INT | RB0 | 电源事件中断,接外部中断 0 |
| EN | 普通 IO 或直接上拉 | 主控可控制 PMIC 输出使能 |
关于 SYS 输出和 PIC 供电,有一个容易被忽略的点:PIC18F87J50 是一颗 3.3V 器件,所以 SYS 电压要设置在 3.3V 附近,或者再串一级 LDO。我在项目里直接把 SYS 配置为 3.3V,省掉额外的稳压环节,但前提是确认 PMIC 在电池电压低于 3.3V 的时候仍能维持 SYS 输出。如果 PMIC 是降压拓扑,电池电压 3.3V 以下时 SYS 会跟随电池跌落,这种情况就得在 SYS 和 MCU 之间补一个 LDO。
2.2 电源域与地线处理的几个细节
硬件上最需要注意的是功率路径。VBUS 输入、BAT、SYS 这条主回路上的电流是安培级别的,PCB 走线宽度要根据铜厚和最大电流换算。我一般按 2A 去留余量,1oz 铜厚情况下走线宽度不低于 40mil,并且尽量短粗。地线要单点汇合,功率地、模拟地、数字地在电池负极位置汇聚,避免充电电流在数字地上造成压降干扰 I2C 通信。
去耦电容这方面不能省:VBUS 输入放 10uF 陶瓷电容,SYS 输出根据负载瞬态情况放 22uF 左右,BAT 引脚也要 1uF 到 10uF。PCA9422 内部的充电器是开关拓扑,电池端、输入端的开关噪声需要电容吸收。网上经常有人问"为什么我的 PMIC 一工作,采样电压就跳个不停",多数是布局和电容问题,不是芯片问题。
2.3 上拉电阻与中断引脚的设计
I2C 总线需要外部上拉电阻。100kHz 模式下用 4.7k 比较稳,400kHz 模式建议换成 2.2k,具体看总线电容。SDA 和 SCL 走线要远离功率电感,最好加地线隔离。充电电流大时,电感附近的磁场会在闭合环路里感应出噪声,直接污染 I2C 波形,这个在后面调试部分还会提到。
INT 脚的处理也很关键。PMIC 的中断输出一般是低有效,因此 PIC18F87J50 的外部中断要配置成下降沿触发。INT 线上建议串一个小电阻加滤波电容,形成 RC 低通,滤掉电源切换瞬间的尖峰,否则很容易一上电就误触发中断。我在早期版本里没有加这个滤波器,结果适配器插入时 INT 连续抖动,状态机被反复打断。
3. I2C 配置顺序:让 PCA9422 按预期工作
3.1 底层 I2C 驱动与寄存器读写函数
PCA9422 是一颗 I2C 可配置器件,所以软件层面的第一件事就是把 I2C 主模式跑通。我用的是 PIC18F87J50 的 MSSP 硬件模块,这样既省 CPU,稳定性也远比软件模拟好。
void I2C_Init(void) { TRISC3 = 1; // SCL 设为输入,开漏由外部上拉 TRISC4 = 1; // SDA 设为输入 SSPCON1 = 0x28; // 使能 MSSP,主模式 SSPCON2 = 0x00; SSPSTAT = 0x80; // 使能 slew rate 控制 // 8MHz 系统时钟,目标 100kHz SSPADD = ((8000000 / 4) / 100000) - 1; SSPIF = 0; } void I2C_Start(void) { SEN = 1; while (!SSPIF); SSPIF = 0; } void I2C_Stop(void) { PEN = 1; while (!SSPIF); SSPIF = 0; } unsigned char I2C_WriteByte(unsigned char dat) { SSPBUF = dat; while (!SSPIF); SSPIF = 0; return SSPCON2bits.ACKSTAT; // 1 表示未收到 ACK } unsigned char PCA9422_WriteReg(unsigned char reg, unsigned char val) { I2C_Start(); if (I2C_WriteByte(0xC4)) return 1; // 从机地址 0x62 左移 1 位,写方向 if (I2C_WriteByte(reg)) return 1; if (I2C_WriteByte(val)) return 1; I2C_Stop(); return 0; }这里要特别说明两点:第一,SSPADD 的公式在全速模式下是(Fosc / 4 / Fsck) - 1,如果主频不是 8MHz,要把数值重新算。第二,ACKSTAT 返回 1 代表没有收到从机应答,也就是通信失败。上面的写法里if (I2C_WriteByte(...))实际上是在判断 ACKSTAT,一开始不熟悉的时候容易把逻辑看反。同样可以写一个读寄存器函数,只是多一次重复起始位去切方向。
3.2 上电后的配置序列
很多人在 PMIC 上电后直接往寄存器里写参数,不看 ID 也不回读,结果某个值没写进去,整机行为变得非常诡异。稳妥的顺序应该是:
- 确认 VBUS 或 BAT 有输入,SYS 已经有一条基础输出路径。
- 通过 I2C 读器件 ID 寄存器,确认通信没问题。
- 设置输入限流、充电截止电压、充电电流、终止电流。
- 配置中断使能,把需要的事件打开。
- 配置内部 ADC 的采样触发方式。
- 每个关键寄存器写完以后回读一次,确认写入成功。
下面是我在一颗 1000mAh 单节锂电池上用的参数:
| 参数 | 设置值 | 说明 |
|---|---|---|
| 输入限流 | 500mA | 必须和适配器能力匹配 |
| 充电截止电压 | 4.2V | 常见锂电满电电压 |
| 充电电流 | 300mA | 约 0.3C,兼顾速度和发热 |
| 终止电流 | 50mA | 约 1/6 充电电流,过高充不满,过低易过充 |
| SYS 输出 | 3.3V | 匹配 MCU 与外设 |
| ADC 采样 | 关闭自动,由 MCU 触发 | 降低休眠电流,按需读取 |
这里最容易被忽略的是输入限流和充电电流之间的关系。如果适配器只能输出 500mA,系统负载还要吃掉 200mA,那充电电流就不能设成 500mA,否则输入总电流超限,适配器会被拉垮,SYS 电压也跟着往下掉。正确的做法是让"输入限流 ≥ 系统电流 + 充电电流",留出至少 20% 的余量。
3.3 参数怎么定:与电池规格逐项核对
我不建议拿着别人项目里的参数直接抄,因为电池规格不同,充电曲线完全不同。拿到一块新电池,至少要把这几项和数据手册对上:充电截止电压、最大充电电流、建议充电倍率、终止电流范围、放电截止电压。
举例来说,有些高能量密度电池标称充电截止电压是 4.35V,如果还按 4.2V 去充,会永远充不满,容量白白浪费。反过来,如果只能承受 0.5C 充电,你设了 1C 电流,轻则过热保护,重则鼓包。设置完成后,最好实测一段完整的 CC-CV 充电曲线,确认充电电流从恒流阶段平滑过渡到恒压阶段,并且在终止电流附近能正确停充。
4. 电源状态机:从充电、放电到休眠的完整调度
4.1 状态定义与事件转移
有了 PMIC 的寄存器读写能力,下一步就是把整机的电源状态抽象成有限状态机。我一般只保留四个状态:
- CHARGE:适配器接入,电池未满,PMIC 正在充电,SYS 由适配器供电。
- BATTERY:无适配器,由电池给 SYS 供电,系统正常运行。
- SLEEP:系统无任务,MCU 休眠,PMIC 保持基础输出并监测事件。
- SHIP:长期存放状态,PMIC 切断绝大部分负载,进入船运模式,等待下一次上电恢复。
状态转移关系可以用一张表描述:
| 当前状态 | 触发事件 | 下一状态 | 完成动作 |
|---|---|---|---|
| CHARGE | 充电完成 / 适配器拔出 | BATTERY | 读取状态并上报 |
| BATTERY | 适配器插入 | CHARGE | 重新配置充电参数 |
| BATTERY | 系统进入空闲 | SLEEP | MCU 休眠 |
| SLEEP | 外部中断唤醒 | BATTERY | 恢复外设并处理事件 |
| BATTERY | 电池电压低于关机阈值 | SHIP | 写入船运模式指令 |
| SHIP | 适配器插入 / 恢复供电 | CHARGE | 重新初始化 PMIC |
这里最核心的一点是:不要把适配器拔出事件放在主循环里轮询。我在硬件上已经把 PMIC 的 INT 脚接到 PIC18F87J50 的 RB0,适配器拔出时会触发下降沿中断,MCU 即使处于 SLEEP 也能立刻被唤醒。这样既省电又及时。
4.2 主控侧的调度代码骨架
主循环里的状态机代码并不复杂,大致长这样:
typedef enum { STATE_CHARGE, STATE_BATTERY, STATE_SLEEP, STATE_SHIP } PowerState; PowerState currentState = STATE_BATTERY; void Power_StateMachine(void) { uint8_t vbusFlag = PCA9422_ReadVBUSStatus(); uint16_t vbatMv = PCA9422_ReadBatteryMillivolts(); if (vbusFlag) { if (vbatMv < FULL_THRESHOLD_MV) { currentState = STATE_CHARGE; PCA9422_WriteReg(REG_CHARGE_CTRL, 0x03); } else { currentState = STATE_BATTERY; } } else { if (vbatMv < SHUTDOWN_THRESHOLD_MV) { currentState = STATE_SHIP; PCA9422_WriteReg(REG_SHIP_MODE_CTRL, 0x01); } else if (system_is_idle()) { currentState = STATE_SLEEP; enter_sleep_mode(); } else { currentState = STATE_BATTERY; } } }这段代码只是骨架,实际项目里我会把"读取 VBUS 状态"和"读取电池电压"分别做成函数,底层通过 I2C 访问 PMIC 的寄存器,再转换成易读的状态值。进入 SHIP 模式之前必须想清楚唤醒路径:如果你只靠适配器插入唤醒,那么整机在存放期间就不会被按键意外点亮,逻辑更干净。
4.3 低功耗模式下的协作机制
低功耗不是让 MCU 睡个觉就算完,PMIC 那边的配置也要同步配合。PIC18F87J50 进入 SLEEP 之前,我通常会做三件事:
- 关闭 PMIC 内部 ADC 的周期采样,避免它每隔几十毫秒醒一次去读一次电压。
- 屏蔽不需要关心的中断源,只保留适配器插入、拔出和电池低压事件。
- 把 I2C 的 SDA、SCL 引脚固定在确定电平,避免悬空带来的漏电。
进入休眠的代码也很短:
// 进入休眠前的准备 PCA9422_WriteReg(REG_ADC_CTRL, 0x00); // 关闭周期采样 PCA9422_WriteReg(REG_INT_MASK, 0xFE); // 屏蔽不必要事件 INTCONbits.INT0IF = 0; // 清中断标志 INTCONbits.INT0IE = 1; // 使能外部中断 Sleep(); // 被唤醒后,先恢复 ADC 采样再继续跑 PCA9422_WriteReg(REG_ADC_CTRL, 0x01);这里有个容易被忽视的细节:PIC 从休眠唤醒后,外设寄存器状态通常还保留着,但 I2C 模块如果之前在传输中途被禁止,有可能进入异常状态。保险起见,我唤醒后第一步重新调用 I2C_Init,再恢复 PMIC 配置。我遇到过唤醒后第一次 I2C 通信一直等不到 ACK 的情况,重新初始化后就好了。
5. 实测量产调试:三个容易翻车的环节
5.1 I2C 通信在充电噪声下失败
现象是:适配器插着充电时,MCU 偶尔读到全 0xFF,寄存器写不进去,整机时不时进入异常状态。用示波器抓 SDA 波形,能看到充电电流较大时,信号边沿明显变缓,低电平上还叠着周期性毛刺。根源是功率电感离 I2C 走线太近,以及 SYS 输出的纹波偏大。
排查顺序是先看供电再走线。我在 SYS 输出端补了一颗 22uF 陶瓷电容,把负载瞬态引起的跌落压住;同时把 I2C 两条线从电感下方移开,保持 3mm 以上间距;上拉电阻从 4.7k 换成 2.2k,让信号边沿更陡。改完之后连续跑 72 小时读写测试,没有再出现错误。
这条经验可以推广:凡是 PMIC 参与的项目,I2C 调试一定要在"真实充电电流"状态下进行,不能只在空载时验证。很多问题在小电流下根本暴露不出来。
5.2 充电终止条件一直不满足
比较隐蔽的坑。我的系统负载直接挂在 SYS 上,PMIC 处于路径管理模式时,适配器会优先给 SYS 供电,同时给电池充电。问题在于:PMIC 检测终止电流时,看到的其实是"电池电流 + 系统电流"的混合结果。系统在运行,负载电流波动,终止判断条件就永远达不到,电池一直停在恒压阶段,芯片发热,充电永远显示"进行中"。
排查思路是用万用表串联在电池负极,发现电池端电流确实已经降到 20mA 以下,但 PMIC 寄存器里的充电状态一直没有变成"完成"。处理方式是二选一:要么在充电末尾阶段通知系统把负载降到最小,让检测到的电流更接近真实的电池电流;要么不依赖 PMIC 的自动终止,由 MCU 来读电池电流,低于阈值后手动写寄存器停止充电。我在量产固件里选了后者,虽然多了几行代码,但可控度更高。
5.3 休眠电流比规格书多出近一倍
整机休眠目标电流 50uA,实测却有 110uA。逐段排查法比盲猜高效得多:先把 PCA9422 的 SYS 输出断开,测 MCU 侧电流;再把 MCU 和其他负载逐个恢复,测增量。最后发现问题在两处:一是 PMIC 内部 ADC 还开着周期采样,每秒醒来一次读电压,每次都要发作约 20uA 的电流;二是 PIC 的 I2C 两根引脚在休眠时悬空,I/O 漏电加上总线寄生,多吃了 15uA。
修法是按 4.3 节那样,进休眠前关闭 ADC 周期采样、把 I2C 引脚钳到确定电平。改完以后整机休眠电流降到 28uA,再写入船运模式后基本可以到 5uA 以内。这里想提醒一句:规格书上的超低电流往往是特定配置下的结果,自己完整配置后必须实测,不要想当然拿 Demo 的默认值当最终数据。
6. 写在最后的几条经验
这套 PCA9422 加 PIC18F87J50 的方案,我前后调了一个多月才完全稳定下来。个人最大的体会是:一定先把功率路径单独跑通,再接系统软件。硬件上电以后,先什么都不配置,观察 SYS 默认有没有输出,电池是否被正常充电,再用 I2C 去改寄存器。如果上来就写一堆配置,一旦出问题,很难分清是通信失败还是功率部分本身没搭对。
另外,PMIC 寄存器大部分在掉电后不会保留,所以 MCU 每次上电都要重新跑一遍完整初始化流程,不能指望芯片记住上次配置。回读校验这点,在量产阶段真的能救命,能帮你把"写进去没生效"的问题快速定位。
最后想到一个可以继续扩展的方向:PIC18F87J50 本身支持 USB 检测,可以配合 VBUS 状态来识别适配器能力,动态调整 PCA9422 的输入限流和充电电流。比如识别到大功率适配器就快充,识别到普通 USB 口就慢充,把整机的充电策略再往上提一档。这套组合的潜力,比我最初预想的要多不少。