4. 任务调度架构:裸机前后台系统、RTOS任务划分、任务间通信机制

任务调度,说白了就是决定CPU先干谁、后干谁。很多刚入行的朋友觉得这玩意儿是RTOS才需要考虑的事,裸机随便写写就行。嗯,我当年也这么想,直到在一个车载项目中,因为中断和主循环的时序冲突,导致按键响应偶尔卡顿,排查了整整三天……从那以后,我对任务调度架构再也不敢马虎了。

这一章,咱们就聊聊三种常见的调度形态:裸机前后台、轻量级协作调度、以及RTOS的任务划分与通信。你想想看,搞懂了这些,你就能根据项目资源,选最合适的方案。

4.1 裸机前后台系统:最朴素的调度

裸机前后台,也叫超级循环(Super Loop)。前台是中断,后台是主循环。中断处理紧急事件,主循环处理常规任务。

核心原则:中断里只做标记,主循环里做处理。千万别在中断里干耗时活。

我见过有人直接在中断里跑延时、打印日志,结果系统响应一塌糊涂。记住,中断服务函数要快进快出。

一个典型的前后台框架长这样:

// 标志位定义
volatile uint8_t g_flag_key_scan = 0;
volatile uint8_t g_flag_display_update = 0;

// 定时器中断服务函数 (1ms触发一次)
void TIM_IRQHandler(void) {
    static uint16_t tick_counter = 0;
    tick_counter++;
    if (tick_counter % 10 == 0) {   // 10ms扫描一次按键
        g_flag_key_scan = 1;
    }
    if (tick_counter % 100 == 0) {  // 100ms刷新一次显示
        g_flag_display_update = 1;
    }
    // 清除中断标志
}

// 主循环
void main(void) {
    // 初始化硬件、外设、变量
    while(1) {
        if (g_flag_key_scan) {
            g_flag_key_scan = 0;
            key_scan_and_process();   // 按键扫描与处理
        }
        if (g_flag_display_update) {
            g_flag_display_update = 0;
            display_update();         // 显示刷新
        }
        // 其他任务...
    }
}

这种架构的优点是简单、零开销。缺点也很明显:任务之间是顺序执行的,一个任务卡住,后面的全得等。我曾经在一个项目中,因为某个传感器读取函数加了阻塞等待,导致显示刷新延迟,用户体验极差。

特性 裸机前后台 RTOS
资源开销 极低(几乎为零) 需要RAM/ROM(几KB到几十KB)
实时性 依赖中断优先级,任务级无抢占 支持任务优先级抢占
任务耦合度 高(都在一个循环里) 低(独立任务栈)
调试难度 低(单线程思维) 中等(需处理同步、竞争)
适用场景 简单控制、传感器采集、小家电 复杂多任务、通信协议栈、人机交互

我的建议:如果项目功能少于5个,且对实时性要求不高,裸机前后台完全够用。别为了用RTOS而用RTOS,那会增加复杂度。

4.2 RTOS任务划分:怎么切才合理?

引入RTOS后,最大的变化是任务可以独立运行、互相抢占。但任务怎么划分,是个经验活。我见过有人把每个函数都搞成一个任务,结果系统切换开销比干活还多。

任务划分的几个原则,我总结了一下:

  1. 按功能边界划分:一个任务只做一件事。比如按键扫描、LED控制、通信处理,各归各的。
  2. 按实时性要求划分:紧急的任务给高优先级,不紧急的给低优先级。比如电机控制要1ms响应,日志打印100ms一次就行。
  3. 按触发源划分:周期性任务(如数据采集)用定时器触发;事件驱动任务(如收到报文)用信号量或消息队列唤醒。
  4. 控制任务数量:任务不是越多越好。我个人习惯,一个项目任务数控制在8~15个以内,超过这个数,调度开销和调试复杂度会急剧上升。

举个例子,一个智能家居网关的任务划分:

任务名称 优先级 周期/触发 功能描述
wifi_task 高 事件驱动(数据到达) WiFi数据收发与解析
sensor_task 中 100ms周期 读取温湿度、光照传感器
display_task 中 200ms周期 刷新OLED屏幕
key_task 中 50ms周期 按键扫描与消抖
cloud_task 低 事件驱动(定时上报) 与云端同步状态
log_task 低 1s周期 日志输出与存储

避坑指南:我曾经把一个高优先级的任务里加了延时等待,结果低优先级的任务一直得不到CPU,系统直接“假死”。记住,高优先级任务不能阻塞等待低优先级任务释放资源,否则会死锁。

4.3 任务间通信机制:让任务好好说话

任务之间不通信,那就是孤岛。RTOS提供了几种标准的通信方式,咱们一个一个看。

4.3.1 信号量(Semaphore)

信号量就是个计数器,用来做同步或资源管理。二值信号量像一把钥匙,互斥信号量用来保护共享资源。

// 创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();

// 中断中释放信号量(通知任务)
void TIM_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务中等待信号量
void vTaskFunction(void *pvParameters) {
    while(1) {
        if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            // 收到信号,执行处理
            process_data();
        }
    }
}

4.3.2 消息队列(Message Queue)

队列用来传递数据,支持多个发送者、一个接收者。我习惯用队列来解耦生产者和消费者。

// 创建队列,每个元素10字节,队列深度5
QueueHandle_t xQueue = xQueueCreate(5, 10);

// 发送任务
void vSenderTask(void *pvParameters) {
    char data[10] = "hello";
    while(1) {
        xQueueSend(xQueue, data, 0);
        vTaskDelay(100);
    }
}

// 接收任务
void vReceiverTask(void *pvParameters) {
    char recv_data[10];
    while(1) {
        if (xQueueReceive(xQueue, recv_data, portMAX_DELAY) == pdTRUE) {
            // 处理接收到的数据
            handle_data(recv_data);
        }
    }
}

4.3.3 事件标志组(Event Group)

当一个任务需要等待多个条件同时满足时,事件标志组就派上用场了。比如等待按键按下且网络就绪,才执行某个操作。

EventGroupHandle_t xEventGroup = xEventGroupCreate();
#define BIT_KEY_PRESSED  (1 << 0)
#define BIT_NET_READY    (1 << 1)

// 任务等待两个事件同时发生
void vTask(void *pvParameters) {
    EventBits_t uxBits;
    while(1) {
        uxBits = xEventGroupWaitBits(
            xEventGroup,
            BIT_KEY_PRESSED | BIT_NET_READY,
            pdTRUE,   // 退出时清除标志
            pdTRUE,   // 等待所有标志
            portMAX_DELAY
        );
        // 两个条件都满足,执行操作
        execute_action();
    }
}

选型建议:传递数据用队列,同步信号用信号量,等待多个条件用事件标志组。别混用,也别滥用。我见过有人用队列传递一个空结构体当信号量用,那纯粹是浪费内存。

4.4 三种调度架构的对比

为了让你看得更清楚,我画了一张对比图。这张图展示了从裸机到RTOS的演进关系,以及各自的核心特点。

任务调度架构演进与对比 裸机前后台 协作式调度 RTOS抢占式调度 特点 • 零额外开销 • 顺序执行,无抢占 • 适合简单控制 特点 • 任务主动让出CPU • 无优先级抢占 • 适合中等复杂度 特点 • 抢占式调度 • 丰富通信机制 • 适合复杂多任务 选型建议 • RAM < 2KB,功能简单 → 裸机前后台 • RAM 2~8KB,任务数5~10 → 协作调度或轻量RTOS • RAM > 8KB,任务数10+,强实时 → 完整RTOS(FreeRTOS/RT-Thread)

4.5 实战中的选择与权衡

说了这么多,到底怎么选?我个人的经验是:

  • 先评估资源:MCU的RAM、ROM、主频。如果RAM只有1KB,别想RTOS了,老老实实前后台。
  • 再评估复杂度:任务之间有没有复杂的依赖?有没有多个外设需要同时响应?如果有,RTOS能帮你理清逻辑。
  • 最后评估团队:如果团队里没人用过RTOS,学习成本也是成本。别为了炫技引入不必要的复杂度。

核心观点:架构没有好坏,只有合不合适。裸机前后台用好了,一样稳定可靠;RTOS用不好,照样出bug。关键是理解每种架构的适用边界。