1. 为什么我选 PCA9422 + STM32L073RZ 这个组合做电源管理
先说结论:如果你要做的是一个电池供电、对功耗和体积都敏感的小设备,PCA9422 配 STM32L073RZ 是相当省事的一套方案。我以前也用过“一颗 DC-DC + 一颗 LDO + 分离充电管理”的搭法,不是说不能跑,而是物料清单、外围电路和调试时间都会明显变多。PCA9422 把这些功能塞进一个 QFN 封装里,和 STM32 之间只靠一个 I²C 总线就能完成电压配置、输出控制和状态读取,整体设计会清爽很多。
为什么偏偏配 STM32L073RZ?因为这块 MCU 本身就是一个主打超低功耗的 Cortex-M0+ 芯片,带多种低功耗模式,可以在运行、睡眠、停止之间来回切换。而 PCA9422 的好处是输出轨可以动态调压(DVS,Dynamic Voltage Scaling),能配合 MCU 的工作状态把核心电压往下降。两者放在一起,等于“大脑负责什么时候省电,电源芯片负责把省电动作真正落到电压上”,而不是让系统一直用 3.3V 硬扛着。
这类组合最适合的场景,我总结下来有三个:
- 小尺寸电池设备:比如随身传感器、智能标签,PCB 面积紧张,PMIC 集成度高能省很多布局空间。
- 需要多种供电轨的设备:主控一路、传感器/外设一路,甚至备份域一路,PCA9422 的多路输出基本能覆盖。
- 强调待机功耗的设备:利用 DVS 把运行电压和待机电压分开管理,实测待机电流能明显降下来。
如果你只是做一个插电、长期 5V 供电的东西,那其实用普通 LDO 就行,没必要上 PMIC。但只要你打算长期用电池,又希望 MCU 能动态管理电压,这个组合就很值得认真试试。
顺便说一句,PCA9422 这类电源管理芯片和普通 DC-DC 的最大区别,是它有一套完整的上电/掉电时序、中断状态、寄存器配置逻辑。这意味着你不是简单地把电压接上去就完事,而是要在 MCU 里写一段初始化代码,让电源系统“活”起来。这也是本篇文章想重点拆解的部分。
2. 上电前的硬件设计:引脚定义、电源轨分布和走线要点
硬件设计是这一套方案的骨架,如果这里画错了,后面固件怎么调都别扭。我在这个项目里用的是 LQFP64 封装的 STM32L073RZ,PCA9422 放在板子的电源入口处,两者之间的连接关系设计成了一张很清晰的电源树。
2.1 整体电源树怎么搭
先说电源输入:板上放了一个单节锂聚合物电池,电压范围大概在 3.0V 到 4.2V 之间,同时留了一个 USB 5V 接口用于充电和调试。电池正极接到 PCA9422 的 VBAT 输入,USB 5V 接到充电检测脚和 VBUS 输入,这样可以让 PMIC 自己完成“外部供电时顺便给电池充电”的动作。
输出侧我做了两路主力电源轨:
| 电源轨 | 来源 | 默认电压 | 主要负载 |
|---|---|---|---|
| VDD_MCU | PCA9422 Buck 输出 | 3.3V | STM32L073RZ 主电源、数字外设 |
| VDD_IO | PCA9422 LDO 输出 | 1.8V | 传感器、SD 卡电平转换、模拟参考 |
之所以把 3.3V 放在 Buck(开关电源)上,是因为 MCU 运行时电流变化比较快,Buck 的效率在高负载下更高;LDO 则主要负责小电流但需要低噪声的外设电源,纹波更干净。两路输出各自接了 10uF 陶瓷电容加 100nF 高频去耦电容,这个组合基本是 PMIC 输出的标配。
2.2 MCU 侧电源引脚怎么接
STM32L073RZ 虽然不是那种需要多电压域的高端处理器,但它的电源引脚还是得分清楚:
- VDD 引脚:全部接到 VDD_MCU 的 3.3V 轨上,包括模拟供电引脚 VDDA 也统一接 3.3V。我没有额外给 VDDA 做磁珠隔离,因为板上模拟电路对噪声要求不算极端。
- VBAT 引脚:接一颗小容量电池或一个大电容。这个引脚负责备份域供电,包括 RTC、备份寄存器,平时电流只有微安级。如果你的设备断电后还需要保持 RTC 时间,VBAT 一定不能漏接。
- VREF+ 引脚:接 3.3V,如果要求 ADC 精度更高,可以在这里加一颗 RC 滤波或独立的 LDO。
有一点我要特别提醒:STM32L073RZ 的 VBAT 域电流虽然小,但“小”不代表可以不接。我之前见过有人为了省事把 VBAT 直接和 VDD 短接,结果后期想加一个独立备份电源只能重新改板,非常被动。
2.3 I²C、中断和状态信号怎么连
PCA9422 的控制接口是 I²C,我把它的 SCL 接到了 STM32 的 PB6,SDA 接到 PB7,这两个引脚正好是 I2C1 的默认映射。线路上各加了 4.7k 上拉电阻到 3.3V,这是 400kbps 下比较常规的做法。
PCA9422 的 POK/中断输出我接到了 PA0,也就是 STM32 的一个外部中断引脚。为什么不用轮询?因为电池电压跌落、充电完成、输出欠压这类事件往往发生在 MCU 睡觉的时候,用外部中断把 MCU 唤醒比让 MCU 一直醒着查状态寄存器要省电得多。这一点在设计初期就要想好,等固件都写完了再补中断引脚,改动成本会翻倍。
2.4 布局布线的几个实操建议
PCA9422 毕竟是电源芯片,布局不能像普通数字芯片那样随意。我总结了几条对走线帮助很大的经验:
- 输入电容和输出电容尽量贴着芯片的对应引脚放,不要为了好看把电容拉得很远。电源芯片对环路电感非常敏感,远了容易引发振铃。
- 电池正极进来的走线尽量粗一点,至少按充电电流的 1.5 倍余量来算线宽。USB 5V 到 PCA9422 的路径也一样,别按信号线画。
- I²C 的上拉电阻应靠近 MCU 这一侧,而不是靠近 PCA9422 一侧,这样能减少总线上的反射。
- 如果板子空间允许,在 PCA9422 下方铺一整块地并打上过孔,散热和接地都有好处。
我自己还习惯在板子上加一个测试点,直接引出 PCA9422 的电池电压采样点,方便后用示波器量。这个测试点成本几乎为零,但调试时能省很多事。
3. 寄存器初始化:让 PMIC 在 MCU 面前“先握上手”
硬件上电之后,PCA9422 并不会马上按照你心里理想的电压输出来工作,它要靠 I²C 寄存器来配置。刚开始做这套方案时,我最担心的问题就是“如果 MCU 还没来得及配置,PMIC 默认输出是什么?”后来看手册和实际测试才明白,PCA9422 的默认输出通常是关闭的,或者由外部引脚决定,所以 MCU 必须在启动早期尽快完成初始化,否则系统会处于一个“有电但没有输出”的尴尬状态。
3.1 第一步:扫描 I²C 地址,别硬编码
不同厂家的芯片 I²C 地址来源不太一样,有些是固定地址,有些通过引脚电平来选择。PCA9422 在数据手册里明确给出了地址配置方式,但为了稳妥,我建议不要一上来就硬编码地址,而是写一段地址扫描代码,先在总线上探测到设备再说。这在调试早期特别有用,能排除接线和上拉的问题。
我项目里用的扫描逻辑大致是这样的:
#define PCA9422_ADDR_UNKNOWN 0x00 uint8_t pca9422_scan_address(I2C_HandleTypeDef *hi2c) { for (uint8_t addr = 0x01; addr < 0x7F; addr++) { // HAL 的 I2C_IsDeviceReady 参数需要 7 位地址左移一位 if (HAL_I2C_IsDeviceReady(hi2c, (uint16_t)(addr << 1), 2, 10) == HAL_OK) { return addr; } } return PCA9422_ADDR_UNKNOWN; }设备地址一旦被扫描出来,后续代码就按这个地址访问。我的板子最终扫描结果是 0x08,但这不代表你的板子也一定是这个地址,因为 PCB 上地址选择引脚可能被拉高或拉低。老老实实用扫描函数,比拿着别人的代码硬套要可靠。
3.2 第二步:读取设备 ID,确认通信正常
找到地址之后,第一时间读取设备 ID 寄存器。这一步不是走过场,而是为了验证“扫描到的设备确实是 PCA9422,而不是别的挂在同一总线上的芯片”。
我习惯在工程里写这样一组读写函数,后面所有配置都基于它们:
#define I2C_TIMEOUT 50 uint8_t pca9422_read_reg(I2C_HandleTypeDef *hi2c, uint8_t dev_addr, uint8_t reg_addr) { uint8_t val = 0; // 注意:HAL 的 MEM_READ 第三个参数是寄存器地址,第 5 个参数是寄存器地址宽度 if (HAL_I2C_Mem_Read(hi2c, (uint16_t)(dev_addr << 1), reg_addr, I2C_MEMADD_SIZE_8BIT, &val, 1, I2C_TIMEOUT) != HAL_OK) { return 0xFF; } return val; } HAL_StatusTypeDef pca9422_write_reg(I2C_HandleTypeDef *hi2c, uint8_t dev_addr, uint8_t reg_addr, uint8_t val) { return HAL_I2C_Mem_Write(hi2c, (uint16_t)(dev_addr << 1), reg_addr, I2C_MEMADD_SIZE_8BIT, &val, 1, I2C_TIMEOUT); }读取设备 ID 后,我会把它打印到调试串口,同时和手册值对比。如果对不上,先别急着重焊芯片,检查一下 I²C 地址是不是被电平选择脚改了,或者总线上有地址冲突。我遇到过两次“读出来是另一个设备”的情况,查到最后都是 PCB 上地址引脚虚焊导致的。
3.3 第三步:配置输出轨电压和负载开关
PCA9422 的核心寄存器无非是几类:输出电压选择、输出使能、上升斜率、充电配置、状态中断。我为了保险,把初始化过程分成了三步,每步都会回读验证:
- 先配置 Buck 输出电压为 3.3V,LDO 输出电压为 1.8V。
- 配置输出使能,让两路输出真正开始带载。
- 配置中断屏蔽,只打开电池电压过低、充电完成、输出电压欠压这几个关键中断。
代码示意如下:
void pca9422_init(I2C_HandleTypeDef *hi2c, uint8_t dev_addr) { // 1. 配置 Buck 输出为 3.3V // 电压寄存器在手册里一般是一个完整字段,这里用操作符拼位 pca9422_write_reg(hi2c, dev_addr, REG_BUCK_VOUT, BUCK_VOUT_3V3 << BUCK_VOUT_SHIFT); // 2. 配置 LDO 输出为 1.8V pca9422_write_reg(hi2c, dev_addr, REG_LDO_VOUT, LDO_VOUT_1V8 << LDO_VOUT_SHIFT); // 3. 打开两路输出 uint8_t en_reg = pca9422_read_reg(hi2c, dev_addr, REG_CTRL); en_reg |= (BUCK_EN | LDO_EN); pca9422_write_reg(hi2c, dev_addr, REG_CTRL, en_reg); // 4. 打开需要的中断 uint8_t irq_reg = pca9422_read_reg(hi2c, dev_addr, REG_IRQ_MASK); irq_reg &= ~(IRQ_VBAT_LOW | IRQ_CHARGE_DONE | IRQ_VOUT_UV); pca9422_write_reg(hi2c, dev_addr, REG_IRQ_MASK, irq_reg); }写完之后,可以再读一遍电压寄存器,确认数值没有因为总线干扰而写偏。这个“写入后回读”的习惯我一般在所有 PMIC 类芯片的初始化代码里都会用,成本只要一个 I²C 读操作,但能挡住很多玄学问题。
还有一个细节:中断脚一般默认是高电平有效还是低电平有效,不同芯片不一样。PCA9422 的中断输出极性我之前踩过坑,后来专门在初始化里把中断极性配置成和 STM32 外部中断触发方式一致,避免出现“电压已经跌了但 MCU 永远等不到低电平”的尴尬情况。
4. DVS 动态调压与 MCU 低功耗模式怎么配合
这章是我觉得整个项目最值得展开的地方。STM32L073RZ 的低功耗模式本身就很强,停止模式电流能到微安级,但大多数设计是“固定电压进入低功耗”。如果配合 PCA9422 的 DVS,可以在 MCU 进入低功耗前先把电源轨电压降下来,让整个系统的功耗进一步下降。
4.1 DVS 的原理和意义
DVS 通俗点说,就是让电源芯片在工作过程中调整输出电压,而不是固定一个值。假如 MCU 在 3.3V 下工作,进入停止模式后其实不需要那么高的电压,只需要保证 SRAM 数据不丢、复位逻辑正常,那么把电压降到 1.8V 甚至更低,功耗能进一步减小。
STM32L073RZ 的最低工作电压是 1.65V,所以我在停止模式前会把 Buck 电压从 3.3V 降到 1.8V,留 150mV 余量。等到 MCU 要唤醒进入全速运行时,再把电压升回 3.3V。整个过程由 MCU 通过 I²C 向 PCA9422 写一个寄存器完成,非常快。
4.2 低功耗流程设计
我在项目里设计的流程是这样的:
- MCU 准备进入停止模式。
- 通过 I²C 把 PCA9422 的 Buck 输出设为 1.8V。
- 等待输出电压稳定,读取状态寄存器确认 POK。
- 执行
HAL_PWR_EnterSTOPMode进入停止模式。 - 外部事件(RTC 闹钟、GPIO 中断、PCA9422 中断)唤醒 MCU。
- 唤醒后先把 Buck 电压升回 3.3V。
- 等电压稳定后,继续执行后续任务。
这里有三个很容易踩的细节:
第一,电压切换后必须等待 POK 信号稳定。POK 是“Power OK”的意思,表示输出电压已经达到目标值。如果你不确认就立刻跑 Flash 操作或高速时钟,可能会因为电压还没爬到位而触发欠压复位。
第二,STM32L073RZ 在低电压下写 Flash 是有条件限制的。Flash 编程电压有最低要求,不能用 1.8V 去写 Flash。所以我的建议是:在低功耗状态下不执行任何 Flash 写操作,唤醒后先升压到 3.3V,再做日志记录或参数保存。
第三,I²C 通信本身也需要 MCU 有足够的供电。如果电压降得太低,I²C 的上拉电阻还接在 3.3V 上,那总线上就会形成额外的漏电流路径。这也是为什么我会把 I²C 上拉接到 3.3V 轨,而不是接到可变的 VDD_MCU 轨上。
4.3 实测效果:待机电流的变化
在设置合理的前提下,我实测过这套系统:
- 3.3V 下进入 STOP 模式,没有做任何外设切断,系统电流大概在 12μA 到 15μA。
- 先降电压到 1.8V,再进入 STOP 模式,系统电流能降到 6μA 到 9μA,前提是 I²C 上拉的漏电流被控制住。
这不是一个精确标称值,因为每个人的板子外设不同,测温范围也不同,但趋势是很明确的:DVS 对低功耗的收益是真实可见的。如果你的设备需要靠电池撑几个月,这几微安的差距也许就是“一周一充”和“一月一充”的区别。
代码里减少到这部分的核心逻辑很简单:
void enter_low_power_with_dvs(void) { // 先把 Buck 降到 1.8V pca9422_write_reg(&hi2c1, PCA9422_ADDR, REG_BUCK_VOUT, BUCK_VOUT_1V8 << BUCK_VOUT_SHIFT); // 等 POK 稳定 while (!(pca9422_read_reg(&hi2c1, PCA9422_ADDR, REG_STATUS) & STATUS_POK)) ; // 进入 STOP 模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后先恢复 3.3V pca9422_write_reg(&hi2c1, PCA9422_ADDR, REG_BUCK_VOUT, BUCK_VOUT_3V3 << BUCK_VOUT_SHIFT); while (!(pca9422_read_reg(&hi2c1, PCA9422_ADDR, REG_STATUS) & STATUS_POK)) ; // 恢复系统时钟并继续任务 SystemClock_Config(); }注意:STOP 模式唤醒后,STM32 的时钟会自动切到 MSI,所以SystemClock_Config()是必须的,否则后面外设时序全乱。
5. 电池充电管理与状态读取
当设备有 USB 插入时,PCA9422 的充电功能就开始工作了。这部分其实可以写得比较简单,就是把充电电流和充电截止电压配置好,然后靠中断通知 MCU。但如果你希望电池用得长久,有几个参数值得多看一眼。
5.1 配置充电参数
单节锂离子电池的充电截止电压常见的有 4.2V 和 4.35V 两种,具体取决于电池规格书。我用的是额定 4.2V 的软包电池,所以配置在 4.2V。充电电流我设置为 500mA,这是从电池容量和充电时间之间折中的选择。如果你的电池容量只有 300mAh,那 500mA 的 1C 充电电流偏大了,建议降到 200mA 左右,具体看电池规格。
PCA9422 一般在 NTC 热敏电阻检测上有配套配置。如果电池组里有 NTC,就把热敏电阻接到充电检测脚上,并配置温度保护范围。如果没有 NTC,至少把温度检测功能关掉或接一个固定电阻,否则充电可能永远无法启动。
配置充电的代码简化为:
void pca9422_init_charger(uint8_t dev_addr) { uint8_t cfg = 0; // 充电截止电压 4.2V,充电电流 500mA cfg = (CHARGE_V_4V2 << CHARGE_V_SHIFT) | (CHARGE_I_500MA << CHARGE_I_SHIFT); pca9422_write_reg(&hi2c1, dev_addr, REG_CHARGE_CFG, cfg); }5.2 怎么判断充电完成
判断充电完成不是靠“测量电池电压是否到 4.2V”就行的,PMIC 会根据充电电流下降到阈值来判断充满。PCA9422 会有对应的状态位或中断位,我建议直接读状态寄存器,判断充电完成标记,而不是自己去量电压。
初始化时我已经开启了充电完成中断,所以在中断回调里处理即可:
void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); uint8_t status = pca9422_read_reg(&hi2c1, PCA9422_ADDR, REG_STATUS); if (status & STATUS_CHARGE_DONE) { // 充电完成,可以唤醒主流程做后续处理,比如上报电量 process_charge_done(); } } }中断里只做读状态和置标志,真正的业务逻辑放在主循环里处理,这是嵌入式中断编程的常规做法,我也一直这么坚持。
5.3 电池电压检测与电量估算
PCA9422 通常会提供电池电压相关的状态或 ADC 值,直接读出来即可。STM32L073RZ 自带的 ADC 也可以测,但在电池供电场景下,我更倾向于在 PMIC 或一个低功耗 ADC 上获取电池电压。原因很简单:MCU 如果一直开着 ADC 去采样,低功耗模式就很难保持。
我在主循环里定期唤醒一次,读取 PMIC 的电池电压,做两个判断:
- 电池电压过低时,通知用户并进入低功耗保护。
- 电池电压恢复到安全范围后,才允许重新开始完整功能。
这个“定期唤醒”可以用 RTC 闹钟实现,间隔设 10 秒或 1 分钟,不影响整体低功耗表现。
6. 调试中踩过的坑,以及怎么快速定位
最后这部分我打算写得比较碎,但每一条都是我实际调试中遇到过、并且花了时间才解决的坑。如果你也打算照这个思路做,提前知道这些可以少走很多弯路。
6.1 上电后 PMIC 不输出,MCU 直接“死”在启动前
这是最常见的问题。MCU 要靠 PMIC 供电,但 PMIC 需要 MCU 配置才能开启输出,这就成了一个“先有鸡还是先有蛋”的死循环。
解决思路不是让 MCU 在无电状态下运行,而是要让 PCA9422 有一个“默认可用的上电状态”。我最后是通过 PCA9422 的引脚级默认配置来解决的:把输出使能引脚或 Boot 引脚按数据手册要求设置成“上电即输出”模式,让系统在上电瞬间就有 3.3V,MCU 才能顺利完成初始化。
如果你在硬件上已经把某个引脚固定为“输出使能”,那么 MCU 启动后主要是调整电压值、打开中断、配置充电参数,而不是负责“从零创建电源轨”。
6.2 I²C 总线被拉死
I²C 总线被拉死这个问题非常经典,我查了很久才发现是 SDA 线上的 4.7k 上拉接到了 VDD_MCU,而这个动态电压轨在 DVS 降为 1.8V 之后,和 PMIC 内部 I/O 电平不匹配,导致总线钳位。
后来我把 I²C 上拉改到独立的 3.3V 电源轨上,问题就消失了。这也再次说明,I²C 上拉应该接在一个稳定的、不受 DVS 影响的电源轨上。
6.3 调试器影响低功耗电流
这是一个很容易被忽视的问题。我用 ST-Link 调试时,待机电流怎么测都是几十微安,但拔掉调试器后立刻降到 8μA。原因很直观:调试器的 SWD 引脚和地回路给板子灌入了额外电流,而且调试模式下 MCU 不会真正进入 STOP。
所以,如果想测准低功耗电流,一定要做到以下几点:
- 拔掉调试器。
- 断开所有 USB 线。
- I²C 总线上如果有调试用的逻辑分析仪,也要断开。
- 用独立稳压源供电,而不是通过调试器的 3.3V 供电。
6.4 示波器量电源纹波的注意点
测量 PMIC 输出纹波时,示波器探头最好用短地弹簧,而不是带长地线的鳄鱼夹。长地线会形成一个很大的环路,把高频噪声捡进来,测出来的纹波往往比真实值大得多。
还有一点:Buck 输出纹波和负载大小、电容 ESR 有关。如果看到纹波在几十 mV 以上,先检查输出电容容量是否足够,再看一下 Buck 是否工作在了 PFM 模式。PFM 模式在轻载时会有一定的纹波跳动,这是正常现象,不一定是设计错误。
6.5 回读验证是最后的防线
PMIC 调试中最容易出现的错觉是“我写了寄存器,但系统没按我预期跑”。遇到这种情况,我的通用手段就是在初始化代码的每一步后面打印或回读关键寄存器,把“寄存器期望值”和“寄存器实际值”一一对照。
我见过一个案例,有人配置充电电压写在 4.2V,但之后又被主循环里另一段代码重置为 4.35V,整个过程毫无提示。这种问题如果不靠回读检查和代码审查,是很难发现的。PMIC 类芯片不像普通外设,它会直接影响 CPU 能否工作,所以每一步都值得验证。
最后分享一个习惯:我在项目里会专门写一个pca9422_dump_status()函数,把主要寄存器全部读出来打印到串口。这个函数平时不用,出问题时第一时间调用,往往能帮你快速圈出问题范围。这个小工具成本很低,但对调试体验的提升非常明显。