第三讲:任务创建与管理——TCB、状态切换与API实战
大家好,我是你们的老朋友。今天我们来聊聊RTOS里最核心的一个概念——任务。说白了,整个RTOS就是围着任务转的。你创建任务、调度任务、删除任务,所有操作都离不开一个东西:任务控制块(TCB)。
我个人习惯把TCB比作任务的“身份证”。每个任务都有唯一的一张身份证,系统靠它来识别和管理任务。你想想看,如果没有这张身份证,系统怎么知道哪个任务该运行了?哪个任务在等什么?
一、任务控制块(TCB)——任务的“身份证”
TCB到底是什么?它是一个数据结构,里面存了任务的所有关键信息。包括任务栈指针、任务状态、优先级、任务名称等等。每个任务在创建时,系统都会给它分配一个TCB。
我在项目中遇到过一个问题:某个任务莫名其妙就挂了,查了半天发现是TCB被意外改写了。从那以后,我特别强调TCB的保护——它必须是只读的,除了内核谁都不能碰。
一个典型的TCB结构体长这样:
typedef struct tcb {
uint32_t *stack_ptr; // 栈指针
uint32_t stack_size; // 栈大小
uint8_t priority; // 任务优先级
uint8_t state; // 任务状态
char name[16]; // 任务名称
void (*entry)(void*); // 任务入口函数
void *param; // 入口参数
struct tcb *next; // 链表指针
} tcb_t;
嗯,这里要注意:stack_ptr指向的是任务栈的当前位置。每次任务切换时,CPU的寄存器值都会保存到这个栈里。所以栈的大小一定要算好,我曾经见过一个同事把栈设得太小,结果任务一运行就栈溢出,整个系统都崩了。
核心要点:TCB是内核管理任务的基础。每个任务必须有一个TCB,且TCB中的数据只能由内核修改。
二、任务状态切换——从生到死的完整旅程
一个任务从创建到删除,会经历多个状态。RTOS里常见的状态有:就绪态、运行态、阻塞态、挂起态和终止态。
为什么会这样设计?因为CPU只有一个,但任务可能有几十个。系统必须知道每个任务当前在干什么,才能做出正确的调度决策。
| 状态 | 说明 | 典型场景 |
|---|---|---|
| 就绪态 | 任务已准备好,等待CPU | 刚创建完成,或从阻塞中恢复 |
| 运行态 | 任务正在占用CPU | 当前正在执行的任务 |
| 阻塞态 | 任务在等待某个事件 | 等待信号量、消息队列、延时 |
| 挂起态 | 任务被暂停执行 | 调试时手动挂起,或低功耗模式 |
| 终止态 | 任务已结束 | 任务函数返回或被删除 |
我记得有一次调试扫地机的避障模块,发现避障任务总是卡住。后来一查,原来是任务在等待超声波传感器数据时进入了阻塞态,但传感器驱动有bug,一直没发信号过来。任务就这么永远阻塞了。这就是典型的“死等”问题。
避坑指南:我曾经在项目中遇到任务状态切换混乱的问题。后来发现是中断服务程序里直接调用了任务切换函数,导致状态机错乱。记住:中断里尽量不要做任务切换,用信号量或消息通知任务去处理。
三、动态创建 vs 静态创建——两种方式怎么选?
RTOS通常提供两种创建任务的方式:动态创建和静态创建。它们各有优劣,我根据自己的经验给大家分析一下。
3.1 动态创建
动态创建使用task_create()这类API。系统会自动从堆中分配TCB和栈空间。优点是方便,你不需要手动管理内存。缺点是堆可能碎片化,而且如果堆不够大,创建会失败。
// 动态创建任务示例
tcb_t *task_handle;
task_handle = task_create("sensor_task", // 任务名称
sensor_entry, // 入口函数
NULL, // 参数
1024, // 栈大小(字节)
3); // 优先级
if (task_handle == NULL) {
// 创建失败,处理错误
printf("任务创建失败!\n");
}
我在扫地机项目里,对于不确定数量的任务(比如用户自定义任务),会用动态创建。但要注意:动态创建后一定要检查返回值,别想当然认为一定成功。
3.2 静态创建
静态创建需要你提前定义好TCB和栈的数组。系统不会动态分配内存,而是使用你提供的空间。优点是确定性高,没有堆碎片问题。缺点是需要你手动计算栈大小,比较麻烦。
// 静态创建任务示例
static uint32_t sensor_stack[256]; // 预定义栈空间
static tcb_t sensor_tcb; // 预定义TCB
void create_static_task(void) {
task_create_static("sensor_task",
sensor_entry,
NULL,
sensor_stack,
sizeof(sensor_stack),
3,
&sensor_tcb);
}
我的建议:对于系统核心任务(比如看门狗任务、通信任务),用静态创建。因为这些任务必须可靠,不能因为堆碎片而创建失败。对于用户任务或临时任务,用动态创建更灵活。
四、实战:在扫地机上创建三个任务
好了,理论讲完了,我们来点实际的。假设我们要在扫地机上创建三个任务:传感器采集任务、路径规划任务、电机控制任务。
// 任务入口函数
void sensor_task_entry(void *param) {
while(1) {
read_sensors(); // 读取传感器
task_delay(10); // 延时10ms
}
}
void planner_task_entry(void *param) {
while(1) {
wait_sensor_data(); // 等待传感器数据
plan_path(); // 规划路径
send_motor_cmd(); // 发送电机指令
}
}
void motor_task_entry(void *param) {
while(1) {
wait_motor_cmd(); // 等待电机指令
control_motor(); // 控制电机
}
}
// 创建任务
void create_all_tasks(void) {
// 传感器任务:优先级最高,因为需要实时采集
task_create("sensor", sensor_task_entry, NULL, 512, 5);
// 路径规划任务:中等优先级
task_create("planner", planner_task_entry, NULL, 1024, 3);
// 电机控制任务:低优先级
task_create("motor", motor_task_entry, NULL, 256, 1);
}
你想想看,这三个任务配合起来,扫地机才能正常工作。传感器任务负责采集数据,路径规划任务根据数据做决策,电机控制任务执行指令。这就是多任务调度的魅力。
警告:任务优先级不是越高越好。我曾经把传感器任务优先级设得特别高,结果它一直占用CPU,其他任务饿死了。优先级要合理分配,高优先级任务要尽快让出CPU。
五、总结与思考
今天的内容就到这里。我们讲了TCB是什么、任务状态怎么切换、动态和静态创建怎么选。这些都是RTOS开发的基础,但也是最重要的部分。
最后留个思考题:如果你的扫地机在运行时突然卡住了,你会怎么排查?是从任务状态入手,还是从TCB入手?欢迎在评论区讨论。
下一讲,我们会深入任务调度算法,讲讲RTOS是怎么决定“下一个该谁跑”的。到时候见!