找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3152

积分

0

好友

448

主题
发表于 昨天 20:13 | 查看: 0| 回复: 0

很多嵌入式工程师都有这样的困惑:项目不大,用RTOS感觉杀鸡用牛刀;但任务一多,代码就乱成一锅粥。今天我们就来聊聊,不用RTOS,怎么把多任务处理得井井有条。

一、先搞清楚:为什么需要“多任务”?

假设你正在做一个智能温控器项目,需要同时处理这些事情:

  • 每100ms读取一次温度传感器
  • 每500ms刷新一次LCD显示
  • 实时响应按键操作
  • 每1秒检查一次是否需要开启加热

如果用最原始的写法,代码可能是这样的:

int main(void)
{
    System_Init();

    while(1)
    {
        Read_Temperature();    // 读温度
        Update_LCD();          // 刷屏
        Check_Key();           // 检测按键
        Control_Heater();      // 控制加热
    }
}

看起来挺简洁?但问题来了:

  1. 时序乱套:每个函数执行时间不一样,根本没法保证“每100ms读一次温度”。
  2. 互相拖累:LCD刷新慢,其他任务都得等着。
  3. 响应迟钝:按键可能要等好久才能被检测到。

这就是典型的“伪多任务”——看着像并行,实际是串行排队。

二、时间片轮询:最简单的多任务方案

解决上面问题的第一步,就是给每个任务加上“时间管理”。核心思想很简单:记录每个任务上次执行的时间,到点了才执行

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呼吸灯任务,需要:

  1. 亮度从0渐变到100(耗时1秒)
  2. 保持最亮500ms
  3. 亮度从100渐变到0(耗时1秒)
  4. 保持最暗500ms
  5. 循环往复

如果用阻塞式写法:

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了:

  1. 任务之间需要严格的优先级抢占:比如电机控制必须在10us内响应
  2. 有复杂的任务同步需求:多个任务需要等待同一个事件
  3. 任务数量超过10个:管理起来越来越乱
  4. 需要动态创建/删除任务:裸机方案很难做到
  5. 团队协作开发:RTOS提供了更好的模块化边界

记住一句话:能用简单方案解决的,就别上复杂的。很多产品用裸机跑得好好的,没必要为了“高大上”而引入RTOS。

总结

回顾一下今天讲的内容,可以通过下图总结裸机实现多任务的四个核心方法:
裸机多任务方案总结流程图

这四板斧组合起来,足以应对大多数中小型嵌入式项目。代码清晰、内存管理直观、调试方便,何乐而不为?

当然,如果你的项目确实复杂到需要RTOS,那也别硬撑。工具是为项目服务的,选择合适的才是最好的。如果你想深入学习更多嵌入式开发技巧或与其他开发者交流,欢迎访问 云栈社区 探索更多相关资源。




上一篇:实测Zephyr以太网协议栈:STM32H743下TCP吞吐率达94.5Mbps
下一篇:C++静态变量生命周期详解:跨编译单元的顺序问题与解决方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-31 01:59 , Processed in 0.320796 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表