1. 课程导论与开发环境搭建
1.1 为什么需要任务调度?
先问大家一个问题:你写单片机程序时,是不是经常这样——
- 主循环里放一堆函数,一个一个轮着跑
- 用延时死等,CPU 在那空转
- 中断一多,逻辑就乱成一锅粥
我以前刚做嵌入式那会儿,也这么干。直到有一次,一个温控项目出了问题:按键扫描、LCD 刷新、温度采集、PID 计算全塞在主循环里。结果按键按下去,要等半秒才有反应。用户投诉说「这机器是不是死机了?」
嗯,问题就出在——没有任务调度。
任务调度,说白了就是让 CPU 知道:什么时候该干什么事。比如:
- 每 1ms 扫描一次按键
- 每 10ms 刷新一次屏幕
- 每 100ms 采集一次温度
- 每 500ms 上报一次数据
如果没有调度,你就得自己算时间、自己拆延时、自己处理优先级。项目一复杂,代码就变成「面条代码」——牵一发而动全身。
核心观点:任务调度不是 RTOS 的专利。裸机一样可以做调度,而且很多时候,裸机调度比 RTOS 更轻量、更可控。
1.2 裸机 vs RTOS:怎么选?
很多新手会纠结:到底用裸机还是上 RTOS?
我个人的经验是——看项目复杂度。别盲目上 RTOS,也别死磕裸机。
| 对比维度 | 裸机调度 | RTOS |
|---|---|---|
| 资源占用 | 极低,几 KB ROM 即可 | 通常需要 10KB+ ROM |
| 实时性 | 可控,但需手动优化 | 内核保证,优先级明确 |
| 任务数量 | 适合 3~8 个任务 | 适合 10+ 个任务 |
| 调试难度 | 简单,直接看代码流 | 需要理解内核机制 |
| 学习曲线 | 平缓 | 较陡 |
| 典型场景 | 传感器采集、简单控制 | 多协议、多外设、复杂状态机 |
你看,没有绝对的好坏。我做过一个智能家居网关,8 个任务,裸机调度跑得稳稳的。也做过一个工业控制器,20 多个任务,不上 RTOS 根本管不过来。
我的建议:如果你的项目任务数 ≤ 5,且实时性要求不高,裸机调度完全够用。如果任务数 ≥ 10,或者有严格的 deadline 要求,考虑 RTOS。
那这门课讲什么?——裸机任务调度。我会带你从零搭建一套轻量级调度框架,配合中断管理,让你在裸机上也能实现「类 RTOS」的效果。
1.3 课程知识体系
先看看我们这 30 章要学什么。我画了一张图,帮你建立全局视角:
你看,整个课程是层层递进的。先打好基础,再学调度和中断,然后实战演练,最后进阶提升。别跳着看——我踩过的坑,不想让你再踩一遍。
1.4 开发环境搭建
好,理论说完了,咱们动手。工欲善其事,必先利其器。
1.4.1 安装 STM32CubeIDE
我推荐用 STM32CubeIDE,原因有三:
- 官方出品,兼容性最好
- 免费,没有 license 限制
- 集成 CubeMX,图形化配置外设
安装步骤很简单:
- 去 ST 官网下载最新版 STM32CubeIDE
- 双击安装,一路 Next
- 选择安装路径(建议不要有中文)
- 等待安装完成
注意:安装路径不要有空格和中文。我以前吃过这个亏——项目路径带中文,编译报错查了半天,结果是路径问题。
1.4.2 创建工程模板
我们创建一个基础工程模板,后续所有章节都基于这个模板。
步骤:
- 打开 STM32CubeIDE,点击 File → New → STM32 Project
- 选择芯片型号(我用的 STM32F103C8T6,你随意)
- 在 Pinout & Configuration 中配置:
- RCC → HSE → Crystal/Ceramic Resonator
- SYS → Debug → Serial Wire
- 时钟树 → HCLK 设为 72MHz
- Project Manager 设置:
- Project Name: stm32_scheduler_template
- Toolchain / IDE: STM32CubeIDE
- 点击 Finish,生成工程
生成后,我们添加一个基础调度框架文件。先创建两个文件:
Core/
├── Inc/
│ └── scheduler.h // 调度器头文件
└── Src/
└── scheduler.c // 调度器源文件
这是最简调度器的头文件骨架:
// scheduler.h
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
#include "main.h"
/* 任务控制块 */
typedef struct {
void (*task_func)(void); // 任务函数指针
uint32_t period_ticks; // 任务周期(tick数)
uint32_t elapsed_ticks; // 已运行tick数
uint8_t running; // 运行标志
} Task_t;
/* 调度器初始化 */
void Scheduler_Init(void);
/* 注册任务 */
uint8_t Scheduler_RegisterTask(void (*func)(void), uint32_t period_ms);
/* 调度器运行(放在主循环) */
void Scheduler_Dispatch(void);
/* 系统tick中断处理(放在SysTick_Handler) */
void Scheduler_TickHandler(void);
#endif
这个框架很简单,但它是我们后面所有调度策略的基础。现在先不展开实现细节——后面章节会一步步完善。
小技巧:每次新建工程,我都用这个模板。省去重复配置的时间,直接聚焦业务逻辑。
1.5 验证环境
写一个简单的测试代码,验证环境是否正常:
// main.c
#include "main.h"
#include "scheduler.h"
void Task1_LED(void) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
void Task2_UART(void) {
printf("Scheduler running...\r\n");
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
Scheduler_Init();
Scheduler_RegisterTask(Task1_LED, 500); // 500ms 翻转LED
Scheduler_RegisterTask(Task2_UART, 1000); // 1000ms 打印信息
while (1) {
Scheduler_Dispatch();
}
}
编译下载,如果 LED 每 500ms 闪烁一次,串口每 1s 打印一次——恭喜,环境搭建成功!
记住:这个模板就是你的「嵌入式瑞士军刀」。后面所有章节的代码,都基于这个框架扩展。
好,第一章就到这里。环境搭好了,框架有了,接下来我们就要深入调度器的核心——时间片轮转、优先级调度、状态机调度……一步步把裸机调度玩出花来。