做电池供电的嵌入式产品,电源设计往往比主控代码更让人头疼。我用 STM32F767BI 做一款手持数据采集设备时,系统里除了主控还有外部 Flash、加速度传感器和低功耗蓝牙模块,3.3V、1.8V、1.2V 三路电压一个都不能少。一开始堆了一堆分立器件,板上密密麻麻不说,上电时序还得靠 RC 延时硬凑,低功耗更是无从谈起。后来整机电源部分换成一颗 PCA9422 PMIC,通过 I2C 控制多路输出和动态调压,驱动代码加硬件改动不到一周,整体功耗和 BOM 都明显优化。这篇文章把整个方案完整拆开讲:为什么选 PMIC、电路怎么设计、驱动怎么写、低功耗怎么联动、以及调试中踩过的坑,给做低功耗物联网和手持设备的朋友一个可以直接参考的模板。
1. 为什么要用独立 PMIC 做电源管理
1.1 先给整机画一棵电源树
很多做单片机的朋友对电源的理解停留在“输入 5V,用个 AMS1117 转 3.3V,完事”。但到了 STM32F767BI 这种量级的产品,事情就复杂多了。单说主控本身,就有 VDD(数字主电源)、VDDA(模拟电源)、VBAT(备份域电源)以及 VCAP(内部 LDO 输出,需要外部电容)。如果外设再多一点:Flash 和蓝牙模块要 1.8V 逻辑电平,传感器要 1.2V 内核电压,RTC 需要 3.0V 备份电源。这还没算有些模块对电源纹波敏感,模拟部分和数字部分要隔离。
我用的设备是两节锂电池供电,电池电压范围大概在 2.8V 到 4.2V 之间波动,这就要求所有电源轨都必须在这么宽的输入范围内稳定输出。当时理出来的电源树如下:
| 电源轨 | 电压 | 主要负载 | 预估最大电流 | 纹波要求 |
|---|---|---|---|---|
| VDD_MAIN | 3.3V | STM32F767BI(VDD/VDDA) | 约 300mA | 较低,数字电路 |
| VDD_FLASH | 1.8V | 外部 QSPI Flash、蓝牙模块 | 约 150mA | 低,通信模块敏感 |
| VDD_SENSOR | 1.2V | 加速度计内核 | 约 50mA | 低 |
| VBAT | 3.0V | STM32 备份域、RTC | 约 10mA | 一般 |
这个电源树非常典型,也是绝大多数低功耗手持设备的基本形态。确认了需求之后,再去看到底用什么器件来实现这几路电源。
1.2 分立方案看着省,账其实算不过来
以前我习惯的做法是:主电源用一个 DC-DC 降压芯片,再挂几个 LDO 把电压降下去。DC-DC 选型要考虑最大输出电流、开关频率、反馈分压电阻、补偿网络,LDO 又要看压差、静态电流、噪声。每一路电源都有自己的使能脚、反馈电阻、输入输出电容,加起来一片 PCB 上光是电源部分就有二十几个元器件。
更麻烦的是上电时序。STM32F767BI 对时序没有特别严格的要求,但外设不行。蓝牙模块要求主电源稳定之后 10ms 内给到模块电源,否则上电可能锁死;Flash 芯片又要求在 VDD 上升到 70% 之前不能拉高片选。用分立器件做这个时序,最原始的办法是 RC 延时,但 RC 延时在量产时一致性差,温度一变时间就漂。用电源监控芯片加逻辑门控,又平白多出两三颗芯片。
低功耗方面,分立方案更吃亏。LDO 的静态电流动辄几十微安,DC-DC 的轻载效率在 1mA 以下时跌得很厉害。整机待机电流想压到 10μA 以内,很难。
1.3 PCA9422 这类的 PMIC 到底解决了什么
PCA9422 是一个多通道 PMIC,输入电压范围覆盖 2.0V 到 5.5V,恰好兼容两节锂电池和 USB 供电。内部集成了两个降压转换器和两个 LDO,每个输出都可以通过 I2C 独立配置电压、独立使能。这意味着我上面说的四路电源,用一颗芯片加几个电感电容就全部搞定。
真正打动我的不是省面积,而是以下几点:
- 输出电压全部由寄存器控制,不用精密反馈电阻,改电压只在驱动函数里改一个数。这颗芯片的电压调节范围从 0.6V 到 3.3V,步进精度也够用。
- 每个通道都有独立的软启动和输出使能,上电顺序可以通过驱动代码灵活调整,比 RC 延时可靠得多。
- 内置过流、过温保护,还有一些状态标志位,可以通过中断脚告诉 MCU 当前电源有没有异常。
- 芯片的静态电流非常低,关断模式下只有微安级别。这对整机待机功耗的压降很关键。
选型的时候我也对比过同级别的其他 PMIC:有的输出路数不够,必须再挂一个 LDO;有的 I2C 寄存器过于复杂,学习成本高;有的轻载效率不行,1mA 附近掉到百分之七八十。PCA9422 在我这个需求下刚好是输出路数、调压范围、低功耗表现比较均衡的一个。
2. 硬件电路设计:每一路电压都有讲究
2.1 电源输入与整体连接
硬件上道理不复杂:电池进来,先经过输入滤波电容,再进 PCA9422 的 VIN 脚。PCA9422 的每一路输出再分别接到对应的负载侧。
VIN 输入端我放了 10μF 陶瓷电容加 100nF 高频去耦电容,尽量靠近芯片管脚。之所以要两个不同容量的电容并联,是因为大容量陶瓷电容的 ESR 在高频段其实不低,而 100nF 的小电容反而能在几十 MHz 频段提供低阻抗路径,两者互补,把电池线带来的高频毛刺滤干净。
电池供电还有一个细节:如果设备支持 USB 充电,VIN 最好通过一个防倒灌二极管再进 PMIC,防止电池电流回流到 USB 口。也可以用负载开关做过压保护,但一般手持设备用二极管就够了。
2.2 电感计算与选型实操
PCA9422 的降压转换器需要外部电感。电感值选多大,直接按数据手册的公式算:
L = Vout × (VIN - Vout) / (Fsw × ΔIL × VIN)
以我的 BUCK1 为例:VIN 典型值 3.7V,Vout 3.3V,开关频率 1.5MHz,负载电流约 300mA,纹波电流 ΔIL 取负载电流的 30%,即 0.09A。代入公式:
L = 3.3 × (3.7 - 3.3) / (1.5×10⁶ × 0.09 × 3.7) = 1.32 / 499500 ≈ 2.64μH
取标准值 2.2μH。这里要注意,电感量不能只看计算值,还要看饱和电流。电感的饱和电流至少要是最大输出电流的 1.5 倍,否则重载时电感量会急剧下降,输出电压直接拉垮,严重的还会啸叫。我选了额定饱和电流 800mA 的 2.2μH 电感,留了接近一倍的裕量。
BUCK2 电流要求低一些,1.8V 输出配 1.5μH 也够用,但为了物料统一,我直接全部用了 2.2μH,实测效果没区别。
2.3 STM32F767BI 的电源脚怎么接
STM32F767BI 这片 MCU 有几组电源脚,首次画板特别容易漏。逐一说明:
VDD 和 VDDA 都接 3.3V,从 PCA9422 的 BUCK1 输出取。VDDA 是模拟部分电源,如果 ADC 精度要求高,最好在接入点加一个小磁珠和一个 1μF 电容做隔离,防止数字开关噪声串进模拟地。我实测加了磁珠之后,ADC 采样值跳动低了大概 20%。
VCAP 是芯片内部主调节器的输出,不是输入,千万不能接电源。这一脚需要外接 2×2.2μF 电容到地。很多新手把 VCAP 理解成核心电压输入,硬接一个外部 1.2V 进去,直接把芯片烧了,这个问题后面排查部分还会细说。
VBAT 接备份域电源,我从 PCA9422 的 LDO1 取 3.0V。正常情况下 VBAT 电流很小,主要维持 RTC 和备份寄存器,但要注意:如果主电源 3.3V 还在,VBAT 又单独供电,芯片内部会自动切换选择较高的一路,所以 VBAT 不能高于 VDD 太多,我用的 3.0V 合理。实际连接时在 VBAT 和 LDO1 之间串一个 10Ω 电阻,防止热插拔时瞬间大电流冲击 LDO。
2.4 I2C 和中断引脚的连接
PCA9422 的 I2C 接口接 STM32F767BI 的 I2C1。SDA 和 SCL 各用一个 4.7kΩ 电阻上拉到 3.3V。上拉电阻选 4.7kΩ 是针对 400kHz 的 I2C 速率,如果后期想跑 1MHz,可能要改成 2.2kΩ,否则上升沿太慢会导致时序违规。
INTB 中断脚接到 STM32 的一个空闲 GPIO,配置成外部中断输入,下降沿触发。PCA9422 检测到过流、欠压、过温等异常时,会拉低 INTB 通知 MCU。这个脚在低功耗联动部分非常有用。
还有 EN 脚。PCA9422 通常有一个总使能脚,我接在 STM32 的一个 GPIO 上,软件可以随时切断整机电源。有些设计直接把 EN 接到电池正极,也可以,但那样就失去软件关机的控制了。
2.5 上电时序的处理
PCA9422 的输出通道可以独立使能,所以我在初始化代码里固定了一个顺序:先使能 BUCK1(3.3V 主控电源),延时 5ms,等 VDD 稳定,再使能 LDO1(VBAT);BUCK2 和 LDO2 放到最后,因为蓝牙模块和传感器对时序要求最高。实测这个顺序下,所有外设都能正常复位,没有再出现上电死机问题。
如果不想在固件里搞时序,PCA9422 也支持通过配置寄存器实现固定的内部延时。不过我建议用 MCU 的 GPIO 控制 EN 或直接用 I2C 指令按顺序使能,因为这样最灵活,调试时也能随时单独开关某路电源来定位问题。
3. 软件驱动:让 PMIC 完全可编程
3.1 硬件 I2C 初始化
我用 STM32CubeMX 生成工程,I2C1 配置为主模式,400kHz,7 位地址。HAL 初始化代码如下,几乎不需要改动:
void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }PCA9422 的 I2C 地址是 7 位地址,具体数值要查规格书,不同子型号可能有差异。我在驱动头文件里做了宏定义,方便后续更换批次时调整:
#define PCA9422_I2C_ADDR (0x48 << 1) // 7位地址左移一位转成8位写地址3.2 寄存器读写接口封装
PCA9422 的寄存器是 8 位地址加 8 位数据。操作接口非常简单,直接基于 HAL 的存储读写函数封装:
uint8_t PCA9422_WriteReg(PCA9422_t *dev, uint8_t reg, uint8_t val) { return HAL_I2C_Mem_Write(dev->hi2c, PCA9422_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); } uint8_t PCA9422_ReadReg(PCA9422_t *dev, uint8_t reg, uint8_t *val) { return HAL_I2C_Mem_Read(dev->hi2c, PCA9422_I2C_ADDR, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); }这两个函数是所有上层业务的基础。调试时强烈建议先写一个简单的读取 ID 函数,确认 I2C 物理链路没问题再往下走:
#define PCA9422_REG_DEVICE_ID 0x00 bool PCA9422_CheckID(PCA9422_t *dev) { uint8_t id = 0; if (PCA9422_ReadReg(dev, PCA9422_REG_DEVICE_ID, &id) != HAL_OK) return false; return (id == 0x22); // 具体值以规格书为准 }如果这一关就不过,后面所有配置都白搭。我在调试时发现,ID 读取失败往往不是芯片坏了,而是 I2C 上拉电阻没焊或者电源没起来。
3.3 输出轨配置与初始化流程
初始化函数做的事情非常直观:把四路输出的电压寄存器写上目标值,然后按顺序使能。参考代码如下:
#define PCA9422_REG_BUCK1_VOUT 0x10 #define PCA9422_REG_BUCK2_VOUT 0x12 #define PCA9422_REG_LDO1_VOUT 0x14 #define PCA9422_REG_LDO2_VOUT 0x16 #define PCA9422_REG_ENABLE 0x20 #define PCA9422_VOUT_3V3 0x2A #define PCA9422_VOUT_1V8 0x18 #define PCA9422_VOUT_1V2 0x0C #define PCA9422_VOUT_3V0 0x24 void PCA9422_Init(PCA9422_t *dev) { // 先设电压 PCA9422_WriteReg(dev, PCA9422_REG_BUCK1_VOUT, PCA9422_VOUT_3V3); PCA9422_WriteReg(dev, PCA9422_REG_BUCK2_VOUT, PCA9422_VOUT_1V8); PCA9422_WriteReg(dev, PCA9422_REG_LDO1_VOUT, PCA9422_VOUT_3V0); PCA9422_WriteReg(dev, PCA9422_REG_LDO2_VOUT, PCA9422_VOUT_1V2); // 先使能 BUCK1 和 LDO1,延时后使能 BUCK2,最后 LDO2 PCA9422_WriteReg(dev, PCA9422_REG_ENABLE, 0x03); HAL_Delay(5); PCA9422_WriteReg(dev, PCA9422_REG_ENABLE, 0x07); HAL_Delay(5); PCA9422_WriteReg(dev, PCA9422_REG_ENABLE, 0x0F); }注意:这里的电压编码值只是示意。PCA9422 的每个输出通道电压映射表都在规格书里有明确编码,不同批次芯片的 LSB 值可能不一样。我实际开发中是直接对着规格书的表查编码,再写进宏定义的,布局代码前一定要做这一步,别想当然。
3.4 动态调压的实现
我最看重的功能之一,就是可以动态改输出电压。以 LDO2(给传感器供电)为例,传感器在工作时需要 1.2V,进入休眠时可以降到 0.9V,降低漏电和功耗。软件实现就是写两次寄存器:
void PCA9422_SetLDO2Voltage(PCA9422_t *dev, uint8_t vout_code) { PCA9422_WriteReg(dev, PCA9422_REG_LDO2_VOUT, vout_code); }在实际项目里,我在传感器进入低功耗模式之前调用它,把 LDO2 从 1.2V 调到 0.9V,实测整机休眠电流降低了 30μA 左右。动态调压对电池设备意义很大,但有一个前提:被调压的负载必须能接受电压变化。不要给 STM32F767BI 的 VDD 乱调压,它需要 3.3V,调低了直接复位。给外设调压前,也要确认外设芯片支持相应的电压范围。
3.5 中断处理
PCA9422 的 INTB 脚在发生异常事件时会被拉低,STM32 外部中断触发后,去读取中断状态寄存器,根据标志位做处理:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == PCA9422_INT_PIN) { uint8_t flags = 0; PCA9422_ReadReg(&pca9422, PCA9422_REG_INT_STATUS, &flags); if (flags & 0x01) { // 过流保护触发 } if (flags & 0x02) { // 过温保护触发 } if (flags & 0x04) { // 输入电压低于阈值 } } }中断处理函数里不要做大动作,只设置标志位,真正的响应逻辑放在主循环。我一开始在里面直接写 I2C 操作,结果中断里又触发 I2C,HAL 库不重入,直接卡死。这个坑踩过一次就记住了:中断里读状态可以,但后续操作一律丢给主循环。
4. 低功耗模式联动:把每一微安都抠出来
4.1 STM32F767BI 能省到什么程度
STM32F767BI 的低功耗档位很丰富:Sleep、Stop、Standby。对电池产品来说,最常用的是 Stop 和 Standby。
Stop 模式下,内核时钟停止,但 SRAM 和寄存器的内容都保留,醒来可以无缝继续执行。代价是功耗还有几十到上百微安。Standby 模式更狠,除了备份域和 RTC,整个数字逻辑都断电,待机电流可以做到微安级别,但醒来是冷启动,必须重新初始化。
如果追求极致续航,我会选择:平时进 Stop,需要真正关机时进 Standby,同时让 PCA9422 关掉所有输出。
4.2 软件侧的低功耗操作序列
进入低功耗之前,要做的事比调用一个 HAL 函数多得多。我的标准序列是:
- 先让所有外设进入休眠模式,包括蓝牙模块发休眠指令、传感器下电。
- 通过 PCA9422 关闭外设对应的电源轨(BUCK2、LDO2),只保留主控和 RTC。
- 把不需要的 GPIO 全部配置成模拟输入或者固定电平,避免悬空引脚的漏电。
- 执行 STM32 的 Stop 或 Standby 指令。
代码大致如下:
void EnterLowPower(void) { // 1. 外设下电 BT_Sleep(); Sensor_Shutdown(); // 2. 关掉外设电源轨 PCA9422_WriteReg(&pca9422, PCA9422_REG_ENABLE, 0x03); // 3. GPIO 状态整理 HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // ... 其他 GPIO 处理 // 4. 进入 Stop HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWREGULATOR_MODE, PWR_STOPENTRY_WFI); // 唤醒回来后恢复系统时钟 SystemClock_Config(); HAL_ResumeTick(); }这里有个容易犯的错误:默认很多 GPIO 是浮空输入,浮空脚在低功耗下会反复翻转,通过内部的输入缓冲形成漏电通道。我最初测试时,Stop 模式电流始终降不下去,查了一圈发现是一个没接上拉的 GPIO 在做怪,配置成模拟模式后电流立刻掉下来。所以 GPIO 状态整理这一步千万别省。
4.3 用 PCA9422 实现真正的硬件关机
在 Stop 模式下已经能压到几十微安了,但对一个可能几个月不充电的设备还远远不够。我最终在“长按电源键关机”的场景里,实现了真正的硬件关机:
- MCU 收到关机指令,保存必要数据到备份寄存器或外部 Flash。
- 通过 I2C 依次关掉所有输出轨,最后关掉 BUCK1 自己。
- PCA9422 进入最低功耗的待机状态,整机电流只剩下 PMIC 的静态电流,实测 2μA 左右。
这样做之后,系统已经算彻底断电,下一次开机只能靠 PMIC 的 EN 脚重新上电,或者按键接到 EN 上直接触发。等于把关机和开机做成了最原始的硬件行为,比任何软件低功耗都可靠。
4.4 实测功耗数据
整个系统调通之后,我记录了几组典型的整机电流数据,供大家参考:
| 工作状态 | 整机电流 | 说明 |
|---|---|---|
| 运行模式(216MHz + 蓝牙广播) | 约 85mA | 含所有外设 |
| 运行模式(降频到 96MHz) | 约 45mA | 关闭不必要外设 |
| Sleep 模式(Cortex-M7 睡眠) | 约 3mA | 约 2mA 来自蓝牙模块 |
| Stop 模式(外设全关) | 约 18μA | 主要是 RTC 和备份域 |
| 硬件关机(PCA9422 全断) | 约 2μA | PMIC 静态功耗 |
这组数据说明,如果只依赖 MCU 的 Stop 模式,功耗瓶颈不在 MCU,而在外设。要真正省电,必须连外设的电源轨一起切掉,这正是独立 PMIC 的核心价值。
5. 常见问题与排查实录
5.1 I2C 扫描不到 PCA9422
症状:读设备 ID 超时,I2C 总线 SCL 和 SDA 波形异常。
排查思路:先量 PCA9422 的 VIN,确认有电;再量 SCL/SDA 两个脚的波形,看是否有正常的地址帧;最后确认 I2C 地址到底是多少。
我遇到的一个典型坑:I2C 上拉电阻选的是 10kΩ,SCL 频率 400kHz 时波形上升沿明显变缓,偶尔能读到 ID,偶尔超时。这个属于上拉电阻过大导致的,改成 4.7kΩ 后问题消失。如果总线长度比较长,建议降到 2.2kΩ。
还有一个隐蔽问题:如果 PCA9422 的 VIN 不是干净的电池电压,而是经过一个低压差负载开关供电,负载开关还没完全导通时,MCU 就已经开始扫描 I2C,结果自然失败。解决方法是把扫描开始时间延后到负载开关完全稳定之后。
5.2 BUCK 输出偏低且伴随啸叫
症状:BUCK1 配置 3.3V,实测只有 2.8V,并且电感有细微的吱吱声。
分析:啸叫通常说明电感工作在饱和状态,电感量急剧下降,开关电流不受控。检查发现是电感饱和电流只比最大负载电流高 20%,而 BUCK1 在启动瞬间电流尖峰很大,直接让电感饱和了。
处理方案:换饱和电流更大的电感,从 800mA 换成 1.2A 规格,啸叫消失,输出电压稳定在 3.3V。这个经验给选型提了个醒:电感参数不能只看稳态,还要看瞬态和启动瞬间的峰值电流。
5.3 上电后 MCU 无法正常复位
症状:按下复位键能跑起来,但每次冷启动大概率卡死。
这个问题的根源在于上电时序:BUCK1 的输出上升时间太长,或者在上电过程中有短暂跌落,导致 MCU 内部的上电复位电路没有干净地触发。我用示波器观察 3.3V 波形,发现 BUCK1 从开始输出到稳定花了接近 25ms,远超预期,原因是输出电容加得太多,软启动时间被拉长。
处理办法是适当减小 BUCK1 的输出电容,从 22μF 降到 10μF,同时检查软启动配置。改完上电波形陡峭了许多,冷启动不再死机。这里要注意,输出电容也不是越小越好,太小会让瞬态响应变差,需要在瞬态和软启动之间找一个平衡点。
5.4 Stop 模式电流始终下不去
症状:软件明明进入了 Stop,整机电流还是 500μA 以上。
排查过程基本是暴力但有效的:把所有外设的电源轨全部关掉,电流立刻降到 30μA。然后一路一路打开,发现是加速度传感器的电源没断,传感器内部还在跑,自然耗电。确认外设电源后,电流还有 50μA,继续查,是一个 GPIO 接了外部上拉电阻到 3.3V,但是软件把它配置成浮空输入,导致内部保护二极管反向偏置产生漏电。
解决办法:所有 GPIO 在进入低功耗前统一收敛——能设低的设低,不需要的设置为模拟输入,需要保持电平的保持稳定电平。这条规则建议直接写进代码规范,每次用新硬件都复查一遍。
5.5 硬件关机后还能量到几十微安
这个问题的原因是我把 PCA9422 的 INTB 上拉到了 BUCK1,关机后上拉还在耗电。后来把 INTB 的上拉改接到 VIN(电池侧),关机后只有上拉电阻的微安级电流。实际上,真正追求低功耗关机的话,PCBA 上所有上拉电阻都要检查一遍,尽量选择功耗路径都走 VIN 或 GPT 控制的电平。
还有一种情况:STM32 的 VBAT 如果直接接 LDO1,而 LDO1 在关机时如果还在输出,备份域就还在耗电。需要把 LDO1 也一起关掉,或者接受备份域的电流。我在量产版本里选择了把 LDO1 也关掉,代价是 RTC 时间会丢,从用户角度看可以接受,因为下次开机自动同步网络时间。这个取舍每个产品都不一样,提前想清楚就好。
最后说几句实在话
这套方案在我手上跑了两轮迭代,第一轮踩的坑基本都集中在硬件细节和上电时序上,第二轮把低功耗和多路电源轨的协同理顺之后,软件的复杂度反而很低。现在回头看,用 PCA9422 这类 PMIC 配合 STM32F767BI,最大的收益不是省了几颗电感电容,而是把电源管理从硬件时序问题变成了软件配置问题,迭代起来非常舒服。
如果觉得这个方案思路有用,可以试试把它扩展到更多场景:比如配上超级电容做掉电保护,或者把动态调压用在外置高速存储上,原理都一样,只是多配几路电压而已。
最后分享一个调试阶段的小心得:我把 PCA9422 的全部寄存器做成一个 dump 函数,每次遇到奇怪问题先跑一遍 dump,把寄存器值打印出来,和正常状态对比,很多问题一眼就能定位。如果连 dump 都超时,别折腾软件了,大概率是硬件链路断了,直接拿示波器从头量到尾,比反复试错快得多。