ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MCU功耗优化实战:从200mA到微安级的低功耗设计全流程

MCU功耗优化实战:从200mA到微安级的低功耗设计全流程

说到MCU功耗优化,我脑子里第一个画面不是示波器上漂亮的电流波形,而是一块什么都不干的板子在电流表上吃掉两百多毫安的场景。那天本来只想把一块电池设备的工作电流压到几毫安以内,结果一接上电流表,数字直接让我愣了半天——系统刚跑起来,屏幕上显示两百三十多毫安,这要是靠一块几百毫安时的电池供电,别说撑过一年,怕是连一个礼拜都够呛。

后来一路把整块板子从两百多毫安压到稳态两三个微安,整个过程里的坑比我想象的多得多。这文章我打算把整套方法如实写出来:一开始为什么功耗会这么高,测量电流时有哪些容易被忽视的误差,固件侧怎么睡、硬件侧怎么查漏电路径,最后再把“运行状态下继续降电流”的思路也聊一遍。不管是刚接触低功耗设计的初学者,还是已经在和睡眠电流较劲的开发者,这中间应该至少有一两个点是之前没注意到的。

1. 初次上电两百多毫安,先别骂芯片,看看默认配置吃了多少电

很多人拿到一块新板子,测到电流偏高第一反应是“这芯片怎么这么耗电”。其实绝大多数情况下芯片本身挺无辜的,真正有问题的是它周围那一圈默认状态:内核全速跑着、所有外设时钟都开着、调试器还挂着、几个GPIO处于浮空输入状态、板上指示灯常亮。这些开销加在一起,轻轻松松让一个“空载”系统吃掉几十毫安。

1.1 空载系统几十毫安的构成拆解

我自己做过一次比较典型的记录:某低功耗MCU,外部什么都没有接,只把调试器连上、让程序空转,实测电流大约是三十多毫安。这个数字乍看离谱,但拆开看就合理了:

耗电来源典型电流说明
MCU内核运行加Flash取指3到8mA全速运行的主频功耗,主要是动态功耗
未关闭的外设时钟1到5mA每个外设模块的时钟都有对应电流,开得越多越费
调试器在线保持连接1到8mA调试接口一直工作,电流随调试器型号差异很大
GPIO浮空输入每脚几uA到几十uA引脚越多越明显,几十个脚加起来不可忽略
板载指示灯和辅助电路2到10mA一个LED加限流电阻就是几毫安量级

这里面调度器连接这一项特别容易踩:很多人测功耗时调试器没拔,程序在调试模式下跑,电流高到怀疑人生。把调试连接断开之后,电流立刻掉下来一大截。更麻烦的是有些芯片只要调试器在线,低功耗模式本身就退不进去。

所以拿到一块高功耗的板子,第一件事不是改代码,而是“做减法”:把外围全部断开,调试器拔掉,烧录完成后用独立电源供电,再来看芯片本身的基线电流。基线正常了,再一样一样把外设加回去,看是哪一笔开销最大。

1.2 跑起来两百多毫安,通常不是MCU一个人的问题

文章开头说的两百三十毫安,真正印在MCU内核上的并不占大头。那个系统里有一个射频模块、一个显示屏背光、两颗传感器加一个指示灯,还有一个低压差线性稳压器。MCU在这中间只吃了大概十分之一,剩下的全是外围功能模块在跑。

这种时候做功耗优化,千万别一头扎进固件里扣微安,先把外围模块逐个断电,找出哪个是大头。比方说我当时把射频模块的电源单独关掉,电流立刻从两百三十毫安掉到四十毫安左右;再把显示屏背光关掉,又掉到十几毫安。这两个模块一关,问题的主要矛盾瞬间就清楚了——MCU本身调得再漂亮,外围不关也都是白搭。

2. 电流测量方案:测不准的话,后面优化全是玄学

功耗优化最忌讳的事情,就是拿一块普通万用表往供电回路里一串联,看到数字就开始改代码。这里头有太多测量误差来源,尤其到了微安级别,很多人在这一步就被带偏了。

2.1 万用表串进去的“负担电压”和保险丝坑

万用表测电流本质上是把一个小电阻串进回路里,根据这个电阻上的压降换算电流。问题就在这个内阻上:一般的万用表在毫安档内阻有零点几欧到几欧,睡眠电流本来只有几微安,串进去之后压降微乎其微;但系统从睡眠唤醒的瞬间,工作电流忽然跳到几十毫安,万用表内阻上就会出现几百毫伏的压降,这个压降直接把MCU的供电电压拉低,甚至可能导致系统在唤醒过程中复位。

还有一种更隐蔽的情况:有人用万用表的大电流档位去量睡眠电流,测出来是零。不是真的零,而是大电流档位分辨率不够,几微安的电流变化在显示上根本看不出来。要是反过来用微安档去量活动电流,保险丝可能直接烧了。

所以我的做法是把电流测量分成两套方案:

  • 总体活动电流:用串联采样电阻加示波器,看完整的工作周期波形。
  • 睡眠稳态电流:用高精度毫微安表或者专用功耗分析设备,长时间采集平均值。

采样电阻的选择也需要算一笔账。假设目标是把睡眠电流测到1uA级别,用10Ω采样电阻,1uA也只能产生10uV电压,普通示波器的底噪就在这个级别,根本分不出来。所以测微安电流时我会把采样电阻加大到1kΩ,这样1uA对应1mV,示波器还能看个大概;代价是系统唤醒瞬间会有明显压降,所以只能用于测量睡眠稳态,不能拿这套配置去测唤醒过程。

2.2 我自己常用的三档测量套路

说一个在实际项目中反复验证过的土办法,不依赖昂贵设备也能把问题定位清楚。

第一档:改代码之前,先用示波器加采样电阻抓完整的系统工作周期。把采样电阻选在能兼顾活动电流和睡眠电流的中间值,比如100Ω。记录唤醒瞬间的电流尖峰、活动阶段的电流平台、睡眠阶段的电流水平,这样能直观看到整个系统的功耗分布,而不是只看到一个平均值。

第二档:睡眠电流单独测量。把系统强制进入睡眠状态后,在供电回路里串一个1kΩ采样电阻,示波器设在直流耦合档,观察稳定后的电压。这一步能排查掉大部分“睡眠电流虚假偏高”的问题。注意示波器探头本身也有输入阻抗,最好用10x档位,避免探头的负载电阻影响测量结果。

第三档:用万用表或专用微安表做最终验证。确认睡眠电流后,再把工作周期的平均电流算出来,用来做电池寿命估算。

测电流时还有一个细节:睡眠电流并不会在一进睡眠模式之后立刻稳定。唤醒源、内部稳压器、去耦电容都要一点时间过渡,所以测量时要把时间窗口拉长,看跳变之后稳定下来的那段平台值,而不是看刚进睡眠时那几十毫秒的瞬时数字。

3. 固件降电流:睡下去之前,先把每个引脚“交代”清楚

如果说测量是找问题的手段,那固件侧修改就是把问题消灭在代码层。很多人写低功耗程序,只是简单调一个睡眠接口,其他什么都不管,结果睡眠电流永远停在高位。这里最核心的一点是:进入低功耗模式之前,要让系统进入一种“可预期的、无额外负载”的状态。

3.1 几十个微安凭空从哪来?往往是GPIO在“漏”

GPIO真的是低功耗设计里最大的隐藏杀手。常见死法有三种。

第一种是输入浮空。引脚没接外部信号,又配成了输入模式,引脚内部就悬空在高低电平之间,逻辑电平不稳定,引脚内部的输入缓冲电路会来回翻转,产生额外的开关电流。一只脚可能只有几个微安,但一块板子上几十只脚全部浮空,加起来就是几十微安。

第二种是输出低电平却被外部上拉。比如某个引脚接了上拉电阻到电源轨,你把它设为输出低电平,那么从电源轨到地的通路就一直有一条灌电流,同样是被忽略的毫安级损耗。

第三种是唤醒引脚悬空误触发。有些外部中断唤醒引脚一悬空就会受到干扰,系统频繁被唤醒,平均功耗自然降不下来。

我处理GPIO的口诀是:睡觉前把所有不使用引脚统一设置成模拟输入模式,并且关掉上下拉。模拟输入模式会切断数字输入缓冲,引脚上没有逻辑翻转问题,也不存在上拉下拉通路,是很多低功耗MCU漏电流最小的状态。某个引脚要留着做唤醒源,才把它单独设成输入并配上正确的上下拉。

下面是伪代码,思路比具体寄存器更重要:

void Board_EnterSleep(void) { /* 把所有用不到的GPIO切成模拟态,切断数字输入路径 */ for (uint32_t i = 0; i < GPIO_TOTAL_NUM; i++) { GPIO_SetMode(io_map[i].port, io_map[i].pin, GPIO_MODE_ANALOG); GPIO_DisablePull(io_map[i].port, io_map[i].pin); } /* 再关掉没用到的外设时钟 */ Periph_DisableClock(PERIPH_UART0); Periph_DisableClock(PERIPH_SPI0); Periph_DisableClock(PERIPH_I2C0); Periph_DisableClock(PERIPH_TIM0); /* 禁止调试器在停机模式下的连接 */ DBG_DisableInStopMode(); /* 选择需要的唤醒源 */ PM_EnterSTOPMode(PM_WAKEUP_RTC | PM_WAKEUP_EXTI); }

这段代码里最容易被新手跳过的,其实是“关外设时钟”和“关调试器连接”。很多芯片的外设时钟一开,即使外设不工作也会产生基础电流;调试器连接在低功耗模式下更是会强制保持调试逻辑供电。这两项不处理,即便GPIO全切模拟态,睡眠电流照样下不去。

3.2 睡眠模式不是越深越好,要会算“平均电流”

低功耗MCU通常提供几档睡眠深度:浅睡眠保留全部时钟和RAM,唤醒快但功耗高;停机模式把主要时钟关掉,保留RAM和部分唤醒源;待机模式功耗最低,但RAM内容丢失,唤醒相当于重新启动。选哪一档不是拍脑袋,而是要看你的系统在睡眠期间到底要保留什么。

举个例子:系统需要定时采集传感器,每秒钟醒来一次,每次运行20毫秒后继续睡。假设工作电流是5mA,停机模式睡眠电流是2uA。平均电流这样算:

平均电流 = 工作电流 × 工作时长占比 + 睡眠电流 × 睡眠时长占比

代入数值:5mA × (20ms / 1000ms) + 2uA × (980ms / 1000ms) = 100uA + 1.96uA ≈ 102uA。

这个数字很有指导意义:如果系统绝大部分时间都在睡觉,那么睡眠电流做不做得好,决定了平均电流的底线;但如果运行电流特别大、运行时间又不可压缩,那“把运行电流压下来”才是更重要的任务。另外也可以看到,睡眠电流只要在几个微安级别,对平均值的影响就很小,这时候再去纠结零点几个微安的睡眠电流,性价比可能就不高了。

3.3 唤醒后的处理顺序,同样影响功耗

把设备从睡眠唤醒是不够的,唤醒之后第一件事做什么也很关键。我的习惯是:能推迟的外设初始化尽量推迟,只在用到那一刻再打开时钟和电源。因为外设上电瞬间会有充电电流,时钟开启瞬间也有电流尖峰,如果唤醒后把所有外设一股脑全部拉起来,功耗波形上就是一排尖峰,虽然每个尖峰只有几百微秒到几毫秒,但配合频繁唤醒,累计起来很可观。

具体到实现上,我会用一个事件驱动架构而不是轮询架构:唤醒后先判断唤醒源,是外部按键就只处理按键逻辑,是定时器就到点采集一次数据,处理完立刻再进入睡眠。不要把“反正醒了,顺手把其他外设也刷一遍”这种习惯带进低功耗代码里。

4. 硬件侧排查:那些“你明明没接外设也耗电”的寄生漏电路径

一段时间后固件侧优化基本做完,睡眠电流依然卡在几百微安降不下去。这种时候问题多半已经不在代码里了,而在电路板的硬件层面。很多看起来和功耗毫无关系的元器件,会在系统睡眠时悄悄把电流抽走。

4.1 最小系统板上几条容易漏电的常见路径

先列一个检查清单,照着逐项排查基本不会漏。

电源芯片是最容易被低估的。很多普通稳压器本身就有几十微安的静态电流,这个电流不管你睡得多沉都会持续存在。做低功耗系统选稳压器的时候,不能只看最大输出电流,还要看“静态电流”这个参数,常见低功耗稳压器能做到几百纳安到几微安,而普通器件可能是几十微安甚至更高。

LED指示灯和限流电阻也是元凶。一个绿色LED加1kΩ限流电阻接在3.3V上,平时看着不亮或者微亮,实际上限流电阻上流过几十到几百微安电流。计算很简单:3.3V除以1kΩ就是3.3mA,换成10kΩ也有0.33mA。在微安级别的目标面前,0.33mA绝对是不能轻视的大数字。所以低功耗产品里的电源指示灯,要么干脆不接,要么用非常高的限流电阻并只在唤醒瞬间点亮。

还有一类是各种上拉电阻和分压电阻。传感器数据引脚的上拉、I2C总线的上拉、复位引脚的上拉、使能引脚的分压,每一路都构成从电源轨到地的通路。睡觉的时候,这些电阻并没有被断开,微安级电流就顺着它们一直流着。

串口电平转换芯片和USB转串口芯片也要注意。有些板子为了调试方便,常驻一个USB转串口芯片,它自己的空闲电流就有几毫安。做低功耗产品时,这个芯片必须放在可切断的电源域里,通过一个GPIO控制供电,调试完成后直接整个断掉。

4.2 用一颗MOS管给外围模块加一个“总电闸”

我习惯在硬件设计阶段就规划好电源域:MCU核心供电常开,所有非必要外设都挂在一个由MCU GPIO控制的电源开关后面。开关器件优先选低导通电阻的PMOS或者负载开关,控制信号用高电平或者低电平要结合具体的MOS管设计。

这条总电闸的价值在于,软件进入睡眠之前先把外围电源全部切断,让外设“一点电都吃不到”,睡眠电流自然就只剩MCU核心部分。硬件设计时还需要注意开关自身的漏电流,以及开关关断后负载端是否还有剩余电压和寄生电容放电问题。

伪代码里的电源控制很简单:

void Board_SetPeriphPower(uint8_t on) { /* PMOS低电平导通,高电平断开 */ GPIO_WritePin(PIN_PERIPH_PWR, on ? 0 : 1); if (on) { DelayMs(2); /* 等电源稳定,传感器上电初始化 */ } }

这里面那个2毫秒延时也是功耗优化的一部分。外围模块上电瞬间有浪涌电流,如果不等稳定就初始化,可能产生多次重复初始化,既费时间又费电。延时虽然会拉长唤醒时间,但通过精确控制可以做到整体电流最优。

4.3 PCB板的“脏”在微安级别下也是漏电流

这是我在几个项目里反复踩过的坑:硬件设计没问题,芯片选型也没问题,睡眠电流就是比规格书多出几个微安。后来把板子放进超声波清洗机洗了一遍,再烘烤除湿,数值立刻恢复到正常水平。

原因是助焊剂残留。回流焊或者手工焊接后,板上会残留助焊剂,这些东西在潮湿环境下会形成微弱导电通路,正好漏掉几微安电流。在正常功耗级别下完全感觉不到,但一旦你追求微安级别,这个问题就藏不住了。低功耗产品在量产阶段,必须把PCB清洗和表面涂覆列进工艺要求;实验室里手工焊接的样板,也要在测试前做一次清洗和充分烘干。

同样的道理,环境湿度也会影响漏电。同一个固件,在干燥环境里睡眠电流能到2uA,湿度大的时候可能涨到6到8uA。做低功耗验收时,最好把温湿度条件也记录下来,不然两个团队拿着两台环境不同的设备测同一个固件,数字对不上,互相怀疑。

5. 运行时功耗:把“干活”那段时间的电流也压下来

睡眠电流优化到微安级之后,很多人就觉得大功告成了。但只要系统还要定期醒来干活,运行那段电流就不能不管。大多数电池系统其实是靠着“运行尖峰的时间占比”和“睡眠底线电流”两个指标共同决定续航的,只顾一头都会吃亏。

5.1 用事件和DMA代替轮询,让CPU没事就睡

传统写法里最费电的,是“while(1)里反复查询标志位”的轮询结构。查询本身虽然不干重活,但每次查询都要CPU取指、判跳、再查一次,主频上来了,功耗自然高。低功耗固件的核心思路是:没事做就睡,有事做才醒。

一个典型的优化点是外设数据收发的等待过程。假设要用串口发一串数据,老办法是:

for (i = 0; i < len; i++) { while (!TX_REG_EMPTY); /* 忙等发送寄存器空 */ TX_REG = buffer[i]; }

这段循环跑的时候,CPU一直在等待状态里打转,活动电流拉满。改成DMA发送后,把数据交给DMA搬运,CPU可以做一次短睡眠或者直接进入低功耗模式,等发送完成中断再醒来。

void UART_SendDMA(uint8_t *buf, uint32_t len) { /* 配置DMA传输,传输完成后触发中断 */ DMA_Start(buf, len); /* 此时CPU不需要等待,可以进入浅睡眠 */ PM_EnterSleep(); }

代价是代码结构变得复杂,但功耗收益非常明显。尤其是通信频率高的场景,把等待时间里的电量省下来,能让运行平均电流降掉一半还多。

5.2 工作电压、时钟源和电源模式的选择

运行功耗本质上由动态功耗决定,动态功耗和电压的平方成正比、和时钟频率成正比。这个公式意味着:能降低工作电压就尽量降低,能降低时钟频率就尽量降低,能关掉的就关掉。同样一颗MCU,在3.3V供电和2.0V供电下运行相同任务,电流差距可能非常可观;有些MCU还提供低功耗稳压模式或者DC-DC模式,在高电压输入下用DC-DC模式能把内核供电电压降下来,运行电流比LDO模式低不少。

时钟源的选择同样影响运行电流。内部高速振荡器通常比外部晶振功耗高,但外部晶振需要先起振等待稳定,起振阶段也耗电。对我的很多项目而言,运行频率够用就行,不盲目追求高主频。同样的任务用低主频做,虽然花的时间长一点,但总能量可能是差不多的;如果还能配合睡眠,把空闲时间睡过去,平均功耗就会更低。

5.3 一个隐藏的功耗大头:复位引脚和启动配置电阻

最后说一个很多人在原理图阶段就忽略的细节。为了让MCU可靠复位,大家都习惯在复位引脚上接一个上拉电阻到电源轨;如果这个电阻阻值太小,它就成了一个常驻漏电点。还有启动配置引脚,某些系统会固定接上下拉到电源或地,这部分电路也会形成通路。

低功耗设计时要重新审视这些“常规接法”:复位上拉电阻可以选100kΩ以

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进