1. 任务调度基础:什么是任务调度?为什么单片机需要任务调度?裸机编程的痛点分析

大家好,我是你们这门课的老朋友。咱们今天聊点实在的——任务调度。

很多工程师做了好几年单片机开发,天天写中断、跑裸机,但你要问他“任务调度到底是个啥”,他可能支支吾吾说不清楚。这不怪你,因为裸机编程压根儿就没这个概念。但等你真正开始接触RTOS,或者自己动手写调度器,你就会发现——任务调度,是整个嵌入式系统的灵魂。

1.1 什么是任务调度?

说白了,任务调度就是决定“谁先运行、谁后运行、运行多久”的一套规则。

你想想看,一个单片机只有一个CPU核心,同一时刻只能执行一条指令。但你的系统里可能有多个任务:读传感器、刷显示屏、处理按键、发送数据……它们都想占用CPU。怎么办?

任务调度器就是那个“交通警察”。它按照某种策略,把CPU时间分给各个任务,让它们看起来像是“同时运行”的。

核心定义:任务调度 = 任务切换 + 调度策略。前者负责保存/恢复上下文,后者决定切换时机和切换给谁。

我记得刚入行那会儿,有个老工程师跟我说:“调度器就是个‘骗术’,它骗每个任务说‘你独占CPU’。” 现在想想,这话真精辟。

1.2 为什么单片机需要任务调度?

有人会问:“我裸机写得好好的,干嘛要搞调度?”

嗯,这个问题我当年也问过。直到我接手了一个项目——四轴飞行器飞控。那玩意儿要求:姿态解算要跑在1kHz,GPS数据解析不能丢包,遥控信号要实时响应,还得抽空往SD卡写日志。裸机?我试过,最后代码写得跟意大利面条似的,一个中断里套三个标志位,debug到怀疑人生。

单片机需要任务调度的场景,其实就三个字:复杂度。

  • 多任务并发:系统需要同时处理多个独立功能,比如一边采集数据一边通信。
  • 实时性要求:某些任务必须在规定时间内完成,比如电机控制、PWM输出。
  • 代码可维护性:任务独立后,修改一个功能不影响其他功能,团队协作也方便。
场景 裸机方案 调度方案
按键扫描 + LED刷新 主循环轮询,延时阻塞 两个独立任务,各自延时
传感器采集 + 串口打印 中断采集,主循环打印,容易丢数据 采集任务优先级高,打印任务优先级低
电机PID控制 + 蓝牙通信 定时器中断做PID,主循环处理蓝牙,中断过长会丢包 PID任务跑在1ms周期,蓝牙任务跑在10ms周期

我的经验:当你的裸机主循环超过200行,或者中断里开始出现while循环,你就该考虑上调度了。别硬撑,硬撑的代价是后期改bug改到吐。

1.3 裸机编程的痛点分析

咱们不吹不黑,裸机编程在简单场景下确实够用。但一旦系统复杂起来,痛点就一个个往外冒。

痛点一:超级循环 + 标志位地狱

这是最经典的裸机写法。一个while(1)里塞一堆函数,靠全局标志位控制执行顺序。

// 典型的裸机超级循环
uint8_t flag_sensor_ready = 0;
uint8_t flag_uart_rx = 0;
uint8_t flag_display_update = 0;

void main(void) {
    init_all();
    while(1) {
        if(flag_sensor_ready) {
            process_sensor_data();
            flag_sensor_ready = 0;
        }
        if(flag_uart_rx) {
            handle_uart_command();
            flag_uart_rx = 0;
        }
        if(flag_display_update) {
            refresh_display();
            flag_display_update = 0;
        }
        // 还有更多标志位...
    }
}

看着挺整齐是吧?但实际项目里,这些标志位会在中断里被置位,在主循环里被清零。一旦某个任务执行时间过长,其他任务就被活活饿死。我曾经在一个项目里数过,全局标志位用了37个,debug的时候每个都要查是在哪里置位、哪里清零的——那感觉,就像在迷宫里找老鼠。

痛点二:中断延迟不可控

裸机编程里,中断是最高优先级的。但问题来了:如果你的中断服务函数写得过长,其他中断就会被阻塞。

举个例子,你有一个1ms的定时器中断做PID控制,还有一个串口接收中断。如果PID中断里做了浮点运算,耗时200us,那串口中断就得等200us才能响应。如果串口波特率是115200,一个字节大约87us,200us的延迟足够丢两个字节了。

避坑指南:我曾经在一个工业控制项目里,因为中断里做了EEPROM写入(耗时5ms),导致高频脉冲计数丢了上百个脉冲,电机位置直接跑偏。从那以后,我定了个铁律:中断里只做最轻量的事,复杂逻辑扔到任务里做。

痛点三:时间片无法精确分配

裸机编程里,每个任务的执行时间完全取决于它在主循环里的位置和代码长度。你没法精确控制“这个任务跑5ms,那个任务跑2ms”。

比如你有一个显示刷新任务,它需要每20ms刷新一次。但在裸机里,如果前面有个传感器处理任务偶尔耗时100ms,那显示刷新就被硬生生推迟了100ms。结果就是屏幕闪烁,用户体验极差。

痛点四:代码耦合严重,难以复用

裸机代码里,任务A和任务B经常共享同一个全局变量、同一个延时函数。你想把其中一个模块移植到另一个项目?对不起,得把整个main函数一起搬走。

我见过最夸张的一个项目,一个.c文件写了8000行,里面什么都有:初始化、中断、主循环、算法、驱动……那代码,除了原作者,没人敢动。

1.4 任务调度 vs 裸机:一张图看懂

下面这张图,是我自己画的。它展示了从裸机到任务调度的核心变化。

裸机编程 vs 任务调度:核心架构对比 裸机编程模型 中断服务函数 (ISR) 全局标志位 主循环 while(1) 任务A → 任务B → 任务C 顺序执行,互相阻塞 硬件外设 (Timer/UART/GPIO) 任务调度模型 中断服务函数 (ISR) 任务调度器 (Scheduler) 任务队列 任务A (优先级高) → 任务B → 任务C 按优先级/时间片切换执行 硬件外设 (Timer/UART/GPIO) 架构升级 核心变化:从“顺序执行+标志位”到“调度器+任务队列”,实现任务解耦和CPU时间精确分配

1.5 什么时候该上调度?

我个人的判断标准很简单,满足以下任意两条,你就该考虑任务调度了:

  1. 任务数量 ≥ 3:三个以上独立功能,互相有交互。
  2. 有硬实时要求:某个任务必须在固定时间内完成,否则系统崩溃。
  3. 代码超过2000行:这时候裸机代码的耦合度已经很高了。
  4. 团队协作开发:多个人同时改一个main.c,版本冲突能让你疯掉。
  5. 需要低功耗管理:调度器可以统一管理空闲时间,让CPU进入休眠。

我的建议:别等到项目做了一半才想着加调度器。前期架构设计时就把任务划分好,哪怕先用裸机模拟任务切换,也比后期重构强十倍。