做嵌入式久了,几乎都会遇到这样的需求:用树莓派Pico做一个电池供电的小设备,比如温湿度传感器、门磁、遥控器,功能跑起来不难,难的是让电池能撑上几个月而不是几天。问题的答案基本都落在“低功耗”这三个字上。Pico这颗RP2040芯片本身在低功耗方面的能力不弱,但想把功耗真正降下来,靠的不只是硬件选型,更重要的是软件层面的控制——也就是通过SDK或固件暴露的休眠API,把芯片从几十毫安降到几百微安级别。这篇文章准备把Pico低功耗相关的API用法、完整实操流程和我踩过的坑一次性说清楚。
先说一个容易混淆的点:这里的API不是网站后台那种HTTP接口,而是Pico开发环境里用来控制休眠、唤醒、时钟切换的这些编程接口。不管是C/C++ SDK还是MicroPython固件,低功耗功能的本质都是通过调用这些接口实现的。我会把两种开发方式都覆盖到,并给出可以直接抄的代码和实测数据。
1. 低功耗项目,为什么偏偏选树莓派Pico
1.1 RP2040的功耗基本面
先说芯片本身。RP2040是双核Arm Cortex-M0+架构,最高主频133MHz,在默认时钟跑满的时候,整块Pico开发板的电流大概在20到80mA这个范围。这个数字对USB供电的开发阶段不算什么,但对电池设备来说就是巨大开销——一节2000mAh的锂电池,如果设备一直以50mA运行,理论续航也就40小时,这还没算电池自放电和电压转换损耗。
但RP2040的数据手册里明确写了,在dormant(休眠)模式下,芯片核心电流可以降到微安级别,典型值大概在180µA附近,具体数值和供电电压、板子型号有关系。也就是说,这颗芯片本身具备做低功耗产品的硬件基础,关键是你能不能通过软件把它“指挥”到位。很多人的Pico项目续航短,不是芯片不行,而是压根没把休眠功能用起来。
1.2 三种低功耗状态怎么选
RP2040的低功耗状态,从浅到深可以分成这样几个层次:
- sleep(睡眠):CPU时钟停止,外设时钟可以继续运行。适合需要快速唤醒、外设还要保持工作的场景。电流大概在几毫安到十几毫安。
- dormant(休眠):几乎所有时钟都停掉,PLL关闭,外部晶振和内部振荡器都可以停,只保留GPIO边沿唤醒等极少模块供电。功耗最低,适合电池供电、大部分时间不干活的设备。
- 软件深度睡眠(deepsleep组合拳):严格说RP2040没有传统单片机那种一次性关断大部分电源的deepsleep,而是通过“dormant + 关闭不必要外设 + GPIO收敛 + 禁用ADC”这一套组合,达到等效深度睡眠的效果。
这里有个容易踩的误区:Pico SDK里直接暴露的是sleep/dormant,而MicroPython里暴露的是lightsleep/deepsleep。名字不同,但底层硬件能力是同一套。理解这一点,遇到库函数名对不上号的时候就不会懵。
1.3 软件控制才是真正拉开差距的地方
低功耗设计里,硬件部分是固定的,软件才是决定功耗高低的变量。比如同样进入dormant,GPIO处理得好不好,可以差出几十上百微安;唤醒后时钟恢复没恢复,又会影响下一次休眠甚至让程序跑飞。再比如ADC模块,很多新手在采集过一次模拟值之后直接进休眠,结果ADC电路还在持续耗电,休眠电流直接翻倍。
所以标题里把“软件控制”和“API”并列,不是故意拗口,而是这个项目的核心真的就是API的用法。硬件电路再简洁,软件没做好低功耗状态迁移,一切都是白搭。
2. 用C/C++ SDK手工打磨功耗
2.1 认识必用的低功耗API
如果你选择Pico SDK做C/C++开发,低功耗相关接口主要集中在hardware/sleep.h头文件里。下面这几个函数是我实际项目里最常用的,建议对照SDK文档一起看:
sleep_run_from_rosc():休眠前把系统时钟源切到芯片内部振荡器(ROSC),关闭PLL,达到省电目的。sleep_run_from_xosc(xosc_hz):切到外部晶振(通常是12MHz),适合需要保留精准时钟的场景。sleep_goto_sleep_until(pc, cycles):进入sleep状态,并设置唤醒周期。sleep_goto_dormant_until_edge_high/low(pc, gpio):进入dormant状态,等待某个GPIO的上升沿或下降沿唤醒。sleep_goto_dormant_until_time(cycles):进入dormant,按指定周期醒来。sleep_run_from_dormant_source(clock_index):唤醒后,让系统从一个可用的时钟源恢复运行。
这些函数的名字在较新的SDK版本里可能略有调整,但基本逻辑一致。真正用好它们的关键,不是记住每个参数,而是理解“休眠前关时钟”与“唤醒后恢复时钟”的配套关系。这点我后面会展开讲。
2.2 sleep模式实战代码
我经常用一个简单例子来说明sleep模式:设备稳定运行5秒完成一次数据采集,然后sleep 10秒,再醒来继续采集。完整代码如下:
#include <stdio.h> #include "pico/stdlib.h" #include "hardware/clocks.h" #include "hardware/pll.h" #include "hardware/rosc.h" #include "hardware/sleep.h" void measure_and_report(void) { // 模拟采集与上报,实际项目中替换为传感器读取和无线发送 for (volatile int i = 0; i < 1000; i++); printf("measure done\r\n"); } int main(void) { stdio_init_all(); while (1) { measure_and_report(); // 休眠前切换到内部振荡器,关闭PLL sleep_run_from_rosc(); // 进入sleep,持续一段时间(这里先给一个估算值) sleep_goto_sleep_until(0, 1000 * 1000 * 10); // 唤醒后恢复PLL和系统时钟 clocks_init(); } }这里有个非常关键的点:sleep_goto_sleep_until的cycles参数不是毫秒,也不是秒,而是“当前时钟源下的时钟周期数”。休眠前切到了ROSC,ROSC的频率不是标准的133MHz,而是一个由内部振荡器决定的数值,所以如果直接按133MHz去算周期,睡眠时间会差很多。
稳妥的做法是用hardware/clocks.h提供的时钟频率查询函数,拿到当前clk_sys的实际频率再计算周期。MicroPython的lightsleep接收的是毫秒参数,相比之下友好很多,这也是很多人直接选MicroPython的原因之一。
2.3 dormant模式与GPIO唤醒实战
GPIO唤醒是dormant模式最常见的唤醒方式,特别适合接按键、干簧管、PIR人体传感器这类开关量信号。下面这段代码演示了:进入dormant,等待GPIO14从高电平变成低电平(比如按键另一端接GND,按下时产生下降沿)唤醒。
#include <stdio.h> #include "pico/stdlib.h" #include "hardware/sleep.h" int main(void) { stdio_init_all(); gpio_init(14); gpio_pull_up(14); // 按键另一端接GND,按下产生下降沿 while (1) { printf("enter dormant\r\n"); sleep_goto_dormant_until_edge_low(0, 14); printf("wake up\r\n"); // 唤醒后系统时钟可能仍处于低功耗时钟源,按需恢复 clocks_init(); sleep_ms(50); // 简单防抖 } }这个例子里有几个必须注意的细节:
第一,GPIO14必须是Pico排针引出的引脚。Pico板上不是所有GPIO都引到了排针,如果把休眠唤醒引脚的编号设成一个没引出的内部引脚,线接不上,程序死活唤不醒,排查起来会非常抓狂。
第二,进入dormant前,唤醒引脚的上拉/下拉一定要提前配好。不配上拉的话,按键悬空时电平不确定,一下沿唤醒一下不唤醒,或者干脆误触发唤醒。
第三,唤醒之后不是自动回到原来的高主频。dormant唤醒后,系统可能还在ROSC或XOSC这种低频时钟源下运行,如果你马上调用依赖高频率的延时函数、PWM、串口,行为会完全不对。所以我在唤醒后立即调用了clocks_init()把PLL重新初始化一遍。
2.4 时钟源切换的底层逻辑
为什么切个时钟就能省电?因为RP2040的正常运行高度依赖PLL(锁相环)将外部12MHz晶振倍频到133MHz系统时钟。PLL本身一直工作是要耗电的,而dormant模式下PLL关闭、外部晶振可以停、内部振荡器也可以停,只有GPIO唤醒模块和必要的IO保持供电,功耗自然就下来了。
这也是我在2.2里强调“唤醒后重新clocks_init()”的原因。一个常见的bug是:工程师把系统切到了ROSC,接着进入休眠,唤醒后没有恢复PLL,程序虽然能跑,但所有延时、PWM频率、串口波特率全都变了。排查这类问题通常要花很长时间,所以我在休眠入口和出口都会加上时钟打印,确认当前频率符合预期再继续跑。
3. 用MicroPython快速实现低功耗
3.1 lightsleep与deepsleep API
对不少项目来说,用C SDK手动管时钟确实有点折腾。MicroPython把低功耗接口封装得很简洁,代码量能少一个数量级,适合快速验证、对功耗没有极致要求的项目。
MicroPython里两个核心API:
machine.lightsleep(ms):进入浅睡,指定时间后自动唤醒;也可以额外传入wake_pin参数,让GPIO也能唤醒。唤醒后程序从sleep调用处继续往下执行。machine.deepsleep(ms, wake_pin=...):进入深睡,唤醒后程序会从main.py重新执行,效果近似于重新上电。
在RP2040移植版里,deepsleep底层本质上走的是dormant模式的GPIO唤醒机制,所以功耗比lightsleep低很多,但代价是唤醒后上下文不保留。理解这一点,对设计程序流程影响很大。
3.2 一个可复用的低功耗模板
这是我实际项目里经常用的模板,比如做一个土壤湿度采集节点,每半小时唤醒一次,采集发送后立刻接着睡。代码可以直接参考:
import machine import time def read_sensor(): # 这里换成你的传感器读取逻辑 return 66 def send_data(value): # 这里换成你的网络/无线发送逻辑 pass print("boot reason:", machine.reset_cause()) # 判断是否从deepsleep唤醒 if machine.reset_cause() == machine.DEEPSLEEP_RESET: print("wake from deepsleep") else: print("cold boot") value = read_sensor() send_data(value) # 进入deepsleep前,先把用不到的东西关掉 led = machine.Pin(25, machine.Pin.OUT) led.value(0) # 把未使用的GPIO设置成输入下拉,避免悬空漏电 for pin_num in range(23): try: p = machine.Pin(pin_num, machine.Pin.IN, machine.Pin.PULL_DOWN) except ValueError: pass # 30秒后唤醒 machine.deepsleep(30_000)这个模板里有三个值得说明的点:
第一,进入低功耗前,板载LED必须关掉。GPIO25在Pico板上直接驱动一个LED,如果之前跑过点灯程序,忘了拉低,这个LED会一直亮着,额外吃掉几毫安电流。
第二,把未使用的GPIO遍历设置成输入下拉,能有效降低悬空引脚带来的漏电流。这个我在后面第5章会专门展开讲,它是低功耗项目里最容易出问题也最容易被忽视的环节。
第三,deepsleep唤醒后从头执行,所以程序开头一定要用reset_cause()判断唤醒原因,决定后续初始化流程。如果不判断,每次唤醒都会重新打印开机信息、重新初始化传感器,逻辑上可能会重复。
3.3 唤醒原因判断与数据保存
因为deepsleep唤醒后类似重启,Python运行过程中产生的变量会全部丢失。如果需要在唤醒后知道“是第一次上电还是深度睡眠唤醒”,只能靠machine.reset_cause():
import machine cause = machine.reset_cause() if cause == machine.DEEPSLEEP_RESET: print("deepsleep wake") elif cause == machine.POWER_ON_RESET: print("normal power on") else: print("unknown reset cause: %d" % cause)如果需要在睡眠期间保留下一次的计数、状态字、计数器,有两个办法:一是直接写到Flash里的文件系统,唤醒后读取;二是用RTC备份寄存器。需要特别说明的是,RP2040在dormant模式下RAM其实是保持供电的,但MicroPython唤醒后重新初始化了运行时环境,所以Python对象不能靠RAM保留。这是一个反直觉的点,我第一次用的时候也踩过坑——在RAM里存了个计数变量,deepsleep之后一读,变成初始值了。
所以在设计低功耗数据流时,我的原则是:一切要跨睡眠保留的数据,要么写文件,要么主动存到RTC寄存器,绝不在RAM里赌运气。
4. 实测功耗:怎么测才靠谱
4.1 测量工具与接线
低功耗项目的功耗数据必须实测,不能只看数据手册。最简单的测量方式是拿一块万用表串联到供电回路里,把量程调到mA挡,测整体功耗。但测几百微安时要注意,很多万用表在不同电流量程下内阻不一样,对结果影响很大。我一般先用mA挡测一个大数,再切换到µA挡重新测量,确认两个量程的读数是否一致。
更专业的做法是使用INA219或INA226这类电流检测模块,配合数据记录工具连续采样,能看到睡眠和唤醒过程中的完整电流波形。我在实际项目里会用INA226连续采样,记录一个完整唤醒周期的电流曲线,比只看单个瞬时值有价值得多——比如你能清楚看到“唤醒瞬间电流尖峰有多大”“进入睡眠花了多久”“睡眠电流是否稳定”。
4.2 从几十毫安到几百微安
同样一块Pico,在不同状态下实测数据大致如下。注意这些数值受板子型号、供电电压、外设多少影响很大,只做为量级参考,不代表每一块板子都完全一样:
| 运行状态 | 电流量级 |
|---|---|
| 正常运行,133MHz,LED亮 | 30~80mA |
| 正常运行,降频到50MHz左右 | 15~30mA |
| C SDK sleep模式 | 3~15mA |
| C SDK dormant模式 + GPIO唤醒 | 200~800µA |
| MicroPython lightsleep | 2~8mA |
| MicroPython deepsleep | 300µA~1.5mA |
一个很容易忽略的点是,这组数据里其实已经包含了Pico板上电源指示灯的固定消耗。那个电源灯是由硬件电阻直接接在3V3上的,软件怎么都关不掉,大概会吃掉一小部分电流。如果项目要做极致续航,考虑在硬件上处理掉这颗LED,或者接受它的存在。
4.3 功耗数据对照表的价值
很多人测出一次数据就完事了,但我建议把不同配置下的数据做成一张对照表。这样能快速定位是哪个配置导致功耗异常。比如:
- 默认运行:55mA
- 关掉LED后运行:50mA
- 关LED + 进入dormant:1.2mA
- 关LED + 全部GPIO下拉 + dormant:800µA
- 再关掉不用的外设时钟:400µA
多花十分钟建立这张表,后面排查问题会省很多时间。低功耗优化本质上就是一个“逐个消灭耗电源”的过程,没有数据支撑,靠猜是猜不出来的。
5. 注意事项与避坑指南
5.1 GPIO悬空漏电
这是低功耗项目里最常见的坑,没有之一。GPIO引脚如果闲置不管,处于高阻输入状态,引脚上感应到的噪声电压会让内部输入缓冲器反复翻转,形成明显的漏电流。一块Pico几十个GPIO,每个漏一点点,加起来就很可观了。
解决办法是在进入休眠前,把未使用的GPIO全部设置成确定状态:要么输入+PULL_DOWN,要么输入+PULL_UP,要么输出低电平。我一般统一设置成输入下拉,这样即使外部接线意外碰触也不容易倒灌电流。MicroPython可以用循环遍历,C SDK也一样,写一个小函数在每次休眠前调用。
5.2 板载LED与电源指示灯
板载LED对应GPIO25,高电平点亮,进入休眠前必须输出0。这个大家都记得住,但电源指示灯不是GPIO控制的,是硬件直接接了电源,软件怎么都关不掉。我在4.2里说了它会固定消耗一部分电流。
如果项目真的非常在意功耗,硬件上可以考虑用跳线把它断开,或者接受它的消耗。我的个人建议是:开发阶段别动它,等产品化定型了再决定要不要焊掉。毕竟为了几十微安牺牲调试便利,不划算。
5.3 外设没关干净
进入休眠后,如果ADC、PWM、I2C、SPI等外设还在运行,它们各自仍会消耗电流。MicroPython的deepsleep会帮你处理大部分外设,但C SDK里没人帮你关。我在dormant示例里没有用到外设,所以相对干净;如果在实际项目中用过ADC,需要在休眠前把ADC相关状态重置掉。
ADC尤其容易漏:RP2040的ADC如果采样过,会保持上电状态,某些板子会从VREF路径漏电。我遇到过一次,休眠电流怎么都降不下去,排查了很久才发现是ADC在捣鬼。所以我的原则是:用不到的ADC功能,从一开始就不要初始化,Raw ADC pin也别碰,这样最省心。
5.4 RTC与32.768kHz晶振
很多做定时唤醒的人会第一反应去找RTC唤醒功能。RP2040内部确实有RTC模块,但如果要求它在低功耗状态下独立工作并定时唤醒,需要一个外部32.768kHz晶振作为时钟源。Pico板载并没有焊接这个晶振,所以纯靠内部RTC做超低功耗定时唤醒,在标准Pico板子上是不现实的。
替代方案有三种:
- 外部RTC芯片(比如DS3231),由RTC芯片的INT引脚输出信号唤醒Pico。这是我最常用的方案,稳定可靠,还能顺便拿到准确的日历时间。
- 自己在Pico的对应引脚焊接32.768kHz晶振。理论可行,但手工焊接晶振对电容匹配要求高,新手不建议折腾。
- 用GPIO边沿唤醒配合外部定时事件。比如传感器本身就带定时中断输出,或者用一颗定时器芯片周期性地产生脉冲触发Pico唤醒。
我绝大多数项目都选第一种或第三种,因为稳定、可维护。
5.5 唤醒后恢复时钟
C SDK场景下,从休眠恢复后时钟不一定是你常用的133MHz,这直接影响外设和延时。我的习惯是在休眠入口记录“当前时钟源 + 当前频率”,唤醒后用同一套配置恢复,并打印实际频率做确认:
#include "hardware/clocks.h" // 休眠前记录 uint32_t freq_before_sleep = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); printf("clk_sys before sleep: %d kHz\r\n", freq_before_sleep); // 唤醒后恢复 clocks_init(); uint32_t freq_after_wake = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); printf("clk_sys after wake: %d kHz\r\n", freq_after_wake);MicroPython则相对省心,lightsleep唤醒后时钟自动恢复,deepsleep唤醒后直接重启,不存在这个坑。所以如果你用MicroPython开发,这一节不用太担心。
我个人的习惯是,把Pico的低功耗开发分成两档来用:简单一次性采集上报的项目,直接用MicroPython加deepsleep,开发快,逻辑直观,后期也好维护;需要精确控制、低延迟唤醒、复杂外设协同的场合,就走C/SDK手动管时钟和dormant,虽然代码多一点,但每个环节都在掌控中。
调试低功耗项目时,我总会先在桌面上把板子架起来测一轮baseline,再把所有引脚处理和外设关闭逐项加上,每加一项就记一次电流。这样做虽然慢一点,但每次改动带来的功耗变化都清清楚楚。等板子真正装到电池盒里再去猜哪里漏电,成本就太高了。希望这篇文章能帮你少走一点这些弯路。