干嵌入式电源设计这么多年,我最怕的不是主控跑飞,而是整板待机电流悄悄从几十微安涨到几毫安还查不出原因。这个项目要做一台USB和锂电池双供电的小型便携设备,主控选了低功耗8位单片机PIC18LF4680,电源芯片则换成了集成多路DCDC降压和锂电池充电管理的PCA9422。最开始我其实想过分立方案:一颗升压芯片、两路LDO、一个独立充电IC,再加一堆电源管理逻辑,评估下来板子面积和静态电流都不太好看。换成PIC18LF4680加PCA9422的组合后,整板电源架构简化成一颗PMIC加一颗MCU,MCU通过I2C就能控制每一路输出的开关、电压基准和充电参数,剩下的精力全放在状态切换和功耗调优上。这篇文章就围绕这套方案,把硬件设计、寄存器配置、电源状态机和调试踩坑完整过一遍,适合正在做电池供电设备、想真正把待机功耗做下来的嵌入式工程师参考。
1. 方案选型:为什么是PCA9422加PIC18LF4680
1.1 一颗PMIC顶掉半块电源板
先说我为什么盯上PCA9422。便携设备里的电源需求其实很固定:电池电压进来,要给MCU、传感器、无线模块分别供不同的电压档位,同时还要负责锂电池充电和基本的保护逻辑。以前的做法是每个电压一路芯片,Buck、LDO、充电IC散落在板上,每个芯片都有自己的使能时序,上电顺序全靠硬件RC延迟或者MCU的IO硬拉,调试的时候特别痛苦。
PCA9422这类PMIC把几路DCDC、LDO和线性充电模块集成到一个封装里,而且全部寄存器可控。我这颗以3路Buck和2路LDO为例:主路Buck负责MCU和数字外设的3.3V,第二路给1.8V的逻辑器件,第三路给1.2V的射频核,两路LDO再分出模拟参考电压和低噪声电源。充电部分也直接集成在PMIC内部,USB的VBUS进来,充电电流在线性充电模块里被限制到设定值,电池电压到达终止电压后自动转入恒压和截止。
这带来的直接好处是上电时序不再靠分散的使能引脚猜,只要在固件里按顺序写寄存器,就能控制每路输出的开启和软启动。对MCU来说,整个电源系统在总线上就是一个I2C从设备,读写几个寄存器就能把电源板的状态摸得清清楚楚。
1.2 PIC18LF4680在电源管理里能干什么
PIC18LF4680是8位MCU里很典型的低功耗选手,LF后缀代表低压版本,工作电压可以到2.0V到5.5V。这个电压范围意味着它可以被PMIC的动态电压调节直接馈电——待机时把主Buck降到2.0V,MCU还能运行,功耗却能省下一截。芯片内部有64KB闪存和3.3KB RAM,电源管理这种逻辑不算复杂的固件放进去绰绰有余。
它真正适合做电源管理的原因,我觉得有三个。第一是集成度合适,MSSP模块直接支持I2C主机模式,不用软件模拟I2C,省掉一大堆时序头痛问题。第二是中断和唤醒源丰富,定时器、外部中断、I2C地址匹配都能把芯片从Sleep状态拉起来,这对实现“待机-定时唤醒-处理-再待机”的循环特别重要。第三是内部振荡器配合可配置分频,MCU自身功耗可以随工作状态调整,Active跑16MHz,Standby降到1MHz,Sleep直接用31kHz内部低频振荡器维持定时唤醒。
1.3 MCU加PMIC的分工逻辑
有人会问,PMIC自己就能管理寄存器,为什么还要一个MCU在中间?我的理解是PMIC负责“执行”,MCU负责“决策”。PMIC内部虽然有状态机,但它没有业务逻辑,不知道当前设备是正在采集数据还是已经空闲了三分钟。而MCU知道完整的系统状态,可以通过I2C动态告诉PMIC:现在把无线模块那路关掉、把主路电压降一点、电池该进入预充模式了。
这样分工之后,功耗策略可以做得非常细。我在项目里把设备分成几个电源状态,每个状态对应一组寄存器配置,切换状态时只需要几条I2C写入,整个系统的功耗就跟着变化。这套思路比单纯靠MCU自身睡眠要强得多,因为PMIC可以直接把外设的供电整体断掉,而不是让外设进入一个不可控的“待机”。所谓“完整电源管理”,本质上就是把这个决策链路打通。
2. 硬件设计:电源架构和关键外围计算
2.1 供电路径与功能划分
先把系统电源架构说清楚。电池选用3.7V单节锂离子,容量500mAh左右,USB的5V VBUS作为充电输入。电池电压直接进PCA9422的VBAT脚,Buck输出三路,LDO两路,具体分配如下:
| 电源轨 | 电压 | 主要负载 | 状态控制策略 |
|---|---|---|---|
| BUCK1 | 3.3V | MCU、传感器数字部分 | Active全速,Standby降至2.5V,Sleep降至2.0V |
| BUCK2 | 1.8V | 外部存储、逻辑电路 | Active开启,Standby关闭 |
| BUCK3 | 1.2V | 无线模块核电压 | 无线工作时才开启 |
| LDO1 | 2.8V | 模拟前端参考 | 采集时开启 |
| LDO2 | 1.8V | 低噪声模拟电源 | 传感器模拟部分使用 |
这个分法有一个原则,就是把“关键维持负载”和“可休眠负载”严格分开。MCU和实时时钟必须一直在电,放在BUCK1;其他一切能在待机时断电的负载,统统放到独立的一路上。这样待机时能把它们彻底断电,而不是靠外设自己的睡眠引脚去碰运气——很多外设标称的关断电流“看着很低”,实际量产批次差异非常大。
2.2 电感电容怎么选
Buck电路的储能电感是选型里最容易影响效率的元件。PCA9422这类PMIC的开关频率通常在1MHz以上,电感值不需要很大。我按输出电流和纹波要求算:以BUCK1输出3.3V、负载300mA、输入电池电压3.7V附近为例,电感值取4.7µH到10µH之间比较稳妥,额定电流至少留1.5倍裕量,DCR(直流电阻)越低越好。电感选太大,轻载效率会下降;选太小,纹波电流增大,输出电压纹波难看,还可能触发电流过流保护。
输出电容建议每路至少10µF的X7R陶瓷电容,必要时再补一个0.1µF高频去耦。输入侧靠近VBAT放一个22µF左右的陶瓷大电容,注意陶瓷电容直流偏压特性,耐压建议选16V而不是6.3V,否则实际容量会被电压“吃掉”不少。这个坑我踩过:最初用6.3V额定,2.5V输入时还能看,电池充满4.2V时容量掉到标称的六成,输入电压纹波明显变大。
2.3 PCB布局要点
电源的PCB布局决定了硬件是否稳定,这个环节省不得。我总结下来三条铁律。第一,SW开关节点面积要小,它是整个板上dv/dt最高的地方,铺铜面积一大就成了天线,辐射和EMI都难受。第二,电感靠近PMIC的SW脚放,输出电容靠近电感输出端,所有地过孔就近打过孔到主地平面,避免“接地环路”把纹波串进模拟部分。第三,I2C的SCL/SDA走线要远离SW节点和高频时钟线,拉上拉到电源轨的电阻放在PMIC和MCU中间靠总线一侧。
另外,如果板子上有模拟前端,LDO1和LDO2的输出走线要做单独的地回路,不要蹭数字回流路径。我在这块板子的初版布局里,LDO2的地线经过了一小段数字噪声区,导致ADC采集值在无线模块工作时跳了4个LSB,重新分割地平面之后才恢复正常。
3. I2C通信与寄存器配置
3.1 MSSP主机模式初始化
PCA9422和PIC18LF4680之间的通信,我用的PIC18的MSSP模块,配置为I2C主机模式。总线上拉电阻选了2.2kΩ,对应400kHz快速模式。系统时钟用内部PLL到16MHz,初始化代码大概是这样:
void i2c_master_init(void) { // FOSC = 16MHz, 目标总线频率 400kHz // SSPADD = FOSC / (4 * FSCK) - 1 SSPADD = 0x09; // 16MHz / (4 * 400kHz) - 1 = 9 SSPSTAT = 0x80; // SMBus规范关闭,禁用低电平总线超时 SSPCON1 = 0x28; // SSPEN=1, I2C主机模式 }初始化时最容易被忽略的是上拉电阻。PMIC和MCU都在3.3V域,上拉电阻接到3.3V,但必须在两个芯片的电源都稳定之后再开始通信。我遇到过一种情况:MCU先上电后跑起来,PMIC电压还没起来,I2C从机没有供电时SCL或SDA被内部钳位,主机启动后第一次发送就直接撞总线冲突。解决方法是上电后先延时等PMIC就绪,再初始化I2C外设。
3.2 寄存器读写函数实现
MSSP在主机模式下发送一个字节的流程是:写SSPBUF,等SSPIF置位,清标志。底层时序函数我封装成下面这样:
static void i2c_start(void) { SEN = 1; // 发起START while (SEN); // 等待START完成 } static void i2c_restart(void) { RSEN = 1; while (RSEN); } static void i2c_stop(void) { PEN = 1; while (PEN); } static uint8_t i2c_write_byte(uint8_t data) { SSPBUF = data; while (!SSPIF); SSPIF = 0; return SSPCON1 & 0x01; // 返回ACK状态,0为从机应答 } void pmic_write_reg(uint8_t reg, uint8_t value) { i2c_start(); i2c_write_byte(PCA9422_ADDR << 1); // 7位地址左移一位,最低位=0表示写 i2c_write_byte(reg); i2c_write_byte(value); i2c_stop(); }注意,PCA9422的从机地址与硬件引脚有关,我项目里实际用到的7位地址是0x68,写字节是0xD0。不同封装或批次可能存在差异,动手前务必打开官方数据手册核对一遍,不要照抄我的定义。读寄存器多用“重复起始”方式,先写寄存器地址再重发START转成读操作,避免先STOP再START之间总线被其他设备抢走占用。
3.3 关键寄存器配置参考
寄存器具体地址我不在这里写死,因为不同版本的PMIC寄存器映射有差异,但配置思路是一致的。我习惯把常用配置宏定义好,后续代码只调用宏,改起来方便:
#define REG_BUCK1_CTL 0x01 // 示例地址,以手册为准 #define REG_BUCK2_CTL 0x02 #define REG_BUCK3_CTL 0x03 #define REG_LDO_CTL 0x04 #define REG_CHG_CTL 0x05 #define REG_STATUS 0x06 #define BUCK_EN 0x80 // 使能位 #define BUCK1_3V3 0x10 // 电压档位编码,按手册位域配置 #define BUCK1_2V5 0x08 #define BUCK1_2V0 0x04 #define LDO1_2V8 0x30 #define LDO2_1V8 0x10 #define CHG_EN 0x80写配置时我的习惯是:改电压前先查目标负载的电压容忍范围,尤其是MCU动态降压这一步,PIC18LF4680虽然支持2.0V,但板上如果有其他器件挂在同一条线上,它们不一定能跟着降。所以动态降压调整的一定是“专属电源轨”,而不是共享轨。
4. 固件电源状态机设计与实现
4.1 状态定义与切换条件
电源状态机是整个固件的核心。我定义了几个状态:系统启动后进入Active,所有轨全开,MCU跑16MHz;检测到连续5秒无按键、无通信任务,进入Standby,关闭非必要轨,BUCK1降到2.5V,MCU降到1MHz;Standby里再持续30秒没有事件,进入Sleep,BUCK1降到2.0V,MCU执行SLEEP指令;只有充电事件和USB插入时进入Charging状态;任何异常进入Fault,关断所有输出,靠看门狗或外部恢复。
切换条件不是简单的定时器堆叠,而是由一个统一的事件队列驱动。按键、定时器溢出、I2C中断、充电完成标志都往队列里投事件,状态机主循环根据当前状态和事件做迁移。这样做的好处是逻辑清晰,新增一个唤醒源只需要加一个枚举值。
4.2 状态切换函数实现
状态切换函数的实现,核心就是一组PMIC写入和MCU外设的配合动作。以进入Sleep为例:
void pm_enter_sleep(void) { // 1. 先关掉外设供电,再降主路,顺序不能反 pmic_write_reg(REG_BUCK2_CTL, 0); // 关BUCK2 pmic_write_reg(REG_LDO_CTL, 0); // 关LDO pmic_write_reg(REG_BUCK3_CTL, 0); // 关BUCK3 pmic_write_reg(REG_BUCK1_CTL, BUCK1_2V0); // 主路降到2.0V // 2. 配置唤醒定时器,31kHz低频时钟分频得到10秒周期 T1CON = 0x31; // 定时器1开启,1:8预分频 TMR1H = 0x00; TMR1L = 0x00; PIR1bits.TMR1IF = 0; PIE1bits.TMR1IE = 1; // 3. 调整MCU时钟到最低功耗状态 OSCCON = 0x00; // 切换到31kHz内部振荡器 // 4. 睡眠 SLEEP(); // 5. 唤醒后恢复时钟和电源 OSCCON = 0x70; // 恢复16MHz pm_restore_active(); }我特别强调顺序:必须先关闭外设供电,再降主路电压。如果反过来,外设还挂在BUCK2上时先关了BUCK1,MCU瞬间没有时钟来源,I2C可能刚好在读一个半途的寄存器写操作,唤醒后电源状态就乱了。实际调试中我把“关外设”和“降主路”之间加了一个2ms延时,给PMIC内部电容放电时间,避免下电瞬间的负压尖峰干扰MCU。
4.3 实测功耗数据与优化
整套系统调优后的待机链路是这样的:Sleep状态下BUCK1输出2.0V,MCU运行在31kHz振荡器上,定时器10秒唤醒一次,唤醒后迅速采集两路ADC、写入日志、再睡回去。实测整板Sleep电流约35µA,其中MCU自身约5µA,PMIC静态功耗约3µA,剩下主要是电池电压采样分压电阻的漏电和BUCK1空载损耗。
Standby状态整板电流约1.2mA,Active全速运行约35mA,无线模块开启瞬间峰值能到120mA。500mAh电池按这个节奏用,一天待机消耗不到1mAh,正常使用一周充一次电,基本达到预期。功耗调优里最容易出成绩的三个点:采样分压电阻从100kΩ加大到1MΩ级别;关掉所有不用的ADC通道;PMIC里不用的Buck直接断电而不是只禁用输出。
5. 锂电池充电管理
5.1 充电参数设置
充电参数设置直接关系到电池寿命和安全性。PCA9422内部是线性充电架构,充电电流要按电池容量来,一般取0.3C到0.5C。500mAh电池我设置150mA恒流,这样即便充电时系统同时运行,线性充电器的发热也在可接受范围。设置代码:
void charger_set_150mA(void) { pmic_write_reg(REG_CHG_CTL, CHG_EN); // 使能充电 pmic_write_reg(REG_CHG_CC, 0x1E); // 充电电流编码,按手册换算 }如果电池电压过低,比如低于3.0V,线性充电器会先进入预充模式,充电电流只有恒流档的十分之一左右。预充的目的很明确:防止深度放电的电池被大电流硬充导致内部短路或析锂。这个阶段一定不要人为调大电流,要等电池电压恢复到安全窗口后,再由充电器自动切换到恒流阶段。
5.2 充电状态监测
我写了一个简单的充电状态轮询函数,挂在系统1秒定时器里:
void charger_poll(void) { uint8_t st = pmic_read_reg(REG_STATUS); if (st & STATUS_FAULT) { // 过温、输入过压或电池异常,进入电源Fault状态 pm_enter_fault(); } else if (st & STATUS_DONE) { // 充满状态,关闭充电使能避免反复补充电 pmic_write_reg(REG_CHG_CTL, 0); charge_done = 1; } else if (st & STATUS_CHARGING) { charge_done = 0; } }充满之后我选择主动关闭充电使能,而不是依赖充电器内部的截止逻辑。原因很简单,有些线性充电器在电池电压回落到截止电压以下一点时会重新开启补电,导致电池长时间处在“浮充”状态,长期来看对电池循环寿命没好处。系统只需要在电池电压低于3.6V时重新开启充电即可。
5.3 充电安全注意事项
这部分内容不多,但每条都是事故教训换来的。第一,充电温度范围必须在0到45摄氏度之间,如果系统没接NTC,至少要通过软件策略限制:检测到电池温度异常直接停充。第二,USB VBUS输入要有过压保护,PMIC虽然内部有OVP,但外部输入路径上我加了一颗TVS二极管和串联的PTC保险丝,双保险。第三,充电和放电不要共用一个电流采样电阻,否则采样误差会影响恒流转恒压的判断。
6. 常见问题与排查实录
6.1 I2C总线上电就被锁死
这个现象很典型:上电后程序跑起来,第一次读PMIC寄存器就超时,用示波器看SCL一直低。排查后发现是PMIC的电源轨还没起来时,它的I2C引脚内部钳位二极管把SCL拉住了。解决办法是在MCU侧加了总线恢复流程——初始化时如果有总线冲突标志(BCLIF)置位,先连续翻转SCL九个时钟周期,再尝试发起通信。另外,上电顺序调整为PMIC先使能BUCK1,MCU等VDD稳定后再初始化I2C,问题基本消失。
6.2 Buck输出电压偏低
有一版板子回来,BUCK1设3.3V,实测只有3.05V。一开始怀疑寄存器没写进去,读回来看配置确实是对的。后来用示波器看SW节点波形,发现开关占空比明显异常,排查到最后是输出电容容量不够。我换上了额定16V的22µF陶瓷电容,并且确认使用的X7R材质,问题解决。陶瓷电容偏压特性影响很大,低额定电压电容在偏压下实际容量严重缩水,这个细节在前面提过,这里再次强调,电源设计里电容的实际容量曲线一定要看数据手册表格。
6.3 待机电流居高不下
有一次待机电流怎么调都停在300µA下不去,跟预期35µA差了近十倍。逐个排查后发现问题出在一个传感器上:它的供电虽然接在BUCK2上,但它的I2C地址引脚通过一个10kΩ电阻下拉到了地,而BUCK2关闭后,这个引脚仍然通过传感器内部的ESD二极管往PMIC的3.3V域漏电,等于外设没有完全断电。解决方法是把I2C地址引脚接到BUCK2的输出域,BUCK2关了整条路径彻底断开。这个教训说明,待机漏电排查不能只看供电网络,还要检查所有与外设相连的IO引脚路径。
6.4 充电电流始终为零
充电功能联调时发现,插上USB后充电电流一直为0。查寄存器发现充电使能位已经写入,但状态寄存器显示没有电池接入。最后定位到是电池连接器的接触电阻偏大,电池电压检测点放在连接器之前,导致充电器认为电池没接好。把电压检测点挪到电池端,或者直接缩短连接器到PMIC的走线,问题解决。充电回路的走线阻抗比一般信号线敏感得多,大电流路径上尽量做开尔文检测,这是后来一直坚持的做法。
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| SCL一直低 | PMIC未上电,钳位二极管拉死总线 | 调整上电顺序,增加总线恢复流程 |
| Buck输出偏低 | 陶瓷电容偏压导致容量下降 | 换更高耐压、X7R材质电容 |
| 待机电流异常大 | 外设IO经过ESD二极管漏电 | 隔离外设IO与常供电源域 |
| 充电电流为0 | 电池检测点位置不当 | 电压检测开尔文接法移到电池端 |
7. 实操心得与后续扩展
7.1 几个值得坚持的做法
整套项目做下来,有几个做法是我想推荐大家坚持的。一是把电源配置做成参数表,而不是散落在各个函数里。我的做法是在代码里定义一张状态表,每一行代表一个电源状态,列是各路的电压和使能配置,状态切换只是查表写入,后续调参只需要改表的数值,不用动逻辑。二是每次改动PMIC寄存器都要做读写回读校验,写进去再读回来比对,防止I2C偶发错误导致电源状态错乱。三是在调试阶段就把每路电源的电流测量点预留出来,板子上留0欧电阻焊盘或者铜皮断点,测量功耗时直接串微安表,比靠软件估算准得多。
7.2 后续可以扩展的方向
这套架构后续扩展空间也挺大。如果设备需要更精确的电量显示,可以在I2C总线上挂一颗库仑计芯片,MCU读累计电荷量,比现在的电压查表法靠谱很多。如果无线模块需要快速随机唤醒,可以让PMIC的中断输出脚直接连MCU的外部中断输入,把轮询改成事件触发,还能再省一点功耗。另外,PIC18LF4680的闪存里其实还留着约30KB空间,后续想加简单的数据记录和离线日志分析,硬件不用改动。
个人实际的体会是,电源管理做得好的设备,用户感受不到电源的存在,续航长、发热低、充电稳,这背后靠的全是“把每一微安算清楚”的功夫。PCA9422和PIC18LF4680这套组合,把复杂的电源控制收敛到一条I2C总线上,是我目前在做小型电池设备时会优先考虑的搭配。