2. embOS任务创建与任务控制块

在 embOS 中,任务不是一个抽象概念,而是由一段任务函数、一个任务控制块(TCB)、一块任务栈以及一个优先级共同构成的实体。理解这四者的关系,是掌握 embOS 调度机制的第一步。本节围绕任务函数原型、优先级分配、栈空间规划、TCB 结构、状态迁移与 API 实践展开,帮助读者建立从“定义任务”到“任务被调度运行”的完整认知。

任务函数原型:任务入口的固定约定

embOS 的任务函数必须遵循固定原型:

void TaskName(void);

它没有参数,也没有返回值。任务函数通常写成无限循环,因为一旦返回,embOS 会认为任务异常结束,可能触发系统错误或未定义行为。典型结构如下:

void AppTask(void) {
    while (1) {
        /* 任务主体 */
        OS_TASK_Delay(100);   /* 主动让出 CPU,单位:系统节拍 */
    }
}

任务函数内部可以调用 embOS API,例如延时、等待事件、发送信号量等。需要注意的是,任务函数本身不负责“创建自己”,创建动作由其他任务或 main() 在启动调度器之前完成。

任务优先级分配:数值越小优先级越高

embOS 采用固定优先级抢占式调度。优先级用整数表示,数值越小,优先级越高。例如优先级 1 高于优先级 10。系统保留优先级 0 给内部使用,用户任务通常从 1 开始分配。优先级范围由 OS_LOWEST_PRIO 决定,常见配置为 0~31 或 0~255。

分配优先级时,应遵循以下原则:

  • 硬实时任务(如电机控制、通信协议栈)分配高优先级,确保响应时间可预测。
  • 人机交互、日志记录等软实时任务分配低优先级。
  • 避免多个任务共用同一优先级,除非明确使用轮转调度或它们不会同时就绪。
  • 空闲任务由 embOS 自动创建,优先级最低,用于填充 CPU 空闲时间。

例如,一个典型的嵌入式系统可这样分配:控制任务优先级 2,通信任务优先级 5,显示任务优先级 10,日志任务优先级 20。

任务栈空间规划:栈是任务私有的工作区

每个任务拥有独立的栈空间。栈用于保存局部变量、函数调用返回地址、中断现场以及任务切换时的寄存器上下文。栈空间不足会导致栈溢出,表现为数据被破坏、任务跑飞或系统死机。

栈大小的单位通常是 OS_STACKPTR 的字节数。规划栈空间时,需要考虑:

  • 函数调用深度:嵌套越深,栈消耗越大。
  • 局部变量大小:大数组、结构体应避免放在栈上。
  • 中断嵌套:中断服务程序也使用当前任务栈,需预留余量。
  • embOS 上下文保存:任务切换时保存寄存器组,约几十字节。

一个实用的估算方法是:先给一个保守值(如 256 字节),运行最坏路径后通过栈填充模式(如 0x55)检查实际使用量,再留 30%~50% 余量。例如实测使用 180 字节,可分配 256 或 320 字节。

任务控制块 TCB 结构

TCB 是 embOS 管理任务的核心数据结构。每个任务创建时,embOS 会分配一个 OS_TASK 结构体,记录该任务的全部调度信息。虽然不同版本字段略有差异,但核心成员包括:

  • 任务栈指针:指向该任务当前栈顶,任务切换时用于恢复上下文。
  • 优先级:当前任务优先级,调度器据此选择最高优先级就绪任务。
  • 状态:就绪、运行、阻塞、挂起等。
  • 时间片/延时计数器:用于延时和超时管理。
  • 事件对象指针:任务等待的信号量、邮箱或事件标志。
  • 链表节点:将任务挂入就绪链表或等待链表。

TCB 由 embOS 在创建任务时初始化,用户通常不需要直接修改其字段,但理解其结构有助于分析调度行为和排查问题。

任务状态迁移

embOS 任务在生命周期中会在多个状态之间迁移。典型状态包括:

  • 就绪(Ready):任务已创建,具备运行条件,等待调度器选中。
  • 运行(Running):当前占用 CPU 的任务。
  • 阻塞(Blocked):任务调用延时或等待事件,暂时不参与调度。
  • 挂起(Suspended):任务被显式挂起,不参与调度,直到被恢复。

状态迁移由 API 调用和系统事件驱动。例如,运行任务调用 OS_TASK_Delay() 后进入阻塞态,延时结束后回到就绪态;调用 OS_TASK_Suspend() 进入挂起态,调用 OS_TASK_Resume() 恢复就绪。

embOS 任务状态迁移与 TCB 核心字段 任务控制块 TCB • 栈指针 SP • 优先级 Prio • 状态 State • 延时计数器 • 事件对象指针 • 就绪/等待链表节点 • 时间片余量 • 任务函数入口 就绪 Ready 运行 Running 阻塞 Blocked 挂起 Suspended 调度选中 被抢占 Delay/等待事件 超时/事件到达 Suspend Resume 优先级数值越小越高;TCB 记录状态与链表节点, 调度器据此在就绪链表中选出最高优先级任务运行。

API 实践:创建任务并观察状态

embOS 创建任务的核心 API 是 OS_TASK_Create(),其简化原型如下:

void OS_TASK_Create(
    OS_TASK* pTask,        /* TCB 指针 */
    void (*pFunc)(void),   /* 任务函数 */
    OS_PRIO Prio,          /* 优先级 */
    void* pStack,          /* 栈基址 */
    OS_UINT StackSize      /* 栈大小(字节) */
);

下面是一个完整的创建示例,包含两个任务:高优先级控制任务和低优先级日志任务。

#include "RTOS.h"

static OS_TASK tcbControl;
static OS_TASK tcbLog;

static OS_STACKPTR unsigned int stackControl[128];  /* 512 字节 */
static OS_STACKPTR unsigned int stackLog[64];       /* 256 字节 */

void ControlTask(void) {
    while (1) {
        /* 控制逻辑 */
        OS_TASK_Delay(10);
    }
}

void LogTask(void) {
    while (1) {
        /* 日志输出 */
        OS_TASK_Delay(100);
    }
}

int main(void) {
    OS_Init();
    OS_TASK_Create(&tcbControl, ControlTask, 2,
                   stackControl, sizeof(stackControl));
    OS_TASK_Create(&tcbLog, LogTask, 10,
                   stackLog, sizeof(stackLog));
    OS_Start();
    return 0;
}

运行后,ControlTask 每 10 个节拍唤醒一次,LogTask 每 100 个节拍唤醒一次。由于 ControlTask 优先级更高,当两者同时就绪时,ControlTask 先运行。可以通过 OS_TASK_GetState() 或调试器观察 TCB 中的状态字段,验证任务在就绪、运行、阻塞之间的迁移是否符合预期。

创建任务时,栈数组建议使用 OS_STACKPTR 类型并保持对齐,避免因对齐问题导致上下文保存异常。栈大小以字节为单位,不是元素个数,务必用 sizeof() 计算。
不要在任务函数中直接返回。如果任务逻辑需要结束,应调用 OS_TASK_Delete() 或进入无限循环等待。返回会导致栈指针异常,系统行为不可预测。

掌握任务函数原型、优先级分配、栈规划、TCB 结构和状态迁移后,读者应能独立创建多个任务,并通过 API 控制它们的运行与等待。下一步可以进一步学习任务间通信与同步机制,让多个任务协同完成复杂功能。