嵌入式面试真题第 17 题:资源受限系统中的分层低功耗与低延迟唤醒架构设计

在这里插入图片描述

问题

在电池供电、能量采集供电或热设计受限的嵌入式设备中,系统通常同时存在多级 CPU/SoC 低功耗状态、多个可独立关断的电源域,以及时钟、存储、传感器、显示、音频、无线通信和外部协处理器等设备。不同休眠状态的静态功耗、进入时间、退出时间、上下文保留能力、可用唤醒源和恢复复杂度差异很大:浅睡眠退出快但待机功耗较高,深睡眠功耗低但可能关闭 PLL、主 SRAM、总线、外设或整个数字域,唤醒后甚至需要从复位入口重新启动。

业务侧又可能同时提出多种约束:按键、触摸、旋钮、门磁、佩戴检测等人机事件要求快速反馈;采样、控制、告警和协议时隙要求确定性响应;网络、音频、显示和传感器只需在需要时恢复;后台日志、统计、同步和自检可以延后。系统还可能频繁出现短空闲、突发交互、定时任务、通信重传、传感器中断和误触发。如果每次空闲都进入最深休眠,恢复成本、唤醒抖动和设备状态错乱可能抵消节能收益;如果长期停留在浅睡眠,又无法满足续航目标。

你会如何设计一套通用的低功耗与唤醒管理框架,在不绑定某一款 MCU、某一种外设或某一个业务场景的前提下,综合处理以下问题:

  1. 如何根据预计空闲时间、唤醒延迟预算、能量收益、设备依赖和唤醒源能力选择休眠深度?
  2. 如何把唤醒拆分为快速响应路径和完整恢复路径,使关键事件先被可靠捕获并得到及时反馈,而不是等待全部设备串行初始化?
  3. 如何保存和校验上下文,协调 PLL、总线、存储、I2C/SPI 设备、显示、音频、无线和后台任务的恢复顺序?
  4. 如何处理多模块并发请求、禁止休眠区间、超时、误唤醒、重复事件、进入休眠与中断同时发生等竞态?
  5. 如何量化能耗与响应延迟,设计可测试、可观测、可回退的策略,并借鉴 Linux、Zephyr、FreeRTOS、RT-Thread、Apache NuttX、ESP-IDF 等开源实现?

回答

结论:低功耗设计的核心不是“系统一空闲就进入最深睡眠”,而是建立一套受延迟约束、受驻留时间约束、按电源域分层、支持分阶段恢复、由业务意图驱动并可在线度量的电源管理框架

系统应把休眠与唤醒拆成四个相互解耦的层次:

  1. 策略层根据预计空闲时间、当前延迟约束、设备使用计数、唤醒源、能量模型和历史行为选择最深但仍满足约束的状态。
  2. 事务层把一次休眠视为可提交、可拒绝、可中止、可回滚的系统事务,统一处理模块投票、设备依赖、上下文保存和竞态。
  3. 快速唤醒层只完成事件锁存、时钟最小恢复、关键状态机推进和首个可感知反馈,目标是尽快达到“事件已确认”或“最小业务可用”。
  4. 后台恢复层按依赖图异步恢复非关键设备、重建连接、执行校准、补偿时间、刷新界面和恢复日志,避免阻塞关键路径。

因此,用户或控制环真正关心的不是“所有设备是否已经恢复”,而是几个不同的时间点:

1
2
3
4
5
T_irq        = 唤醒源触发到 CPU 开始执行唤醒入口的时间
T_capture = 唤醒原因被可靠锁存并完成最小去抖/合法性判断的时间
T_ack = 用户、上位机或控制状态机得到第一响应的时间
T_service = 本次业务所需最小设备集合恢复完成的时间
T_full = 全部后台设备与非关键服务恢复完成的时间

系统设计应优先约束 T_captureT_ackT_service,而不是强行让 T_full 等于 T_ack。一个按键可以在主 PLL、Codec、外部 Flash 和无线协议栈完全恢复前先被记录并点亮低功耗 LED;一个工业告警可以先锁存故障、拉高告警输出并更新时间戳,再恢复网络上传;一个周期采样任务可以只恢复低速时钟、ADC 和 DMA,而不必唤醒显示与音频域。

总体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TD
A[业务任务 / 驱动 / 协议栈] --> B[PM Intent API\n延迟约束 / 活动提示 / 设备引用]
B --> C[Constraint Aggregator\n聚合最严格约束]
C --> D[Power Policy Governor\n预计空闲时间 + 能量模型 + 历史行为]
D --> E{选择电源状态}
E -->|浅睡 / Idle| F[CPU Idle / Tickless]
E -->|系统睡眠| G[Sleep Transaction Manager]
G --> H[Wake Source Manager\n配置与验证唤醒源]
G --> I[Context Manager\n保存 / CRC / 版本]
G --> J[Device Dependency Graph\nprepare / suspend]
J --> K[SoC PM Backend\n时钟 / 电源域 / WFI/WFE]
K --> L[Always-on / RTC / PMU 域]
L --> M[Wakeup ISR / Resume Stub]
M --> N[Fast Resume Path\n捕获事件 + 最小反馈]
N --> O[Critical Service Set\n关键设备恢复]
O --> P[Deferred Resume Worker\n后台并行恢复]
P --> Q[Telemetry & Policy Tuning\n延迟 / 能量 / 失败统计]
Q --> D

这个架构中,应用不直接决定“立刻进 Deep Sleep”,驱动也不各自无序地关闭时钟。上层只描述意图和约束,例如“未来 5ms 内不能接受超过 100us 的退出延迟”“SPI DMA 传输期间禁止关闭该总线”“当前交互可能持续 3s”“网络下一次监听窗口在 120ms 后”。策略层把这些约束聚合后选择状态;事务层协调设备;平台后端完成具体寄存器操作;唤醒后快速路径和后台路径分开执行。

机制与开源实现的对应关系

本文机制 Linux 或开源机制 能否直接使用 主要参考价值
多级休眠状态与驻留时间 Zephyr System Power Management、Linux cpuidle 对应系统可直接使用;裸机或其他 RTOS 主要参考 每个状态定义最小驻留时间、退出延迟,策略只选择满足下一事件时间窗口的状态。
唤醒延迟约束 Zephyr PM policy latencyLinux PM QoS Linux/Zephyr 可直接使用接口 多个模块提交最大可接受退出延迟,系统取最严格值,禁止进入过深状态。
设备运行时电源管理 Zephyr Device Runtime PMLinux Runtime PM 对应系统可直接使用 使用计数、自动挂起、运行时恢复,使设备功耗与系统级休眠解耦。
电源模式请求/投票 RT-Thread PM RT-Thread 可直接使用;其他系统参考 业务或驱动成对请求/释放模式,未释放的请求限制系统进入更深状态。
活动驱动与 PM 域 Apache NuttX Power Management NuttX 可直接使用;其他系统参考 驱动上报活动,按 PM domain 管理状态,并通过 prepare/notify 回调协调驱动。
无周期 Tick 的空闲睡眠 FreeRTOS Tickless Idle FreeRTOS 可直接配置 停止周期 Tick,按下一超时点设置低功耗定时器,唤醒后补偿内核时间。
浅睡、深睡与保留域 ESP-IDF Sleep Modes ESP 芯片可直接使用;其他平台参考 区分状态保留的 Light Sleep 与大部分数字域掉电的 Deep Sleep,配置多个唤醒源和 RTC 保留资源。
唤醒源与设备依赖 Linux wakeup source、driver model、runtime PM Linux 可直接使用;MCU 系统参考 明确设备是否具备唤醒能力、是否允许唤醒、父子设备恢复顺序及引用关系。
快速路径与延后恢复 Linux resume phases、workqueue;Zephyr work queue 需按产品设计封装 ISR/恢复桩只做最小工作,非关键恢复通过工作队列异步执行。

这些开源机制并不是可以机械拼接的“标准答案”。Linux 面向复杂 SoC 和完整驱动模型,Zephyr、RT-Thread、NuttX、FreeRTOS 更贴近 MCU/RTOS,但它们共同体现了几个稳定原则:状态必须有可比较的延迟和能耗属性;模块必须能表达约束;设备必须按依赖和引用管理;系统必须知道下一次事件;睡眠时间必须补偿;唤醒必须可追踪和可失败。

先定义目标:系统不是只有“睡”和“醒”

低功耗架构应先把“可用”拆成不同等级,否则所有模块都会要求完整恢复,最终又退化为串行初始化。

可用等级 定义 典型动作 建议指标
L0:事件已锁存 唤醒原因、边沿、时间戳不会丢失 AON GPIO/RTC 捕获,写 retention record T_capture
L1:最小反馈可用 可以给用户或控制系统第一确认 低功耗 LED、GPIO、蜂鸣器占位、状态机 ACK T_ack
L2:关键业务可用 本次事件真正需要的设备已恢复 按键扫描、ADC、关键总线、显示首帧、协议应答 T_service
L3:完整业务可用 所有前台功能恢复 完整 UI、音频链路、无线连接、存储访问 前台恢复时间
L4:后台恢复完成 日志、统计、自检、同步和缓存重建完成 日志刷写、传感器重校准、云同步 T_full

例如,按键唤醒并不天然要求立刻恢复所有 I2C 传感器;RTC 定时采样不需要点亮显示;BLE 连接事件不需要启动音频 Codec;充电插入事件可以先由 PMIC/AON 域确认,再决定是否启动主系统。只有先定义“事件所需的最小服务集合”,才能把恢复路径真正并行化和分阶段化。

电源状态模型

不要只用 ACTIVESLEEP 两个枚举。通用框架至少应为每个系统状态记录以下属性:

1
2
3
4
5
6
7
8
9
10
11
state_id
entry_latency_us
exit_latency_us_worst
min_residency_us
steady_power_uw
retained_memory_mask
available_wakeup_mask
powered_domain_mask
clock_domain_mask
context_loss_level
requires_reset_resume

一个抽象状态表可以如下设计:

状态 典型硬件行为 上下文 退出延迟 适用场景
RUN_HIGH 主 PLL、高频总线、全部关键外设运行 全保留 0 高负载、实时处理
RUN_LOW 降频或切换低速时钟,设备仍可运行 全保留 数微秒到数十微秒 低负载连续任务
IDLE CPU 停止取指,系统 Tick 可继续 全保留 极低 极短空闲
LIGHT_SLEEP CPU/部分时钟关闭,主 SRAM 保留 大部分保留 低到中等 短期空闲、交互活跃期
DEEP_SLEEP 主 PLL、总线、部分 SRAM 和外设断电 仅保留域 中到较高 较长空闲、少量唤醒源
STANDBY 数字域大部分掉电,唤醒近似复位 少量备份寄存器/RTC RAM 长时间待机
SHUTDOWN 仅 PMIC/AON 保持,主控重新上电 基本不保留 最高 运输、极低电量、长期存放

状态名称只是抽象,具体芯片的 Stop、Standby、Backup、System OFF、Hibernate 等模式必须映射到统一属性。策略层不应写死“模式 3 一定比模式 2 好”,而应通过属性判断:某状态即使静态功耗低,如果退出要 20ms、上下文恢复再要 30ms,而预测空闲只有 40ms,就没有实际收益。

休眠状态选择原则

基本约束

候选状态 s 至少同时满足以下条件:

1
2
3
4
5
6
L_exit_worst(s) + L_critical_restore(s, event_class) <= L_budget_effective
T_idle_predicted >= T_entry(s) + T_exit(s) + T_min_residency(s) + T_guard
WakeSourceRequired ⊆ WakeSourceAvailable(s)
RequiredRetention ⊆ RetentionCapability(s)
RequiredDomains ∩ PoweredOffDomains(s) = ∅
NoUnreleasedLockBlocks(s)

其中:

  • L_budget_effective 是所有活动模块提交的延迟约束中的最小值。
  • event_class 表示当前最可能或必须支持的唤醒类别,不同事件的关键恢复集合不同。
  • T_guard 用于覆盖时钟漂移、调度抖动、IRQ 屏蔽窗口和估计误差。
  • NoUnreleasedLockBlocks(s) 表示 DMA、Flash 擦写、总线事务、关键区或协议时隙没有禁止该状态。

由浅到深还是由深到浅

实现上建议从“最深状态”向“最浅状态”筛选,第一个满足所有约束的状态即为候选。这样策略的含义清晰:系统始终尝试在约束内获得最大节能收益,而不是靠大量业务分支手工选择模式。

1
2
3
4
5
for state in states_sorted_by_power_ascending:
if latency_ok(state) and residency_ok(state) and wake_source_ok(state) and
retention_ok(state) and domain_ok(state) and lock_ok(state):
return state
return RUN_OR_IDLE

但最终选择不能只看静态功耗,还必须看一次进入和退出的固定成本。

能量盈亏平衡点

深睡并非时间越短越省电。一次状态切换包含保存上下文、关时钟、配置唤醒源、进入硬件模式、退出、锁定 PLL、恢复内存和设备等成本。

令:

1
2
3
4
5
6
P_shallow    = 浅状态稳态功耗
P_deep = 深状态稳态功耗
E_entry = 进入深状态的额外能量
E_exit = 退出深状态的额外能量
E_restore = 恢复上下文和设备的额外能量
T_idle = 实际空闲时间

深睡相对浅睡的净收益近似为:

1
E_saved = (P_shallow - P_deep) * T_idle - (E_entry + E_exit + E_restore)

只有 E_saved > 0 时,深睡才有能量收益。对应的盈亏平衡时间为:

1
T_break_even = (E_entry + E_exit + E_restore) / (P_shallow - P_deep)

策略中应使用:

1
T_idle_predicted >= T_break_even + T_guard

而不是简单地配置“空闲超过 10ms 就 Deep Sleep”。如果外部晶振和 PLL 启动需要较大冲击电流,或者无线、Codec、传感器每次恢复都要校准,E_restore 可能相当可观。频繁深睡甚至可能比保持低频运行更耗电。

加入提前唤醒概率

实际空闲时间具有不确定性。可把候选状态的期望代价表示为:

1
2
3
4
Cost(s) = E_expected(s)
+ lambda * Probability(L_service(s) > L_budget)
+ mu * Probability(resume_failure(s))
+ nu * WakeupWearOrCalibrationCost(s)

其中 lambdamunu 表示产品对延迟违约、恢复失败和频繁校准的惩罚权重。资源受限 MCU 不一定要实现复杂概率模型,但至少应统计“预测空闲时间与实际空闲时间的误差”,动态调整阈值,避免长期使用拍脑袋常量。

延迟预算必须分解

只要求“唤醒小于 20ms”不够,因为无法定位优化对象。应把总预算分配到各阶段:

1
2
3
4
5
6
7
L_total = L_hw_exit
+ L_clock_min
+ L_vector_to_stub
+ L_reason_capture
+ L_scheduler_ready
+ L_critical_devices
+ L_first_response

例如某类交互的预算可以是:

阶段 预算 说明
硬件退出与低速时钟可用 1.5ms 从深睡到可执行恢复桩
唤醒原因锁存与去抖首判 0.5ms 不做完整业务处理
内核与关键 RAM 恢复 1ms 调度器、栈、关键队列可用
最小反馈设备恢复 2ms AON LED/GPIO 或轻量显示路径
首次状态反馈 1ms 用户或上位机可感知
关键业务设备恢复 8ms 必需总线、传感器或协议
安全余量 6ms 覆盖 P99/P99.9 抖动
合计 20ms 不包含后台完整恢复

测量时要关注最坏值和高分位,而不是只看平均值。一次 Flash cache miss、PLL 偶发锁定变慢、I2C 设备上电自检、无线重连或高优先级中断抢占,都可能让平均 5ms 的路径出现 30ms 长尾。

快速唤醒路径与完整恢复路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sequenceDiagram
participant W as AON/RTC 唤醒源
participant S as Resume Stub
participant F as Fast Path
participant C as Critical Resume
participant B as Background Resume
participant A as Application

W->>S: 唤醒信号 + 硬件锁存
S->>S: 建立最小栈/低速时钟
S->>F: 读取并保存 wake reason
F->>F: 首次去抖/合法性检查
F->>A: 发布 WAKE_EVENT_CAPTURED
F->>A: 最小反馈或状态机 ACK
F->>C: 请求关键设备集合
par 关键设备按依赖恢复
C->>C: PLL/总线/必要 RAM
C->>C: 关键 GPIO/I2C/SPI/ADC
and 后台路径准备
F->>B: 投递 deferred resume
end
C->>A: 发布 SERVICE_READY
B->>B: 恢复显示/音频/无线/日志
B->>A: 发布 FULLY_RESUMED

快速路径应该做什么

快速路径只做具有以下特征的工作:

  1. 必须在唤醒标志被其他初始化覆盖前完成。
  2. 执行时间短、上界明确、无需动态分配。
  3. 不依赖尚未恢复的复杂驱动。
  4. 失败后能留下可诊断信息。
  5. 可以安全地重复执行或检测重复执行。

典型动作包括:读取 PMU/RTC/AON 唤醒寄存器,锁存 GPIO 电平与边沿,保存低功耗计数器时间戳,建立最小栈,恢复低速系统时钟,确认 retention record,清除必要的硬件标志,向无锁队列或静态事件槽写入事件,启动关键恢复状态机。

快速路径不应该做什么

不要在唤醒 ISR 或恢复桩中完成以下工作:

  • 扫描全部 I2C 地址并同步等待超时。
  • 初始化文件系统、遍历目录或校验整个资源分区。
  • 完整启动无线协议栈并等待入网。
  • 执行 Codec 全寄存器配置、长时间校准或播放完整提示音。
  • 刷新整个显示帧、加载字体和图片。
  • 动态申请大块内存、打印大量日志或等待互斥锁。
  • 直接调用可能睡眠、可能重入或依赖调度器的普通驱动 API。

快速路径的目标是“可靠捕获并启动恢复”,不是“把整个系统一次性恢复完”。

唤醒原因管理

唤醒事件记录

建议在 retention RAM、备份寄存器或 AON SRAM 中维护固定格式的唤醒记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief Retained wakeup record shared by the low-power entry and resume paths.
*/
typedef struct
{
uint32_t magic;
uint16_t version;
uint16_t size;
uint32_t boot_sequence;
uint32_t sleep_sequence;
uint64_t sleep_enter_time;
uint64_t wake_capture_time;
uint32_t raw_wakeup_flags;
uint32_t normalized_reason_mask;
uint32_t gpio_snapshot;
uint32_t pending_event_mask;
uint32_t context_generation;
uint32_t crc32;
} pm_retention_record_t;

记录中应区分:

  • 原始硬件标志:便于分析 PMU、RTC、GPIO、通信外设的真实状态。
  • 归一化原因:例如 BUTTONTIMERSENSORCHARGERNETWORKWATCHDOGBROWNOUT
  • 事件快照:GPIO 电平、计数器、边沿状态、外部 PMIC 原因寄存器。
  • 序列号:区分本次休眠、重复中断、复位恢复和旧记录。
  • 版本与 CRC:防止固件升级或掉电造成结构不兼容和脏数据。

先读后清

常见故障是恢复代码过早清除唤醒标志,随后驱动初始化又改变 GPIO 或外设寄存器,导致真实原因丢失。正确顺序一般是:

1
2
3
4
5
6
7
读取全部相关唤醒寄存器
读取必要 GPIO/RTC/PMIC 快照
写入 retention record
执行内存屏障
再次确认需要保持的锁存位
按硬件规定清除中断/唤醒标志
发布软件事件

不同芯片对清除顺序、读清零、写 1 清零和跨域同步有不同要求,平台后端必须封装,不能让业务层直接操作寄存器。

去抖与事件确认

按键、门磁、霍尔和机械触点可能在唤醒时产生多次边沿。快速路径只需完成“首个事件锁存”和最小合法性判断;完整去抖可以在稳定时钟恢复后进行。

一个通用策略是:

  1. AON 域锁存首边沿与时间戳。
  2. 快速路径读取当前电平并创建候选事件。
  3. 启动低功耗定时器或短延时工作项。
  4. 到期后再次采样,确认事件类型。
  5. 使用 sleep_sequence + source_id + edge_timestamp 去重。
  6. 对持续低电平唤醒源启用屏蔽或电平反转,防止刚入睡又立即被唤醒。

上下文保留不是简单地 memcpy

上下文分级

建议按恢复价值和一致性要求把上下文分为四类:

类别 示例 保存位置 恢复方式
必须保留 唤醒原因、事务序列号、关键状态机阶段 备份寄存器/AON RAM 恢复桩直接读取
快速恢复 当前页面、音量、连接状态摘要、传感器基线 retention RAM 版本和 CRC 校验后加载
可重建 缓存、索引、统计窗口、非关键队列 普通 SRAM 或外部存储 后台重建
不应保留 指向掉电内存的裸指针、锁拥有者、DMA 活动描述符 不保存 重新初始化并显式失效

不能把整个 SRAM 原样保存后直接恢复,因为内存中可能包含外设寄存器影子、过期指针、锁状态、正在进行的 DMA、旧定时器和与固件版本绑定的数据结构。恢复时应使用有版本的逻辑状态,而不是盲目恢复物理运行现场。

提交式保存

上下文保存应采用双槽或提交标志,避免保存过程中掉电留下“看似有效”的半份数据:

1
2
3
slot A: header + payload + CRC + generation
slot B: header + payload + CRC + generation
commit_marker: active_slot + generation + inverse

保存流程:

  1. 选择非活动槽。
  2. 写入头部和 payload。
  3. 计算并写入 CRC。
  4. 执行缓存清理和内存屏障。
  5. 最后更新提交标志。

恢复时选择 CRC 正确且 generation 最新的槽。对极小 MCU,可把必须保留内容压缩到固定结构中,但仍建议包含 magic、version、length、generation 和 CRC。

设备依赖图与分阶段恢复

设备不能按“驱动注册顺序”随意恢复。典型依赖关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart LR
P[电源轨 / PMIC] --> X[晶振 / PLL]
X --> C[时钟树]
C --> B[AHB/APB/总线控制器]
B --> G[GPIO / Pinmux]
B --> D[DMA]
B --> I[I2C/SPI/UART 控制器]
G --> I
I --> S[传感器 / Codec / PMIC 子设备]
D --> S
S --> U[音频 / 显示 / 采集服务]
C --> T[系统定时器 / Tick]
T --> K[RTOS 调度与超时]
K --> U

恢复顺序至少要满足:父电源域先于子设备、时钟先于依赖时钟的外设、Pinmux 先于总线事务、总线控制器先于总线设备、内存和 DMA 描述符先于 DMA、系统时间恢复先于依赖绝对超时的协议。

关键设备集合

对每类唤醒事件预先定义最小关键设备集合:

唤醒类别 关键集合 可延后集合
按键/触摸 AON GPIO、低速时钟、输入状态机、最小反馈设备 全量显示、音频、无线、日志
RTC 采样 RTC、传感器电源、I2C/SPI、ADC/DMA、存储队列 UI、音频、网络
网络事件 高频时钟、网络控制器、协议定时器、必要密钥状态 显示、非关键传感器
告警输入 GPIO/比较器、时间戳、告警输出、关键通信 统计、日志整理、UI 动画
充电/电源事件 PMIC、ADC、充电状态机、热保护 用户界面、同步服务

关键集合可以通过依赖图自动展开。例如业务声明需要 Codec,系统自动加入其电源轨、MCLK、I2C 控制器、Pinmux 和 DMA,而不是由业务代码逐个调用初始化函数。

设备状态机

每个可管理设备建议至少支持:

1
2
3
4
OFF -> POWERING -> RESETTING -> CONFIGURING -> READY
READY -> QUIESCING -> SUSPENDED
SUSPENDED -> RESUMING -> READY
any state -> ERROR

驱动接口应区分:

1
2
3
4
5
6
prepare_suspend()   检查是否可挂起,可拒绝
suspend() 保存必要状态并关闭设备
resume_early() 恢复关键寄存器或唤醒能力
resume() 恢复普通功能
resume_deferred() 校准、缓存重建、耗时检查
abort_suspend() 休眠事务中止后的回滚

只有一个 resume() 回调通常不够,因为它会迫使所有恢复工作串行执行。

系统休眠应作为事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
stateDiagram-v2
[*] --> Awake
Awake --> Evaluating: idle candidate
Evaluating --> Preparing: policy selects state
Preparing --> Aborting: module rejects / event arrives
Preparing --> Committing: all prepare callbacks pass
Committing --> Sleeping: wake sources armed + context committed
Sleeping --> ResumingEarly: wakeup
ResumingEarly --> ResumingCritical: reason captured
ResumingCritical --> Awake: critical service ready
ResumingCritical --> ResumingDeferred: background work queued
ResumingDeferred --> Awake: all optional work complete
Aborting --> Awake: rollback

两阶段提交

休眠入口可以借鉴事务的两阶段思想:

  1. Prepare 阶段:冻结新请求,询问所有相关模块是否允许进入目标状态,等待可控的在途事务完成,配置候选唤醒源,但不真正切断关键资源。
  2. Commit 阶段:保存上下文,确认没有新事件,原子地标记休眠事务已提交,关闭设备和时钟,执行 WFI/WFE 或平台休眠指令。

如果 Prepare 期间任何模块返回忙、发生新唤醒事件、下一超时点改变或状态选择失效,应执行 abort_suspend(),恢复已修改的设备。不要在半数设备已关闭后直接返回普通运行,否则系统会处于不可预测状态。

最后时刻检查

进入休眠前必须在受控临界区内再次检查:

1
2
3
4
5
6
7
是否已有 pending IRQ
是否出现更早的软件定时器
是否新增 PM lock/latency request
唤醒源是否已正确使能
唤醒标志是否已清理到预期状态
关键写缓冲、缓存和外设事务是否已完成
目标状态是否仍满足约束

典型竞态是:策略判断可以睡,随后按键中断到来并被置 pending,但代码仍执行 WFI;如果唤醒事件被错误清除或中断屏蔽顺序不当,可能造成丢事件或立即睡死。平台入口应使用芯片推荐的原子序列,并在关闭中断与执行休眠指令之间避免普通业务代码。

PM 约束、锁和引用计数

延迟约束

模块不应直接指定具体休眠模式,而应声明最大可接受退出延迟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief Acquire a wakeup latency constraint.
*
* @param max_exit_latency_us Maximum acceptable system exit latency.
* @param owner Static owner identifier used for diagnostics.
* @return Constraint handle, or an invalid handle on failure.
*/
pm_constraint_handle_t pm_latency_acquire(uint32_t max_exit_latency_us,
const char *owner);

/**
* @brief Release a previously acquired wakeup latency constraint.
*
* @param handle Constraint handle returned by pm_latency_acquire().
*/
void pm_latency_release(pm_constraint_handle_t handle);

系统聚合时取最小值。这与 Linux PM QoS 和 Zephyr latency request 的思想一致:模块表达性能期望,策略负责把期望映射为允许的状态。

状态锁

对 DMA、Flash 擦写、协议时隙等“绝对不能进入某类状态”的区间,可使用状态锁:

1
2
3
4
5
6
PM_LOCK_NO_IDLE
PM_LOCK_NO_LIGHT_SLEEP
PM_LOCK_NO_DEEP_SLEEP
PM_LOCK_KEEP_HF_CLOCK
PM_LOCK_KEEP_BUS
PM_LOCK_KEEP_MEMORY_BANK

锁必须成对获取和释放,并记录 owner、调用位置、获取时间和超时诊断。低功耗系统无法进入深睡的常见原因不是策略错误,而是某个异常路径漏释放锁。

设备运行时引用

设备应使用 get/put 或 usage count,而不是业务随意调用 power_on()/power_off()

1
2
3
4
device_pm_get(dev)     usage_count++,必要时恢复设备
device_pm_put(dev) usage_count--,为 0 后允许自动挂起
device_pm_get_async() 异步恢复,完成后通知
device_pm_mark_busy() 延后 autosuspend 截止时间

引用计数解决“多个模块共享一个 I2C 控制器、SPI Flash、电源轨或时钟”的问题。只有所有使用者都释放后,设备才可挂起。

运行时 PM 与系统级 PM 应分离

系统级休眠不是唯一节能手段。即使 CPU 仍在运行,长期空闲的显示、传感器、Codec、无线、外部 Flash 和高速时钟也应独立进入低功耗状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
flowchart TD
A[系统处于 RUN/IDLE] --> B{设备 usage_count = 0?}
B -->|否| C[保持 ACTIVE]
B -->|是| D[启动 autosuspend 延时]
D --> E{延时内再次使用?}
E -->|是| C
E -->|否| F[运行时 suspend]
F --> G[设备 SUSPENDED]
G --> H{新请求到达}
H -->|同步请求| I[同步 resume]
H -->|异步请求| J[排队 resume + completion]
I --> C
J --> C

这样做有两个好处:

  1. 系统真正进入深睡前,许多设备已经处于稳定挂起状态,系统级 suspend 不必临时串行关闭全部设备。
  2. 业务活跃但只使用少数设备时,仍能获得显著节能收益。

Linux Runtime PM 和 Zephyr Device Runtime PM 都采用使用关系驱动设备挂起/恢复。MCU 系统可以实现简化版本:固定设备表、静态引用计数、可配置 autosuspend 时间和少量回调即可。

自适应选择睡眠深度

基于下一超时点

最基础的方法是读取调度器下一次定时事件:

1
2
3
4
5
6
7
T_next = min(
next_rtos_timeout,
next_protocol_slot,
next_sensor_sample,
next_watchdog_service,
next_custom_deadline
)

T_idle_predicted = T_next - now。Zephyr 的 residency 策略和 FreeRTOS Tickless Idle 都体现了这一思想:系统知道预计空闲时长后,才决定是否停止 Tick 或进入更深状态。

基于交互热度

仅看下一定时器无法预测用户在刚按完一次键后很可能继续操作。可以维护交互热度:

1
heat(t) = heat(t0) * exp(-(t - t0) / tau) + event_weight

当热度较高时,限制在浅睡或降低 autosuspend 激进程度;热度衰减后再允许深睡。无需浮点运算,可用定点衰减或分段计数器实现。

基于迟滞与最小保持时间

如果状态阈值没有迟滞,系统会在 Light Sleep 和 Deep Sleep 之间抖动。建议配置:

1
2
3
4
5
enter_deep_after_idle_ms
stay_shallow_after_wakeup_ms
min_active_hold_ms
min_device_on_time_ms
min_device_off_time_ms

例如刚从深睡恢复后的 500ms 内保持浅睡,以覆盖连续按键;无线接收窗口结束后仍保持高速时钟 2ms,防止立即又收到重传;传感器上电后至少保持到稳定时间结束,避免频繁启停。

基于电量与温度

低电量时可以提高深睡倾向、减少后台恢复;高温时可能降低主频、延后非关键任务。但策略变化不能破坏硬实时约束。电量只应影响“可选动作”和阈值,不应绕过唤醒源、告警和安全控制要求。

异步恢复与就绪通知

业务调用设备时,可能遇到设备仍在恢复。框架应明确提供同步和异步语义:

1
2
3
4
service_get_sync(service, timeout)
service_get_async(service, callback, context)
service_is_ready(service)
service_release(service)

关键路径可用短超时同步等待,非关键任务应异步等待。恢复完成后发布分级事件:

1
2
3
4
5
6
7
PM_EVENT_WAKE_CAPTURED
PM_EVENT_KERNEL_READY
PM_EVENT_CRITICAL_CLOCK_READY
PM_EVENT_SERVICE_READY(service_id)
PM_EVENT_FULLY_RESUMED
PM_EVENT_RESUME_DEGRADED
PM_EVENT_RESUME_FAILED

不要只用一个全局 system_resumed = true,因为它无法表达“系统已执行但 Codec 未就绪”“网络可用但外部 Flash 仍在唤醒”“显示可以首帧但背光尚未渐亮”等状态。

时间基准与 Tick 补偿

深睡时 SysTick、高速定时器和部分外设计数器可能停止。系统必须用休眠期间仍工作的 RTC、低功耗定时器或 AON counter 计算睡眠时间,并更新内核时间。

1
2
3
sleep_ticks = convert_aon_delta_to_os_ticks(wake_count - sleep_count)
os_tick_advance(sleep_ticks)
monotonic_time_adjust(delta)

需要处理:

  • 低速时钟精度和温漂。
  • 计数器回绕。
  • 读计数器的跨域同步。
  • 唤醒延迟是否计入睡眠时间。
  • 相对定时器和绝对定时器的差异。
  • 网络协议、重传和证书时间对时间跳变的敏感性。

FreeRTOS Tickless Idle、RT-Thread PM 时间补偿和 Zephyr 的超时管理都提供了可参考的框架。产品仍需根据硬件低功耗计时器精度进行校准,尤其不能假设标称 32.768kHz 在全温范围内完全准确。

外设恢复策略

PLL 与时钟树

PLL 恢复应分成“最小时钟可执行”和“高性能时钟就绪”两个阶段:

  1. 先用内部 RC 或低速时钟执行恢复桩。
  2. 启动外部晶振和 PLL,但不在 ISR 中忙等完整超时。
  3. 对时钟稳定中断或状态位设置最大等待时间。
  4. 必要时先用降级频率运行关键业务。
  5. PLL 失败时进入可诊断的降级模式,而不是无限等待。
  6. 时钟切换后更新 Flash wait state、总线分频、串口波特率、定时器和采样时钟。

升频和降频顺序必须分别设计。一般升频前先提高供电档位和 Flash wait state,降频后再降低 wait state 和电压,具体以芯片手册为准。

I2C/SPI 设备

外部器件上电后可能需要稳定时间、复位脉冲、设备 ID 检查、寄存器恢复或校准。建议把驱动恢复拆为:

1
2
power_enable -> reset_release -> ready_wait -> essential_config -> ready
-> full_config -> calibrated

关键路径只执行 essential_config。完整寄存器同步、FIFO 清理、自检和校准可以延后。I2C 总线还需处理睡眠期间从设备异常拉低 SDA、主控复位但从设备未复位、时钟频率改变导致时序配置失效等问题。

外部 Flash 与文件系统

外部 Flash 可能处于 Deep Power-Down。恢复时应先退出低功耗、等待器件规定的 tRES,再访问 JEDEC ID 或资源。不要让 UI 首次反馈依赖文件系统挂载和资源加载;常用图标、短提示和关键配置可以保存在内部 Flash、retention RAM 或预解压缓存中。

显示

显示恢复可分为:

  1. 背光保持关闭或低亮。
  2. 恢复显示控制器和最小帧缓冲。
  3. 输出静态首帧或状态图标。
  4. 后台加载字体、图片和复杂页面。
  5. 渐亮背光,避免电流冲击和白屏。

音频

Codec 和功放常有上电时序、偏置建立和 pop noise 风险。快速反馈可以先用低功耗蜂鸣器、片上 PWM 或预留提示,而不是直接在 ISR 中启动完整音频链路。恢复顺序通常涉及 MCLK、I2C 配置、模拟偏置、数字接口、静音解除和功放使能,应通过状态机和定时器推进。

无线通信

无线恢复成本可能远高于 MCU 唤醒本身。应区分:

  • 保持连接的 modem sleep。
  • 控制器保留、主 CPU 休眠。
  • 断开链路后的重连。
  • 深睡后重新初始化射频和协议栈。

如果业务对首包延迟敏感,应在连接参数、监听窗口和系统休眠策略之间协同,而不是只优化 MCU 的 1~2ms 唤醒时间。

并发、竞态与幂等性

重复恢复

多个唤醒源可能同时触发,或者一个事件在硬件和软件层重复上报。恢复入口必须使用原子状态防止重复执行:

1
SLEEPING -> RESUMING_EARLY -> RESUMING_CRITICAL -> AWAKE

只有成功完成状态转换的执行单元负责全局恢复;其他路径只合并新的唤醒原因。恢复函数应尽量幂等:设备已经 READY 时重复 resume 不应再次复位或破坏状态。

休眠期间新请求到达

Prepare 阶段冻结普通设备请求后,新请求可以:

  1. 设置 abort_requested 并等待事务回滚。
  2. 被放入 pending queue,唤醒后立即处理。
  3. 对极高优先级请求直接中止休眠。

不能让新请求在设备关闭一半时直接进入普通驱动路径。

锁顺序

PM 框架容易与驱动锁、总线锁、调度器锁形成死锁。建议:

  • PM core 不在持有全局锁时调用可能阻塞的驱动回调。
  • Prepare 回调只做短检查和状态切换,不等待未知时长。
  • 驱动恢复使用统一依赖顺序。
  • ISR 不获取普通互斥锁。
  • 完成事件使用静态对象或无锁标志。
  • 任何同步等待都必须有超时和失败路径。

失败、降级与回退

唤醒并不保证成功。应为每个阶段定义超时和降级:

失败点 处理策略
外部晶振/PLL 未锁定 使用内部时钟降级运行,记录故障,限制高带宽功能
I2C 设备无响应 总线恢复、设备复位、隔离故障设备,不阻塞其他服务
Codec 校准失败 保持静音,使用替代提示或禁用音频
外部 Flash 未就绪 使用内置最小资源,延后重试,避免前台无限等待
retention CRC 错误 丢弃上下文,走冷启动恢复,标记原因
时间补偿异常 使用 RTC 绝对时间校正,重建定时器,记录跳变
关键设备恢复超时 返回 RESUME_DEGRADED 或受控重启
连续唤醒风暴 屏蔽故障源、指数退避、保留诊断快照

系统应区分“可继续运行的降级”和“必须复位的不可恢复错误”。不要为了追求表面成功而吞掉错误;也不要因为一个非关键温湿度传感器失败就重启整个设备。

通用 API 设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* @brief Power state properties used by the policy governor.
*/
typedef struct
{
uint8_t state_id;
uint32_t entry_latency_us;
uint32_t exit_latency_us;
uint32_t min_residency_us;
uint32_t steady_power_uw;
uint32_t wakeup_source_mask;
uint32_t retained_domain_mask;
uint32_t powered_domain_mask;
bool reset_resume;
} pm_state_desc_t;

/**
* @brief Runtime constraints aggregated before selecting a power state.
*/
typedef struct
{
uint32_t max_exit_latency_us;
uint32_t required_wakeup_mask;
uint32_t required_retention_mask;
uint32_t blocked_state_mask;
uint64_t next_deadline_us;
uint32_t activity_heat;
} pm_constraints_t;

/**
* @brief Normalized wakeup event delivered to the application layer.
*/
typedef struct
{
uint32_t sequence;
uint32_t reason_mask;
uint32_t source_id;
uint64_t timestamp_us;
uint32_t raw_flags;
uint32_t snapshot;
} pm_wakeup_event_t;

推荐的框架接口包括:

1
2
3
4
5
6
7
8
9
10
11
12
pm_latency_acquire(max_us, owner)
pm_latency_release(handle)
pm_state_lock_acquire(mask, owner)
pm_state_lock_release(handle)
pm_activity_hint(type, expected_duration_ms)
pm_wakeup_source_enable(source, config)
pm_wakeup_source_disable(source)
pm_service_get_async(service, callback)
pm_service_release(service)
pm_get_last_wakeup_event(event)
pm_get_statistics(stats)
pm_trace_enable(mask)

接口应避免让上层依赖芯片特定模式名。业务说“最大退出延迟 2ms”和“需要 GPIO+RTC 唤醒”,比说“请进入 STOP2”更可移植。

策略伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @brief Select the deepest power state that satisfies all active constraints.
*
* @param now_us Current monotonic time.
* @param constraints Aggregated runtime constraints.
* @return Selected power state descriptor.
*/
static const pm_state_desc_t *pm_policy_select(uint64_t now_us,
const pm_constraints_t *constraints)
{
uint64_t idle_us = 0U;

if (constraints->next_deadline_us > now_us)
{
idle_us = constraints->next_deadline_us - now_us;
}

for (size_t i = 0U; i < g_state_count; ++i)
{
const pm_state_desc_t *state = &g_states_deep_to_shallow[i];
uint64_t required_residency;

if ((constraints->blocked_state_mask & (1UL << state->state_id)) != 0U)
{
continue;
}

if (state->exit_latency_us > constraints->max_exit_latency_us)
{
continue;
}

if ((constraints->required_wakeup_mask & ~state->wakeup_source_mask) != 0U)
{
continue;
}

if ((constraints->required_retention_mask & ~state->retained_domain_mask) != 0U)
{
continue;
}

required_residency = (uint64_t)state->entry_latency_us +
(uint64_t)state->exit_latency_us +
(uint64_t)state->min_residency_us +
pm_policy_guard_time_us(state, constraints);

if (idle_us < required_residency)
{
continue;
}

if (!pm_energy_model_is_profitable(state, idle_us))
{
continue;
}

return state;
}

return &g_run_or_idle_state;
}

真实实现还需加入:关键服务恢复时间、当前活动热度、设备 autosuspend 状态、平台 errata、温度、电量、最近恢复失败、唤醒源电平和系统安全策略。

进入休眠流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
sequenceDiagram
participant Idle as Idle Task
participant Policy as PM Policy
participant Tx as PM Transaction
participant Dev as Devices
participant SoC as SoC Backend

Idle->>Policy: next deadline + active constraints
Policy-->>Idle: selected state
Idle->>Tx: begin(state)
Tx->>Tx: freeze new PM-sensitive requests
Tx->>Dev: prepare_suspend(state)
alt any device rejects
Dev-->>Tx: busy/error
Tx->>Dev: abort_suspend()
Tx-->>Idle: aborted
else all prepared
Tx->>Tx: save retention context
Tx->>SoC: arm wake sources
Tx->>Tx: final pending-event check
alt event pending
Tx->>Dev: abort_suspend()
Tx-->>Idle: retry later
else commit
Tx->>Dev: suspend in dependency order
Tx->>SoC: enter low-power state
SoC-->>Tx: wakeup
Tx->>Tx: capture wake reason
Tx->>Dev: resume critical dependency set
Tx-->>Idle: service ready
end
end

开源方案原理详解

Zephyr:状态属性、驻留策略和延迟请求

Zephyr 的系统电源管理把电源状态描述为带属性的对象,其中包括最小驻留时间和退出延迟。其 residency 思路是:只有“到下一调度事件的时间”足以覆盖最小驻留与退出延迟时,才选择该状态。Zephyr 还提供 latency request,使应用或驱动注册最大可接受退出延迟;策略不能选择超过该限制的状态。

可借鉴点:

  1. 状态属性数据化,不把判断散落在业务代码中。
  2. 策略函数与平台进入低功耗的实现分离。
  3. 允许应用增加自定义下一事件时间来源。
  4. 延迟约束由多个模块注册,系统统一聚合。
  5. 系统级 PM 与设备 PM 分开,但可以协同。

在小型 RTOS 中,可以用静态状态数组和少量约束槽实现同样思想,不必复制 Zephyr 的全部框架。

Zephyr Device Runtime PM:设备使用计数

Zephyr Device Runtime PM 允许驱动、子系统和应用通过使用关系决定设备是否保持活动。第一个使用者到来时恢复设备,最后一个使用者离开时允许挂起,并支持同步或异步操作。

可借鉴点:

  • 共享设备使用引用计数。
  • 设备状态明确区分 ACTIVE、SUSPENDING、SUSPENDED。
  • 系统进入低功耗前,不必重复挂起已经 runtime suspended 的设备。
  • 高层不必了解设备具体寄存器,只声明使用关系。

Linux cpuidle 与 PM QoS:治理器不越过延迟上限

Linux cpuidle 通过 governor 在多个 CPU idle state 中选择状态。状态具有退出延迟和目标驻留特征,governor 必须考虑 PM QoS 的 CPU 延迟约束,不应选择退出延迟超过有效约束的状态。

可借鉴点:

  • 策略和驱动分离:governor 决策,driver 执行平台状态。
  • 约束聚合取最严格值。
  • 选择结果可以根据实际驻留和命中情况反馈调整。
  • “最省电状态”必须服从延迟服务质量。

Linux Runtime PM:设备级自动挂起

Linux Runtime PM 为设备提供 runtime_suspendruntime_resumeruntime_idle 等回调,并由 PM core 维护状态和同步。设备可以根据 usage count 和 autosuspend 策略独立挂起,而不是等待系统级 suspend。

可借鉴点:

  • 设备电源状态由统一核心管理。
  • 自动挂起需要防止并发恢复和重复挂起。
  • 设备父子关系、总线和 PM domain 影响回调顺序。
  • runtime PM 与 system sleep 交互时要同步真实硬件状态。

Linux wakeup source:唤醒能力与是否允许唤醒分离

一个设备“硬件上能唤醒系统”与“当前策略允许它作为唤醒源”是两个不同概念。产品中也应区分:

1
2
3
4
wakeup_capable   硬件和驱动是否支持
wakeup_enabled 当前模式是否允许
wakeup_armed 本次休眠是否已经配置成功
wakeup_pending 是否已有待处理事件

这可以避免把所有可唤醒设备永久开启,导致漏电或唤醒风暴,也便于按场景切换允许的唤醒源。

FreeRTOS Tickless Idle:停止无意义周期唤醒

普通 RTOS Tick 会周期性唤醒 CPU,即使没有任务可运行。FreeRTOS Tickless Idle 在预计空闲期间停止周期 Tick,设置能覆盖下一任务超时的低功耗定时器,唤醒后校正内核 Tick。

可借鉴点:

  • 休眠深度选择必须知道预计空闲时长。
  • 睡眠前后要处理 Tick 抑制和时间补偿。
  • 外部中断提前唤醒时,补偿实际睡眠时间而不是计划时间。
  • Tickless 只解决周期唤醒,不自动解决设备依赖和深睡上下文。

RT-Thread PM:模式投票、运行频率与设备回调

RT-Thread PM 将运行模式与睡眠模式分开,并通过请求/释放机制限制系统进入过深状态。应用或设备请求某个模式后,只有释放对应请求,系统才可能进入更低功耗模式。框架还支持设备 suspend/resume、频率变化通知和睡眠时间补偿。

可借鉴点:

  • 投票接口简单,适合资源受限系统。
  • 请求和释放必须成对,可用于保护 DMA 或关键事务。
  • 运行时降频与休眠是两个独立维度。
  • 外设对频率变化敏感,必须重新计算波特率、定时器和采样参数。

需要额外补强的部分是:为投票增加 owner、超时、泄漏诊断和延迟数值约束,避免只有离散模式请求导致策略过于粗糙。

Apache NuttX PM:活动上报、PM Domain 与回调

NuttX PM 允许驱动上报活动,并按 domain 管理不同区域,例如 UI 域和网络域。状态改变前,驱动的 prepare 回调可以拒绝转换;统一状态改变后通过 notify 回调通知驱动。

可借鉴点:

  • 活动本身可以影响低功耗策略。
  • 多 PM domain 适合独立电源岛或功能域。
  • Prepare 与 Notify 分开,便于拒绝和回滚。
  • 平台 idle loop 保留最终决策权,可结合电量、硬件状态等信息。

ESP-IDF:状态保留域和多唤醒源

ESP-IDF 清晰区分 Light Sleep 与 Deep Sleep:前者通常保留更多数字状态,后者关闭更多 CPU、RAM 和数字外设,只保留 RTC 控制器、RTC 外设和可配置的 RTC memory。系统可配置定时器、GPIO 等多种唤醒源。

可借鉴点:

  • 休眠模式本质上是电源域、时钟域、内存保留和唤醒源的组合。
  • 深睡恢复可能接近冷启动,应用必须显式保存逻辑上下文。
  • AON/RTC 域适合完成事件捕获、计时和极小状态机。
  • 多唤醒源需要统一原因归一化和去重。

方案选型建议

系统类型 推荐起点 重点补充
裸机 MCU 静态状态表 + PM lock + AON 唤醒记录 + 分阶段初始化 原子休眠入口、时间补偿、设备依赖
FreeRTOS Tickless Idle + 自定义 PM governor + 设备引用计数 延迟约束、系统级事务、后台恢复
RT-Thread 原生 PM 模式请求/释放 + PM device owner 诊断、数值延迟约束、关键服务集合
Zephyr System PM + latency constraints + Device Runtime PM 产品能量模型、恢复服务分级、故障降级
Apache NuttX PM domain + activity + prepare/notify 量化退出延迟、能量盈亏、异步恢复
Linux SoC cpuidle + PM QoS + Runtime PM + wakeup source 用户态服务启动顺序、设备树/驱动依赖、整机 trace
ESP-IDF Light/Deep Sleep + wakeup source + RTC memory 逻辑上下文版本化、连接保持策略、恢复长尾

容量与资源预算

低功耗框架本身也要控制资源。小型 MCU 可以采用固定容量:

1
2
3
4
5
6
7
PM constraint slots       8~32 个
PM lock records 8~32 个
Wake event queue 4~16 项
Device descriptors 按静态设备数配置
Retention record 64B~1KB
Trace ring 1KB~8KB,可裁剪
Deferred work items 按关键设备并发度配置

所有关键路径对象应静态分配。Trace ring 可以使用定长记录:

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief Compact power-management trace entry.
*/
typedef struct
{
uint32_t timestamp_low;
uint16_t event_id;
uint8_t state_id;
uint8_t cpu_or_domain;
uint32_t argument;
} pm_trace_entry_t;

在极小系统中,即使只能保留 64 条记录,也足以定位“为什么没睡”“为何被唤醒”“哪个设备恢复超时”。

可观测性与统计

至少统计以下指标:

1
2
3
4
5
6
7
8
9
10
11
12
每个状态的进入次数
每个状态的累计驻留时间
计划驻留时间与实际驻留时间误差
各唤醒源次数、误唤醒次数和唤醒风暴次数
休眠 prepare 拒绝次数及 owner
PM lock 当前持有者与最长持有时间
T_irq / T_capture / T_ack / T_service / T_full 的分布
各设备 suspend/resume 平均值、P99、最大值和失败数
PLL、外部晶振、总线、Flash、无线等恢复超时数
retention CRC 或版本失败数
降级运行和受控重启次数
每次交互或采样事务的估算能量

推荐通过 GPIO 打点配合逻辑分析仪或示波器:

1
2
3
4
5
6
GPIO_A: 唤醒源物理边沿
GPIO_B: Resume Stub 开始
GPIO_C: Wake reason captured
GPIO_D: First ACK
GPIO_E: Critical service ready
GPIO_F: Full resume complete

这样可以把软件 trace 与硬件波形对齐。只看串口日志会受到 UART 尚未恢复、日志缓冲和调度延迟影响,不能作为唯一的时序依据。

测试矩阵

功能测试

  1. 对每个休眠状态验证所有声明的唤醒源。
  2. 验证不允许的唤醒源不会错误唤醒。
  3. 验证多个唤醒源同时触发时原因不会丢失。
  4. 验证 retention 正常、CRC 错误、版本变化和全丢失路径。
  5. 验证每个设备的 suspend、resume、abort 和重复调用。
  6. 验证睡眠期间 RTC、相对超时和绝对时间补偿。
  7. 验证深睡后冷启动式恢复与浅睡状态保留式恢复。

竞态测试

  1. 在 Prepare、Commit 前、WFI 前、刚唤醒、关键恢复和后台恢复各阶段注入中断。
  2. 在设备 autosuspend 与新请求同时发生时重复压测。
  3. 随机打断 PLL、I2C、SPI、Flash、无线恢复步骤。
  4. 强制 PM lock 泄漏,验证诊断和 watchdog 策略。
  5. 重复触发电平型唤醒源,验证不会形成无限唤醒循环。
  6. 在日志写入、Flash 擦除、DMA 和协议时隙期间请求深睡。

时序测试

对每类唤醒事件至少测量平均值、P95、P99、P99.9 和最大值。测试温度、电压、电量、不同晶振启动条件、存储忙状态和系统负载。不能只在实验室室温和空载下测一次。

能耗测试

  1. 测量每个状态的稳态电流。
  2. 测量进入和退出波形的能量积分。
  3. 测量一次完整用户交互或采样事务的能量,而不只看待机电流。
  4. 比较不同阈值下的日均能耗和延迟违约率。
  5. 统计误唤醒和过早唤醒造成的能量损失。
  6. 验证深睡阈值是否高于实测盈亏平衡时间。

常见反模式

反模式一:每次 Idle 都进入最深睡眠

问题:忽略进入/退出能量、恢复延迟、交互连续性和设备校准成本。短空闲会频繁抖动,实际功耗可能更高。

改进:使用预计空闲时间、盈亏平衡点、迟滞和交互热度选择状态。

反模式二:唤醒后按固定顺序初始化所有设备

问题:关键事件被显示、日志、网络和非关键传感器阻塞,恢复时间随设备数量线性增长。

改进:按唤醒事件定义关键设备集合,依赖图展开,后台异步恢复剩余设备。

反模式三:在 ISR 中完成完整恢复

问题:中断延迟不可控、可能等待锁或超时,影响其他实时任务并增加死锁风险。

改进:ISR 只捕获原因和发布静态事件,复杂恢复交给高优先级线程或工作队列。

反模式四:只保存变量,不保存版本和一致性信息

问题:固件升级、掉电或保存中断后,旧结构可能被误当作有效上下文。

改进:使用 magic、version、length、generation、CRC 和提交标志。

反模式五:模块直接指定芯片休眠模式

问题:业务与芯片绑定,多个模块互相覆盖,无法统一权衡。

改进:模块声明延迟、唤醒源、保留域、设备使用和活动期限,由 governor 选择模式。

反模式六:只看平均唤醒时间

问题:长尾由 PLL、总线超时、Flash、无线和任务抢占触发,用户真正感知的是偶发慢响应。

改进:测量 P99/P99.9、最大值和分阶段耗时,并设置每阶段超时。

反模式七:PM lock 没有 owner

问题:系统无法进入深睡时无法定位是谁持锁,最终只能关闭低功耗功能。

改进:记录 owner、获取时间、调用点、嵌套计数和最大允许持有时间。

反模式八:把设备恢复成功等同于业务恢复成功

问题:驱动 READY 不代表协议会话、缓存、页面或控制状态机已经一致。

改进:分离设备就绪、服务就绪和业务状态恢复事件。

一套可落地的最小实现

对于资源有限、现有工程尚无 PM 框架的 MCU,可以按以下顺序增量实现:

  1. 建立统一 pm_state_desc 状态表,记录退出延迟、最小驻留、保留域和唤醒源。
  2. 把 RTOS Idle Hook 或空闲线程接入统一 pm_policy_select()
  3. 实现下一超时点计算和 Tickless/低功耗定时器补偿。
  4. 实现固定容量 PM lock,并提供 owner 诊断。
  5. 在 retention 区保存唤醒原因、序列号、时间戳、版本和 CRC。
  6. 把唤醒 ISR 改为只锁存事件,创建高优先级 Fast Resume Worker。
  7. 为关键设备拆分 resume_earlyresumeresume_deferred
  8. 建立静态设备依赖表,至少保证电源、时钟、总线和子设备顺序正确。
  9. 对共享设备增加 usage count 和 autosuspend。
  10. 增加 GPIO 打点与定长 PM trace ring。
  11. 实测每个状态的稳态功耗、切换能量和延迟,计算盈亏平衡阈值。
  12. 最后再引入交互热度、动态阈值、分电源域和预测策略。

这条路径避免一开始就构造过度复杂的通用框架,同时保留后续演进空间。

最终回答组织方式

回答这类题时,可以按以下顺序展开:

  1. 先指出核心矛盾:最深睡眠降低稳态功耗,但增加进入/退出成本、上下文丢失和恢复长尾,因此不能按单一模式解决。
  2. 给出总体原则:延迟约束、驻留时间、能量盈亏、唤醒源和设备依赖共同决定状态。
  3. 说明快速路径与完整恢复路径:先捕获事件和提供最小反馈,再恢复关键服务,最后后台恢复非关键设备。
  4. 说明系统休眠是事务:Prepare、Commit、Abort、Resume,处理并发和最后时刻竞态。
  5. 说明设备管理:运行时引用计数、autosuspend、依赖图和分阶段回调。
  6. 给出量化公式:退出延迟预算、最小驻留、能量盈亏平衡和安全余量。
  7. 对照开源实现:Zephyr latency/residency、Linux cpuidle/PM QoS/Runtime PM、FreeRTOS Tickless、RT-Thread 投票、NuttX PM domain、ESP-IDF 保留域。
  8. 最后补充可观测性、失败降级和测试矩阵,证明方案可实现、可验证、可维护。

一句话概括:低功耗系统不应在“最省电”和“最快唤醒”之间二选一,而应通过可量化的状态模型、业务约束聚合、休眠事务、事件锁存、关键服务分阶段恢复和在线反馈,让系统在每一次具体空闲窗口中选择最合适的能耗—延迟工作点。

参考链接