裸机编程回顾:超级循环、ISR与状态机
说实话,很多从学校出来的工程师,第一份工作接触的都是裸机编程。我自己也是这么过来的。那时候觉得,一个 while(1) 循环就能搞定一切,简单直接,没什么不好。
但当你开始做复杂项目——比如同时处理按键、显示、通信、传感器采集——你就会发现,裸机编程其实藏着不少坑。今天我们就来好好聊聊裸机编程的三大核心:超级循环、中断服务程序、状态机设计模式。
超级循环架构:最朴素的调度方式
超级循环,说白了就是主循环。它的结构非常简单:
void main(void)
{
// 初始化硬件
System_Init();
while(1)
{
Task_KeyScan(); // 按键扫描
Task_Display(); // 显示刷新
Task_CommProcess(); // 通信处理
Task_SensorRead(); // 传感器读取
}
}
每个任务按顺序执行,周而复始。我在早期做智能家居面板时,用的就是这种架构。当时觉得挺稳的,直到遇到一个问题——按键按下时,显示会卡顿一下。
为什么会这样?因为所有任务都是串行执行的。如果 Task_Display 里有个延时操作,后面的任务就得等着。这就是超级循环最大的问题:实时性差。
- 优点:代码简单、逻辑清晰、资源占用极低
- 缺点:任务互相阻塞、实时性无法保证、扩展性差
你想想看,如果某个任务执行时间长了,整个系统就像卡住了一样。这在工业控制、汽车电子等场景下,是绝对不能接受的。
中断服务程序(ISR):打破循环的利器
为了解决实时性问题,我们引入了中断。中断允许 CPU 暂停当前任务,去处理更紧急的事件。
我记得第一次用中断时,觉得这东西太神奇了。按键按下,CPU 立刻响应,主循环完全不受影响。但后来发现,中断也不是万能的。
// 定时器中断服务程序
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 处理定时任务
g_uiTick++;
// 注意:不要在ISR里做耗时操作!
// Delay(100); // 这是大忌
}
}
中断服务程序有几个铁律:
- 快进快出:ISR 里不能做耗时操作,比如延时、打印、复杂计算
- 尽量短小:只做必要的处理,比如设置标志位、读取数据
- 注意优先级:高优先级中断会打断低优先级中断,可能导致嵌套
中断的引入,让裸机系统有了初步的实时响应能力。但问题来了:多个中断同时触发怎么办?哪个先处理?这就引出了中断优先级的概念。
| 中断特性 | 说明 | 注意事项 |
|---|---|---|
| 抢占优先级 | 决定中断能否打断另一个中断 | 数值越小优先级越高 |
| 子优先级 | 同抢占优先级下的响应顺序 | 只在同组内有效 |
| 中断嵌套 | 高优先级打断低优先级 | 嵌套层数不宜过多 |
| 临界区 | 需要关中断保护的代码段 | 时间要尽量短 |
状态机设计模式:让逻辑变得可控
有了超级循环和中断,系统能跑起来了。但业务逻辑一复杂,代码就变得像意大利面条一样,到处都是 if-else 和标志位判断。
这时候,状态机就派上用场了。我个人习惯用状态机来处理按键、通信协议、菜单导航这类有明确状态转换的场景。
// 按键状态机
typedef enum
{
KEY_STATE_IDLE, // 空闲状态
KEY_STATE_PRESS, // 按下状态
KEY_STATE_LONG, // 长按状态
KEY_STATE_RELEASE // 释放状态
} KeyState_t;
KeyState_t g_eKeyState = KEY_STATE_IDLE;
void KeyFSM_Process(void)
{
uint8_t ucKeyVal = GPIO_ReadInputDataBit(KEY_PORT, KEY_PIN);
switch(g_eKeyState)
{
case KEY_STATE_IDLE:
if(ucKeyVal == 0) // 按键按下
{
g_eKeyState = KEY_STATE_PRESS;
g_uiPressTick = 0;
}
break;
case KEY_STATE_PRESS:
if(ucKeyVal == 1) // 按键释放
{
g_eKeyState = KEY_STATE_RELEASE;
// 触发短按事件
Event_Send(EVENT_KEY_SHORT);
}
else if(g_uiPressTick > LONG_PRESS_TIME)
{
g_eKeyState = KEY_STATE_LONG;
// 触发长按事件
Event_Send(EVENT_KEY_LONG);
}
break;
case KEY_STATE_LONG:
if(ucKeyVal == 1) // 长按后释放
{
g_eKeyState = KEY_STATE_RELEASE;
}
break;
case KEY_STATE_RELEASE:
g_eKeyState = KEY_STATE_IDLE;
break;
default:
g_eKeyState = KEY_STATE_IDLE;
break;
}
}
状态机的核心思想其实很简单:把系统行为抽象成有限个状态,每个状态只关心自己能做什么、能跳转到哪些状态。这样一来,代码的可读性和可维护性都大大提升。
三种模式的协同工作
在实际项目中,超级循环、中断、状态机往往是配合使用的。我一般这样组织代码:
- 中断:处理硬件事件,设置标志位或填充缓冲区
- 主循环:轮询标志位,调用状态机处理逻辑
- 状态机:管理业务逻辑,控制行为转换
这种架构,说白了就是「中断收数据,主循环做处理,状态机管逻辑」。它虽然不是 RTOS,但在很多简单场景下已经够用了。
下面这张图,是我自己总结的裸机编程核心架构,你可以参考一下:
裸机编程的局限性
说了这么多裸机编程的好处,也得说说它的不足。毕竟我们这门课是要往 RTOS 方向走的。
裸机编程最大的问题,我总结为三点:
- 任务调度不公平:超级循环里,前面的任务占用了太多时间,后面的任务就得等着。你没法保证每个任务都能按时执行。
- 实时性难以保证:虽然有中断,但中断处理也是有时延的。而且多个中断同时到来时,优先级处理不当就会出问题。
- 代码耦合度高:所有任务都在一个循环里,改一个地方可能影响全局。项目大了之后,维护成本直线上升。
- 任务数量超过 5 个
- 有严格的实时性要求(毫秒级响应)
- 任务之间有复杂的同步和通信
- 代码规模大,需要模块化设计