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_ptr和stack_base是两个不同的东西。栈底是固定的,栈指针会随着函数调用不断变化。我见过有人把这两个搞混,结果任务切换时栈指针指向了错误的位置,系统直接HardFault。
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);防止意外返回。
删除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等)
- 如果任务持有互斥锁或信号量,删除前要释放,否则会造成死锁
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就是每个任务的“户口本”,状态机是任务的行为逻辑。下一章咱们聊聊任务调度算法,看看调度器是怎么决定“谁先跑、谁后跑”的。