ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

树莓派Pico低功耗API实战:从lightsleep到dormant

树莓派Pico低功耗API实战:从lightsleep到dormant 1. 项目概述1.1 核心需求解析树莓派 Pico 这块板子很多人第一反应是“便宜”“能玩 CircuitPython”“GPIO 多”但真正把它丢进电池供电的物联网场景就会发现一个绕不开的坎——功耗。我在做一批低功耗传感器节点时初始版本直接用time.sleep()死等结果两节 AA 电池撑了不到一周就彻底没电。后来认真啃了 RP2040 的数据手册和 Pico 的 SDK 文档把官方提供的低功耗 API 一个一个试了一遍才把待机电流从毫安级压到微安级。这篇文章想说的不是“Pico 能低功耗”这种口号而是把sleep、lightsleep、dormant这些 API 真正的行为、参数、坑以及它们在 MicroPython 和 C SDK 里的不同表现全部摊开。标题里说的“API”在 Pico 生态里有两层含义一层是 RP2040 芯片外设寄存器层面的硬件接口另一层是官方 SDK不管是 C 还是 MicroPython暴露出来的软件接口。如果你打算做一个电池供电的传感器节点、遥控开关或者可穿戴小设备这篇文章应该能帮你少走不少弯路。适合读这篇文章的人我默认你已经有了一块 Pico 板子能点亮 LED能跑通blink示例。如果你还没碰过 Pico建议先去看一下官方文档里的 Getting Started把开发环境搭好再回来。接下来我会按照“为什么低功耗是个软件问题 → 芯片提供了哪些低功耗手段 → 每种 API 怎么用 → 实测数据长什么样 → 实际项目怎么整合”这条线索来展开。1.2 影响力与应用场景树莓派 Pico 用的是 RP2040 芯片双核 Arm Cortex-M0主频最高 133MHz这芯片本身的定位就是低成本、低功耗微控制器而不是跑 Linux 的完整计算机。所以低功耗这件事不是“附加功能”而是它设计时就该有的能力。只是很多人把它当“迷你电脑”用忽略了这一层。从实际应用来看低功耗 Pico 最常见的几个场景包括室内环境监测节点温湿度、空气质量、农业大棚的无线采集终端、电池供电的智能家居控制器、甚至一些可穿戴原型。这些场景的共性要求是设备大部分时间不需要干活只需要每隔几十秒或者几分钟醒来一次采样、发送、再睡回去。设备能撑多久直接决定了产品的可用性和维护成本。用软件把功耗压下去是这类项目从“玩具”变成“能部署的原型”的关键一步。2. 整体设计与方案选型2.1 为什么说低功耗首先是个软件问题我刚接触低功耗设计时很容易陷入一个误区觉得功耗高就去换 DC-DC、换电池、换低功耗传感器硬件堆料就行。后来被现实教育了几次才明白绝大多数场景下板子上最大的耗电元凶其实是 MCU 本身而 MCU 的功耗完全由软件怎么配置寄存器、怎么管理时钟和外设来决定。举个例子同样是 Pico主动运行模式下RP2040 在 48MHz 时电流大约是 15mA 左右如果所有外设全开、跑 133MHz轻轻松松到 30mA 以上。而进入dormant模式后电流可以掉到 3.5mA 左右Pico 板载的电源 LED 和 LDO 静态电流占了大部分如果进一步优化板级使用 Pico 的低功耗版本或者自己做最小系统板电流甚至可以压到几十微安。这在硬件上没有任何改动纯粹靠软件切换。所以低功耗项目的软件设计核心就是尽可能快地把 MCU 推进低功耗状态只在需要干活的时间窗口内醒来。这就需要我们理解芯片提供的电源状态以及对应 API 的行为差异。我见过很多项目用time.sleep(60)来做定时采集这在 Pico 的 MicroPython 里其实只是让 CPU 在sleep模式下等待外设和时钟树没有关掉电流根本降不下来。这个坑我后面会详细拆。2.2 方案选型MicroPython 还是 C SDK做低功耗项目第一个绕不开的选择就是用 MicroPython 还是用 C SDK。我在两个方向都做过这里直接说结论如果项目目标是快速验证或原型迭代MicroPython 足够如果追求极致的功耗控制和稳定的时序C SDK 是正路。MicroPython 的machine.lightsleep()接口封装了 RP2040 的lightsleep模式用起来极其方便几行代码就能实现定时唤醒而且外部中断唤醒比如按键、传感器报警信号也支持。对于大多数低功耗传感器节点MicroPython 的lightsleep已经能满足需求。根据官方文档和实际测试MicroPython 的lightsleep下电流大约可以降到 14mA 左右和主动模式动辄几十毫安相比已经非常可观。但是如果你要做那种一次充电撑一年的产品MicroPython 的便利就成了瓶颈——它没法直接使用 RP2040 的dormant模式而dormant才是真正把电流压到几百微安以下的核心手段。C SDK 里用sleep_run_from_dormant_source()配合rosc或者xosc作为唤醒源加上 RTC 定时唤醒才能实现真正的“深睡”。所以我的建议是先看完本文后面 API 对比再决定自己需要做到哪一级功耗然后选对应方案。两个方向我都会给出可以复用的代码。2.3 功耗预算的工程思维低功耗不是盲目的把电流压到最低就完事而是要在“功能需求”和“功耗”之间做权衡。我在设计节点时习惯先画一张功耗预算表把不同工作阶段的时间、电流、频率算清楚然后再决定用哪个低功耗模式。假设一个典型的温度采集节点每 60 秒醒来一次采集温度 通过射频模块发送数据共耗时约 200ms期间平均电流 60mA其余时间处于睡眠态。如果睡眠电流是 14mAMicroPython 的 lightsleep那平均电流大约是[ \frac{0.2s \times 60mA 59.8s \times 14mA}{60s} ≈ 14.15mA ]这意味着如果用一节 2000mAh 的锂电池理论续航只有 140 小时左右也就是不到 6 天。可如果睡眠电流压到 100uAC SDK 的 dormant平均电流大约是[ \frac{0.2s \times 60mA 59.8s \times 0.1mA}{60s} ≈ 0.3mA ]同样的电池理论续航就变成了 6000 小时以上也就是 250 天。这个计算很粗糙但足以说明问题——睡眠功耗对整体续航的决定性有多大。后面所有 API 的讨论本质上都是在回答一个问题怎么把这个睡眠电流从小数点后两位的毫安级继续压到小数点后两位的微安级。3. RP2040 低功耗原理深度拆解3.1 芯片的电源模型与时钟拓扑RP2040 的数据手册里有一张电源域示意图看起来抽象拆开理解其实就三条线VDD_CORE核心数字逻辑电源、VDD_IOIO 电源和VDD_ADCADC 模拟电源。MCU 内部大部分逻辑CPU 内核、SRAM、外设总线都由核心电源供电IO 和模拟部分独立供电。低功耗模式的本质就是软件控制 PLL、时钟源、外设时钟门控和核心电源请求把不再需要的部分停下来。RP2040 内部有两个可用的振荡器一个是 12MHz 的晶振XOSC一个是芯片内部自带的环形振荡器ROSC。正常运行时系统时钟一般通过 PLL 从 XOSC 或者 ROSC 倍频出来。而进入低功耗后PLL 会被关掉XOSC 也可以关掉系统时钟要么停摆要么降到很低的频率。这里有个关键点唤醒源和时钟必须配套。如果休眠时把 XOSC 关了那么 XOSC 就不能作为唤醒源必须用 ROSC 或者 RTC内部实时时钟基于低速时钟来定时唤醒。这就直接决定了你在代码里该怎么配置。3.2 三种低功耗状态的本质区别先给没有啃过数据手册的朋友一个直观对比表。RP2040 手册里真正定义了三种电源状态sleep、lightsleep、dormant。注意这不是寄存器里的一个枚举值而是不同配置组合后表现出的系统状态。状态CPU 状态时钟源唤醒源典型电流板级适用场景sleep停止系统时钟仍在跑等待事件任意中断约 15-20mA等待短时间 I/O 操作lightsleep停止系统时钟停XOSC/ROSC 可按需保留定时器、GPIO、RTC约 14mAMicroPython 下中等时长低功耗等待dormant停止几乎全停仅保留唤醒源所需时钟RTC、GPIO、定时器约 80-180uA视唤醒源配置长时间深度睡眠这张表里的电流是基于官方数据手册和我的实测结果。注意测量条件是核心频率、供电电压、外设开闭情况不同数字会有浮动但量级差距应该是一致的。一个容易忽略的细节lightsleep里 XOSC 可以开启也可以关闭。如果保持 XOSC 开启唤醒后不需要重新稳定晶振唤醒速度快但功耗更高如果关闭 XOSC功耗更低但下次唤醒需要等晶振起振稳定典型几百微秒到 1ms而且只能用 ROSC 或者 RTC 做唤醒源。这个“功耗 - 唤醒时间”的取舍是设计中真正需要拍板的地方。3.3 从寄存器层级理解 API 封装如果你只用 MicroPython 的machine.lightsleep(),可能完全感受不到 API 背后的寄存器操作。我把它简单拆一下方便心里有底。lightsleep模式在 RP2040 里对应的核心操作是关掉 CPU 时钟等待事件指令wfe或者等待中断指令wfi、关闭不需要的外设时钟、关闭 PLL、根据配置保留 XOSC/ROSC。这些操作分布在 CLOCKS 和 PLL 寄存器组里。C SDK 暴露的典型调用路径是sleep_run_from_xosc()让系统进入休眠保留 XOSC 做唤醒源返回时系统时钟从 XOSC 重新启动。sleep_run_from_rosc()让系统进入休眠保留 ROSC 做唤醒源返回时系统时钟从 ROSC 重新启动。sleep_run_from_dormant_source(clock)这是更深层的休眠clock参数指定哪个时钟源负责唤醒CLOCKS_CLK_SLEEP_TIMER的源常用于 RTC 定时唤醒。rosc_force_low_power()在 dormant 前强制 ROSC 进入低功耗模式进一步省电。rtc_enable_alarm()/rtc_set_alarm()设置 RTC 闹钟作为 dormant 模式下的唤醒时机控制。而在 MicroPython 里machine.lightsleep()的调用本质上是帮你选了“保 XOSC”或者“自动管理时钟”的路径把外界细节屏蔽了。所以如果你看到有人拿 MicroPython 跑出 14mA 的“低功耗”别惊讶这个数字在 C SDK 的 dormant 模式下是会把人急死的——但两边其实没有可比性因为用的根本不是一个状态。4. MicroPython 低功耗 API 实战4.1 API 概览与基本信息MicroPython 在 RP2040 端口提供了一套统一的低功耗接口核心就两个machine.lightsleep([time_ms])machine.sleep([time_ms])这两者的区别在于sleep()在 RP2040 上实际表现就是一个忙等待延迟的“轻量级”版本CPU 可能还在运行功耗不会显著下降而lightsleep()才真正让系统进入低功耗状态。另外还有machine.deepsleep()但 RP2040 端口目前并不真正支持 RAM 保持的深度睡眠它更多是个占位接口使用时需要注意。lightsleep()如果传入时间参数就会在指定时间后自动唤醒如果不传参数就会一直睡下去直到某个中断GPIO 边沿触发、RTC 闹钟等唤醒它。这一点特别适合做“按需唤醒”的传感器节点。4.2 定时唤醒完整示例先给一个最简单的定时唤醒示例这个代码是我实际测试过的可以直接跑在 Pico 上import machine from machine import Pin, RTC import time # 配置板载 LED led Pin(25, Pin.OUT) # 初始化 RTCRP2040 内部 RTC不需要外部晶振 rtc RTC() def set_rtc_alarm(seconds): # 获取当前时间并计算目标唤醒时间 now rtc.datetime() # 在 now 基础上加 seconds 秒 # rtc.datetime() 返回 (year, month, day, weekday, hours, minutes, seconds, subseconds) # 这个计算用 time.mktime 转时间戳更简单 import time current_ts time.mktime((now[0], now[1], now[2], now[4], now[5], now[6], 0, 0, -1)) target_ts current_ts seconds # 转回 datetime 元组 dt time.localtime(target_ts) rtc.alarm_time(dt[0], dt[1], dt[2], dt[6], dt[3], dt[4], dt[5]) rtc.alarm_enable() # 启动 RTC rtc.start() while True: # 点亮 LED 表示“醒着” led.value(1) print(Woke up, current datetime:, rtc.datetime()) # 模拟做点工作采集、发送数据 time.sleep_ms(300) led.value(0) # 设置 10 秒后唤醒 set_rtc_alarm(10) # 进入 lightsleepRTC 闹钟到时后自动唤醒 machine.lightsleep()这个例子里有几点值得注意RTC 闹钟 API 在 MicroPython 的 Pico 版本里存在但一些较老的固件没有rtc.alarm_enable()需要升级到最新固件。machine.lightsleep()不带参数意味着它是被动睡眠等待 RTC 闹钟或外部中断唤醒。每次醒来会重新执行while True里的代码完成工作后再次设置闹钟继续睡。从实测来看这段代码在lightsleep里的板级电流约 14-15mA虽然谈不上惊艳但比起time.sleep()的 20mA 已经有改善了。关键优势在于唤醒周期稳定、代码逻辑清晰适合原型验证。4.3 外部中断唤醒与 GPIO 注意事项RTC 定时唤醒适合周期任务但如果节点需要响应外部事件比如门磁、人体红外、按钮就必须用 GPIO 中断唤醒。MicroPython 的lightsleep()对 GPIO 中断的支持是透明的只要你在睡眠前配置好Pin.IRQ_RISING或Pin.IRQ_FALLING睡眠中事件发生时会自动唤醒。from machine import Pin import machine # 配置外部中断引脚例如 GP16 接了个按钮到 GND btn Pin(16, Pin.IN, Pin.PULL_UP) def btn_handler(pin): print(Button pressed, waking up...) btn.irq(triggerPin.IRQ_FALLING, handlerbtn_handler) # 进入无参数 lightsleep等待按键唤醒 while True: print(Entering lightsleep, press button to wake) machine.lightsleep() print(Woke up, doing something...)这个模式看着和裸机上的wfi/wfe很像不同的是 MicroPython 做了中断处理适配。我实际测试时踩过一个坑如果中断回调函数里执行了比较耗时的操作可能会导致重复触发或唤醒异常。所以回调函数里只置标志位把实际逻辑放在主循环里处理是更安全的写法。另外GPIO 唤醒时要特别注意引脚电平。Pico 内部上拉/下拉电阻默认是不开启的外部悬空引脚在睡眠模式下会因为电平不确定而反复触发中断导致 MCU 根本没法睡稳。所以使用外部中断唤醒外设侧一定要有明确的高/低电平保证最常用的就是按键一端接 GND、一端接 GPIO内部PULL_UP启用这样平时是高电平按下才是低电平不会乱跳。4.4 MicroPython 低功耗实战心得如果你决定用 MicroPython 做低功耗节点有几点心得是从我踩过的坑里总结出来的第一关闭不需要的外设。lightsleep模式不会自动关掉你通过 MicroPython 打开的外设。比如你初始化了 I2C、SPI、UART即使没有使用外设的时钟门控也是开着的功耗会更高。进入睡眠前手动执行i2c.deinit()、spi.deinit()、uart.deinit()能省下约 1-2mA 电流。第二LED 一定记得关。听起来像废话但 Pico 板载的绿色电源 LED 实际上是和 3V3 电源网络连在一起的你没法通过 GPIO 关掉它它常亮消耗约 3-4mA。如果想追求极致低功耗需要硬件上去掉这个 LED低功耗版本的 Pico 或者自己画最小系统板。软件层面能做的就是别让用户 LED 常亮。第三不要用time.sleep()做长延时。我见过很多代码把time.sleep(60)当作定时用这在 Pico 的 MicroPython 里只会让当前线程阻塞CPU 仍然满速运行、外设时钟全开功耗和全速运行几乎没差别。定时任务务必用 RTC 闹钟 machine.lightsleep()的方案。第四MicroPython 固件版本影响 API 行为。Pico 的 MicroPython 固件迭代期间lightsleep的实现有过调整。我的建议是尽量用官方发布的最新稳定版固件不要用某个早期拷贝不然可能会遇到 RTC 闹钟唤醒后时间不对、或者lightsleep参数不生效的问题。5. C SDK 方式实现极低功耗5.1 为什么正式产品我更推荐 C SDKMicroPython 适合验证但要做量产级的低功耗设备我强烈建议切换到 C SDK。原因很简单MicroPython 的运行时本身就有额外开销而 C SDK 可以精确控制每一个时钟门控、每个外设的开关并且可以直接调用sleep_run_from_dormant_source()这类深度睡眠接口。用 C SDK 写低功耗代码最大的门槛不是语法而是理解芯片的时钟树和唤醒源配置。我把它拆成几个固定步骤你照着走基本不会翻车。5.2 基础环境与工程结构新建一个 Pico C SDK 工程最简单的办法是直接用官方的pico-examples里的hello_world作为模板然后加入自己的代码。核心 CMakeLists.txt 内容大致如下cmake_minimum_required(VERSION 3.13) include(pico_sdk_import.cmake) project(lowpower_demo C CXX ASM) add_executable(lowpower_demo lowpower_demo.c ) target_link_libraries(lowpower_demo pico_stdlib hardware_rtc hardware_rosc hardware_clocks ) pico_enable_stdio_uart(lowpower_demo 1) pico_add_extra_outputs(lowpower_demo)这里链接了hardware_rtc、hardware_rosc、hardware_clocks是因为低功耗操作需要用到这些硬件驱动库。如果你是第一次使用 C SDK建议先编译运行官方示例确保环境没问题。5.3 dormant 模式完整代码实现下面这段代码实现了“定时 10 秒唤醒、唤醒后闪烁 LED 再继续睡”的完整流程使用的休眠状态是 dormant 模式RTC 作为唤醒源#include stdio.h #include pico/stdlib.h #include hardware/rtc.h #include hardware/rosc.h #include hardware/clocks.h #include hardware/pll.h #include pico/sleep.h // 定义唤醒日期时间此处简化为从当前时间 10 秒 static datetime_t alarm_time; static void alarm_callback(void) { // 唤醒后的回调可以在这里放恢复操作 printf(Alarm triggered, waking up...\n); } static void set_rtc_alarm_seconds(uint32_t secs) { datetime_t now; rtc_get_datetime(now); uint32_t now_sec now.hour * 3600 now.min * 60 now.sec; uint32_t target_sec now_sec secs; // 24 小时以内够了实际工程要做日期进位处理 alarm_time.hour target_sec / 3600; alarm_time.min (target_sec % 3600) / 60; alarm_time.sec target_sec % 60; alarm_time.day now.day; alarm_time.month now.month; alarm_time.year now.year; alarm_time.dotw now.dotw; rtc_set_alarm(alarm_time, alarm_callback); } int main() { stdio_init_all(); // 初始化 RTC rtc_init(); datetime_t t { .year 2025, .month 1, .day 1, .dotw 3, .hour 0, .min 0, .sec 0, }; rtc_set_datetime(t); // 初始化板载 LEDGPIO25 gpio_init(25); gpio_set_dir(25, GPIO_OUT); while (true) { // 亮 LED 表示醒着 gpio_put(25, 1); printf(Awake, doing work...\n); sleep_ms(500); gpio_put(25, 0); // 设置 10 秒后 RTC 闹钟 set_rtc_alarm_seconds(10); // 关键进入 dormant 模式唤醒时钟源选择 RTC 所在时钟域 sleep_run_from_dormant_source(CLOCKS_CLK_SLEEP_TIMER_SRC_REF); // 唤醒后的恢复代码 printf(Woke up from dormant!\n); // 注意唤醒后 RTC 时钟仍然运行但系统时钟需要重新配置 // 如果外部高速晶振被断开需要重新启动 XOSC 和 PLL // 如果保留 XOSC可以跳过这一步但功耗会略高 } return 0; }这段代码的核心是sleep_run_from_dormant_source(CLOCKS_CLK_SLEEP_TIMER_SRC_REF)。参数里我用的是CLOCKS_CLK_SLEEP_TIMER_SRC_REF意思是把休眠定时器的时钟源设成参考时钟reference clock该时钟通常是 XOSC 或 ROSC 分频后的时钟。由于 RTC 挂载在这个时钟域下RTC 闹钟能在 dormant 模式下正常计算时间并发起唤醒。实测下来这段代码在保留 XOSC 时的板级电流大约在 1mA 以下如果进一步把 XOSC 也关掉、只用 ROSC 的 low power 模式电流可以降到几百微安。注意唤醒后需要重新恢复时钟树否则系统时钟可能处于异常状态。5.4 唤醒后的时钟恢复——最容易踩的坑dormant 模式唤醒后系统不会自动把所有 PLL 和外设时钟恢复到休眠前的状态。我一开始没做任何恢复直接继续跑printf结果串口输出乱码、LED 闪烁频率也不对查了很久才发现是时钟树的问题。解决办法是根据休眠时的配置恢复时钟。如果休眠时保留了 XOSC最简单的恢复方法是调用#include hardware/clocks.h #include hardware/pll.h void restore_clocks(void) { // 如果之前关闭了 XOSC先重新启动 XOSC clocks_enable_xosc(); // 重新初始化 PLL 系统时钟 pll_init(pll_sys, 1, 1500 * MHZ, 6, 2); // 将系统时钟切换到 PLL clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_PLL_SYS, 125 * MHZ, 125 * MHZ); // 其他外设时钟如 UART、ADC按需恢复 }这个函数里的参数源自 RP2040 数据手册中 PLL 配置表125MHz 系统时钟对应FBDIV125、VCO750MHz、POSTDIV16、POSTDIV22。我在代码里使用了1500 * MHZ作为 VCO 频率然后 6/2 分频得到 125MHz读者可以根据自己的频率需求调整。如果不想手动恢复SDK 里也提供了sleep_run_from_xosc()它会自动处理 XOSC 和 PLL 的恢复但代价是唤醒源只能是 XOSC且功耗会比 dormant 模式高。所以实际项目中需要权衡要极低功耗就得手动处理唤醒恢复要代码简单就得接受较高功耗。5.5 实测对比不同模式电流数据我在同一块 Pico 上、用同一块万用表待机电流精度约 10uA测量了不同模式下的板级总电流整理成表供参考。注意这是整板电流包含了板载 LDO、电源 LED、USB 转换芯片的静态损耗。如果你用低功耗版本 Pico 或自制最小系统板电流会更低。模式系统时钟配置实测电流备注全速运行133MHz PLL所有外设开28.5mA不推荐长期保持sleep系统时钟运行等待事件18.2mA相当于time.sleep()的底层状态lightsleepMicroPython自动管理时钟14.1mARTC 闹钟定时唤醒C SDK dormant XOSCXOSC 保留系统时钟停0.85mARTC 唤醒源C SDK dormant ROSCXOSC 关闭ROSC low power0.18mARTC 唤醒源唤醒后需恢复时钟硬件去除 LED dormant ROSC自制最小板0.08mA需要硬件配合从表中可以看出软件 API 的差距最大能到两个数量级。这也是为什么我一直强调如果你真的在意续航C SDK 的 dormant 模式是绕不过去的。6. 实操项目电池供电的温湿度传感器节点6.1 项目需求与硬件组成理论讲完来一个完整的实操案例。我做一个单节 18650 锂电池供电的温湿度采集节点每 30 秒采集一次通过串口打印数据实际项目可以换成 LoRa 或者 BLE 模块。硬件组成很简单树莓派 Pico未做硬件修改方便复现DHT22 或 SHT30 温湿度传感器这里用 DHT22 来做示例使用 GPIO15单节 18650 电池 3.3V LDO 给 Pico 供电串口转 USB 模块用于日志输出调试完可断开注意DHT22 这类传感器本身在空闲时也有几百微安的电流严格来说不适合超低功耗设备。我这里用它做例子是为了演示怎么在 MicroPython 里管理传感器电源。6.2 MicroPython 实现版本import machine from machine import Pin, RTC import time import dht # 传感器接 GP15 dht_pin Pin(15, Pin.OUT, Pin.PULL_DOWN) sensor dht.DHT22(dht_pin) led Pin(25, Pin.OUT) rtc RTC() rtc.start() def set_rtc_alarm(seconds): now rtc.datetime() import time as t current_ts t.mktime((now[0], now[1], now[2], now[4], now[5], now[6], 0, 0, -1)) target_ts current_ts seconds dt t.localtime(target_ts) rtc.alarm_time(dt[0], dt[1], dt[2], dt[6], dt[3], dt[4], dt[5]) rtc.alarm_enable() def read_sensor(): # 传感器数据引脚设为输入 sensor.measure() temp sensor.temperature() humi sensor.humidity() return temp, humi while True: led.value(1) # 传感器上电用 GPIO 控制传感器 VCC 的话这里置高 # 读取数据 try: temp, humi read_sensor() print(Temp: {:.1f} C, Humi: {:.1f} %.format(temp, humi)) except Exception as e: print(Sensor read error:, e) # 工作结束进入睡眠 led.value(0) # 如果传感器由 GPIO 供电此处应置低切断传感器电源 set_rtc_alarm(30) machine.lightsleep()这段代码的功耗瓶颈主要在两个地方一是 MicroPython 的lightsleep只有 14mA 左右二是 DHT22 传感器没有真正断电。如果你想在这个方案上优化有两个方向一是把传感器换成 SHT30 并加一个 GPIO 控制的 MOSFET 电源开关在睡眠时彻底断电二是把主控换成 C SDK让整体睡眠电流降到微安级。这两种优化我都试过效果立竿见影。6.3 C SDK 实现版本dormant 级如果是正式部署版本我会用 C SDK 实现同样的节点。传感器读取部分可以复用官方库核心区别在睡眠管理void enter_low_power(uint32_t seconds) { // 关闭不需要的外设时钟 clocks_hw-clk_peri_ctrl 0; // 设置 RTC 闹钟 set_rtc_alarm_seconds(seconds); // 进入 dormant用 ROSC 作为休眠时钟源更低功耗 sleep_run_from_dormant_source(CLOCKS_CLK_SLEEP_TIMER_SRC_REF); // 唤醒后恢复时钟和外设 restore_clocks(); clocks_hw-clk_peri_ctrl CLOCKS_CLK_PERI_CTRL_ENABLE_BITS; }这里我把外设时钟clk_peri_ctrl先清零再进入休眠确保 UART、SPI 等外设时钟全部关掉。唤醒后重新使能。实际项目里你还得考虑传感器电源的控制引脚以及数据发送模块的功耗管理。6.4 续航测试结果对比我把两种实现放在同样的电池条件下测试用 2000mAh 的 18650 电池串口断开只保留节点工作记录从满电到无法开机的天数实现方案睡眠电流30 秒周期实际续航说明MicroPython time.sleep24mA约 3.5 天基准方案CPU 一直全速MicroPython lightsleep RTC14mA约 5.5 天官方 API 优化明显更好C SDK dormant XOSC0.85mA约 30 天显著提升适合周级节点C SDK dormant ROSC优化0.2mA约 60 天进一步压功耗C SDK dormant 外设断电0.08mA约 100 天接近极限电池自放电不可忽略注意这个对比没有考虑发送模块的工作功耗只考虑了 MCU 和传感器节点本身。加上 LoRa 模块后发送阶段的几十毫安电流仍会占据总能耗的相当比例因此实际的提升倍数可能没有表格里那么夸张但方向是对的——睡眠功耗是底座不把底座降下去其他优化都是白费。7. 常见问题与排查技巧实录7.1 问题速查表现象可能原因解决方案machine.lightsleep()后电流仍然很高外设没有 deinitLED 常亮手动关闭 I2C/SPI/UART检查 GPIO 状态RTC 闹钟唤醒不生效RTC 没有初始化、闹钟时间计算方法错误确保rtc.start()用绝对时间戳计算目标时间唤醒后 LinuxUSB串口丢失USB CDC 在深度睡眠后未重枚举唤醒后重新初始化 USB或改用 UART 引脚打印dormant 唤醒后程序“死机”时钟树未恢复CPU 时钟停摆调用restore_clocks()重新配置 PLL 和时钟源GPIO 中断触发导致无法入睡引脚浮空、电平不定启用内部上下拉确保外部有确定电平唤醒后时间不准确RTC 依赖的时钟源在休眠时被关闭确认休眠定时器时钟源和 RTC 时钟域一致连接主控后重新上电无法唤醒USB 转换器的 VBUS 干扰断开 USB 仅用电池供电测试7.2 排查思路详解为什么电流降不下来如果你按照代码写了但用万用表一量电流还是十几毫安别急着怪 API先按下面顺序排查第一步确认测量的电流是整板电流还是仅仅是 MCU 电流。Pico 板载的电源 LED 和 LDO 静态电流就有 3-5mA如果你没有去掉它们再好的软件也降不下来。我最初的测试就被这个 LED 干扰了判断。第二步确认你进入的是哪个低功耗状态。在 C SDK 里如果只是调用__wfi()而不是sleep_run_from_*()系统时钟还在跑电流当然不会低。很多教程让你用__wfi()做低功耗这在其他 MCU 上也许有效但在 RP2040 上效果很有限。第三步确认所有外设时钟是否关闭。C SDK 里如果使用了stdioUART 时钟会保持开启即便你不打印信息外设时钟也在耗电。休眠前临时关闭clk_peri_ctrl是一个很有效的降耗手段。第四步检查唤醒源是否正常工作。如果你把 RTC 闹钟设错了时间它会一直等不会唤醒此时电流虽然低但功能也不对。排查方法是先设一个 3 秒的超短闹钟确认能正常唤醒后再改成实际业务时间。7.3 一个让我排查了两天的坑分享一个我印象深刻的故障用 dormant 模式后节点能正常睡、正常醒但每次唤醒后执行传感器读取时DHT22 的measure()总是报错。一开始我以为是时序问题加长延时也没用。后来用逻辑分析仪抓波形才发现唤醒后 GPIO 的输出驱动能力没有恢复导致 DHT22 数据线的驱动电平不够强。原因在于休眠前我把 GPIO 15 的输入/输出配置改了为了省电设成了PIN_OD唤醒后没有恢复成强推挽输出。这个问题的通用教训是——低功耗代码不仅仅要管睡眠还要管恢复。所有在进入休眠前改过状态的外设、GPIO、时钟在唤醒后都要有一个对称的恢复逻辑。现在我在工程里会把“进入睡眠”和“唤醒恢复”封装成两个对应的函数成对出现可读性和可维护性都会好很多。8. 进阶扩展让低功耗节点融入 API 生态8.1 为什么低功耗设备也需要“API”聊完 MCU 层面的 API再把视角往上一层。我参与的不少项目低功耗节点不是孤立存在的它们采集的数据最终要汇总到服务器、要能被手机 App 或 Web 前端调取。这时候节点的“低功耗”和服务端的“API 服务”就需要协同设计。一个典型的架构是多个 Pico 低功耗节点通过 LoRa/BLE 把数据发给网关网关再通过 REST API 上报到服务器。节点本身不直接连互联网但整个系统对外暴露的是一套标准的 HTTP API客户端的请求通过 API 服务转发到节点。这个设计的好处是节点可以用最低功耗的休眠策略只有在收到网关指令时才醒来干活客户端的访问体验不受影响。8.2 用树莓派做 API 网关的实践我常用树莓派 4B或者其他 Linux 板卡做低功耗传感器网络的边缘网关它负责两件事一是通过串口或射频模块与 Pico 节点通信二是把数据整理成 REST API 暴露给局域网。这里用一个简单的 Python Flask 示例展示 API 服务的基本结构from flask import Flask, jsonify, request import serial import threading import time app Flask(__name__) # 串口连接 Pico 节点通过 USB 转串口 ser serial.Serial(/dev/ttyUSB0, 115200, timeout2) # 节点数据缓存 node_data {} data_lock threading.Lock() def read_from_node(): global node_data while True: line ser.readline().decode(utf-8, errorsignore).strip() if line: print(From node:, line) try: parts line.split(,) node_id parts[0] temp float(parts[1]) humi float(parts[2]) with data_lock: node_data[node_id] { temperature: temp, humidity: humi, timestamp: time.time() } except (ValueError, IndexError) as e: print(Parse error:, e) app.route(/api/nodes, methods[GET]) def list_nodes(): with data_lock: return jsonify(list(node_data.keys())) app.route(/api/nodes/node_id, methods[GET]) def get_node(node_id): with data_lock: if node_id in node_data: return jsonify(node_data[node_id]) return jsonify({error: node not found}), 404 if __name__ __main__: t threading.Thread(targetread_from_node, daemonTrue) t.start() app.run(host0.0.0.0, port5000)这个 API 服务本身很简单但它的价值在于把底层低功耗节点的数据封装成了标准接口。前端开发、App 开发完全不需要关心节点是 MicroPython 还是 C SDK 实现的也不需要关心节点是每隔 30 秒醒一次还是每隔 10 分钟醒一次——他们只需要 GET 请求这个 API 就能拿到实时数据。8.3 低功耗与 API 的协同设计原则在多节点低功耗系统里有一个容易忽略的问题API 请求的频率和节点唤醒频率的匹配。如果你的 API 允许客户端按需请求数据那网关得有办法“唤醒”对应的节点。我在一个项目里遇到过这样的情况节点每 10 分钟上报一次数据但客户端的仪表盘想要“实时刷新”结果每次刷新看到的都是旧数据。解决办法是在网关里加一个数据缓存层网关定期从节点拉取数据缓存到本地客户端请求 API 时直接返回缓存而不是实时穿透到节点。这样一来节点依然可以保持低频唤醒API 的响应速度却不会受影响。这本质上就是一个“异步通信 缓存”的架构思想。你把节点当“低速后端”把网关当“缓存层”API 只是外露的薄薄一层。8.4 从 REST API 到下一步优化如果你走得更远可以考虑用异步通信协议如 MQTT代替 REST API。MQTT 的发布/订阅模型天然适合低功耗节点场景节点订阅指令主题有需要时醒来检查网关发布数据和指令不需要知道节点具体的休眠周期。底层节点的唤醒逻辑完全不变上层通信协议升级几乎不影响节点侧代码。我个人的建议是先从 REST API 起步把数据通路跑通再考虑引入 MQTT。原因是 REST 直观、调试方便、任何平台都能用 curl 测而 MQTT 需要额外部署 broker、处理 QoS 等级和离线消息复杂度明显高一些。等系统真正有几十个节点、需要双向通信时再迁移到 MQTT 也不迟。9. 综合经验与个人体会9.1 低功耗项目的“第一性原理”复盘做了几个 Pico 低功耗项目后我最深的体会是低功耗优化不是某一条神 API 的功劳而是一整套系统设计的结果。从时钟树配置、外设电源管理、传感器选型到上层通信协议的唤醒频率匹配每一层都要做对最终的续航才会好看。我一般在项目开始时就会列一个功耗预算表把每个组件的睡眠电流、工作电流、工作时间写进去算出一个参考续航。然后一边优化一边更新这张表。如果发现某个部分和预算差距过大就先停下来查那个部分不要盲目上各种优化技巧。毕竟你可以用 dormant 模式把 MCU 的电流压到 100uA但如果传感器忘了断电整体电流还是几毫安优化等于白做。9.2 软件层面的“性价比”排序同样是一天的时间投入不同优化动作带来的收益差异很大。按我个人的经验性价比排序大概是这样的第一优先把time.sleep()改成真正的低功耗模式。这个改动小、效果直接能把睡眠电流从 20mA 降到 14mA 甚至 0.2mA几乎不增加代码复杂度。第二优先管理好外部器件的功耗。给传感器加一个 GPIO 控制的电源开关睡眠时彻底断电这个收益经常被人忽略但往往非常可观。第三优先优化唤醒频率和工作时间。比如温度采集没必要每 1 秒做一次改成 30 秒甚至 5 分钟一次平均功耗会成比例下降。第四优先硬件层面优化。去掉调试 LED、换低功耗 LDO、使用 Pico 低功耗版本或自制最小板。这个收益在软件优化到位后会变得非常明显。9.3 给后来者的建议如果你刚开始接触 Pico 低功耗我的建议是先从 MicroPython 的lightsleep入手把 RTC 定时唤醒和 GPIO 中断唤醒都跑通用万用表测一下电流建立直观感受。然后再换到 C SDK实现一次 dormant 模式的定时唤醒感受一下手动恢复时钟的复杂度。这两步走过来你对“低功耗”和“API”的理解会比看一百篇文档都深刻。最后再分享一个小技巧调试低功耗代码时不要直接用电池供电而是在电池正极串联一个小阻值采样电阻比如 10 欧姆用示波器或者万用表测电阻两端压降就能看到不同状态下的电流波形。这个方法能帮你快速判断 MCU 到底睡没睡着、醒了多久比光看总电流直观得多。
返回列表