第三讲:任务创建与管理——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是怎么决定“下一个该谁跑”的。到时候见!