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 裸机:一张图看懂
下面这张图,是我自己画的。它展示了从裸机到任务调度的核心变化。
1.5 什么时候该上调度?
我个人的判断标准很简单,满足以下任意两条,你就该考虑任务调度了:
- 任务数量 ≥ 3:三个以上独立功能,互相有交互。
- 有硬实时要求:某个任务必须在固定时间内完成,否则系统崩溃。
- 代码超过2000行:这时候裸机代码的耦合度已经很高了。
- 团队协作开发:多个人同时改一个main.c,版本冲突能让你疯掉。
- 需要低功耗管理:调度器可以统一管理空闲时间,让CPU进入休眠。
我的建议:别等到项目做了一半才想着加调度器。前期架构设计时就把任务划分好,哪怕先用裸机模拟任务切换,也比后期重构强十倍。