一个周末深夜,我在整理上一个电池供电原型机的硬件资料时,想起最初那版电源方案翻车的经历。当时整机耗电、发热、电压不稳一起冒出来,最后决定把电源部分彻底重做,核心就是“使用 PCA9422 和 PIC18F46K40 实现完整电源管理”。这篇文章就把整个设计过程、计算、固件框架和踩坑记录完整整理出来,希望能给正在做锂电供电产品、可穿戴设备或低功耗采集节点的开发者一点参考。
如果你手上有类似需求:单节锂电池供电,需要充电管理、多处降压输出,还要 MCU 去监控电压/温度、响应中断、做整机休眠唤醒,那么这篇内容可以直接作为参考骨架。我会从选型逻辑讲起,再到电源架构、固定代码、实测数据和三个典型坑,尽量做到“能照着做”。
1. 为什么要把电源管理单独拆出来:系统需求与选型理由
1.1 单节锂电池产品的供电链路痛点
以前做小系统时,大家习惯把充电、降压、指示灯各用一颗芯片拼起来:充电IC负责给电池充电,一颗低压差稳压器给MCU供电,再放一两颗LDO给传感器。这样方案不是不能用,但对于一个要做量产化原型的产品,BOM数量、PCB面积、功耗控制都会变成负担。尤其是手上的项目需要在很小的一块主板上同时放电池、传感器、屏幕和电源管理,独立器件一多,布局就非常难受,而且每个器件的中断、状态指示、使能时序都得MCU单独处理,代码碎片化严重,排错时很难说清是电源问题还是程序问题。
如果换成一颗PMIC集中处理充电、路径管理和多路输出,再由一颗8位MCU负责策略控制,整个电源链路就清晰很多。充电状态不用MCU反复用ADC猜,PMIC可以直接给出充电完成/输入接入/故障中断;多路输出电压也可以由寄存器统一配置。这样硬件上省出一大片面积,软件上的状态机也更集中。当时我把这项工作定义为“把电源管理从零散外设收敛成独立子系统”,而PCA9422和PIC18F46K40就是这套子系统里的执行器和大脑。
1.2 PCA9422与PIC18F46K40的分工
我手里的这颗PCA9422面向低压便携设备,单节锂电池应用,内部把充电管理、电源路径管理、多个DC-DC降压输出和LDO输出集成在一起,并带有I2C接口和中断输出。它更接近“电源执行单元”:负责把输入5V或电池电压变成合适的系统电压,同时给出各种电源事件的中断信号。
PIC18F46K40在这里不是去重复实现充电算法,而是承担“系统电源策略控制器”的角色:通过I2C去初始化PCA9422,配置各路输出电压、充电电流、中断使能;再通过ADC读取电池电压、温度、输入电源状态;最后结合整机负载情况管理充电、休眠、唤醒和故障恢复。
| 功能 | PCA9422承担 | PIC18F46K40承担 |
|---|---|---|
| 输入与电池路径 | 路径管理、输入限流 | 监测输入接入中断 |
| 充电动作 | 预充、恒流、恒压、截止 | 充电状态机调度与策略 |
| 电压输出 | DCDC/LDO输出逐路稳压 | 通过I2C配置输出档位 |
| 故障处理 | 过温、过流、欠压中断 | 读取中断寄存器并恢复 |
| 低功耗 | 低功耗输出模式 | 进入休眠并按需唤醒 |
这个分工的好处是:PCA9422保证“电先稳下来”,PIC18F46K40负责“系统什么时候要电、要多少电”。两者通过I2C和一根中断线解耦,代码变化不会影响硬件供电安全。
1.3 选型中我对比过的备选方案
选型的时候并不是直接敲定,当时对比了三条路线:
- 充电IC加独立DCDC/LDO的组合,成本最低,但每路输出都需要反馈电阻、使能时序和状态指示,软件要盯的东西多,而且低功耗待机时外设漏电难以统一管理。
- 用更高端的多通道PMIC,功能强大、输出路数多,但很多通道用不上,寄存器复杂,初始化序列长,对小项目来说有点大材小用。
- 用PCA9422这种“中小规模PMIC加一颗通用MCU”,充电、降压集成得刚刚好,寄存器不算复杂,而且PIC18F46K40的休眠电流和唤醒机制比较适合做低功耗策略。
最终选了第三条路线。这里要强调一点:PMIC并不是集成度越高越好,关键是“够用、可控、可调”。PCA9422的寄存器级配置让整个电源策略可以放在固件里统一管理,这比让充电IC自动处理一切要灵活得多,也比所有逻辑都堆在高端PMIC里更容易掌控。
2. 从电池到每一路电压:电源架构与关键参数计算
2.1 电源树怎么设计
先明确整个系统的电源树。输入侧是USB 5V,PCA9422负责对单节锂电池充电,同时产生系统侧多路电压。我当时的需求是:MCU和数字传感器一路3.3V,低功耗无线链路一路1.8V,模拟前端需要比较干净的3.0V。于是电源树大致是:
- 输入VBUS 5V → PCA9422输入检测脚
- 锂电池正极 → PCA9422电池端,同时分压到PIC的ADC用于电压监测
- PCA9422 DCDC1 → 3.3V数字主供电
- PCA9422 DCDC2 → 1.8V无线链路供电,低功耗时可由寄存器调低待机电压
- PCA9422 LDO → 3.0V模拟供电,主要给传感器和信号链使用
- PCA9422输出电压都接到对应负载的电源输入,并在靠近负载端放10uF陶瓷电容加0.1uF高频电容
设计电源树时要特别注意地回路。数字3.3V和模拟3.0V虽然来自同一颗PMIC,但PCB上模拟地、数字地和电池地要尽量单点连接,否则ADC采样会看到几十毫伏的跳动,后面NTC温度采集也会跟着受影响。
2.2 充电电流与输出电压的量化计算
充电电流是第一个要计算的值。PCA9422这类PMIC一般支持通过外部电阻或寄存器设置恒流电流,常见做法是留一个ISET电阻。假设我们设定的最大充电电流是400mA,那么公式大致是I_CHG = K / R_ISET,K值需要查PCA9422手册。因为手册里对ISET的系数定义每家芯片都不太一样,我这里不写死数字,实际操作时按数据手册把K代进去算即可。
当时选用0.5C左右充电策略,电池容量为800mAh,所以目标充电电流设为350mA到400mA,既兼顾充电速度,也避免小电池在大电流下发热明显。如果你的电池容量是2000mAh,可以相应把ISET电流调大,但必须回头确认PCB上充电路径的阻抗和热耗。
输出电压方面,PCA9422的DCDC输出可以由寄存器直接设置,也可以外接反馈电阻。寄存器配置的好处是切换DVS档位时不需要改硬件。例如我的1.8V低功耗域,在待机时会切到1.2V,只给睡眠保持电路供电。这里需要把默认输出电压的寄存器值和DVS表核对一遍,避免初始化后电压不对导致负载芯片上电就过压。
如果选择反馈电阻方案,以典型DCDC反馈电压VFB计算,R1上端接输出,下端串R2到地,输出满足Vout = VFB × (1 + R1/R2)。假设VFB是0.6V,目标3.3V,取R2=10k,则R1=45k。实际选用标准电阻系列,R1=43.2k、R2=10k,Vout约3.19V,如果负载对电压精度要求高,就把R1换成精密电阻,或者直接用寄存器校准。
2.3 引脚分配与连接细节
PIC18F46K40有PPS功能,外设引脚可以重映射,这给PCB布线省了不少事。不过电源管理相关引脚建议固定下来,避免逻辑混乱。我的接法是:
- I2C:SCL/SDA各接一颗4.7k上拉到3.3V,分别连接PCA9422的I2C引脚,并在靠近PIC处串联33Ω电阻抑制振铃
- PCA9422中断输出INT:接到PIC外部中断输入,并配置下降沿唤醒
- PCA9422充电状态指示:接到PIC普通GPIO,用于读取充电状态
- 电池电压分压:用两颗100k电阻串联,中点进ADC通道,接地处放0.1uF滤波电容
- NTC热敏电阻:一端接3.3V,一端经过ADC通道后接一个固定电阻到地,构成分压,放在电池附近
- 系统运行指示灯、负载开关使能信号:各占一个GPIO
这里有个细节:ADC输入通道的等效串联电阻不能太大,否则会与PIC内部采样电容形成分压,导致采样值偏低。我用10k以下源阻抗比较稳妥,电池分压用100k加100k时,建议在ADC引脚并联0.1uF电容,软件里还要加适当采集延时。
3. 固件侧的四层任务:配置、监测、状态机、低功耗
3.1 I2C初始化与PCA9422寄存器配置序列
电源管理固件的第一步,是把PCA9422配置成我们需要的状态。PIC18F46K40作为I2C主机,通信速率400kHz即可满足PMIC控制需求,不需要很快。初始化流程通常是:等待PCA9422上电稳定,写入输出电压配置、充电参数、中断使能,然后回读校验关键寄存器。
下面是我用的初始化函数框架,寄存器地址和数据以实际手册为准,关键点是每一步顺序不能乱,尤其是充电相关寄存器必须最后使能:
#define PMIC_ADDR 0x32 // 7-bit地址,查PCB上ADR引脚决定 #define PMIC_REG_OUT1 0x10 #define PMIC_REG_OUT2 0x11 #define PMIC_REG_LDO 0x12 #define PMIC_REG_CHG_CTRL 0x20 #define PMIC_REG_INT_EN 0x30 #define PMIC_REG_INT_FLAG 0x31 void pmic_write_reg(uint8_t reg, uint8_t val) { i2c_start(); i2c_write((PMIC_ADDR << 1) | 0); i2c_write(reg); i2c_write(val); i2c_stop(); } uint8_t pmic_read_reg(uint8_t reg) { uint8_t val; i2c_start(); i2c_write((PMIC_ADDR << 1) | 0); i2c_write(reg); i2c_restart(); i2c_write((PMIC_ADDR << 1) | 1); val = i2c_read_nack(); i2c_stop(); return val; } void pmic_init(void) { __delay_ms(100); // 等PMIC内部上电完成 pmic_write_reg(PMIC_REG_OUT1, 0x1F); // 3.3V档位,按手册配 pmic_write_reg(PMIC_REG_OUT2, 0x0C); // 1.8V档位,按手册配 pmic_write_reg(PMIC_REG_LDO, 0x10); // 3.0V档位,按手册配 pmic_write_reg(PMIC_REG_INT_EN, 0x07); // 使能输入插入、充电完成、故障中断 pmic_write_reg(PMIC_REG_CHG_CTRL, 0x23); // 使能充电,恒流400mA // 回读校验 if (pmic_read_reg(PMIC_REG_OUT1) != 0x1F) { // 上报配置异常 } }很多人会忘记延迟,开机后马上写I2C寄存器,结果PMIC内部电源还没稳定,写入被丢弃。我在模拟项目X里用示波器抓过,明确看到启动后约几十毫秒内写入是没有ACK响应的,所以初始化前必须做延时。
3.2 电池电压、电流与温度采集
PIC18F46K40的ADC是10位分辨率,对我来说够用。电池电压采集电路是两颗100k电阻分压,分压比1/2,电池4.2V时ADC输入2.1V,选内部参考源2.048V会超量程,所以我选了4.096V参考或者调整分压比到1/2.2,这里要特别注意:不要只看分压比够不够,要按最高电池电压4.4V来算,留出至少5%余量。
实际计算式:
- 设分压比为D,则ADC电压V_adc = V_bat × D
- 10位ADC值为ADC_CODE = V_adc / V_ref × 1023
- 反解电池电压V_bat = ADC_CODE × V_ref / (1023 × D)
温度采集用NTC。我的电路是100k NTC从上端接3.3V,下端接10k电阻到地,ADC采10k电阻上的电压。25℃时NTC为100k,电压很快被拉到接近3V,而低温时NTC阻值升高,变化率很大;高温时阻值下降,ADC值分辨率又变差。后来我专门针对-20℃到60℃做了查表,并在固件里直接采用线性插值,避免公式计算带来的误差。
温度读取的代码如下,重点是根据当前ADC值判断处于哪个区间段,再做线性插值:
#define NTC_ADC_TBL_SIZE 9 const uint16_t ntc_adc_table[NTC_ADC_TBL_SIZE] = {950, 900, 820, 700, 560, 420, 300, 210, 150}; const int8_t ntc_temp_table[NTC_ADC_TBL_SIZE] = {-20, -10, 0, 10, 20, 30, 40, 50, 60}; int8_t ntc_adc_to_temp(uint16_t adc_val) { for (uint8_t i = 0; i < NTC_ADC_TBL_SIZE - 1; i++) { if (adc_val <= ntc_adc_table[i] && adc_val > ntc_adc_table[i + 1]) { int32_t range = ntc_adc_table[i] - ntc_adc_table[i + 1]; int32_t temp_range = ntc_temp_table[i + 1] - ntc_temp_table[i]; return ntc_temp_table[i] + (int32_t)(ntc_adc_table[i] - adc_val) * temp_range / range; } } return (adc_val > ntc_adc_table[0]) ? ntc_temp_table[0] : ntc_temp_table[NTC_ADC_TBL_SIZE - 1]; }3.3 充电状态机的完整实现
PCA9422硬件本身有充电管理,但整机策略仍然建议在MCU侧维护一个充电状态机,这样可以根据产品场景做超时保护、温度门限和异常恢复。一般状态机包括:待机、预充、恒流、恒压、充电完成、故障恢复。
状态迁移条件:
- 待机:有输入电源且电池温度在允许范围内,进入恒流或恒压
- 预充:电池电压低于2.9V,以1/10恒流充电直到电压上升到2.9V以上
- 恒流:以设定400mA恒流充电,直到电池电压达到4.2V
- 恒压:以4.2V恒压充电,充电电流逐渐下降
- 充电完成:充电电流降到0.05C到0.1C门限,例如800mAh电池降到40mA左右,维持一段时间后停止
- 故障恢复:过温、过流、电池过放等条件触发后,进入故障态,等待恢复条件满足再回到待机
在固件里,状态机不应该只靠ADC读到的电压做决定,要以PCA9422的中断和充电状态标志为主,ADC数据作为辅助校验。原因是ADC采样会受电源纹波影响,而PCA9422内部的比较器更稳定。我的做法是:每次状态切换都打印或记录一次事件,方便最后对着日志查问题。
充电状态机示意代码:
typedef enum { CHG_IDLE = 0, CHG_PRE, CHG_CC, CHG_CV, CHG_DONE, CHG_FAULT } chg_state_t; void chg_state_machine(void) { uint16_t vbat = read_battery_voltage(); int8_t temp = read_battery_temp(); uint8_t pmic_flag = pmic_read_reg(PMIC_REG_INT_FLAG); if (temp < 0 || temp > 45) { state = CHG_FAULT; } switch (state) { case CHG_IDLE: if (input_present() && vbat > 2900 && temp_ok()) state = CHG_CC; break; case CHG_PRE: if (vbat >= 2900) state = CHG_CC; break; case CHG_CC: if (vbat >= 4200) state = CHG_CV; break; case CHG_CV: if (pmic_flag & CHG_DONE_BIT) state = CHG_DONE; break; case CHG_DONE: if (!input_present()) state = CHG_IDLE; break; case CHG_FAULT: if (temp_ok() && input_present()) state = CHG_IDLE; break; } }很多情况下恒压阶段并不是直接切到完成,而是要维持一定延迟。我在这里加了一个“稳定计数器”,连续多个采样周期都满足电流小于门限,才真正进入CHG_DONE,避免因负载瞬变误判。
3.4 中断处理与休眠唤醒策略
低功耗是完整电源管理的最后一块。整机不充电、不运行采集时,PIC18F46K40应进入休眠,PCA9422也进入低功耗输出模式。休眠前要做几件事:
- 关闭ADC、关闭I2C外设,但保留I2C引脚的GPIO模式并收成低电平,防止悬空引起漏电
- 将未用GPIO全部配置为输出低,避免浮空输入经过内部保护二极管漏电
- 把PCA9422的输出切换到低功耗档位,比如1.8V域降到1.2V
- 使能外部INT唤醒,进入SLEEP
唤醒源包括:PCA9422的INT中断线(比如输入插入、充电完成、故障),以及RTC定时唤醒。每次唤醒后先读PMIC_INT_FLAG,判断是什么事件,处理完清除标志再决定是否继续休眠。这里有个容易踩的坑:如果中断标志没清干净,MCU一旦清除休眠指令会立刻被再次触发唤醒,导致功耗失控。我实测过这类问题,整机电流从几十微安直接跳到毫安级,后来统一在休眠循环入口先清所有中断标志再睡觉,才恢复正常。
4. 调试链路与实测数据:纹波、充电曲线与开机时序
4.1 上电时序测量与软启动调整
系统联调时第一件事是量上电时序。用示波器同时接VBUS、PCA9422第一路输出、第二路输出和PIC的复位脚,观察谁先起来、爬升时间多少。PMIC一般是VBUS一进来就开始输出,PIC复位脚会由PMIC的某个输出兼任,所以顺序问题主要在MCU启动时总线状态。
实测中发现一个问题:PCA9422第二路输出爬升比第一路慢约1.2ms,而PIC启动后如果直接把I2C初始化执行完,可能PMIC第二路还没准备好,这时读回来的电压状态是错的。解决方法是PIC启动时等待两路输出都稳定,再开始I2C配置。这个“等待”不能简单固定延时,最好通过GPIO读取PMIC的PWR_GOOD状态。
| 信号 | 爬升时间 | 稳定值 | 备注 |
|---|---|---|---|
| VBUS 5V | 0ms参考点 | 5.0V | 输入接入瞬间 |
| DCDC1 3.3V | 约0.9ms | 3.31V | 带10uF负载电容 |
| DCDC2 1.8V | 约2.1ms | 1.79V | 比DCDC1慢 |
| LDO 3.0V | 约3.0ms | 2.98V | LDO随DCDC1启动 |
如果你的负载芯片有明确的上电时序要求,比如内核必须先于IO上电,那么仅靠PMIC默认时序可能不够。这时可以考虑用PIC的一个GPIO控制负载的EN脚,在软件里按顺序打开各路输出电源。
4.2 Buck输出纹波与地回路问题
纹波测量是电源调试的重头戏。测3.3V输出时,示波器探头不要用长地线夹子,否则会引入很大的环路电感,波形里全是开关噪声。正确做法是使用探头原厂的地弹簧,直接搭在输出电容两端,并开启20MHz带宽限制,AC耦合。
实测下来,DCDC1满载300mA时纹波约28mV,DCDC2纹波约22mV,满足给数字负载供电的要求。模拟3.0V LDO纹波更低,约5mV以内。如果发现纹波异常偏大,第一嫌疑是输出电容离负载太远,其次是电感选型不合适。我在第二轮板子上把输出电容从22uF加到47uF,纹波没有明显改善,反而把电容换成了低ESR的X5R后改善明显,这说明电容的真实交流阻抗比容量更重要。
4.3 充电曲线与温度数据
用电池模拟器和真实电池各跑了一遍充电曲线。充电过程从预充到恒流、恒压,整体曲线符合预期。采集到的关键点:
- 2.8V开始预充,电流40mA,电池电压缓慢上升
- 达到2.9V后切入恒流,电流稳定在400mA,电压持续上升
- 电压接近4.2V后进入恒压,电流开始下降
- 当电流降到40mA以下并保持约10分钟,充电完成中断置位
电池温度在恒流阶段明显上升,从室温25℃升到约32℃,进入恒压后逐渐下降。这个数据提醒我:如果充电电流提高到1A以上,必须保证NTC探头紧贴电池表面,否则温度保护形同虚设。另外,夏天环境30℃时,电池表面45℃的停机阈值很容易触发,需要按产品允许温度范围留足余量。
5. 踩过的三个坑:I2C配置失效、NTC误差、休眠漏电
5.1 坑一:PMIC寄存器写入被“吞”
第一个坑出现在首次上电。程序里初始化完PCA9422后,用万用表测输出电压,发现还是默认值,不是配置的3.3V。用逻辑分析仪抓I2C,确认波形有数据、有ACK,但寄存器就是没生效。后来查了PMIC数据手册的启动时序说明,又用示波器对比了VBUS上电到PMIC就绪的时间,发现我在上电后约2ms就发起了I2C写操作,而PMIC内部参考电压还没建立,所以看起来有ACK,数据实际没锁存。
解决办法是初始化前做足延时,并且在写完关键寄存器后立即回读。从那以后,我把pmic_init里的延时从10ms提高到100ms,再没出现配置失效的问题。如果你遇到同样现象,建议先检查PMIC的nRST/INT引脚,确认它已经拉高表示内部状态机就绪。
5.2 坑二:NTC分压误差在低温段被放大
第二个坑比较隐蔽。常温下温度读数很准,但放到-10℃环境一测,读到的温度比实际低了7到8℃。原因是NTC在低温段阻值很高,分压电路里固定电阻与NTC阻值差距悬殊,ADC有一点误差,映射回温度就是好几度。再加上PIC的ADC本身存在约±1LSB误差,放大后更明显。
解决方法是重新设计偏置电阻,让所选NTC在目标温度区间的中值附近有比较好的灵敏度。后来我干脆换成查表线性插值,并在固件里对ADC做多次采样取平均。经过校准后,-20℃到60℃范围内误差控制在±2℃以内。对于直接参与充电温度保护的系统,这一步校准非常重要,否则高温保护可能在53℃就误触发,或者低温充电条件判断偏差。
5.3 坑三:休眠后电池漏电接近2mA
这是最让人头疼的问题。整机进入休眠后,理论预期电流在30uA左右,实测却有2mA。一开始怀疑PCA9422低功耗配置没生效,后来通过逐路断开外部负载,才发现漏电路径来自I2C上拉电阻和MCU引脚状态。
原因很典型:PIC休眠时I2C外设停止工作,SCL/SDA引脚变成高阻,外部4.7k上拉电阻把引脚拉到3.3V。如果PCA9422对应引脚内部有ESD保护二极管或者内部下拉,就会在MCU电源和PMIC电源之间形成一条电流路径。我把这两个引脚在休眠前手动配置为GPIO输出低,再测量,漏电立刻降到30uA以内。如果你的电路I2C上拉接的不是同一路电源,这个问题会更严重,建议统一规划上拉电源域。
最后几句实际操作感受
这套方案做完之后,我对“完整电源管理”有了新的理解:PMIC再聪明,也替代不了系统级的低功耗策略,MCU再灵活,也不能放着硬件时序不管。两者结合的关键是明确边界——硬件负责稳定输出和状态上报,软件负责策略、校准和恢复。如果让我重新做一遍,我会在设计原理图时就把电源树、各路上电时序和中断引脚画在一张表里,并让固件从一开始就支持回读校验,而不是等硬件焊接完再补。希望这篇记录能帮你少走我走过的弯路。