news 2026/9/5 5:29:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Pico低功耗API实战:从lightsleep到dormant

1. 项目概述

1.1 核心需求解析

树莓派 Pico 这块板子,很多人第一反应是“便宜”“能玩 CircuitPython”“GPIO 多”,但真正把它丢进电池供电的物联网场景,就会发现一个绕不开的坎——功耗。我在做一批低功耗传感器节点时,初始版本直接用time.sleep()死等,结果两节 AA 电池撑了不到一周就彻底没电。后来认真啃了 RP2040 的数据手册和 Pico 的 SDK 文档,把官方提供的低功耗 API 一个一个试了一遍,才把待机电流从毫安级压到微安级。

这篇文章想说的,不是“Pico 能低功耗”这种口号,而是把sleeplightsleepdormant这些 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;其余时间处于睡眠态。如果睡眠电流是 14mA(MicroPython 的 lightsleep),那平均电流大约是:

[ \frac{0.2s \times 60mA + 59.8s \times 14mA}{60s} ≈ 14.15mA ]

这意味着,如果用一节 2000mAh 的锂电池,理论续航只有 140 小时左右,也就是不到 6 天。可如果睡眠电流压到 100uA(C 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_IO(IO 电源)VDD_ADC(ADC 模拟电源)。MCU 内部大部分逻辑(CPU 内核、SRAM、外设总线)都由核心电源供电,IO 和模拟部分独立供电。

低功耗模式的本质,就是软件控制 PLL、时钟源、外设时钟门控和核心电源请求,把不再需要的部分停下来。RP2040 内部有两个可用的振荡器:一个是 12MHz 的晶振(XOSC),一个是芯片内部自带的环形振荡器(ROSC)。正常运行时,系统时钟一般通过 PLL 从 XOSC 或者 ROSC 倍频出来。而进入低功耗后,PLL 会被关掉,XOSC 也可以关掉,系统时钟要么停摆,要么降到很低的频率。

这里有个关键点:唤醒源和时钟必须配套。如果休眠时把 XOSC 关了,那么 XOSC 就不能作为唤醒源,必须用 ROSC 或者 RTC(内部实时时钟,基于低速时钟)来定时唤醒。这就直接决定了你在代码里该怎么配置。

3.2 三种低功耗状态的本质区别

先给没有啃过数据手册的朋友一个直观对比表。RP2040 手册里真正定义了三种电源状态:sleeplightsleepdormant。注意,这不是寄存器里的一个枚举值,而是不同配置组合后表现出的系统状态。

状态CPU 状态时钟源唤醒源典型电流(板级)适用场景
sleep停止系统时钟仍在跑,等待事件任意中断约 15-20mA等待短时间 I/O 操作
lightsleep停止系统时钟停,XOSC/ROSC 可按需保留定时器、GPIO、RTC约 14mA(MicroPython 下)中等时长低功耗等待
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) # 初始化 RTC(RP2040 内部 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) # 进入 lightsleep,RTC 闹钟到时后自动唤醒 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_RISINGPin.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(trigger=Pin.IRQ_FALLING, handler=btn_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 SDK

MicroPython 适合验证,但要做量产级的低功耗设备,我强烈建议切换到 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_rtchardware_roschardware_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); // 初始化板载 LED(GPIO25) 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 系统时钟对应FBDIV=125VCO=750MHzPOSTDIV1=6POSTDIV2=2。我在代码里使用了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()的底层状态
lightsleep(MicroPython)自动管理时钟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()后电流仍然很高外设没有 deinit,LED 常亮手动关闭 I2C/SPI/UART,检查 GPIO 状态
RTC 闹钟唤醒不生效RTC 没有初始化、闹钟时间计算方法错误确保rtc.start(),用绝对时间戳计算目标时间
唤醒后 Linux(USB)串口丢失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 里如果使用了stdio,UART 时钟会保持开启,即便你不打印信息,外设时钟也在耗电。休眠前临时关闭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, timeout=2) # 节点数据缓存 node_data = {} data_lock = threading.Lock() def read_from_node(): global node_data while True: line = ser.readline().decode('utf-8', errors='ignore').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(target=read_from_node, daemon=True) t.start() app.run(host='0.0.0.0', port=5000)

这个 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 到底睡没睡着、醒了多久,比光看总电流直观得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 5:28:24

指数移动平均与一阶低通滤波:同一公式的两种视角

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 5:27:43

AsterMem:AI Agent长期记忆系统架构与工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 5:24:35

VLM视觉语言模型学习路径:从CLIP到Qwen-VL微调部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 5:16:42

QQ空间备份神器qzonearchive:本地归档你的青春数据

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 5:15:22

大学生电动方程式赛车算法开发:从架构到车辆模型

从零开始开发一套大学生电动方程式赛车算法——1收到一个大学生电动方程式车队朋友的私信&#xff0c;说他们车队的算法组刚组建&#xff0c;人手一台笔记本电脑&#xff0c;其他全无&#xff0c;问我第一周该干什么。这问题太典型了&#xff0c;我当年带队踩的坑&#xff0c;不…

作者头像 李华
网站建设 2026/9/5 5:14:36

FUTABA短身舵机CT500拆解评测:PWM控制与Arduino驱动全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华