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() 恢复就绪。
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 控制它们的运行与等待。下一步可以进一步学习任务间通信与同步机制,让多个任务协同完成复杂功能。