尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树莓派 Pico 低功耗实战:从 light sleep 到 dormant 的功耗压榨指南

树莓派 Pico 低功耗实战:从 light sleep 到 dormant 的功耗压榨指南 低功耗这三个字放在树莓派 Pico 上很多人的第一反应往往是——“一个开发板而已省那点电有什么意义”这种想法我太熟悉了直到我自己做了一个电池供电的田间环境采集节点在户外跑了快两个月才明白低功耗软件控制这件事的含金量到底在哪里。树莓派 Pico 用的 RP2040 芯片本身并不是以低功耗见长的 MCU但正因为这样它的功耗优化空间反而更值得挖同一个项目如果只是“能跑”整机休眠电流可能在 20mA 以上如果把 MicroPython 或 C SDK 提供的电源管理 API 吃透配合合理的外设处理和供电方案可以把待机电流压到 1mA 以内甚至在 dormant 模式下做到 0.2mA 左右的水平。这个差距决定了设备是“一周一充”还是“半年不管”。这篇文章我会直接切入正题分两条线讲一条是 MicroPython 路线适合快速验证和中小规模项目另一条是 C SDK 路线适合把潜伏电流往极致压。重点不是贴一堆 API 文档而是说清楚每个 API 背后到底做了什么、为什么这么做、在真实板子上会踩哪些坑。包括 GPIO 唤醒、定时唤醒、外设关闭、供电方案、电流测量方法、常见故障排查都会给到可直接抄作业的参考实现。1. 低功耗软件控制到底在控制什么1.1 搞清楚“低功耗”的对象RP2040 的功耗模型很多人把低功耗设计理解成“程序里调用一个睡眠函数就完事了”这是最常见的误区。真实情况是睡眠函数只解决 MCU 核心的功耗问题整个板子的功耗还包括外设、电源转换电路、LED、电平转换芯片、USB 接口等一大堆东西。所以在写代码之前先要清楚 RP2040 本身到底有哪几种功耗状态。RP2040 的电源域可以粗略分为四个层级运行模式RunCPU 全速或降频运行所有外设可工作电流通常在 20mA 以上板载 LED、USB、外设另算。Sleep 模式CPU 时钟停止但外设时钟和系统时钟仍保持RAM 内容不丢失唤醒速度最快。MicroPython 里对应machine.lightsleep()C SDK 里对应sleep_run()到sleep_goto_sleep()。Dormant 模式几乎所有时钟都停掉包括系统 PLL 和大多数外设时钟只保留少数唤醒源GPIO 边沿、RTC 等。这是 RP2040 真正意义上的深度休眠MicroPython 里对应machine.deepsleep()C SDK 里对应sleep_goto_dormant_until_edge()或sleep_goto_dormant_until_rtc()。外部断电把板子的供电直接切掉功耗为零但 RAM 内容丢失唤醒后是全冷启动。这里关键要理解的是睡眠不是“程序暂停”而是“时钟树裁剪”。睡眠层级越深被关掉的时钟就越多唤醒后重新启动这些时钟的时间就越长。所以低功耗设计本质上是在“功耗”和“响应速度”之间做权衡不是一味追求最深的睡眠模式。我在实际项目里的经验是如果业务逻辑每次唤醒只需要做“读传感器、存数据、发消息”这几件事对唤醒延迟不敏感那就优先考虑 dormant 级别的深度睡眠如果业务逻辑需要快速响应外部事件比如按键、中断信号那就选择 sleep 模式同时把不需要的时钟主动关掉。1.2 为什么选择软件控制为主少谈硬件改动低功耗项目的常见土办法是“硬件改板子”去掉 LDO、拆掉电源 LED、外接低功耗 DC-DC 模块。这些做法当然有效但不是每个项目都有改板的成本和时间。而且软件层面的优化往往被低估——在同一个硬件板子上仅仅通过合理调用电源管理 API把休眠电流从十几毫安降到一两毫安是完全可能的。软件控制的核心思路可以总结成一句话把不需要的东西全部停掉在需要的时候以最快速度恢复。这里的“不需要”包括用不到的定时器、ADC、PWM、I2C、SPI、UART 外设以及板载的电源 LED 和 USB 连接器状态。MicroPython 和 C SDK 都提供了对应的 API 来管理这些资源差别只是控制的粒度。2. 读懂 APIPico 的电源管理接口体系2.1 MicroPython 下的核心 APImachine 模块MicroPython 是树莓派 Pico 最常用的开发方式对新手特别友好。它把电源管理接口封装在machine模块里核心就三个函数machine.freq()设置 CPU 主频可传参数比如machine.freq(50000000)把频率降到 50MHz。降频能显著降低运行电流适合对算力要求不高的采集任务。machine.lightsleep(ms)进入浅睡可设置唤醒时间毫秒也可以不传参由 GPIO 中断唤醒。RAM 保留唤醒后代码从调用点继续执行。machine.deepsleep(ms)进入深度休眠RAM 保留唤醒后代码行为与lightsleep类似在 MicroPython 实现中RP2040 的 deepsleep 只是 dormant 模式的封装。如果传了时间参数会用内置的定时器唤醒。这三个函数看起来简单但使用上有几个关键区别必须要知道。第一lightsleep会保留外设的时钟所以唤醒后 UART、I2C 这些外设还能继续工作deepsleep会停掉大部分外设时钟唤醒后可能需要重新初始化外设。第二lightsleep期间电流大约在 1~5mA 之间取决于引脚状态和外设deepsleep期间电流可以到 0.2~0.4mA但唤醒时间更长。第三deepsleep之后MicroPython 解释器不是从头执行的而是从调用deepsleep那一行的下一行继续这一点和很多人的直觉不一样写代码时要小心。另一个要留意的是machine.Pin的中断唤醒配置。无论是lightsleep还是deepsleep只要在进入睡眠前给某个引脚配置了Pin.IRQ_RISING或Pin.IRQ_FALLING中断睡眠中该引脚出现对应边沿时就会唤醒。这是实现“事件驱动”低功耗的关键手段。2.2 C SDK 下的低功耗接口与映射关系如果要用 C 开发RP2040 的官方 C SDK 也提供了一套电源管理 API集中在pico/stdlib.h的sleep.h和hardware/rtc.h里。常用的有sleep_run()把 CPU 切到低功耗运行状态适合不需要深度休眠的场景。sleep_goto_sleep()进入 sleep 模式需要先配置唤醒源sleep_source_enable。sleep_goto_dormant_until_edge()进入 dormant 模式由 GPIO 边沿唤醒需要指定引脚和边沿类型。sleep_goto_dormant_until_rtc()进入 dormant 模式由内部 RTC 在指定时间唤醒。rosc_、clocks_系列函数手动管理时钟源比如停掉 PLL、切换到内部振荡器。C SDK 的粒度比 MicroPython 细很多。比如你可以只停掉某个外设的时钟而不是整个芯片进入睡眠。但这种粒度也是双刃剑配置错误会导致系统卡死、无法唤醒所以一般建议按照官方示例来不要随意混用。MicroPython 的deepsleep在底层就是对 C SDK 的sleep_goto_dormant_until_edge或sleep_goto_dormant_until_rtc的封装。理解这层映射关系后很多“我明明调了 deepsleep 为什么电流还是很高”的问题就容易排查了——很可能是某个外设没有被正确关闭或者 GPIO 引脚处于浮动状态导致漏电。2.3 外设电源管理什么在偷偷耗电进入睡眠模式不等于外设自动关闭。这是一个我反复强调的点睡眠只是停了 CPU 的时钟外设如果不主动关闭依然会耗电。常见的“偷电大户”按影响从大到小排序外设/组件未关闭时的影响关闭方式板载电源 LED2~5mA 甚至更高拆电阻或用 GPIO 控制 LED 供电ADC 模块0.5~1mA调用machine.ADC(0).deinit()或设置供电关断PWM 输出未接负载引脚电平不确定导致漏电设置为输入模式或固定输出低电平I2C/SPI 上拉电阻数百 uA关闭外设后把 SCL/SDA 引脚设为输入UART 空闲电平引脚电平未定义配置为确定的 GPIO 状态DC-DC/LDO 静态功耗1~5mA取决于方案硬件选低静态电流的 LDOMicroPython 下关闭外设的常用方式是把引脚重新初始化。举个例子如果一个引脚之前用作 PWM 输出进入睡眠前把它重新配置成输入模式并且不启用内部上拉就能避免这个引脚带来的不确定漏电。C SDK 下的做法更直接进入休眠前调用adc_set_temperature_enable(false)、i2c_deinit()、spi_deinit()等函数即可。3. 从 API 到实战一个电池供电温度采集节点的完整实现3.1 整体方案与硬件接线这部分我拿自己做过的项目来举例一个电池供电的户外温度采集节点每 30 分钟起床一次读温度传感器的值通过 Semtech 射频模块发出去然后继续睡觉。硬件清单树莓派 Pico第一代RP2040DS18B20 温度传感器单总线一条 GPIO 即可一个 3.7V 锂电池 低静态电流 LDO我用的 TPS7A0230静态电流约 1uA一块小太阳能板做浮充可选万用表或者 INA219 模块测电流接线很简单传感器数据线接 GP15VCC 和 GND 分别接 LDO 输出和地。GP15 需要接一个 4.7k 上拉电阻到 3.3V这是 DS18B20 单总线协议的要求。在写代码之前有件事很重要把板载 LED 的限流电阻去掉。Pico 的原理图里 LED 是通过一个电阻接在 GP25 上的直接接 3.3V 和地。如果不拆这个电阻即使你不点亮 LED因为 LED 负极已经接地、正极通过电阻接到 3.3V它不会亮但如果你初始化了 GP25 为输出高就会白耗电更常见的情况是如果 GP25 悬空状态不确定也可能产生漏电。稳妥的办法是把那个电阻焊下来或者找一块没有板载 LED 的兼容板。3.2 代码实现一定时唤醒的 lightsleep 版本先给一个最基础的版本用machine.lightsleep实现定时唤醒。import machine import utime import ubinascii import onewire import ds18x20 # 定义传感器引脚 SENSOR_PIN machine.Pin(15) SENSOR ds18x20.DS18X20(onewire.OneWire(SENSOR_PIN)) # 读取温度 def read_temp(): roms SENSOR.scan() if not roms: return None SENSOR.convert_temp() utime.sleep_ms(750) return SENSOR.read_temp(roms[0]) # 发送数据这里用伪代码替代 def send_data(temp): # 在这一步把数据通过无线模块发出去 # 注意发送时功耗很高尽量压缩发送时间 print(send:, temp) # 主循环 while True: # 唤醒后先读取传感器 temp read_temp() if temp is not None: send_data(temp) # 进入浅睡30分钟后唤醒 machine.lightsleep(30 * 60 * 1000)这段代码能跑但电流表现一般。为什么因为lightsleep保留的时钟和外设太多实际测量可能是 1~3mA。对于应用场景要求不高的项目够了但如果要把功耗往死里压就不合适。另外有个坑machine.lightsleep(30000)在 MicroPython 里是毫秒单位注意别写成秒。我曾经在这上面翻过车唤醒间隔直接短了 1000 倍。3.3 代码实现二deepsleep GPIO 唤醒的事件驱动版本如果业务是“有人按键或者传感器触发时才工作”那用事件驱动更合适。下面这段是事件驱动 深度睡眠的典型写法import machine import utime # 唤醒引脚GP0配置为上拉输入下降沿触发 WAKE_PIN machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP) def wake_handler(pin): # 中断回调里只设置标志不在中断里做耗时操作 global wake_flag wake_flag True wake_flag False WAKE_PIN.irq(triggermachine.Pin.IRQ_FALLING, handlerwake_handler) # 关闭不用的外设 adc_sample machine.ADC(0) adc_sample.deinit() # 把其他 GPIO 全部设为输入避免悬空漏电 for i in range(1, 30): if i 15 or i 0: # 跳过传感器和唤醒引脚 continue try: p machine.Pin(i, machine.Pin.IN) p.irq(handlerNone) except Exception: pass # 主流程首次上电时 while True: if wake_flag: wake_flag False # 读取传感器、处理业务 # ... else: # 进入深度睡眠等待 GPIO 边沿唤醒 machine.deepsleep()这段代码的关键点在于中断回调只置标志不放进业务逻辑避免中断上下文里执行耗时操作导致不可预期的行为。睡眠前把所有 GPIO 都设置为输入模式清掉中断避免悬空引脚漏电。这个操作很基础但对电流影响很大。deepsleep()不传时间参数就变成“永久睡眠”只有 GPIO 边沿或复位能唤醒。3.4 实测电流数据与优化前后对比我自己在实际项目里测过几组数据给大家参考模式实测电流说明默认运行CPU 125MHz25~28mA不加任何外设降频到 50MHz 运行17~20mAmachine.freq(50000000)light sleep未优化外设2~5mA外设时钟还在跑light sleep外设全关1~2mA主要剩余是 LDO 静态电流deepsleep外设全关 LDO 低静态电流0.3~0.8mA目标状态理论 dormant 最低约 0.18mA需要 C SDK 精细控制这里特别提醒测量电流时要把万用表串进电源回路里不是浮空接在某个引脚上。很多人接法不对测出来数据完全没参考价值。更稳的办法是用 INA219 这种电流传感器模块挂在主控的 I2C 上代码里定期读数这样还能记录睡眠和唤醒两个状态各自的电流。4. C SDK 路线把休眠压到更低的几种写法4.1 sleep_run 与 sleep_goto_sleep 简单休眠如果项目对实时性要求高、需要频繁唤醒处理数据用 C SDK 的sleep_goto_sleep会更合适。#include pico/stdlib.h #include hardware/clocks.h #include pico/sleep.h int main() { // 初始化必要外设 stdio_init_all(); // 设置唤醒源GPIO0 上升沿 sleep_source_enable(DORMANT_SOURCE_GPIO0); gpio_set_irq_enabled_with_callback(0, GPIO_IRQ_EDGE_RISE, true, gpio_callback); while (true) { // 处理业务 do_something(); // 进入浅睡 sleep_goto_sleep(); } }这个模式唤醒很快因为大部分时钟没停。但电流相对高适合“频繁唤醒 业务实时性强”的场景。如果你想要更低的睡眠电流就得切到 dormant 模式。4.2 dormant 模式与时钟停振注意事项dormant 模式是 RP2040 能提供的真正深度休眠它会停止大部分时钟源包括系统主时钟。唤醒后再恢复时钟。// 进入 dormant 模式等待 GPIO 上升沿 sleep_goto_dormant_until_edge(0, GPIO_IRQ_EDGE_RISE);代码只有一行但背后有几个坑dormant 模式唤醒后USB 等依赖 PLL 的外设可能失效。如果你用 USB 串口调试唤醒后大概率丢了连接需要重新初始化 USB 配置。时钟恢复需要时间。唤醒瞬间系统时钟是从慢速启动的如果马上执行高速 SPI 操作可能出错。稳妥做法是唤醒后调用clocks_init()之类重新配置时钟。不要在 dormant 模式下依赖定时器——除非用 RTC。没有外部 RTC 模块的话RP2040 内部没有真正的掉电保持 RTCdormant 模式的定时唤醒只能靠板上的 RTC 外设需要跑在低功耗时钟源上。4.3 复位与 RTC 唤醒组合方案MicroPython 的deepsleep其实还有一个隐藏特性某些场景下唤醒后会自动复位重新执行脚本。如果你用 C SDK可以自己实现这种“RTC 定时唤醒 冷启动”的组合。方案是在休眠前把断电/重启后的恢复数据存到 Flash 或 RAM retention 区域设置 RTC 唤醒时间然后进入 dormant。唤醒后从main()重新跑但通过检查启动原因判断是“首次上电”还是“RTC 唤醒”从而跳过初始化步骤直接进业务逻辑。这种方案的优点是代码路径清晰、鲁棒性高缺点是每次唤醒都是冷启动中间状态要自己管理。对于“每分钟采集一次、每次采集 2 秒”的应用来说冷启动多出来的时间完全可以接受。5. 注意事项与避坑实录5.1 测量工具与测量方法低功耗优化没有电流数据支撑就是瞎忙活。工具选择上优先推荐UT61E 等带 uA 档的万用表测睡眠电流够用但测瞬态电流比如 RF 发射瞬间不靠谱。INA219 / INA226 模块能通过 I2C 连续采样记录电流曲线适合观察“睡眠—唤醒—业务—再睡眠”的完整周期。示波器 电流探头如果预算够看瞬态电流最直观。测量接线有个很常见的错误把万用表接在地和 GND 之间或者接在某个 GPIO 上。正确的是把万用表串接到电源正极和板子 VIN 之间。测完运行电流后再测睡眠电流两个状态都要有记录。5.2 USB 接口、板载 LED、ADC 的“隐形漏电”板载 LED、USB 连接器、ADC 这三个是最常见的漏电源头。Pico 的板载 LED 正极通过电阻接在 GP25 上负极接地。很多代码第一步就是Pin(25, Pin.OUT)如果忘了设成低电平GP25 输出高就会点亮 LED这可是几毫安的电流。更隐蔽的是 MicroPython 的 ADC——你只要调用过一次machine.ADC()没有 deinit它就可能一直在采样白白耗电。所以睡眠前务必逐个 deinit。USB 连接器在电池供电项目里基本是废的但它接在板子上如果不把板子的 VBUS 检测脚处理掉可能产生持续的漏电。最省事的方式是直接用剪刀或者烙铁把 USB 座子拆掉或者用不带 USB 的兼容板。5.3 引脚悬空与上拉/下拉配置悬空的 GPIO 引脚在睡眠中是“薛定谔电平”可能是有规律的漏电。睡觉前把所有不用引脚统一配置成输入模式不要启用内部上拉/下拉是最省电的做法。因为内部上拉/下拉电阻本身也是一个分流路径每个引脚约几十 kΩ 到上百 kΩ累积起来在电池供电场景下不可忽视。唤醒按键这类外部输入建议在硬件上接一个 100kΩ 左右的外部上拉电阻到 VCC然后配置 GPIO 为输入模式用下降沿触发。这样睡眠时引脚电平有确定值且无内部电阻漏电外部上拉带来的额外功耗也微乎其微。5.4 供电方案LDO 与 DC-DC 的选择低功耗设备供电LDO 和 DC-DC 的选择直接决定底噪和转换效率。LDO低压差线性稳压器优点是纹波小、静态电流低、外围简单缺点是电池电压越高转换效率越低。比如 3.7V 锂电池经 LDO 降到 3.3V效率大概 89%不算差。选型时看静态地电流quiescent current常见低静态 LDO 有 TPS7A02、MCP1700 等静态电流在 1~2uA 量级。DC-DC 开关电源效率高但静态电流通常比 LDO 高轻载时可能 10~50uA而且纹波大对 ADC 采样精度有影响PCB 布局也更讲究。小电流应用整体方案平均电流小于 5mA优先选低静态 LDO这是我在多个项目里的实用结论。如果你需要“待机几个月”这种极端场景DC-DC 的低效率区间反而可能让平均功耗劣化。6. 常见问题速查与排查思路6.1 问题速查表现象可能原因排查方式睡眠后电流依然有 10mA板载 LED 被点亮、USB 座子未处理、GPIO 悬空逐项关掉外设看电流变化醒来后 UART 没有输出light sleep 保留外设时钟但 C SDK dormant 唤醒后未重新初始化唤醒后重新调用stdio_init_all()GPIO 中断唤不醒中断触发方式不对或引脚配置成内部上拉了检查sleep_source_enable和边沿设置deepsleep 后程序从头执行MicroPython 行为可能需要硬复位检查代码里是否用了machine.DEEPSLEEP_RESET测量到的待机电流不稳定万用表接触不良、电池电压波动用 INA219 连续记录曲线看波形唤醒后传感器读不到数据传感器供电未受控睡眠中传感器一直在运行用 GPIO 控制传感器电源工作后才上电外设时钟没关闭导致电流高某些外设初始化后没有 deinit睡眠前把所有外设对象 deinit 或引脚重新设成输入6.2 一个典型的排查案例我遇到过最典型的一个问题deepsleep 后电流怎么测都是 2.5mA怎么也降不下来。排查过程供大家参考。先怀疑 LDO 静态电流过大把 Pico 拔下来只测电源板电流是 3uA排除。又怀疑传感器把传感器拔掉电流没变。最后怀疑 MicroPython 环境写了个最小测试程序只配置唤醒引脚、关闭所有外设、进入 deepsleep结果还是 2.5mA。最后发现是万用表测法的问题。我最初把万用表串在电池负极和系统 GND 之间但 Pico 上电后 GND 和电池负极之间有一个电源管理芯片的旁路导致表笔中间形成了寄生回路。把表挪到电池正极和主板 VIN 之间读数是 0.4mA。数据一差距思路就通了。排查这类问题的通用方法最小化测试把代码砍到只剩“进入睡眠”一个功能确认底数。模块化排除外设逐个启用每启用一个就测一次电流定位差异来源。检查测量方式确认万用表接法正确排除测量误差再谈优化。最后分享一个我觉得特别实用的习惯在做低功耗项目时把“测量设备”也纳入整个系统的设计里。比如在 PCB 上留一个专门的电流测试焊盘或者预留 INA219 的 I2C 接口这样后续调试不用每次拿镊子戳触点省去大量无效时间和情绪损耗。软件控制的 API 固然重要但从硬件上把测量和调试路径设计好才是长期项目能持续优化的基础。
返回列表