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 三者如何协同?

现在我们把三个概念串起来。我画了一张图,展示它们在实际项目中的协作关系:

低功耗软件架构三层协同图 App 层 OS 层 HAL 层 事件驱动引擎 中断 / DMA / 定时器 睡眠 / 唤醒管理 状态机调度 状态切换 功耗策略绑定 协同工作流程 1. App 层状态机决定当前状态(如:采集 → 处理 → 发送 → 睡眠) 2. 事件驱动引擎根据状态配置中断/DMA,然后让 CPU 进入睡眠 3. HAL 层提供统一的硬件接口,屏蔽芯片差异

这张图想表达的是:分层架构是骨架,事件驱动是血液,状态机是大脑。三者缺一不可。

  • 分层架构保证代码可维护、可移植,每一层都能独立优化功耗。
  • 事件驱动保证 CPU 只在必要时工作,其余时间深度睡眠。
  • 状态机保证系统行为可预测,每个状态都绑定对应的功耗策略。

我的建议:新项目起步时,先画状态机图,再搭分层框架,最后用事件驱动把各个状态串起来。不要一上来就写代码。架构设计花 30% 的时间,能省掉后期 70% 的调试痛苦。

3.5 一个实战案例:低功耗温湿度采集器

最后,我分享一个实际项目的简化版。这个采集器每 10 秒采集一次温湿度,通过 BLE 发送,其余时间深度睡眠。目标平均电流 < 10μA。

架构设计:

  1. HAL 层:封装 SHT30 驱动、NRF52832 的 GPIOTE、SAADC、定时器。
  2. OS 层:用定时器产生 10 秒周期事件,用 GPIOTE 处理按键唤醒。
  3. 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() 里统一做这件事,而不是散落在各个状态里。这样改一个地方,所有睡眠路径都受益。