嵌入式面试真题第 17 题:资源受限系统中的分层低功耗与低延迟唤醒架构设计
问题
在电池供电、能量采集供电或热设计受限的嵌入式设备中,系统通常同时存在多级 CPU/SoC 低功耗状态、多个可独立关断的电源域,以及时钟、存储、传感器、显示、音频、无线通信和外部协处理器等设备。不同休眠状态的静态功耗、进入时间、退出时间、上下文保留能力、可用唤醒源和恢复复杂度差异很大:浅睡眠退出快但待机功耗较高,深睡眠功耗低但可能关闭 PLL、主 SRAM、总线、外设或整个数字域,唤醒后甚至需要从复位入口重新启动。
业务侧又可能同时提出多种约束:按键、触摸、旋钮、门磁、佩戴检测等人机事件要求快速反馈;采样、控制、告警和协议时隙要求确定性响应;网络、音频、显示和传感器只需在需要时恢复;后台日志、统计、同步和自检可以延后。系统还可能频繁出现短空闲、突发交互、定时任务、通信重传、传感器中断和误触发。如果每次空闲都进入最深休眠,恢复成本、唤醒抖动和设备状态错乱可能抵消节能收益;如果长期停留在浅睡眠,又无法满足续航目标。
你会如何设计一套通用的低功耗与唤醒管理框架,在不绑定某一款 MCU、某一种外设或某一个业务场景的前提下,综合处理以下问题:
- 如何根据预计空闲时间、唤醒延迟预算、能量收益、设备依赖和唤醒源能力选择休眠深度?
- 如何把唤醒拆分为快速响应路径和完整恢复路径,使关键事件先被可靠捕获并得到及时反馈,而不是等待全部设备串行初始化?
- 如何保存和校验上下文,协调 PLL、总线、存储、I2C/SPI 设备、显示、音频、无线和后台任务的恢复顺序?
- 如何处理多模块并发请求、禁止休眠区间、超时、误唤醒、重复事件、进入休眠与中断同时发生等竞态?
- 如何量化能耗与响应延迟,设计可测试、可观测、可回退的策略,并借鉴 Linux、Zephyr、FreeRTOS、RT-Thread、Apache NuttX、ESP-IDF 等开源实现?
回答
结论:低功耗设计的核心不是“系统一空闲就进入最深睡眠”,而是建立一套受延迟约束、受驻留时间约束、按电源域分层、支持分阶段恢复、由业务意图驱动并可在线度量的电源管理框架。
系统应把休眠与唤醒拆成四个相互解耦的层次:
- 策略层根据预计空闲时间、当前延迟约束、设备使用计数、唤醒源、能量模型和历史行为选择最深但仍满足约束的状态。
- 事务层把一次休眠视为可提交、可拒绝、可中止、可回滚的系统事务,统一处理模块投票、设备依赖、上下文保存和竞态。
- 快速唤醒层只完成事件锁存、时钟最小恢复、关键状态机推进和首个可感知反馈,目标是尽快达到“事件已确认”或“最小业务可用”。
- 后台恢复层按依赖图异步恢复非关键设备、重建连接、执行校准、补偿时间、刷新界面和恢复日志,避免阻塞关键路径。
因此,用户或控制环真正关心的不是“所有设备是否已经恢复”,而是几个不同的时间点:
1 | T_irq = 唤醒源触发到 CPU 开始执行唤醒入口的时间 |
系统设计应优先约束 T_capture、T_ack 和 T_service,而不是强行让 T_full 等于 T_ack。一个按键可以在主 PLL、Codec、外部 Flash 和无线协议栈完全恢复前先被记录并点亮低功耗 LED;一个工业告警可以先锁存故障、拉高告警输出并更新时间戳,再恢复网络上传;一个周期采样任务可以只恢复低速时钟、ADC 和 DMA,而不必唤醒显示与音频域。
总体架构
1 | flowchart TD |
这个架构中,应用不直接决定“立刻进 Deep Sleep”,驱动也不各自无序地关闭时钟。上层只描述意图和约束,例如“未来 5ms 内不能接受超过 100us 的退出延迟”“SPI DMA 传输期间禁止关闭该总线”“当前交互可能持续 3s”“网络下一次监听窗口在 120ms 后”。策略层把这些约束聚合后选择状态;事务层协调设备;平台后端完成具体寄存器操作;唤醒后快速路径和后台路径分开执行。
机制与开源实现的对应关系
| 本文机制 | Linux 或开源机制 | 能否直接使用 | 主要参考价值 |
|---|---|---|---|
| 多级休眠状态与驻留时间 | Zephyr System Power Management、Linux cpuidle |
对应系统可直接使用;裸机或其他 RTOS 主要参考 | 每个状态定义最小驻留时间、退出延迟,策略只选择满足下一事件时间窗口的状态。 |
| 唤醒延迟约束 | Zephyr PM policy latency、Linux PM QoS | Linux/Zephyr 可直接使用接口 | 多个模块提交最大可接受退出延迟,系统取最严格值,禁止进入过深状态。 |
| 设备运行时电源管理 | Zephyr Device Runtime PM、Linux 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 域确认,再决定是否启动主系统。只有先定义“事件所需的最小服务集合”,才能把恢复路径真正并行化和分阶段化。
电源状态模型
不要只用 ACTIVE、SLEEP 两个枚举。通用框架至少应为每个系统状态记录以下属性:
1 | state_id |
一个抽象状态表可以如下设计:
| 状态 | 典型硬件行为 | 上下文 | 退出延迟 | 适用场景 |
|---|---|---|---|---|
| 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 | L_exit_worst(s) + L_critical_restore(s, event_class) <= L_budget_effective |
其中:
L_budget_effective是所有活动模块提交的延迟约束中的最小值。event_class表示当前最可能或必须支持的唤醒类别,不同事件的关键恢复集合不同。T_guard用于覆盖时钟漂移、调度抖动、IRQ 屏蔽窗口和估计误差。NoUnreleasedLockBlocks(s)表示 DMA、Flash 擦写、总线事务、关键区或协议时隙没有禁止该状态。
由浅到深还是由深到浅
实现上建议从“最深状态”向“最浅状态”筛选,第一个满足所有约束的状态即为候选。这样策略的含义清晰:系统始终尝试在约束内获得最大节能收益,而不是靠大量业务分支手工选择模式。
1 | for state in states_sorted_by_power_ascending: |
但最终选择不能只看静态功耗,还必须看一次进入和退出的固定成本。
能量盈亏平衡点
深睡并非时间越短越省电。一次状态切换包含保存上下文、关时钟、配置唤醒源、进入硬件模式、退出、锁定 PLL、恢复内存和设备等成本。
令:
1 | P_shallow = 浅状态稳态功耗 |
深睡相对浅睡的净收益近似为:
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 | Cost(s) = E_expected(s) |
其中 lambda、mu、nu 表示产品对延迟违约、恢复失败和频繁校准的惩罚权重。资源受限 MCU 不一定要实现复杂概率模型,但至少应统计“预测空闲时间与实际空闲时间的误差”,动态调整阈值,避免长期使用拍脑袋常量。
延迟预算必须分解
只要求“唤醒小于 20ms”不够,因为无法定位优化对象。应把总预算分配到各阶段:
1 | L_total = L_hw_exit |
例如某类交互的预算可以是:
| 阶段 | 预算 | 说明 |
|---|---|---|
| 硬件退出与低速时钟可用 | 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 | sequenceDiagram |
快速路径应该做什么
快速路径只做具有以下特征的工作:
- 必须在唤醒标志被其他初始化覆盖前完成。
- 执行时间短、上界明确、无需动态分配。
- 不依赖尚未恢复的复杂驱动。
- 失败后能留下可诊断信息。
- 可以安全地重复执行或检测重复执行。
典型动作包括:读取 PMU/RTC/AON 唤醒寄存器,锁存 GPIO 电平与边沿,保存低功耗计数器时间戳,建立最小栈,恢复低速系统时钟,确认 retention record,清除必要的硬件标志,向无锁队列或静态事件槽写入事件,启动关键恢复状态机。
快速路径不应该做什么
不要在唤醒 ISR 或恢复桩中完成以下工作:
- 扫描全部 I2C 地址并同步等待超时。
- 初始化文件系统、遍历目录或校验整个资源分区。
- 完整启动无线协议栈并等待入网。
- 执行 Codec 全寄存器配置、长时间校准或播放完整提示音。
- 刷新整个显示帧、加载字体和图片。
- 动态申请大块内存、打印大量日志或等待互斥锁。
- 直接调用可能睡眠、可能重入或依赖调度器的普通驱动 API。
快速路径的目标是“可靠捕获并启动恢复”,不是“把整个系统一次性恢复完”。
唤醒原因管理
唤醒事件记录
建议在 retention RAM、备份寄存器或 AON SRAM 中维护固定格式的唤醒记录:
1 | /** |
记录中应区分:
- 原始硬件标志:便于分析 PMU、RTC、GPIO、通信外设的真实状态。
- 归一化原因:例如
BUTTON、TIMER、SENSOR、CHARGER、NETWORK、WATCHDOG、BROWNOUT。 - 事件快照:GPIO 电平、计数器、边沿状态、外部 PMIC 原因寄存器。
- 序列号:区分本次休眠、重复中断、复位恢复和旧记录。
- 版本与 CRC:防止固件升级或掉电造成结构不兼容和脏数据。
先读后清
常见故障是恢复代码过早清除唤醒标志,随后驱动初始化又改变 GPIO 或外设寄存器,导致真实原因丢失。正确顺序一般是:
1 | 读取全部相关唤醒寄存器 |
不同芯片对清除顺序、读清零、写 1 清零和跨域同步有不同要求,平台后端必须封装,不能让业务层直接操作寄存器。
去抖与事件确认
按键、门磁、霍尔和机械触点可能在唤醒时产生多次边沿。快速路径只需完成“首个事件锁存”和最小合法性判断;完整去抖可以在稳定时钟恢复后进行。
一个通用策略是:
- AON 域锁存首边沿与时间戳。
- 快速路径读取当前电平并创建候选事件。
- 启动低功耗定时器或短延时工作项。
- 到期后再次采样,确认事件类型。
- 使用
sleep_sequence + source_id + edge_timestamp去重。 - 对持续低电平唤醒源启用屏蔽或电平反转,防止刚入睡又立即被唤醒。
上下文保留不是简单地 memcpy
上下文分级
建议按恢复价值和一致性要求把上下文分为四类:
| 类别 | 示例 | 保存位置 | 恢复方式 |
|---|---|---|---|
| 必须保留 | 唤醒原因、事务序列号、关键状态机阶段 | 备份寄存器/AON RAM | 恢复桩直接读取 |
| 快速恢复 | 当前页面、音量、连接状态摘要、传感器基线 | retention RAM | 版本和 CRC 校验后加载 |
| 可重建 | 缓存、索引、统计窗口、非关键队列 | 普通 SRAM 或外部存储 | 后台重建 |
| 不应保留 | 指向掉电内存的裸指针、锁拥有者、DMA 活动描述符 | 不保存 | 重新初始化并显式失效 |
不能把整个 SRAM 原样保存后直接恢复,因为内存中可能包含外设寄存器影子、过期指针、锁状态、正在进行的 DMA、旧定时器和与固件版本绑定的数据结构。恢复时应使用有版本的逻辑状态,而不是盲目恢复物理运行现场。
提交式保存
上下文保存应采用双槽或提交标志,避免保存过程中掉电留下“看似有效”的半份数据:
1 | slot A: header + payload + CRC + generation |
保存流程:
- 选择非活动槽。
- 写入头部和 payload。
- 计算并写入 CRC。
- 执行缓存清理和内存屏障。
- 最后更新提交标志。
恢复时选择 CRC 正确且 generation 最新的槽。对极小 MCU,可把必须保留内容压缩到固定结构中,但仍建议包含 magic、version、length、generation 和 CRC。
设备依赖图与分阶段恢复
设备不能按“驱动注册顺序”随意恢复。典型依赖关系如下:
1 | flowchart LR |
恢复顺序至少要满足:父电源域先于子设备、时钟先于依赖时钟的外设、Pinmux 先于总线事务、总线控制器先于总线设备、内存和 DMA 描述符先于 DMA、系统时间恢复先于依赖绝对超时的协议。
关键设备集合
对每类唤醒事件预先定义最小关键设备集合:
| 唤醒类别 | 关键集合 | 可延后集合 |
|---|---|---|
| 按键/触摸 | AON GPIO、低速时钟、输入状态机、最小反馈设备 | 全量显示、音频、无线、日志 |
| RTC 采样 | RTC、传感器电源、I2C/SPI、ADC/DMA、存储队列 | UI、音频、网络 |
| 网络事件 | 高频时钟、网络控制器、协议定时器、必要密钥状态 | 显示、非关键传感器 |
| 告警输入 | GPIO/比较器、时间戳、告警输出、关键通信 | 统计、日志整理、UI 动画 |
| 充电/电源事件 | PMIC、ADC、充电状态机、热保护 | 用户界面、同步服务 |
关键集合可以通过依赖图自动展开。例如业务声明需要 Codec,系统自动加入其电源轨、MCLK、I2C 控制器、Pinmux 和 DMA,而不是由业务代码逐个调用初始化函数。
设备状态机
每个可管理设备建议至少支持:
1 | OFF -> POWERING -> RESETTING -> CONFIGURING -> READY |
驱动接口应区分:
1 | prepare_suspend() 检查是否可挂起,可拒绝 |
只有一个 resume() 回调通常不够,因为它会迫使所有恢复工作串行执行。
系统休眠应作为事务处理
1 | stateDiagram-v2 |
两阶段提交
休眠入口可以借鉴事务的两阶段思想:
- Prepare 阶段:冻结新请求,询问所有相关模块是否允许进入目标状态,等待可控的在途事务完成,配置候选唤醒源,但不真正切断关键资源。
- Commit 阶段:保存上下文,确认没有新事件,原子地标记休眠事务已提交,关闭设备和时钟,执行 WFI/WFE 或平台休眠指令。
如果 Prepare 期间任何模块返回忙、发生新唤醒事件、下一超时点改变或状态选择失效,应执行 abort_suspend(),恢复已修改的设备。不要在半数设备已关闭后直接返回普通运行,否则系统会处于不可预测状态。
最后时刻检查
进入休眠前必须在受控临界区内再次检查:
1 | 是否已有 pending IRQ |
典型竞态是:策略判断可以睡,随后按键中断到来并被置 pending,但代码仍执行 WFI;如果唤醒事件被错误清除或中断屏蔽顺序不当,可能造成丢事件或立即睡死。平台入口应使用芯片推荐的原子序列,并在关闭中断与执行休眠指令之间避免普通业务代码。
PM 约束、锁和引用计数
延迟约束
模块不应直接指定具体休眠模式,而应声明最大可接受退出延迟:
1 | /** |
系统聚合时取最小值。这与 Linux PM QoS 和 Zephyr latency request 的思想一致:模块表达性能期望,策略负责把期望映射为允许的状态。
状态锁
对 DMA、Flash 擦写、协议时隙等“绝对不能进入某类状态”的区间,可使用状态锁:
1 | PM_LOCK_NO_IDLE |
锁必须成对获取和释放,并记录 owner、调用位置、获取时间和超时诊断。低功耗系统无法进入深睡的常见原因不是策略错误,而是某个异常路径漏释放锁。
设备运行时引用
设备应使用 get/put 或 usage count,而不是业务随意调用 power_on()/power_off():
1 | device_pm_get(dev) usage_count++,必要时恢复设备 |
引用计数解决“多个模块共享一个 I2C 控制器、SPI Flash、电源轨或时钟”的问题。只有所有使用者都释放后,设备才可挂起。
运行时 PM 与系统级 PM 应分离
系统级休眠不是唯一节能手段。即使 CPU 仍在运行,长期空闲的显示、传感器、Codec、无线、外部 Flash 和高速时钟也应独立进入低功耗状态。
1 | flowchart TD |
这样做有两个好处:
- 系统真正进入深睡前,许多设备已经处于稳定挂起状态,系统级 suspend 不必临时串行关闭全部设备。
- 业务活跃但只使用少数设备时,仍能获得显著节能收益。
Linux Runtime PM 和 Zephyr Device Runtime PM 都采用使用关系驱动设备挂起/恢复。MCU 系统可以实现简化版本:固定设备表、静态引用计数、可配置 autosuspend 时间和少量回调即可。
自适应选择睡眠深度
基于下一超时点
最基础的方法是读取调度器下一次定时事件:
1 | T_next = min( |
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 | enter_deep_after_idle_ms |
例如刚从深睡恢复后的 500ms 内保持浅睡,以覆盖连续按键;无线接收窗口结束后仍保持高速时钟 2ms,防止立即又收到重传;传感器上电后至少保持到稳定时间结束,避免频繁启停。
基于电量与温度
低电量时可以提高深睡倾向、减少后台恢复;高温时可能降低主频、延后非关键任务。但策略变化不能破坏硬实时约束。电量只应影响“可选动作”和阈值,不应绕过唤醒源、告警和安全控制要求。
异步恢复与就绪通知
业务调用设备时,可能遇到设备仍在恢复。框架应明确提供同步和异步语义:
1 | service_get_sync(service, timeout) |
关键路径可用短超时同步等待,非关键任务应异步等待。恢复完成后发布分级事件:
1 | PM_EVENT_WAKE_CAPTURED |
不要只用一个全局 system_resumed = true,因为它无法表达“系统已执行但 Codec 未就绪”“网络可用但外部 Flash 仍在唤醒”“显示可以首帧但背光尚未渐亮”等状态。
时间基准与 Tick 补偿
深睡时 SysTick、高速定时器和部分外设计数器可能停止。系统必须用休眠期间仍工作的 RTC、低功耗定时器或 AON counter 计算睡眠时间,并更新内核时间。
1 | sleep_ticks = convert_aon_delta_to_os_ticks(wake_count - sleep_count) |
需要处理:
- 低速时钟精度和温漂。
- 计数器回绕。
- 读计数器的跨域同步。
- 唤醒延迟是否计入睡眠时间。
- 相对定时器和绝对定时器的差异。
- 网络协议、重传和证书时间对时间跳变的敏感性。
FreeRTOS Tickless Idle、RT-Thread PM 时间补偿和 Zephyr 的超时管理都提供了可参考的框架。产品仍需根据硬件低功耗计时器精度进行校准,尤其不能假设标称 32.768kHz 在全温范围内完全准确。
外设恢复策略
PLL 与时钟树
PLL 恢复应分成“最小时钟可执行”和“高性能时钟就绪”两个阶段:
- 先用内部 RC 或低速时钟执行恢复桩。
- 启动外部晶振和 PLL,但不在 ISR 中忙等完整超时。
- 对时钟稳定中断或状态位设置最大等待时间。
- 必要时先用降级频率运行关键业务。
- PLL 失败时进入可诊断的降级模式,而不是无限等待。
- 时钟切换后更新 Flash wait state、总线分频、串口波特率、定时器和采样时钟。
升频和降频顺序必须分别设计。一般升频前先提高供电档位和 Flash wait state,降频后再降低 wait state 和电压,具体以芯片手册为准。
I2C/SPI 设备
外部器件上电后可能需要稳定时间、复位脉冲、设备 ID 检查、寄存器恢复或校准。建议把驱动恢复拆为:
1 | power_enable -> reset_release -> ready_wait -> essential_config -> ready |
关键路径只执行 essential_config。完整寄存器同步、FIFO 清理、自检和校准可以延后。I2C 总线还需处理睡眠期间从设备异常拉低 SDA、主控复位但从设备未复位、时钟频率改变导致时序配置失效等问题。
外部 Flash 与文件系统
外部 Flash 可能处于 Deep Power-Down。恢复时应先退出低功耗、等待器件规定的 tRES,再访问 JEDEC ID 或资源。不要让 UI 首次反馈依赖文件系统挂载和资源加载;常用图标、短提示和关键配置可以保存在内部 Flash、retention RAM 或预解压缓存中。
显示
显示恢复可分为:
- 背光保持关闭或低亮。
- 恢复显示控制器和最小帧缓冲。
- 输出静态首帧或状态图标。
- 后台加载字体、图片和复杂页面。
- 渐亮背光,避免电流冲击和白屏。
音频
Codec 和功放常有上电时序、偏置建立和 pop noise 风险。快速反馈可以先用低功耗蜂鸣器、片上 PWM 或预留提示,而不是直接在 ISR 中启动完整音频链路。恢复顺序通常涉及 MCLK、I2C 配置、模拟偏置、数字接口、静音解除和功放使能,应通过状态机和定时器推进。
无线通信
无线恢复成本可能远高于 MCU 唤醒本身。应区分:
- 保持连接的 modem sleep。
- 控制器保留、主 CPU 休眠。
- 断开链路后的重连。
- 深睡后重新初始化射频和协议栈。
如果业务对首包延迟敏感,应在连接参数、监听窗口和系统休眠策略之间协同,而不是只优化 MCU 的 1~2ms 唤醒时间。
并发、竞态与幂等性
重复恢复
多个唤醒源可能同时触发,或者一个事件在硬件和软件层重复上报。恢复入口必须使用原子状态防止重复执行:
1 | SLEEPING -> RESUMING_EARLY -> RESUMING_CRITICAL -> AWAKE |
只有成功完成状态转换的执行单元负责全局恢复;其他路径只合并新的唤醒原因。恢复函数应尽量幂等:设备已经 READY 时重复 resume 不应再次复位或破坏状态。
休眠期间新请求到达
Prepare 阶段冻结普通设备请求后,新请求可以:
- 设置
abort_requested并等待事务回滚。 - 被放入 pending queue,唤醒后立即处理。
- 对极高优先级请求直接中止休眠。
不能让新请求在设备关闭一半时直接进入普通驱动路径。
锁顺序
PM 框架容易与驱动锁、总线锁、调度器锁形成死锁。建议:
- PM core 不在持有全局锁时调用可能阻塞的驱动回调。
- Prepare 回调只做短检查和状态切换,不等待未知时长。
- 驱动恢复使用统一依赖顺序。
- ISR 不获取普通互斥锁。
- 完成事件使用静态对象或无锁标志。
- 任何同步等待都必须有超时和失败路径。
失败、降级与回退
唤醒并不保证成功。应为每个阶段定义超时和降级:
| 失败点 | 处理策略 |
|---|---|
| 外部晶振/PLL 未锁定 | 使用内部时钟降级运行,记录故障,限制高带宽功能 |
| I2C 设备无响应 | 总线恢复、设备复位、隔离故障设备,不阻塞其他服务 |
| Codec 校准失败 | 保持静音,使用替代提示或禁用音频 |
| 外部 Flash 未就绪 | 使用内置最小资源,延后重试,避免前台无限等待 |
| retention CRC 错误 | 丢弃上下文,走冷启动恢复,标记原因 |
| 时间补偿异常 | 使用 RTC 绝对时间校正,重建定时器,记录跳变 |
| 关键设备恢复超时 | 返回 RESUME_DEGRADED 或受控重启 |
| 连续唤醒风暴 | 屏蔽故障源、指数退避、保留诊断快照 |
系统应区分“可继续运行的降级”和“必须复位的不可恢复错误”。不要为了追求表面成功而吞掉错误;也不要因为一个非关键温湿度传感器失败就重启整个设备。
通用 API 设计
1 | /** |
推荐的框架接口包括:
1 | pm_latency_acquire(max_us, owner) |
接口应避免让上层依赖芯片特定模式名。业务说“最大退出延迟 2ms”和“需要 GPIO+RTC 唤醒”,比说“请进入 STOP2”更可移植。
策略伪代码
1 | /** |
真实实现还需加入:关键服务恢复时间、当前活动热度、设备 autosuspend 状态、平台 errata、温度、电量、最近恢复失败、唤醒源电平和系统安全策略。
进入休眠流程
1 | sequenceDiagram |
开源方案原理详解
Zephyr:状态属性、驻留策略和延迟请求
Zephyr 的系统电源管理把电源状态描述为带属性的对象,其中包括最小驻留时间和退出延迟。其 residency 思路是:只有“到下一调度事件的时间”足以覆盖最小驻留与退出延迟时,才选择该状态。Zephyr 还提供 latency request,使应用或驱动注册最大可接受退出延迟;策略不能选择超过该限制的状态。
可借鉴点:
- 状态属性数据化,不把判断散落在业务代码中。
- 策略函数与平台进入低功耗的实现分离。
- 允许应用增加自定义下一事件时间来源。
- 延迟约束由多个模块注册,系统统一聚合。
- 系统级 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_suspend、runtime_resume、runtime_idle 等回调,并由 PM core 维护状态和同步。设备可以根据 usage count 和 autosuspend 策略独立挂起,而不是等待系统级 suspend。
可借鉴点:
- 设备电源状态由统一核心管理。
- 自动挂起需要防止并发恢复和重复挂起。
- 设备父子关系、总线和 PM domain 影响回调顺序。
- runtime PM 与 system sleep 交互时要同步真实硬件状态。
Linux wakeup source:唤醒能力与是否允许唤醒分离
一个设备“硬件上能唤醒系统”与“当前策略允许它作为唤醒源”是两个不同概念。产品中也应区分:
1 | wakeup_capable 硬件和驱动是否支持 |
这可以避免把所有可唤醒设备永久开启,导致漏电或唤醒风暴,也便于按场景切换允许的唤醒源。
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 | PM constraint slots 8~32 个 |
所有关键路径对象应静态分配。Trace ring 可以使用定长记录:
1 | /** |
在极小系统中,即使只能保留 64 条记录,也足以定位“为什么没睡”“为何被唤醒”“哪个设备恢复超时”。
可观测性与统计
至少统计以下指标:
1 | 每个状态的进入次数 |
推荐通过 GPIO 打点配合逻辑分析仪或示波器:
1 | GPIO_A: 唤醒源物理边沿 |
这样可以把软件 trace 与硬件波形对齐。只看串口日志会受到 UART 尚未恢复、日志缓冲和调度延迟影响,不能作为唯一的时序依据。
测试矩阵
功能测试
- 对每个休眠状态验证所有声明的唤醒源。
- 验证不允许的唤醒源不会错误唤醒。
- 验证多个唤醒源同时触发时原因不会丢失。
- 验证 retention 正常、CRC 错误、版本变化和全丢失路径。
- 验证每个设备的 suspend、resume、abort 和重复调用。
- 验证睡眠期间 RTC、相对超时和绝对时间补偿。
- 验证深睡后冷启动式恢复与浅睡状态保留式恢复。
竞态测试
- 在 Prepare、Commit 前、WFI 前、刚唤醒、关键恢复和后台恢复各阶段注入中断。
- 在设备 autosuspend 与新请求同时发生时重复压测。
- 随机打断 PLL、I2C、SPI、Flash、无线恢复步骤。
- 强制 PM lock 泄漏,验证诊断和 watchdog 策略。
- 重复触发电平型唤醒源,验证不会形成无限唤醒循环。
- 在日志写入、Flash 擦除、DMA 和协议时隙期间请求深睡。
时序测试
对每类唤醒事件至少测量平均值、P95、P99、P99.9 和最大值。测试温度、电压、电量、不同晶振启动条件、存储忙状态和系统负载。不能只在实验室室温和空载下测一次。
能耗测试
- 测量每个状态的稳态电流。
- 测量进入和退出波形的能量积分。
- 测量一次完整用户交互或采样事务的能量,而不只看待机电流。
- 比较不同阈值下的日均能耗和延迟违约率。
- 统计误唤醒和过早唤醒造成的能量损失。
- 验证深睡阈值是否高于实测盈亏平衡时间。
常见反模式
反模式一:每次 Idle 都进入最深睡眠
问题:忽略进入/退出能量、恢复延迟、交互连续性和设备校准成本。短空闲会频繁抖动,实际功耗可能更高。
改进:使用预计空闲时间、盈亏平衡点、迟滞和交互热度选择状态。
反模式二:唤醒后按固定顺序初始化所有设备
问题:关键事件被显示、日志、网络和非关键传感器阻塞,恢复时间随设备数量线性增长。
改进:按唤醒事件定义关键设备集合,依赖图展开,后台异步恢复剩余设备。
反模式三:在 ISR 中完成完整恢复
问题:中断延迟不可控、可能等待锁或超时,影响其他实时任务并增加死锁风险。
改进:ISR 只捕获原因和发布静态事件,复杂恢复交给高优先级线程或工作队列。
反模式四:只保存变量,不保存版本和一致性信息
问题:固件升级、掉电或保存中断后,旧结构可能被误当作有效上下文。
改进:使用 magic、version、length、generation、CRC 和提交标志。
反模式五:模块直接指定芯片休眠模式
问题:业务与芯片绑定,多个模块互相覆盖,无法统一权衡。
改进:模块声明延迟、唤醒源、保留域、设备使用和活动期限,由 governor 选择模式。
反模式六:只看平均唤醒时间
问题:长尾由 PLL、总线超时、Flash、无线和任务抢占触发,用户真正感知的是偶发慢响应。
改进:测量 P99/P99.9、最大值和分阶段耗时,并设置每阶段超时。
反模式七:PM lock 没有 owner
问题:系统无法进入深睡时无法定位是谁持锁,最终只能关闭低功耗功能。
改进:记录 owner、获取时间、调用点、嵌套计数和最大允许持有时间。
反模式八:把设备恢复成功等同于业务恢复成功
问题:驱动 READY 不代表协议会话、缓存、页面或控制状态机已经一致。
改进:分离设备就绪、服务就绪和业务状态恢复事件。
一套可落地的最小实现
对于资源有限、现有工程尚无 PM 框架的 MCU,可以按以下顺序增量实现:
- 建立统一
pm_state_desc状态表,记录退出延迟、最小驻留、保留域和唤醒源。 - 把 RTOS Idle Hook 或空闲线程接入统一
pm_policy_select()。 - 实现下一超时点计算和 Tickless/低功耗定时器补偿。
- 实现固定容量 PM lock,并提供 owner 诊断。
- 在 retention 区保存唤醒原因、序列号、时间戳、版本和 CRC。
- 把唤醒 ISR 改为只锁存事件,创建高优先级 Fast Resume Worker。
- 为关键设备拆分
resume_early、resume和resume_deferred。 - 建立静态设备依赖表,至少保证电源、时钟、总线和子设备顺序正确。
- 对共享设备增加 usage count 和 autosuspend。
- 增加 GPIO 打点与定长 PM trace ring。
- 实测每个状态的稳态功耗、切换能量和延迟,计算盈亏平衡阈值。
- 最后再引入交互热度、动态阈值、分电源域和预测策略。
这条路径避免一开始就构造过度复杂的通用框架,同时保留后续演进空间。
最终回答组织方式
回答这类题时,可以按以下顺序展开:
- 先指出核心矛盾:最深睡眠降低稳态功耗,但增加进入/退出成本、上下文丢失和恢复长尾,因此不能按单一模式解决。
- 给出总体原则:延迟约束、驻留时间、能量盈亏、唤醒源和设备依赖共同决定状态。
- 说明快速路径与完整恢复路径:先捕获事件和提供最小反馈,再恢复关键服务,最后后台恢复非关键设备。
- 说明系统休眠是事务:Prepare、Commit、Abort、Resume,处理并发和最后时刻竞态。
- 说明设备管理:运行时引用计数、autosuspend、依赖图和分阶段回调。
- 给出量化公式:退出延迟预算、最小驻留、能量盈亏平衡和安全余量。
- 对照开源实现:Zephyr latency/residency、Linux cpuidle/PM QoS/Runtime PM、FreeRTOS Tickless、RT-Thread 投票、NuttX PM domain、ESP-IDF 保留域。
- 最后补充可观测性、失败降级和测试矩阵,证明方案可实现、可验证、可维护。
一句话概括:低功耗系统不应在“最省电”和“最快唤醒”之间二选一,而应通过可量化的状态模型、业务约束聚合、休眠事务、事件锁存、关键服务分阶段恢复和在线反馈,让系统在每一次具体空闲窗口中选择最合适的能耗—延迟工作点。
参考链接
- Zephyr System Power Management
- Zephyr Device Power Management
- Zephyr Device Runtime Power Management
- Zephyr PM Policy Latency Sample
- Linux CPU Idle Time Management
- Linux PM Quality of Service Interface
- Linux Runtime Power Management Framework
- Linux Device Power Management Basics
- FreeRTOS Low Power Support and Tickless Idle
- RT-Thread Power Management
- Apache NuttX Power Management
- Apache NuttX Power Management Implementation Notes
- ESP-IDF Sleep Modes










