
做电池供电项目的时候我第一块 Pico 板几乎是被自己亲手“玩没电”的逻辑倒是简单DHT22 每隔十分钟读一次数据然后给舵机发一个开合信号剩下时间原地空转。结果一块 18650 只撑了不到一天。问题不在树莓派 Pico 本身而在代码里根本没有调用任何低功耗 API。RP2040 是一颗性能相当不错的双核 M0 芯片但如果你只是写一个 while True 空转电流轻松走到 20mA 级别这在电池场景下基本不可接受。这篇内容想分享的就是我后来整理的一套从 API 到实践的低功耗软件控制方法包括 MicroPython 的 lightsleep/deepsleep、官方 C SDK 的 sleep/dormant 接口以及在实际调试中踩过的各种文档里根本不会写的坑。适合正在用 Pico 做电池设备的同学也适合刚从 STM32/ESP32 转过来、对 RP2040 功耗模式不熟的开发者。1. 先搞清楚 Pico 平时到底把电花在哪了功耗基线实测1.1 运行模式下 20mA 从哪来的很多人对 MCU 功耗的预期还停留在“单片机应该很省电”的阶段但 RP2040 是一颗为功能密度设计的芯片不是为微安级待机设计的。它内部有双核 Cortex-M0、USB 控制器、PLL、ADC、PIO 等一堆外设运行时所有模块都在吃电。即使你的代码只是空转芯片也要做三件事维持 CPU 时钟、从外部 QSPI Flash 取指令、维持系统时钟树。这里有个反直觉的点RP2040 没有内部 Flash代码是放在外部 QSPI Flash 里的CPU 每次取指都要通过 XIP 桥读 Flash这一部分的电流开销比很多人以为的大。当你把主频拉满 125MHz 时空转电流可能到 26mA 以上降到 48MHz也要 18mA 左右。如果程序里还顺手点了一颗板载 LED、开着 USB 或者 ADC那功耗更是肉眼可见地涨。再加上 Pico 开发板上还有板载电源芯片、USB 口的 VBUS 检测电路、用户 LED 的限流电阻这些板级器件自身也有静态损耗。也就是说即便 RP2040 芯片本身进入低功耗状态整块 Pico 板的电流也不是芯片手册上那个漂亮的数字。做项目前一定要先建立一个认知你优化的电量是“整块板子”的电量不是“核心芯片”的电量。1.2 实测环境与基线数据我在测试时的接法很简单USB 不插拿两节 18650 串联经过一个低压差稳压给 VIN 供电万用表用毫安档串在电池正极回路上。固件是 MicroPython 1.22 和官方 C SDK 的 pico-examples 两种都测了。实测下来的基线数据大概是这样的状态条件实测电流运行 125MHz空转 while True无外设26~30mA运行 48MHz空转无外设18~22mAlight sleep仅定时器唤醒GPIO25 LED 关闭0.9~1.2mAdeep sleep复位唤醒各引脚已处理0.5~0.8mAdormantC SDK等待 GPIO 边沿唤醒0.2~0.3mAdormant唤醒引脚带内部上拉增加 10~30uA注意最后一行别小看这个“增加 10~30uA”在 uA 级的世界里它可能就是电池寿命从一年变半年的差距。这也是后面要专门写一节讲 GPIO 状态管理的原因。2. 软件控制低功耗的 API 全景MicroPython 与 C SDK 两条路线2.1 MicroPython 端machine.lightsleep / machine.deepsleep用 MicroPython 开发时低功耗的核心 API 就两个machine.lightsleep()和machine.deepsleep()。lightsleep是“浅睡”进入睡眠后 CPU 暂停但是时钟、内存内容都保留定时器也可以继续工作到了指定的毫秒数就会自动唤醒唤醒后从睡眠位置的下一条指令继续执行。这是绝大多数周期性采集场景的主力 API写法也最简单import machine import time led machine.Pin(25, machine.Pin.OUT) led.off() # 关掉板载 LED这一步忘掉会多吃好几个 mA while True: # 采集逻辑 print(wake) # 睡 60 秒 machine.lightsleep(60000)machine.deepsleep()则是把芯片带到更深的状态唤醒后整个 MicroPython 解释器会重新启动程序从头开始跑。所以如果你的项目需要区分“上电首次运行”和“deepsleep 唤醒恢复”就要用machine.reset_cause()去判断复位来源再把状态存到 Flash 文件系统里比如用一个标志文件记录上一次的执行进度。这里有一个容易误解的地方很多从 ESP32 转过来的朋友习惯性认为 deepsleep 之后还能继续跑“唤醒后的代码”但 Pico 的 MicroPython 没有 ESP32 那样的 RTC memory 概念所有变量清零。这个行为差异一定要在设计状态机时提前考虑。2.2 C SDK 端sleep 库的调用链如果追求更低的功耗就要下沉到 C SDK。RP2040 官方提供了一套 sleep 库头文件是pico/sleep.h。它不是简单的sleep_goto_dormant()一个函数搞定而是要按顺序做三件事先把系统时钟切到低功耗时钟源再进入睡眠/休眠模式唤醒后再恢复高频时钟。典型流程是这样#include pico/stdlib.h #include pico/sleep.h int main() { stdio_init_all(); // 1. 把时钟从 PLL 切换到外部晶振 XOSC避免 PLL 继续耗电 sleep_run_from_xosc(); // 2. 进入 dormant 模式等待 GPIO2 下降沿唤醒 sleep_goto_dormant_until_edge(2, false); // 3. 唤醒后恢复 PLL 高频时钟 sleep_run_from_pll(pll_sys, clock_get_hz(clk_xosc), 125000000, 125000000, 62500000); // 继续你的业务逻辑 while (true) { tight_loop_contents(); } }sleep_run_from_xosc()的作用是让系统只靠板上的 12MHz 晶振运行停掉 PLL因为 PLL 本身也是一个持续的耗电来源。sleep_goto_dormant_until_edge(gpio, edge)会把 RP2040 置入 dormant 状态这个状态下大部分时钟都停了只有 GPIO 边沿检测电路还在工作所以只能靠外部信号唤醒。唤醒后系统会从这条函数调用处继续往下执行但时钟源还停留在低速状态需要显式把 PLL 重新配置回来。2.3 两条路线怎么选维度MicroPythonC SDK sleep 库开发效率高几行代码低要处理时钟树最低功耗0.5mA 左右板级0.2mA 以下板级唤醒方式定时器/GPIO/复位GPIO 边沿/dormant灵活度更高状态保持deepsleep 会重置解释器可在函数调用后继续执行适合场景原型验证、中小项目产品化、长时间电池供电我的建议是原型阶段先拿 MicroPython 把功能和功耗验证跑通确认方案可行后再决定要不要下沉到 C SDK。很多时候 lightsleep 的毫安级几乎够用了不用一上来就为难自己。3. 从 lightsleep 到 dormant四档功耗逐级压低的实测过程3.1 第一档降低主频 关外设如果不想立刻用睡眠最直觉的做法是把 CPU 主频降下来。MicroPython 里一条machine.freq(48000000)就能把主频从 125MHz 拉到 48MHz实测空转电流从约 28mA 降到约 20mA。这个操作对外设基本无影响适合纯计算、不需要高频处理的任务。不过降频只是“省着花”不是“停下来”所以它只能算一个基础动作不能替代睡眠。在这档里还应该顺手把用不到的外设关掉。MicroPython 没有像 C 那样逐外设关时钟的接口但你可以保证不启用的外设不初始化。特别要检查的就是 GPIO25 上的板载 LED很多示例代码里默认点亮一个 LED 加上限流电阻就是几个毫安。3.2 第二档lightsleep 定时唤醒machine.lightsleep(ms)是性价比最高的一档。从代码上看它只是把屏幕停在半路但电流直接掉到 1mA 左右。实测流程是这样的import machine import time last_time time.ticks_ms() print(before sleep) machine.lightsleep(5000) print(after sleep, cost ms:, time.ticks_diff(time.ticks_ms(), last_time))这个模式适用于大部分周期性采集场景传感器上电、延时稳定、读取、关闭传感器、进入 lightsleep到点再醒来。由于定时器在浅睡状态下仍然工作lightsleep的唤醒时间是比较精确的我实测 5 秒的误差在几十毫秒内。另外GPIO 中断也能把它提前唤醒这是后面要讲的“睡眠中被外部事件触发”能力的基础。3.3 第三档MicroPython deepsleep 复位唤醒MicroPython 的machine.deepsleep()会把 RP2040 带到更深的低功耗状态。从整板实测看电流能再往下走一点到 0.5~0.8mA。代价是唤醒后解释器重启代码从main.py第一行重新执行。如果你要在这种情况下区分“冷启动”和“深度唤醒”可以这样处理import machine cause machine.reset_cause() print(cause) # 不同固件版本对唤醒原因的常量命名略有差异 # 建议先打印出来对比再决定分支逻辑由于解释器重启所有全局变量都丢了。实际项目里我会把状态写进一个小 JSON 文件唤醒后先读文件再决定是继续工作还是重新初始化。这个模式的电流虽然只比 lightsleep 低了一点点但它最大的价值是让 CPU、时钟树几乎全部停摆为更深的 dormant 做了铺垫。3.4 第四档C SDK dormant 边沿唤醒如果项目对功耗的追求已经很极致那就用 C SDK 的 dormant 模式。前面提到过sleep_goto_dormant_until_edge()会把系统带入几乎所有时钟都停止的状态只有 GPIO 边沿检测电路在监听唤醒引脚。实测 Pico 整板电流能到 0.2~0.3mA这个数字里其实还包含板载 LDO 的静态电流如果直接用裸芯片做产品电流会低一个数量级。这档的使用门槛主要在时钟恢复从 dormant 唤醒后外设的高频时钟可能是不对的状态UART、ADC 这类依赖时钟树的外设需要重新初始化。我在项目里是这样处理的唤醒后先恢复 PLL 时钟再重新初始化自己用到的外设最后回到主循环。用代码表示就是void wake_and_recover(void) { sleep_run_from_pll(pll_sys, clock_get_hz(clk_xosc), 125000000, 125000000, 62500000); // 重建 I2C/UART 等外设 }哪种唤醒源用哪一档可以对着需求选有定时需求用 lightsleep接受重启逻辑用 deepsleep追求极致功耗且有外部唤醒信号源再考虑 dormant。4. 低功耗唤醒与 GPIO 状态管理最容易翻车的几个细节4.1 悬空 GPIO 的漏电流比想象中更严重这是我在实测中踩过最深的一个坑。第一次用 dormant 模式时我以为只要程序休眠电流就应该自己掉下去结果实测还挂着 2mA。排查了很久发现罪魁祸首是 Pico 上大量悬空的 GPIO 引脚。这个道理其实很简单CMOS 输入引脚如果没有被外部电路稳定在一个确定电平输入缓冲区域会因为外部环境噪声不断在高低电平间抖动内部电路为了跟随这种抖动持续切换电流就上去了。悬浮引脚越多电流越可观。处理办法是进入睡眠前把所有没用到的 GPIO 统一配置成输出低电平或者输入下拉。MicroPython 中可以用类似这样的循环处理import machine unused_pins [0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 26, 27, 28] for gp in unused_pins: p machine.Pin(gp, machine.Pin.OUT, value0)这里有个原则要记住不是把所有引脚都设成低就安全了如果引脚外部恰好有上拉那设成输出低反而会形成持续灌电流。正确做法是逐个引脚确认外接电路没有外部连接的引脚设输出低有外部上拉的引脚要单独算漏电。这个工作唠叨但是值得做。4.2 关不掉的 LED 和 USB 连接板载 LED 的问题前面提过这里再细说一次GPIO25 控制的这颗 LED 在 MicroPython 启动时并不一定默认灭很多库或者 REPL 操作会把它点亮。进入睡眠前不只是要led.off()最好在每次睡眠前都强制置低因为唤醒后的代码可能会再次把它点亮。USB 的问题更隐蔽。调试低功耗时很多人习惯性用 USB 供电顺便开个 REPL这时候测出的电流不是真实数据USB 连接本身会改变 Pico 的供电路径同时 REPL 的 CDC 串口设备在工作时会有额外功耗就算你没打印任何东西USB 主机也在一端持续轮询。我后来做功耗标定的规矩很简单电脑 USB 拔掉换独立电池供电REPL 不连一切以万用表读数说话。4.3 唤醒边沿与内部上拉的搭配唤醒引脚的选择直接影响睡眠电流。比如sleep_goto_dormant_until_edge(2, false)等待 GPIO2 下降沿如果你的外部设备是开漏输出那引脚空闲时要靠上拉维持高电平。这个上拉可以用 RP2040 内部的上拉实现但内部上拉本身有十几到几十微安的漏电流。所以在低功耗场景如果外部已经用了合适的电阻上拉内部上拉就要关掉别让两份电流重复叠加。另一个坑是边沿抖动。休眠时唤醒引脚如果接触到电磁干扰持续快速抖动会让 MCU 反复被唤醒再睡下功耗根本没降下来还会破坏业务逻辑。工业场景下最好在唤醒引脚上加一个简单的 RC 滤波比如 1k 电阻串 100nF 电容到地。唤醒后第一件事也应该是去配置中断边沿防止 GPIO 在睡眠期间已经积累的边沿事件再次触发一次无效唤醒。4.4 ADC 与 PWM 残留MicroPython 的 ADC 类没有显式的 deinit 方法但 RP2040 的 ADC 在没有发生转换时内部会自动降功耗。我遇到过的问题是某些代码在采集完电池电压后没有退出 ADC 模式导致后续进入低功耗时部分外设仍被“维持”在活跃状态。保险做法是采集完立刻把引脚恢复成普通 GPIO 并置低不要让它一直挂在 ADC 模式上。PWM 也一样。舵机信号引脚如果用 PWM 模式即使不给角度输出PWM 模块的时钟可能仍在跑。在 MicroPython 中可以直接调用pwm.deinit()把它关掉或者把引脚重新配成普通输出并置低。这个细节在休眠前很容易被忽视但它确实是电流表上那几十微安差异的来源之一。5. 实战电池供电的 Pico 周期采集 舵机动作节点5.1 系统结构与供电方案这里用一个我实际做过的例子来串起上面的 API 和方法。项目需求室外一个小闸口Pico 每 10 分钟用 DHT22 采集一次温湿度温度高于设定值时舵机带动挡板动作 30 度然后继续睡眠。整体由两节 18650 串联加 LDO 供电要求尽量少换电池。硬件结构上我做了一个关键设计传感器和舵机的电源都是可控的。传感器电源由 GPIO14 通过一个 MOSFET 控制只在采集前上电舵机电源由 GPIO13 通过另一个 MOSFET 控制只在需要动作时上电动作完成立刻断电。原因是 DHT22 在上电后需要 1 到 2 秒稳定才能正确输出数据让它一直通电会白吃电流舵机待机时虽然不转但驱动电路仍有静态损耗切断电源最干净。舵机信号线从 Pico 的 PWM 引脚引出但舵机供电不能直接用 Pico 板上的 3.3V 或 5V哪怕 SG90 这类小舵机启动瞬间的电流也远超 Pico 板上稳压器能稳稳定提供的范围。我这里用外部电池电压直接供给舵机Pico 只负责发信号两者共地即可。5.2 完整代码流程核心思路把空闲引脚全部置低传感器、舵机电源全部关闭PWM 关掉LED 关掉然后machine.lightsleep(600000)。定时唤醒后先恢复用到的硬件再执行采集和动作最后再次进入睡眠。import machine import time from machine import Pin, PWM # 引脚规划 LED Pin(25, Pin.OUT) SENSOR_PWR Pin(14, Pin.OUT) # 传感器电源 MOS SERVO_PWR Pin(13, Pin.OUT) # 舵机电源 MOS SERVO_SIG PWM(Pin(12, Pin.OUT)) # 舵机信号 SERVO_SIG.freq(50) # 50Hz周期20ms def servo_angle(pwm_inst, angle): # 20ms 周期0.5~2.5ms 对应 0~180 度这里用 1ms~2ms 实际脉宽区间 pulse_ms 1.0 (angle / 180.0) # 0度:1ms, 180度:2ms duty_u16 int((pulse_ms / 20.0) * 65535) pwm_inst.duty_u16(duty_u16) def read_dht22(): import dht d dht.DHT22(Pin(15)) d.measure() return d.temperature(), d.humidity() def adc_voltage(pin_no): import machine adc machine.ADC(pin_no) v adc.read_u16() * 3.3 / 65535 return v def prepare_sleep(): # 关闭 PWM避免输出占空比维持 SERVO_SIG.deinit() # 关闭舵机和传感器电源 SERVO_PWR.value(0) SENSOR_PWR.value(0) # 板载 LED 关闭 LED.value(0) # 统计所有未用引脚统一设为输出低 unused [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 26, 27, 28] for gp in unused: try: Pin(gp, Pin.OUT, value0) except Exception: pass # 恢复 PWM 为下一次动作准备 SERVO_SIG PWM(Pin(12, Pin.OUT)) SERVO_SIG.freq(50) def main_cycle(): # 打开传感器电源等 DHT22 稳定 SENSOR_PWR.value(1) time.sleep_ms(1500) temp, hum read_dht22() SENSOR_PWR.value(0) # 温度超过 30 度执行动作 if temp is not None and temp 30: SERVO_PWR.value(1) time.sleep_ms(200) # 等舵机上电稳定 servo_angle(SERVO_SIG, 90) time.sleep_ms(500) # 让舵机走到位 SERVO_PWR.value(0) # 引脚重新配置为输出低防止悬空 Pin(13, Pin.OUT, value0) LED.value(0) while True: main_cycle() prepare_sleep() machine.lightsleep(600000)注意代码里的prepare_sleep()PWMdeinit之后又把引脚重新初始化为 PWM是为了下一次动作时还能发信号。每次deinit与重新初始化之间信号的引脚状态会短暂变化如果你对时序有严格需求可以把这个过程再细化。5.3 功耗测算与电池寿命我把这套系统实际跑了一周用万用表分别测了各阶段的电流然后用积分估算平均功耗。数据大致是这样阶段持续时间电流睡眠阶段597 秒0.5mA传感器上电采集3 秒20mA舵机动作0.5 秒200mA每 10 分钟一个周期电量消耗粗算睡眠597 秒 × 0.5mA 298.5 mAs采集3 秒 × 20mA 60 mAs动作0.5 秒 × 200mA 100 mAs单周期合计约 458.5 mAs换算约 0.127 mAh每小时 6 个周期一天约 18.3mAh。5000mAh 的电池组理论续航约 273 天算上 LDO 自身损耗和电池自放电实际半年以上没什么问题。对比最开始那个 20mA 空转的版本等于直接把设备寿命从 10 天拉长了 20 倍。6. 软件低功耗控制的边界时钟恢复、外部 RTC 与调试期误导6.1 从 dormant 醒来后时钟树一定要重建sleep 库的 dormant 流程里sleep_run_from_xosc()和sleep_goto_dormant_until_edge()要配合使用但醒来之后做的事同样关键。系统从 dormant 恢复时PLL 并没有自动回到原来的状态如果你唤醒后马上跑 UART 串口或者 ADC 转换可能发现数据错乱或者外设不反应。这就是因为外设时钟频率跟代码预期不一致。稳妥的做法是唤醒后统一执行一次时钟恢复函数把 PLL 重新配置到期望频率再初始化依赖时钟的外设。如果是 MicroPython 的 lightsleep这套事解释器已经替我们做了所以大部分用户感觉不到但下沉到 C SDK 后这个责任就回到自己身上。6.2 小时级唤醒外部 RTC 定时中断RP2040 的 dormant 模式功耗低但坏消息是它只能靠 GPIO 唤醒内部定时器/RTC 在 dormant 下是不工作的。如果你希望设备每小时醒来一次且不能接受让定时器一直跑带来的额外电流最省电的方案是外挂一颗 I2C 实时时钟芯片比如 DS3231 或者 PCF8523。它的闹钟输出接到 Pico 的唤醒引脚到点后拉一个边沿把 MCU 从 dormant 叫醒。我在低功耗面板项目里用过 PCF8523整板待机电流能控在 0.3mA 以内其中不少是 Pico 板载电源转换在消耗。外接 RTC 本身在保持闹钟工作时只有微安级电流比用 RP2040 内部定时器在 lightsleep 里跑 1mA 的功耗经济得多。前提是唤醒逻辑要走到 C SDK 的 dormant 路线MicroPython 里要同时控制 I2C 和 GPIO 中断也能实现只是要注意 I2C 外设在睡眠期间必须释放总线。 至于是否需要取决于你的目标续航如果只做夜间采集lightsleep 足以如果要求几个月换一次电池还是上外部 RTC 更稳妥。6.3 低功耗调试时最容易误导自己的三件事第一件是 USB 供电测出来的电流不能信。USB 口一插板内电源路径就变了线性稳压器的输入侧会引入 USB 接口的额外偏置这时候测出的数值既不是真实电池电流也不是模式真实功耗。我后来养成的习惯是测功耗一定用独立电池万用表串在电池正极并且断开所有 USB 线。第二件是 REPL 和串口打印很可能会阻止芯片睡下去。MicroPython 的 USB CDC 在 lightsleep 时会尽量保持连接但这不代表它不吃电。调试低功耗时如果开着 REPL你看到的电流可能比真实高 0.3~0.5mA。更准确的方法是在程序里临时去掉所有print()退出 REPL再测一版。第三件是面包板和杜邦线在潮湿天气会变成一个微安级“漏电器”。我有一次在同一个面包板项目上怎么测都比预期高 0.1mA后来换成焊好的洞洞板数值立刻下来了。低到微安级的测试最好把关键电路直接焊接固定不要靠面包板走线。最后分享一个我自己的习惯每次改完低功耗配置我会连续记录三组电流值并且把进入睡眠前后的引脚状态表打印出来核对。因为低功耗的坑常常不是某一个 API 没调用而是某个引脚漏电、某块外设没关、某条线悬空三个小问题叠加出一个大电流。逐项排查看起来很慢但这也是做低功耗最可靠的方法。