3. embOS任务管理基础

embOS 的任务管理围绕一个核心数据结构展开:任务控制块(Task Control Block,TCB)。理解 TCB 的字段构成,就能理解任务创建、优先级分配、状态迁移和栈管理之间的内在联系。本节从实际使用角度出发,先看创建 API 的参数含义,再分析 TCB 如何承载这些参数,最后给出栈空间估算的可操作方法。

任务创建 API 与参数含义

embOS 提供 OS_CreateTask() 和 OS_CreateTaskEx() 两个主要创建接口。前者创建普通任务,后者支持传入任务间通信的上下文指针。以 OS_CreateTask() 为例,其典型原型如下:

void OS_CreateTask(OS_TASK* pTask,
                   OS_ROUTINE* pfRoutine,
                   void* pStack,
                   unsigned StackSize,
                   U8 Priority,
                   U32 TimeSlice,
                   U32 AutoStart);

各参数的实际含义需要逐一明确。pTask 指向用户分配的 OS_TASK 结构体,它正是 TCB 的实体;pfRoutine 是任务入口函数,签名固定为 void Routine(void),任务通常写成无限循环,不返回;pStack 是栈底指针,StackSize 是以字节为单位的栈大小;Priority 是任务优先级,数值越小优先级越高,embOS 默认支持 0~31 共 32 级;TimeSlice 是时间片长度,仅在多个同优先级任务之间轮转时生效,传 0 表示不轮转;AutoStart 传 1 表示创建后立即进入就绪态,传 0 则任务处于挂起态,需后续调用 OS_StartTask() 激活。

一个典型创建示例如下:

static OS_TASK s_TaskA;
static OS_STACKPTR U32 s_StackA[128];   /* 128 字 = 512 字节 */

static void TaskA(void) {
    while (1) {
        /* 任务处理 */
        OS_Delay(10);
    }
}

void App_Init(void) {
    OS_CreateTask(&s_TaskA, TaskA, s_StackA,
                  sizeof(s_StackA), 5, 0, 1);
}

这里 sizeof(s_StackA) 得到的是字节数,而不是数组元素个数,这是初学者最容易出错的地方。若误传 128,栈空间会被严重低估。

任务优先级分配原则

embOS 采用固定优先级抢占式调度:只要更高优先级任务进入就绪态,当前任务立即被抢占。优先级分配应遵循以下原则。第一,将截止时间最紧、响应要求最高的任务放在最高优先级,例如中断服务延伸出的信号处理任务。第二,将执行时间长、实时性要求低的任务放在低优先级,例如日志写入、界面刷新。第三,同一优先级只分配给功能对等、可轮转的任务,并为其设置相同的时间片。第四,避免优先级反转:当高优先级任务与低优先级任务共享互斥资源时,应使用 embOS 的互斥量或优先级继承机制,而不是简单用二值信号量。

一个常见的分配参考是:优先级 0~3 保留给系统级或紧急处理任务,4~10 分配给周期性控制任务,11~20 分配给事件驱动任务,21~31 分配给后台任务。具体数值需结合项目实际调整,但应保持“越紧急越靠前”的一致约定。

任务控制块与状态迁移

TCB 是 embOS 管理任务的唯一句柄。它记录任务的栈指针、栈边界、优先级、当前状态、时间片剩余量、延时计数器以及链表节点等信息。任务创建时,embOS 会初始化 TCB 并将任务插入就绪链表;任务被抢占、延时或等待事件时,TCB 会从就绪链表转移到相应等待链表;事件到来或延时结束时,再回到就绪链表。因此,任务状态迁移的本质是 TCB 在不同链表之间的移动。

embOS 任务状态迁移与 TCB 链表归属 就绪 Ready TCB 在就绪链表 运行 Running TCB 在就绪链表首 延时 Delay TCB 在延时链表 等待 Wait TCB 在事件等待链表 挂起 Suspended TCB 不在任何链表 被调度选中 等待事件 OS_Delay 延时到期 OS_Suspend OS_Resume 事件到达 状态迁移触发条件 • 调度器选中最高优先级就绪任务 • OS_Delay / OS_WaitEvent 主动让出 • OS_Suspend / OS_Resume 外部控制

从图中可以看到,任务状态迁移并不是抽象概念,而是 TCB 在就绪链表、延时链表、事件等待链表之间的实际移动。理解这一点后,调试时若发现任务“卡住”,就可以检查它当前挂在哪个链表上,从而快速定位是延时未到、事件未发生,还是被挂起未恢复。

栈空间估算与分配

栈空间不足是 embOS 项目中最隐蔽的故障之一:任务可能运行数小时甚至数天后才崩溃,且现象随机。因此栈大小不能凭感觉填写,而应基于实际调用深度和中断嵌套进行估算。

估算方法如下。第一步,确定任务函数自身的局部变量总量,包括数组、结构体和编译器可能分配的临时变量。第二步,确定任务调用链中最深路径上所有函数的局部变量之和,注意递归函数必须单独评估。第三步,加上函数调用时的寄存器保存开销,Cortex-M 上每次调用约 8~16 字节,具体取决于编译器和优化等级。第四步,加上中断嵌套开销:若最高优先级中断可能嵌套,需为每层中断预留约 32~64 字节。第五步,乘以 1.5~2.0 的安全系数。

以 Cortex-M4 为例,假设任务局部变量 64 字节,最深调用链局部变量合计 96 字节,调用开销约 48 字节,中断嵌套预留 128 字节,则基础需求为 336 字节,乘以 1.5 后约 504 字节,可分配 512 字节。若使用 U32 数组定义栈,则数组长度为 128。

embOS 提供栈检查机制。在调试阶段,可以调用 OS_GetStackUsed() 获取任务实际使用的栈峰值,或使用 OS_CheckStack() 检查栈是否溢出。更稳妥的做法是在栈底填充固定模式(如 0xCDCDCDCD),运行一段时间后检查填充区域被破坏的程度,从而得到真实峰值。若峰值超过分配量的 70%,应适当增大栈空间。

栈数组建议使用 static 修饰并放在全局区,避免放在函数局部导致生命周期错误。同时确保栈按 8 字节对齐,以满足 Cortex-M 的 AAPCS 要求。
不要将 StackSize 传成数组元素个数。embOS 期望的是字节数,传错会导致栈越界且难以排查。建议统一使用 sizeof(数组名)。

综合来看,任务创建、优先级分配、TCB 状态迁移和栈管理是一个整体:创建时确定的优先级决定任务在就绪链表中的位置,TCB 记录状态并在链表间迁移,而栈空间则决定任务能否安全运行到下一次状态切换。掌握这四者的关系,就掌握了 embOS 任务管理的基础。