做电池供电的便携设备,最烦人的往往不是业务代码,而是电源那一摊子事。以前我做一个小型手持项目,光电源部分就用了三颗LDO、一颗升压、一颗充电管理芯片,再加上一堆分立MOS和RC延时电路,PCB上密密麻麻全是电源布线的影子。后来换成 PCA9422 和 PIC18F4610 这套组合——一颗多通道PMIC负责把电变出来、把电池充进去,一颗8位单片机负责说“现在该用多少伏、该关哪一路”——电源板面积直接缩小一半,待机功耗也更好控了。这篇文章就把这次完整电源管理项目的选型思路、硬件接法、固件状态机和实测踩坑一次讲透,适合正在做手持表、便携仪表、小型物联网设备的开发者。
1. 先想清楚:这对组合各负责哪块供电活
1.1 为什么不用一堆分立芯片拼
很多人一提到电源管理,第一反应是“我需要几路就放几路电源芯片”。思路没有错,但等你真正做系统级设计,问题就来了。三路DCDC要三套电感、三个反馈分压电阻,还要考虑每一路的启动顺序。充电部分单独一颗芯片,快充、恒流、截止逻辑全得靠MCU或比较器盯着。想给负载动态调压?那基本是重新画一块板。
分立方案的另一个痛点是待机功耗。普通DCDC在轻载下的静态电流经常到几十上百微安,几路加起来,一个本来设计成500uA待机的系统,光电源芯片就吃掉一半预算。PCA9422这类集成PMIC的好处在于内部有多路BUCK和LDO,还有专门的低功耗工作模式,轻载时自动进入PFM,整颗芯片的静态功耗可以压到比较低的水平,系统级待机就很好做。
再加上集成PMIC通常自带充电管理、ADC采样、中断输出和I2C寄存器控制面。供电、充电、电压监测、动态调压这些原本要靠五六颗芯片加一堆逻辑才能做的事,现在一颗芯片加一个单片机就能全部覆盖。硬件BOM减少,组装和测试成本也跟着降,量产一致性比手工拼出来的方案好一截。
1.2 两者的分工:硬件执行 vs 策略大脑
PCA9422负责的是“电力的物理变换”:把电池电压变成稳定的多路电源轨,给电池充电,监测电压电流和温度,在出现过流、过温时做硬件级保护。但这些行为具体怎么执行,给哪一路设定多少伏,什么时候充电、什么时候切到低功耗,需要一个大脑来决策。PIC18F4610在这里就是那个大脑。
有人会觉得奇怪,做个电源管理怎么还要上单片机?其实不是“上单片机”,而是很多系统本来就有一颗主控,只是大多数时候它只顾着跑业务逻辑,没有把供电策略当回事。把电源管理职责放到一颗老资格的8位单片机身上,性价比很高。PIC18F4610有MSSP模块可以做I2C主模式,有10位ADC可以备份采样电池电压,有EEPROM可以存校准参数和日志,有外部中断引脚接PMIC的中断输出,还有多种休眠模式配合低功耗。这些能力做电源策略完全够用,没必要为了一个寄存器配置功能去上更高端的平台。
下面是这套系统里两者的典型分工:
| 功能 | 由谁执行 | 说明 |
|---|---|---|
| 多路电压输出 | PCA9422 | 三路Buck加一路LDO,输出由寄存器设定 |
| 动态调压DVS | PCA9422执行,PIC决策 | PIC写目标电压,PMIC完成调压 |
| 电池充电 | PCA9422 | CC/CV过程由PMIC内部逻辑完成 |
| 充电策略 | PIC18F4610 | 根据供电来源、电量、温度决定充电电流和门限 |
| 状态切换 | PIC18F4610 | 运行、空闲、休眠、关断的状态机 |
| 故障保护 | PCA9422硬件 | 过流、过压、过温触发硬件保动,同时中断通知PIC |
| 电压电流监测 | PCA9422内部ADC,PIC可读 | PIC读取结果做软件策略 |
1.3 什么场景下这套方案才值得上
不是说所有项目都要用PMIC加MCU的组合。如果产品只需要一路3.3V降压、负载固定、没有电池,一颗简单的LDO或DCDC反而是最优解。但下面几种情况,我强烈建议直接参考这套架构:
- 系统需要三路以上电源轨,且不同运行状态下电压要求不同;
- 产品带单节锂电池,需要充电管理、电量监测和充电状态上报;
- 待机电流要求严格,需要把部分电源轨在睡眠时关掉,主核降到低电压运行;
- 需要远程或本地配置供电参数,比如产线校准、固件升级后修改电压配置。
我这次做的设备四条全占,所以当时基本没犹豫就定了这个架构。
2. PCA9422硬件底细:三路Buck、充电与I2C控制面
2.1 一颗芯片里的“供电五脏”
把PCA9422的框图展开看,它基本就是一块小型开发板该有的电源系统全集。芯片内部集成了三路同步降压Buck转换器,每一路都可以通过I2C寄存器调节输出电压,适合给主控核心、内存、外设等不同功耗的负载供电。同步整流结构让轻载和满载效率都能维持在比较高的水平,不需要像非同步方案那样外挂续流二极管。
除了三路Buck,芯片还有一路LDO输出。这部分我一般用作“常电域”,也就是关机后仍然需要保持供电的RTC或者是唤醒逻辑。LDO静态电流小,虽然效率不如Buck,但它干净、便宜、响应快,专门给低功耗的常开电路供电很合适。
充电部分支持单节锂离子或锂聚合物电池,采用典型的恒流恒压两段式充电。芯片内部可以设置充电电流、终止电流、充满电压以及输入限流。比较关键的是芯片内置了电源路径管理,适配器插入时系统优先从适配器取电,同时给电池充电;适配器移除后自动切到电池供电。这个功能在便携设备里非常实用,因为用户插拔充电器的瞬间系统不能断电,不能复位。
I2C控制面是整套设计的核心。PCA9422作为I2C从机,PIC作为主机,通过读写寄存器完成电压设置、状态查询、中断处理和充电参数配置。芯片还带一个中断输出脚,接PIC的外部中断输入,这样PMIC这边一旦出现充电完成、电池低、过温等事件,PIC可以立刻得到通知,不用轮询。
2.2 原理图阶段就要定下来的几件事
硬件设计里我吃过不少亏,这里把踩坑总结成几条经验,都是画原理图时就要确定的:
第一,I2C上拉电阻和上拉电压源的选择。PCA9422的I2C引脚一般在1.2V到3.3V范围内工作,上拉电阻要接到芯片允许的I/O电压上,而不是直接接到电池电压。总线速度按400kHz设计时,常用的上拉值在2.2k到4.7k之间,具体看总线上挂了多少器件、走线有多长。如果总线上接了多颗芯片,走线又长,4.7k往往不够,改用2.2k更稳。
第二,中断引脚一定要留出来。PCA9422的中断脚通常是开漏输出,需要外部上拉到合适的电平,然后接到PIC的INT0或INT1。千万别省这根线,否则后面所有状态都靠轮询,系统响应和低功耗都会很被动。
第三,DVS控制引脚的设计。PCA9422支持通过I2C改电压,也支持通过外部引脚快速切换预置电压档。我的习惯是预留两颗GPIO连到PIC,方便以后做硬件级快速调压。即使初期只在软件里调压,原理图上也把这两个引脚引出来,板子拿回来再想加就麻烦了。
第四,供电轨规划。我给这套系统定的分配是:Buck1给主控核心,Buck2给存储器和其他数字外设,Buck3给模拟前端或者射频部分,LDO专门给RTC和唤醒逻辑。这样分的好处是各路的负载相对固定,调试时某一路电压异常能立刻定位到具体功能模块。
布局布线方面,Buck的电感属于开关节点,要离I2C走线远一点,反馈走线避开电感正下方。PMIC的输入输出电容按照数据手册参考值来,10uF加100nF的组合各轨都别省,不然动态响应和纹波都会给你颜色看。
3. PIC18F4610侧初始化:MSSP走I2C主模式的完整起手式
3.1 先把I2C主模式跑起来
PCA9422配置再强,PIC这边I2C驱动写不好一切都是白搭。PIC18F4610的MSSP模块配置成I2C主模式的流程比较固定,关键是把模式、时钟和引脚三件事搞对。
MSSP的寄存器很多人在第一次接触时容易晕,实际要动的就那么几个:SSPCON1决定模式和使能,SSPADD决定时钟频率,SSPCON2处理应答和启停控制,SSPIF是中断标志。把模块配置成I2C主模式,SSPCON1的低四位写0b1000,SSPEN位置1。时钟频率由振荡器频率分频得到,公式是SPADD = (FOSC / 4) / I2C_FREQ - 1。
下面是我在16MHz主频、400kHz总线上用的初始化代码,可以直接抄:
#define FOSC 16000000UL #define I2C_FREQ 400000UL void i2c_master_init(void) { // SCL/SDA分别配置为输入,由MSSP模块驱动 TRISCbits.TRISC3 = 1; // SCL TRISCbits.TRISC4 = 1; // SDA SSPCON1 = 0x28; // I2C主模式,SSPEN=1,SSPM=0b1000 SSPCON2 = 0x01; // ACKEN=1,允许应答位控制 SSPADD = (uint8_t)(((FOSC / 4) / I2C_FREQ) - 1); SSPSTAT = 0x80; // 常规速率模式,SMBus相关位保持清零 }写字节和启停操作的核心是等SSPIF置位,同时检查ACKSTAT状态。我封装了一组底层函数,读写寄存器都靠它:
static void i2c_wait_until_done(void) { // 用一个计数器做超时,避免总线异常时程序卡死 uint16_t timeout = 0xFFFF; while (!PIR1bits.SSPIF && timeout) { timeout--; } if (timeout == 0) { // 总线异常,做一次复位或者进入错误处理 i2c_bus_recover(); } PIR1bits.SSPIF = 0; } uint8_t i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t val) { uint8_t rc; SSPCON2bits.SEN = 1; // 发送启动条件 i2c_wait_until_done(); rc = i2c_write_byte(dev_addr << 1); // 7位地址左移一位,写方向 if (rc == I2C_NACK) return rc; rc = i2c_write_byte(reg); if (rc == I2C_NACK) return rc; rc = i2c_write_byte(val); if (rc == I2C_NACK) return rc; SSPCON2bits.PEN = 1; // 发送停止条件 i2c_wait_until_done(); return I2C_OK; }读寄存器稍微多一步:先写寄存器地址,然后发重复启动条件,再切到读方向读回数据并发送NACK作为最后一字节。这样的读流程在MSSP里很成熟,网上资源也多,就不贴完整代码了。
3.2 上电时序匹配:先喂饱大脑,再开肌肉
很多人上电就翻车,问题出在PIC和PMIC谁先有电这件事上。我在原理图阶段把PIC的VDD接到了PCA9422的LDO输出,LDO由电池或适配器直接供电,只要系统有电,LDO就输出稳定3.3V,PIC先跑起来。三路Buck默认关闭,由PIC在初始化完成后才逐个打开。这样既保证主控先于大功率负载就绪,又避免了上电瞬间各路电源轨相互竞争电流。
PIC启动后的初始化流程我建议按这个顺序:
- 延时10ms左右,等LDO输出稳定、PMIC内部电路完成复位;
- 配置I2C主模式和中断引脚;
- 读取PCA9422的芯片ID寄存器,确认通信正常。这个步骤别省,等于握手,能过滤掉一大批硬件虚焊问题;
- 配置充电相关参数:输入限流、充电电流、充满电压、终止电流;
- 依次配置三路Buck的目标电压,先设电压再接使能;
- 打开各电源轨的使能位,每打开一路延时一下,确认读回的状态寄存器正确;
- 关掉这条初始化用的I2C通道或者切换到低速率,进入正常业务。
有朋友遇到过开机瞬间整机电流特别大的现象,后来发现是PIC一边初始化I2C一边又把大功率外设的GPIO拉起来了。正确做法是初始化完成前对外设保持复位状态,等Buck稳定后再释放复位。这个顺序在固件里要写死,不然每次上电都像在赌运气。
4. 固件里的电源状态机与充电流程
4.1 我把状态机拆成六档
电源管理固件的核心是一个状态机,不推荐在前台循环里用一堆if去判断电源状态。我这次按运行场景拆了六个状态,每个状态定义清楚“哪些轨开、电压多少、CPU什么频率、外设什么状态”,切换条件也集中管理。
| 状态 | Buck1 | Buck2 | Buck3 | LDO | 典型电流 |
|---|---|---|---|---|---|
| BOOT | 默认电压 | 关闭 | 关闭 | 开 | 30mA |
| RUN | 1.2V | 1.8V | 3.3V | 开 | 80~200mA |
| IDLE | 0.9V | 1.8V | 3.3V | 开 | 20mA |
| SLEEP | 关闭 | 关闭 | 3.3V | 开 | 3mA |
| OFF | 关闭 | 关闭 | 关闭 | 开 | 300uA |
| CHARGE | 默认电压 | 按需 | 按需 | 开 | 依充电电流而定 |
状态切换的核心动作就两类:改一部份Buck电压,或者关闭某一路。DVS用得到位的话,IDLE状态把核心电压从1.2V降到0.9V,功耗能降不少,而切换到RUN也只需一条I2C写指令。下面这个函数就是做这件事的:
void system_set_state(system_state_t next) { switch (next) { case STATE_IDLE: pmic_write_reg(REG_BUCK1_VOUT, 0x??); // 0.9V配置值 // 等待PMIC内部调压完成,再继续下一步 while (!pmic_read_reg(REG_STATUS) & ST_BUCK1_OK); break; case STATE_RUN: pmic_write_reg(REG_BUCK1_VOUT, 0x??); // 1.2V配置值 while (!pmic_read_reg(REG_STATUS) & ST_BUCK1_OK); break; case STATE_SLEEP: pmic_write_reg(REG_BUCK_EN, 0x??); // 关闭Buck1和Buck2 break; default: break; } current_state = next; }写电压寄存器的时候有一个细节:目标电压值要折算成芯片要求的编码格式,很多PMIC的电压步进是10mV或者25mV,不能直接把毫伏数写进去。这个折算公式在数据手册里有,我会在头文件里写一个宏或者转换函数统一处理,避免在业务代码里到处出现魔法数字。
4.2 充电管理别只写几个寄存器就完事
PCA9422的充电器硬件上很省心,CC/CV过程由内部逻辑完成,PIC只需要做三件事:配置参数、监测状态、处理异常。
配置这块,我根据设备电池规格定了充电电压4.2V,充电电流0.5C,终止电流设为满充电流的十分之一左右。输入限流要分场景:接适配器时放高一点,接普通电脑USB口时放低。这块用一根检测脚或者读PMIC的输入源状态寄存器来判断,比在固件里硬编码靠谱。
充电过程中PIC要持续关注几个中断事件:充电完成、充电失败、电池低、温度异常。我把PMIC的中断输出接到PIC的INT0引脚,中断服务函数里读一次中断状态寄存器,判断事件类型,清标志后做对应处理:
void __interrupt() isr(void) { if (INT0IF) { uint8_t st = pmic_read_reg(REG_INT_STATUS); if (st & ST_CHARGE_DONE) { charge_done_flag = 1; } if (st & ST_VBAT_LOW) { system_set_state(STATE_SLEEP); } pmic_write_reg(REG_INT_STATUS, 0xFF); // 按手册要求清中断 INT0IF = 0; } }有个细节容易被忽略:中断状态寄存器读完之后要主动清除,而且很多PMIC的清中断方式是写1清除,不能改成写0。如果清不干净,中断脚会一直拉低,PIC的中断标志也会反复触发,系统就像卡死在随机位置。我在调试时吃了一次这个亏,排查了半天才发现是清中断语句写反了。
4.3 低功耗状态下,轮询就是敌人
进入OFF或SLEEP状态后,PIC最好进入休眠,用外部中断唤醒。这时候如果继续用定时器轮询PMIC状态,不仅把CPU唤醒功耗吃掉了,还让系统电流凭空多出一截。
我的做法是:进入低功耗状态前,把所有没必要的定时器关掉,I2C总线保持高电平不释放,PIC执行SLEEP指令。唤醒来源只留三路:PMIC中断脚、唤醒按键、RTC定时唤醒。每次唤醒后用最快的速度读一下PMIC状态寄存器,判断要不要切状态,不需要处理就立刻再次睡眠。这样整个系统在OFF状态的实测电流能稳定在300uA上下,比我最初设计预算的500uA还要好。
5. 实测排坑:输出电压、I2C应答和DVS毛刺
5.1 第一个坑:写完电压寄存器,输出还是默认值
样机第一次上电,I2C通信正常,芯片ID也读回来了,但用示波器量Buck1,输出纹丝不动是默认的1.0V,我明明写了0x配置值想让它输出1.2V。
排查链路是这样的:先检查代码里配置值是不是算错,把寄存器读回打印出来,发现写入的值没错。然后怀疑是不是使能位没置位,把使能寄存器整个读回来,发现Buck1的状态是“使能但未就绪”。最后定位到原因:我在初始化时先写了输出电压寄存器,再写的使能寄存器,但中间没有等电压稳压完成;而PMIC对于“使能状态下修改电压”有一套影子寄存器机制,某些字段需要在特定条件下才会更新到硬件。
解决办法是严格按数据手册的时序来:先降低写入节奏,每写一个寄存器后延时,再读回验证;打开使能后查询电源正常状态位,确认稳压完成再继续。从那以后我把“写地址-写数据-读回-比对”这个三步校验做成了所有PMIC寄存器写入的标准流程,实测下来通信异常基本都是硬件问题而不是代码问题了。
5.2 第二个坑:I2C偶发无应答,加1ms延时又好了
系统跑起来之后,出现了一个特别邪门的现象:I2C通信大部分时间正常,偶尔一次读充电状态时返回NACK,加了1ms延时之后又好了。这种随机故障最折磨人,因为它不会稳定复现。
我怀疑过三件事:总线电容太大、上拉电阻阻值太高、MSSP速率过快。先用逻辑分析仪抓波形,发现SDA在启动条件后有一处明显的边沿变缓,加上总线还挂了另一颗传感器,总电容确实偏大了。处理办法是换上拉电阻从4.7k降到2.2k,同时把I2C速率从400kHz降到100kHz,坏帧率直接从“偶尔一次”变成“一次都没有”。
这里也想提醒大家:I2C线上跑着多颗芯片、走线又长时,别迷信400kHz的“标准速率”。对电源管理这种低频控制场景,100kHz完全够用,稳定性却高一个数量级。我后来把I2C速率固定在100kHz,运行几个月没再出现通信故障。
5.3 第三个坑:DVS调低电压,系统反而复位数
动态调压看起来很美,但我在把核心电压从1.2V降到0.9V的时候,系统却偶发复位。这位复位不是掉电引起的,因为LDO供电的常电域没掉,复位的只有主核心域。
逐层分析后发现,DVS调低电压的瞬间,主控还挂着比较重的负载,电压从一个台阶跳到另一个台阶,如果PMIC的调压速度很快,而负载电流还没来得及降下来,瞬时电压可能跌到低于主控最低工作电压,触发了内置的掉电复位。
解决思路分两步。第一步,软件上先让负载进入轻载模式再降电压:把主控时钟降下来,关闭不需要的外设,然后再写电压寄存器。第二步,如果PMIC支持调压速率配置,把调压斜率调缓一点,让电压平滑过渡。硬件上我在Buck1的输出再加了一颗22uF的陶瓷电容,增加一点瞬态裕量,从那之后这个复位再没出现过。
这套“先降负载、再调电压、确认完成、再恢复负载”的流程,现在已经成为我所有DVS项目的标配。
5.4 顺带说说低功耗实测方法
很多朋友问低功耗怎么测才能测准,我的经验是不要只看数据手册,要实际用电流探头在供电入口测。开机状态下600uA以下用普通万用表串联测是可以的,但休眠状态300uA左右时,万用表的压降已经会影响系统供电了,最好用电流探头加示波器,或者用专门的功耗分析仪。
测的时候还要把I2C访问的频率降下来。我最初调试时,SLEEP状态下PIC虽然休眠了,但I2C引脚没有完全释放,每一段时间还有个泄漏路径。逐个外设排除时,用了一个简单方法:把每个外设的供电都改成独立跳线,测功耗时一根一根跳线拔掉,看电流变化。这套方法虽然土,但定位快,最终把系统待机从2mA降到了300uA。
最后再分享一个小技巧:整套方案调通之后,建议在固件里加一个调试模式,通过PMIC的ADC把各路电压、充电电流、温度实时上报。这个模式平时不开启,但出问题时能帮你少焊很多飞线和少测很多点。我自己在后续项目里保留了这套架构,还打算把DVS的调压曲线做成自动标定,让产线可以根据每台设备的个体差异微调电压配置,真正做到每一台都工作在最优功耗点上。