3. 软件架构基础:分层架构思想(HAL/OS/App)、事件驱动与轮询的功耗对比、状态机在低功耗中的应用
这一章,我们来聊聊软件架构。很多工程师写单片机代码,上来就开干,功能跑通了就完事。但到了低功耗场景,这种「野路子」往往让你寸步难行。我见过太多项目,前期图快,后期为了省几微安的电流,把整个代码翻了个底朝天。
说白了,低功耗不是靠「调参数」调出来的,而是靠「架构」设计出来的。这一章,我把三个最核心的架构思想揉在一起讲:分层架构、事件驱动、状态机。它们不是孤立的,而是低功耗设计的「三驾马车」。
3.1 分层架构:把代码拆成「三层楼」
先问一个问题:你的代码里,是不是 GPIO 操作、延时函数、业务逻辑全混在一个文件里?如果是,那低功耗优化基本没戏。因为你改一个地方,可能牵动全身。
我习惯把代码分成三层,就像盖楼一样:
| 层级 | 名称 | 职责 | 典型内容 |
|---|---|---|---|
| 顶层 | App(应用层) | 业务逻辑、状态流转 | 按键处理、传感器数据融合、通信协议 |
| 中间层 | OS(操作系统层) | 任务调度、时间管理、资源同步 | RTOS、定时器服务、消息队列 |
| 底层 | HAL(硬件抽象层) | 屏蔽硬件差异、提供统一接口 | GPIO读写、SPI/I2C收发、时钟配置 |
为什么要这么分?我举个例子。有一次我做一个温湿度采集器,MCU 从 STM32L0 换成了 NRF52832。如果代码是分层写的,我只需要重写 HAL 层,App 层几乎不用动。但如果代码是「一锅粥」,那基本等于重写。
更重要的是,分层架构对低功耗的意义在于:每一层都可以独立进入低功耗状态。HAL 层可以关掉不用的外设时钟,OS 层可以调度空闲任务进入睡眠,App 层可以决定什么时候「该睡了」。
核心原则:上层调用下层,下层不依赖上层。HAL 层永远不知道 App 层在干什么,它只负责「把硬件管好」。
3.2 事件驱动 vs 轮询:功耗差距有多大?
这是低功耗设计里最容易被忽视的问题。很多新手喜欢用轮询——在主循环里不停地查标志位、查按键、查传感器就绪。你想想看,CPU 大部分时间都在「空转」,电流自然下不去。
我直接给一组实测数据(基于 STM32L051,8MHz 主频,3.3V 供电):
| 模式 | 平均电流 | CPU 利用率 | 响应延迟 |
|---|---|---|---|
| 纯轮询(while 循环查标志) | 1.2 mA | 100%(空转) | 即时(但浪费电) |
| 轮询 + 短延时(delay(10ms)) | 0.8 mA | ~30% | 10ms 级别 |
| 事件驱动(中断 + 睡眠) | 0.05 mA | ~1% | 中断响应(μs 级) |
| 事件驱动 + DMA | 0.02 mA | ~0.1% | 几乎无 CPU 参与 |
看到了吗?从 1.2mA 降到 0.02mA,差了 60 倍。这 60 倍不是靠换芯片换来的,而是靠架构设计换来的。
事件驱动的核心思想很简单:没事就睡,有事再醒。CPU 平时处于 STOP 或 SLEEP 模式,只有外部中断、定时器溢出、DMA 完成等事件才能把它唤醒。唤醒后执行完任务,立刻再睡。
我的经验:事件驱动不是「不用轮询」,而是「把轮询交给硬件」。比如用定时器的输入捕获代替软件轮询按键,用 SPI 的 RXNE 中断代替查询接收完成。硬件帮你盯着,CPU 只管睡觉。
3.3 状态机:低功耗的「大脑」
有了分层架构和事件驱动,还缺一个东西:怎么管理系统的「行为」?比如,什么时候该采集数据?什么时候该发送?什么时候该睡觉?
答案就是状态机。我几乎每个低功耗项目都会用状态机,不管是简单的 switch-case,还是正式的有限状态机(FSM)。
一个典型的低功耗状态机长这样:
typedef enum {
STATE_INIT,
STATE_IDLE,
STATE_SENSOR_ACQUIRE,
STATE_DATA_PROCESS,
STATE_TX,
STATE_SLEEP,
STATE_ERROR
} SystemState_t;
SystemState_t currentState = STATE_INIT;
void App_Task(void) {
switch (currentState) {
case STATE_INIT:
HAL_Init();
currentState = STATE_IDLE;
break;
case STATE_IDLE:
// 无事可做,准备睡觉
EnterLowPower();
// 等待事件唤醒
break;
case STATE_SENSOR_ACQUIRE:
HAL_Sensor_Start();
// 启动后立即进入睡眠,等 DMA 完成中断唤醒
currentState = STATE_DATA_PROCESS;
break;
case STATE_DATA_PROCESS:
ProcessData();
currentState = STATE_TX;
break;
case STATE_TX:
HAL_UART_Send();
currentState = STATE_SLEEP;
break;
case STATE_SLEEP:
// 深度睡眠,等待 RTC 唤醒
EnterDeepSleep();
currentState = STATE_SENSOR_ACQUIRE;
break;
case STATE_ERROR:
HandleError();
break;
}
}
这个状态机的好处是什么?每个状态都明确知道自己该干什么,以及干完后下一步去哪。更重要的是,每个状态都可以独立控制功耗:IDLE 态进轻度睡眠,SLEEP 态进深度睡眠,ACQUIRE 态只开传感器外设。
我曾经踩过的坑:状态机里忘了处理「异常返回」。有一次传感器 I2C 通信失败,状态机卡在了 STATE_SENSOR_ACQUIRE,永远进不了睡眠。结果电池两天就耗光了。后来我加了一个超时看门狗,每个状态都设一个最大执行时间,超时强制跳转到 STATE_ERROR 或 STATE_IDLE。
3.4 三者如何协同?
现在我们把三个概念串起来。我画了一张图,展示它们在实际项目中的协作关系:
这张图想表达的是:分层架构是骨架,事件驱动是血液,状态机是大脑。三者缺一不可。
- 分层架构保证代码可维护、可移植,每一层都能独立优化功耗。
- 事件驱动保证 CPU 只在必要时工作,其余时间深度睡眠。
- 状态机保证系统行为可预测,每个状态都绑定对应的功耗策略。
我的建议:新项目起步时,先画状态机图,再搭分层框架,最后用事件驱动把各个状态串起来。不要一上来就写代码。架构设计花 30% 的时间,能省掉后期 70% 的调试痛苦。
3.5 一个实战案例:低功耗温湿度采集器
最后,我分享一个实际项目的简化版。这个采集器每 10 秒采集一次温湿度,通过 BLE 发送,其余时间深度睡眠。目标平均电流 < 10μA。
架构设计:
- HAL 层:封装 SHT30 驱动、NRF52832 的 GPIOTE、SAADC、定时器。
- OS 层:用定时器产生 10 秒周期事件,用 GPIOTE 处理按键唤醒。
- App 层:状态机:SLEEP → ACQUIRE → PROCESS → ADVERTISE → SLEEP。
关键代码片段(状态机核心):
void App_StateMachine(void) {
switch (g_state) {
case APP_SLEEP:
// 配置 RTC 10 秒后唤醒
HAL_RTC_SetWakeup(10);
// 进入 SYSTEM_ON 睡眠模式,电流约 1.5μA
HAL_Power_EnterSleep();
// 醒来后自动进入 ACQUIRE
g_state = APP_ACQUIRE;
break;
case APP_ACQUIRE:
// 启动 SHT30 单次采集(耗时约 20ms)
HAL_SHT30_StartMeasure();
// 等待采集完成中断
g_state = APP_PROCESS;
break;
case APP_PROCESS:
// 读取数据,计算温湿度
float temp = HAL_SHT30_GetTemp();
float hum = HAL_SHT30_GetHum();
// 准备 BLE 广播包
PrepareAdvertisementData(temp, hum);
g_state = APP_ADVERTISE;
break;
case APP_ADVERTISE:
// 启动 BLE 广播(持续约 100ms)
HAL_BLE_StartAdvertising();
// 广播完成后自动回到 SLEEP
g_state = APP_SLEEP;
break;
}
}
这个设计跑下来,实测平均电流 6.8μA。如果换成纯轮询,至少 200μA 以上。差距就在这里。
一个小技巧:在 SLEEP 态进入前,把所有不用的外设时钟都关了。我习惯在 HAL_Power_EnterSleep() 里统一做这件事,而不是散落在各个状态里。这样改一个地方,所有睡眠路径都受益。