很多嵌入式工程师都有这样的困惑:项目不大,用RTOS感觉杀鸡用牛刀;但任务一多,代码就乱成一锅粥。今天我们就来聊聊,不用RTOS,怎么把多任务处理得井井有条。
一、先搞清楚:为什么需要“多任务”?
假设你正在做一个智能温控器项目,需要同时处理这些事情:
- 每100ms读取一次温度传感器
- 每500ms刷新一次LCD显示
- 实时响应按键操作
- 每1秒检查一次是否需要开启加热
如果用最原始的写法,代码可能是这样的:
int main(void)
{
System_Init();
while(1)
{
Read_Temperature(); // 读温度
Update_LCD(); // 刷屏
Check_Key(); // 检测按键
Control_Heater(); // 控制加热
}
}
看起来挺简洁?但问题来了:
- 时序乱套:每个函数执行时间不一样,根本没法保证“每100ms读一次温度”。
- 互相拖累:LCD刷新慢,其他任务都得等着。
- 响应迟钝:按键可能要等好久才能被检测到。
这就是典型的“伪多任务”——看着像并行,实际是串行排队。
二、时间片轮询:最简单的多任务方案
解决上面问题的第一步,就是给每个任务加上“时间管理”。核心思想很简单:记录每个任务上次执行的时间,到点了才执行。
2.1 基本原理
先看一张示意图,它清晰地展示了多任务如何在不同时间片执行:

每个任务都有自己的执行周期,系统不断检查“时间到了没”,到了就执行,没到就跳过。
2.2 代码实现
// 任务控制结构体
typedef struct {
uint32_t last_run; // 上次执行时间
uint32_t interval; // 执行间隔(ms)
void (*task_func)(void); // 任务函数指针
} Task_t;
// 获取系统时间(通常用SysTick实现)
extern uint32_t Get_SysTick_Ms(void);
// 任务调度函数
void Task_Run(Task_t *task)
{
uint32_t now = Get_SysTick_Ms();
// 时间到了,执行任务
if(now - task->last_run >= task->interval)
{
task->last_run = now;
task->task_func();
}
}
使用起来也很直观:
// 定义各个任务
Task_t task_temp = {0, 100, Read_Temperature}; // 100ms读温度
Task_t task_lcd = {0, 500, Update_LCD}; // 500ms刷屏
Task_t task_key = {0, 20, Check_Key}; // 20ms检测按键
Task_t task_heater = {0, 1000, Control_Heater}; // 1秒控制加热
int main(void)
{
System_Init();
while(1)
{
Task_Run(&task_temp);
Task_Run(&task_key);
Task_Run(&task_lcd);
Task_Run(&task_heater);
}
}
这样一来,每个任务都能按照自己的节奏执行,互不干扰。
三、进阶版:任务表驱动
上面的写法有个小问题:任务多了,main函数里要写一堆Task_Run()。我们可以用数组把任务统一管理起来。
3.1 任务表设计
// 任务表
Task_t task_table[] = {
{0, 20, Check_Key}, // 按键检测,优先级最高
{0, 100, Read_Temperature}, // 温度采集
{0, 500, Update_LCD}, // 显示刷新
{0, 1000, Control_Heater}, // 加热控制
};
#define TASK_NUM (sizeof(task_table) / sizeof(task_table[0]))
// 调度器
void Scheduler_Run(void)
{
for(uint8_t i = 0; i < TASK_NUM; i++)
{
Task_Run(&task_table[i]);
}
}
int main(void)
{
System_Init();
while(1)
{
Scheduler_Run();
}
}
这样做的好处是:增删任务只需要改表,不用动主循环。
3.2 架构示意图
通过下图可以更直观地理解这种基于任务表的调度流程:

四、状态机:让任务学会“分段执行”
时间片轮询解决了“什么时候执行”的问题,但还有一个坑:如果某个任务执行时间太长怎么办?
比如LCD刷新需要50ms,那在这50ms里,其他任务都得干等着。这时候就需要状态机出场了。
4.1 问题场景
假设有个LED呼吸灯任务,需要:
- 亮度从0渐变到100(耗时1秒)
- 保持最亮500ms
- 亮度从100渐变到0(耗时1秒)
- 保持最暗500ms
- 循环往复
如果用阻塞式写法:
void Breath_LED(void)
{
// 渐亮 - 阻塞1秒!
for(int i = 0; i <= 100; i++) {
Set_PWM(i);
Delay_Ms(10);
}
Delay_Ms(500); // 又阻塞500ms
// 渐暗 - 又阻塞1秒!
for(int i = 100; i >= 0; i--) {
Set_PWM(i);
Delay_Ms(10);
}
Delay_Ms(500);
}
这个函数一跑就是3秒,其他任务全部卡死。
4.2 状态机改造
把长任务拆成多个状态,每次只执行一小步:
typedef enum {
LED_FADE_IN, // 渐亮
LED_HOLD_ON, // 保持亮
LED_FADE_OUT, // 渐暗
LED_HOLD_OFF // 保持暗
} LED_State_t;
void Breath_LED_StateMachine(void)
{
static LED_State_t state = LED_FADE_IN;
static uint8_t brightness = 0;
static uint32_t hold_start = 0;
switch(state)
{
case LED_FADE_IN:
brightness++;
Set_PWM(brightness);
if(brightness >= 100) {
state = LED_HOLD_ON;
hold_start = Get_SysTick_Ms();
}
break;
case LED_HOLD_ON:
if(Get_SysTick_Ms() - hold_start >= 500) {
state = LED_FADE_OUT;
}
break;
case LED_FADE_OUT:
brightness--;
Set_PWM(brightness);
if(brightness == 0) {
state = LED_HOLD_OFF;
hold_start = Get_SysTick_Ms();
}
break;
case LED_HOLD_OFF:
if(Get_SysTick_Ms() - hold_start >= 500) {
state = LED_FADE_IN;
}
break;
}
}
现在每次调用只执行一小步,立刻返回,不会阻塞其他任务。
4.3 状态机执行流程
下图清晰地展示了状态机如何通过多次快速调用完成一个长任务:

五、中断+标志位:处理紧急事件
有些事情等不得,比如串口收到数据、外部信号触发。这时候就需要中断来帮忙了。
但要注意一个原则:中断里只做最少的事,复杂处理放到主循环。
5.1 错误示范
// 串口中断 - 错误写法!
void USART1_IRQHandler(void)
{
uint8_t data = USART1->DR;
// 在中断里解析协议?大忌!
if(data == 0xAA) {
Parse_Protocol(); // 可能耗时很长
Execute_Command(); // 更长...
}
}
中断里干太多活,会导致其他中断被延迟,系统响应变差。
5.2 正确做法:标志位+缓冲区
// 全局标志和缓冲区
volatile uint8_t rx_flag = 0;
volatile uint8_t rx_buffer[64];
volatile uint8_t rx_index = 0;
// 串口中断 - 只收数据,设标志
void USART1_IRQHandler(void)
{
uint8_t data = USART1->DR;
rx_buffer[rx_index++] = data;
// 收到帧尾,设置标志
if(data == '\n') {
rx_flag = 1;
}
}
// 主循环中处理
void Process_UART_Data(void)
{
if(rx_flag)
{
rx_flag = 0;
// 在这里慢慢解析,不影响中断
Parse_Protocol(rx_buffer, rx_index);
rx_index = 0;
}
}
这里的关键在于通过指针和标志位进行快速数据交换,将耗时操作留给主循环。
5.3 中断与主循环的配合
下图展示了中断层与主循环层如何高效协作,实现微秒级与毫秒级任务的分层处理:

六、什么时候该上RTOS?
裸机多任务方案虽好,但也有边界。当你遇到这些情况时,可能就该考虑RTOS了:
- 任务之间需要严格的优先级抢占:比如电机控制必须在10us内响应
- 有复杂的任务同步需求:多个任务需要等待同一个事件
- 任务数量超过10个:管理起来越来越乱
- 需要动态创建/删除任务:裸机方案很难做到
- 团队协作开发:RTOS提供了更好的模块化边界
记住一句话:能用简单方案解决的,就别上复杂的。很多产品用裸机跑得好好的,没必要为了“高大上”而引入RTOS。
总结
回顾一下今天讲的内容,可以通过下图总结裸机实现多任务的四个核心方法:

这四板斧组合起来,足以应对大多数中小型嵌入式项目。代码清晰、内存管理直观、调试方便,何乐而不为?
当然,如果你的项目确实复杂到需要RTOS,那也别硬撑。工具是为项目服务的,选择合适的才是最好的。如果你想深入学习更多嵌入式开发技巧或与其他开发者交流,欢迎访问 云栈社区 探索更多相关资源。