3. 任务创建与管理:任务控制块(TCB),任务状态(就绪、运行、阻塞、挂起),任务创建与删除API

好,咱们进入第三章。这一章讲的是任务创建与管理,说白了就是RTOS里最核心的“人事管理”工作。你想想看,一个系统里跑着好几个任务,谁在干活、谁在睡觉、谁被关禁闭了,总得有个记录吧?这个记录就是任务控制块(TCB)。

3.1 任务控制块(TCB)——任务的“身份证”

每个任务在RTOS里都有一个专属的数据结构,这就是TCB。我个人习惯把TCB想象成任务的“身份证”加“档案袋”。里面存了什么?任务的名字、优先级、栈指针、状态、还有一堆寄存器现场保存值。

我刚开始做电机控制项目时,遇到过一个问题:两个任务共用了同一个栈空间,结果一个任务跑着跑着就把另一个任务的局部变量给踩了。电机直接抖得像抽风一样。后来查出来,就是TCB里的栈指针没初始化对。嗯,从那以后我每次创建任务都会先检查TCB的栈指针字段。

一个典型的TCB结构体长这样:

typedef struct tcb {
    uint32_t    *stack_ptr;      // 当前栈指针
    uint32_t    *stack_base;     // 栈底地址
    uint32_t    stack_size;      // 栈大小(字节)
    uint8_t     priority;        // 任务优先级
    uint8_t     state;           // 任务状态
    char        name[16];        // 任务名字(调试用)
    void        (*entry)(void*); // 任务入口函数
    void        *arg;            // 入口函数参数
    struct tcb  *next;           // 链表指针
} TCB_t;

这里要注意,stack_ptrstack_base是两个不同的东西。栈底是固定的,栈指针会随着函数调用不断变化。我见过有人把这两个搞混,结果任务切换时栈指针指向了错误的位置,系统直接HardFault。

⚠️ 避坑指南: 我曾经在STM32F4上调试一个四轴飞行器项目,因为TCB里的栈大小设置得太小,任务跑着跑着就栈溢出了。现象很诡异——飞控偶尔会莫名其妙复位。后来用调试器一看,栈指针已经跑到TCB结构体里去了,把其他任务的数据都覆盖了。所以,栈大小一定要留够余量,至少比估算值大30%。

3.2 任务状态——就绪、运行、阻塞、挂起

任务在RTOS里不是一直跑着的。它会在几个状态之间跳来跳去。我习惯用“人的一天”来类比:

  • 就绪态:就像你早上起床洗漱完毕,等着出门。任务已经准备好运行,就等CPU分配时间片。
  • 运行态:正在干活。CPU当前正在执行这个任务的代码。
  • 阻塞态:等某件事。比如等一个信号量、等延时结束、等消息队列有数据。任务主动让出CPU。
  • 挂起态:被强制暂停。通常是其他任务或中断调用了挂起API,把这个任务“关禁闭”了。

为什么要有这么多状态?说白了就是为了高效利用CPU。电机控制里,你不可能让一个任务一直占着CPU去等一个霍尔传感器信号。这时候把它阻塞掉,让CPU去干别的活,等信号来了再唤醒它。这就是RTOS的精髓。

状态转换图我建议你记牢:

创建 → 就绪 → 运行 → 阻塞
                ↑       ↓
                就绪 ← 阻塞解除
                
挂起 ← 运行/就绪/阻塞
挂起 → 就绪(解除挂起)

这里有个细节:挂起态比较特殊。它可以从任何状态进入,但解除挂起后,任务会回到进入挂起前的状态。我遇到过有人把挂起和阻塞搞混,结果任务一直不运行,查了半天才发现是挂起后忘了解除。

3.3 任务创建与删除API

好,到了动手环节。任务创建API,说白了就是给任务分配TCB、分配栈空间、初始化寄存器现场,然后把任务加到就绪队列里。

一个典型的创建API长这样:

// 函数原型
int task_create(TCB_t *tcb, 
                const char *name,
                void (*entry)(void*),
                void *arg,
                uint8_t priority,
                uint32_t *stack_buf,
                uint32_t stack_size);

// 使用示例
TCB_t motor_task_tcb;
uint32_t motor_stack[256];

void motor_control_task(void *arg) {
    while(1) {
        // 读取编码器
        // 计算PID
        // 输出PWM
        task_delay_ms(1);  // 1ms周期
    }
}

int main() {
    // 创建电机控制任务
    task_create(&motor_task_tcb,
                "motor_ctrl",
                motor_control_task,
                NULL,
                5,          // 优先级5
                motor_stack,
                sizeof(motor_stack));
    
    // 启动调度器
    scheduler_start();
    return 0;
}

创建时要注意几个点:

  • 栈空间:我建议用静态数组分配,不要用malloc。在嵌入式系统里动态内存分配容易产生碎片,电机控制这种实时性要求高的场景尤其忌讳。
  • 优先级:电机控制任务一般给高优先级,但别给最高。最高优先级通常留给中断服务或心跳任务。
  • 入口函数:必须是一个无限循环。如果函数返回了,RTOS会怎么处理?嗯,有些RTOS会直接删除任务,有些会挂起。我习惯在入口函数最后加一个while(1);防止意外返回。
💡 个人经验: 我在做伺服驱动器项目时,创建了三个任务:电流环(最高优先级)、速度环(中优先级)、位置环(低优先级)。电流环跑在1kHz,速度环500Hz,位置环100Hz。这样分层设计,既保证了实时性,又不会让高优先级任务饿死低优先级任务。

删除API相对简单:

// 函数原型
int task_delete(TCB_t *tcb);

// 使用示例
void user_input_task(void *arg) {
    while(1) {
        if(get_key() == KEY_STOP) {
            // 停止电机控制任务
            task_delete(&motor_task_tcb);
            break;
        }
        task_delay_ms(10);
    }
}

删除任务时要注意:

  • 不能删除正在运行的任务(除非RTOS支持自删除)
  • 删除后要释放资源(栈空间、TCB等)
  • 如果任务持有互斥锁或信号量,删除前要释放,否则会造成死锁
⚠️ 避坑指南: 我曾经在一个项目中,删除任务时忘了释放它占用的一个硬件定时器。结果其他任务再去申请这个定时器时,返回了错误。更坑的是,这个bug只在特定操作顺序下才会触发,调试了两天才找到。所以,删除任务前一定要做好资源清理工作。

3.4 任务状态管理实战

最后,咱们看看任务状态在实际电机控制中怎么用。假设你有一个无刷直流电机(BLDC)控制任务:

void bldc_control_task(void *arg) {
    while(1) {
        // 等待霍尔传感器中断触发的信号量
        task_semaphore_take(&hall_sem, TASK_WAIT_FOREVER);
        
        // 计算换相时序
        calculate_commutation();
        
        // 输出PWM
        set_pwm_duty();
        
        // 这里任务自动进入阻塞态,等待下一个霍尔信号
    }
}

这个任务大部分时间都处于阻塞态,等待霍尔信号。只有信号来了才切换到就绪态,然后被调度器选中进入运行态。这样CPU利用率极高——99%的时间都在干别的活(比如通信、显示、故障检测)。

你想想看,如果不用RTOS,用裸机轮询的方式,你得在主循环里不断检查霍尔信号,还得保证检查频率够快。用RTOS加阻塞态,代码简洁多了,实时性也更好。

嗯,这一章的内容就到这里。任务创建和管理是RTOS的基石,TCB就是每个任务的“户口本”,状态机是任务的行为逻辑。下一章咱们聊聊任务调度算法,看看调度器是怎么决定“谁先跑、谁后跑”的。