在上一篇文章中,事件驱动体现的不太明显,更像是一个状态机,本篇进行提升。
真正的“事件驱动”应该是:主循环什么都不用问,外部中断或硬件回调主动把事件“扔”进队列,状态机只负责被动消费。在嵌入式系统中,要体现“事件驱动”,必须满足以下三个特征:
生产者与消费者分离:硬件中断(生产者)只负责发信号,主循环(消费者)只负责处理逻辑。
异步性:事件可以在任何时刻发生(如按键按下),不需要等待主循环检查到它。
事件队列:这是核心!中断不能直接调用状态机(因为中断上下文不能做耗时操作,且可能重入),必须先把事件存起来。
将对之前的代码进行重构,加入 事件队列 和 中断模拟,改造成真正的状态机+事件驱动
1.定义事件队列
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
// --- 1. 事件定义 ---
typedef enum {
EVT_NONE = 0,
EVT_BTN_POWER,
EVT_BTN_MODE,
EVT_TEMP_LOW,
EVT_TEMP_HIGH,
EVT_TEMP_ERROR,
EVT_TICK_1S,
EVT_MAX
} Event_t;
// --- 2. 简单的环形缓冲区 (事件队列) ---
#define QUEUE_SIZE 16
typedef struct {
Event_t buffer[QUEUE_SIZE];
uint8_t head;
uint8_t tail;
uint8_t count;
} EventQueue_t;
static EventQueue_t g_eventQueue = {0};
// 入队 (通常在中断中调用)
bool event_push(Event_t evt) {
if (g_eventQueue.count >= QUEUE_SIZE) return false; // 队列满,丢弃或报错
g_eventQueue.buffer[g_eventQueue.tail] = evt;
g_eventQueue.tail = (g_eventQueue.tail + 1) % QUEUE_SIZE;
g_eventQueue.count++;
return true;
}
// 出队 (在主循环中调用)
bool event_pop(Event_t *evt) {
if (g_eventQueue.count == 0) return false; // 队列空
*evt = g_eventQueue.buffer[g_eventQueue.head];
g_eventQueue.head = (g_eventQueue.head + 1) % QUEUE_SIZE;
g_eventQueue.count--;
return true;
}
2.模拟硬件中断(生产者)
// --- 模拟硬件中断服务程序 (ISR) ---
// 模拟:用户按下了电源键 (触发外部中断)
void HAL_GPIO_EXTI_Callback_PowerBtn() {
// 【关键】中断只做一件事:把事件扔进队列,立刻退出!
// 绝不在中断里执行风扇转动、延时等耗时操作
event_push(EVT_BTN_POWER);
}
// 模拟:用户按下了模式键
void HAL_GPIO_EXTI_Callback_ModeBtn() {
event_push(EVT_BTN_MODE);
}
// 模拟:定时器溢出中断 (每秒一次)
void HAL_TIM_PeriodElapsedCallback() {
// 定时器中断里也只负责发事件
event_push(EVT_TICK_1S);
}
// 模拟:ADC 采样完成中断 (温度读取)
void HAL_ADC_ConvCpltCallback(float temp) {
Event_t evt = EVT_NONE;
if (temp > 100.0f) evt = EVT_TEMP_ERROR;
else if (temp > 35.0f) evt = EVT_TEMP_HIGH;
else if (temp < 25.0f) evt = EVT_TEMP_LOW;
// 只有当温度跨越阈值产生新事件时才推送,避免队列溢出
// 实际项目中可能需要更复杂的去抖逻辑
if (evt != EVT_NONE) {
event_push(evt);
}
}
3.状态机现在变得非常纯粹,它不关心事件从哪来,只关心从队列里取出的事件是什么
// --- 状态机定义 (简化版,沿用之前的逻辑) ---
typedef enum { STATE_OFF, STATE_AUTO, STATE_FAULT, STATE_MAX } State_t;
typedef void (*Action)(void);
typedef struct {
State_t cur;
Event_t evt;
State_t next;
Action act;
} Transition;
// ... (此处省略具体的 stateTable 数组定义,逻辑同前) ...
// 假设 stateTable 已定义好
static State_t g_currentState = STATE_OFF;
void run_state_machine(Event_t evt) {
// 查找表逻辑...
if (g_currentState == STATE_OFF && evt == EVT_BTN_POWER) {
printf("开启风扇 (自动模式)\n");
g_currentState = STATE_AUTO;
} else if (g_currentState == STATE_AUTO && evt == EVT_TEMP_HIGH) {
printf("风速调至最大\n");
// 状态不变,但执行动作
} else if (g_currentState == STATE_AUTO && evt == EVT_TEMP_ERROR) {
printf("进入故障保护,关闭风扇\n");
g_currentState = STATE_FAULT;
} else {
// printf("状态 %d 忽略事件 %d\n", g_currentState, evt);
}
}
4.main(调度器)
这才是事件驱动的精髓:主循环不再轮询硬件,它只轮询队列。如果没有事件,它就休眠或做其他低优先级任务
int main(void) {
printf("------------------------------------------------\n");
while (1) {
Event_t currentEvt;
// 与上一篇的核心区别
// 之前:主动问硬件 "你有事吗?"
// 现在:问队列 "有没人塞进来的事件?"
if (event_pop(¤tEvt)) {
// 如果有事件,立即处理
// printf("从队列取出事件: %d\n", currentEvt);
run_state_machine(currentEvt);
} else {
// 如果队列是空的,说明没有突发事件
// CPU 可以进入低功耗模式 (WFI),或者处理背景任务 (如日志上传)
// 这里为了演示,我们手动模拟一些中断触发
static int tick = 0;
// 模拟:第 1 秒,触发电源键中断
if (tick == 10) {
HAL_GPIO_EXTI_Callback_PowerBtn(); // 模拟中断发生
}
// 模拟:第 3 秒,温度升高触发中断
if (tick == 30) {
HAL_ADC_ConvCpltCallback(40.5f);
}
// 模拟:第 5 秒,温度异常触发中断
if (tick == 50) {
HAL_ADC_ConvCpltCallback(120.0f);
}
tick++;
// 模拟延时,代表系统空闲时间
// 在真实系统中,这里可以是 __WFI() (Wait For Interrupt)
}
}
return 0;
}
在这个改进版中:
事件驱动体现在“推模型” :硬件中断(生产者)在事件发生时,主动将事件 push 到队列。
状态机体现在“消费模型”:状态机作为消费者,从队列 pop 事件并驱动状态流转。
解耦:main 函数不再包含任何 if (button_pressed) 这样的硬件检测代码。这就是架构上的解耦。
这种架构是 RTOS(如 FreeRTOS, Zephyr)中任务间通信的标准模式(Queue + Task),也是现代嵌入式软件处理复杂交互的核心思想。

1095

被折叠的 条评论
为什么被折叠?



