4. FreeRTOS任务创建与管理:任务控制块、状态机与延时机制
任务管理是FreeRTOS的核心。说白了,你写的每个任务,系统都得有个“档案”来管着它。这个档案就是任务控制块(TCB)。我刚开始用FreeRTOS时,总觉得TCB是个黑盒子,后来调试一个优先级反转的问题,才真正把它看透了。
4.1 任务控制块(TCB)——任务的“身份证”
每个任务被创建时,FreeRTOS都会分配一个TCB。它存了任务的所有关键信息。你可以把TCB想象成任务的“身份证”。
| TCB成员 | 作用 | 我的经验 |
|---|---|---|
pxTopOfStack |
栈顶指针 | 栈溢出时,这个值会异常 |
uxPriority |
任务优先级 | 数值越大优先级越高 |
pxStack |
栈起始地址 | 调试时看这个能定位栈使用量 |
eTaskState |
任务状态 | 就绪、运行、阻塞、挂起 |
xStateListItem |
状态链表节点 | 任务切换全靠它 |
4.2 任务状态机——任务的一生
任务不是一直在跑的。它有四种状态:运行、就绪、阻塞、挂起。我画了张图,帮你理清它们的关系。
你想想看,一个任务刚被创建时,它处于就绪态。调度器一瞅,哎,这个任务优先级最高,那就让它跑。跑着跑着,它调用了vTaskDelay,就进入阻塞态。等延时时间到了,它又回到就绪态,等着下次被调度。
4.3 任务优先级——谁先跑谁后跑
FreeRTOS支持抢占式调度。优先级高的任务,会抢优先级低任务的CPU。数值越大,优先级越高。
我在项目中遇到过一个问题:一个低优先级的任务,因为要处理一个紧急事件,但一直被高优先级任务抢占,导致事件响应超时。后来我用了优先级继承机制才解决。
| 优先级范围 | 典型用途 | 注意事项 |
|---|---|---|
| 最高(configMAX_PRIORITIES-1) | 中断级任务、时间关键任务 | 不要长时间占用CPU |
| 中等 | 常规控制任务、数据处理 | 注意优先级反转 |
| 最低(0) | 后台任务、空闲任务 | 空闲任务优先级固定为0 |
4.4 vTaskDelay vs vTaskDelayUntil——延时也有讲究
这两个函数都能让任务阻塞,但用法完全不同。我刚开始用的时候,也搞混过。
4.4.1 vTaskDelay——相对延时
vTaskDelay是相对延时。它让任务阻塞指定的tick数,从调用时开始算起。
void vTaskFunction(void *pvParameters)
{
for(;;)
{
// 执行任务操作
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
// 延时1000个tick
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
这里有个坑:如果任务执行时间不稳定,那么两次执行之间的间隔就不固定。比如任务执行花了200ms,延时1000ms,那实际周期就是1200ms。时间长了,误差会累积。
4.4.2 vTaskDelayUntil——绝对延时
vTaskDelayUntil是绝对延时。它保证任务以固定的频率执行。
void vTaskFunction(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
// 执行任务操作
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
// 延时到下一个1000ms时刻
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1000));
}
}
你想想看,不管任务执行花了多少时间,vTaskDelayUntil都会让任务在固定的时间点醒来。这对于需要精确周期的任务,比如ADC采样、PWM控制,特别有用。
| 特性 | vTaskDelay | vTaskDelayUntil |
|---|---|---|
| 延时类型 | 相对延时 | 绝对延时 |
| 周期精度 | 受任务执行时间影响 | 固定周期 |
| 适用场景 | 非周期性延时、按键消抖 | 周期性任务、采样、控制 |
| 参数 | 延时tick数 | 上次唤醒时间指针 + 周期tick数 |
vTaskDelayUntil。比如一个100Hz的PID控制循环,用vTaskDelayUntil能保证每次执行间隔正好是10ms,不会因为某次计算超时而导致周期漂移。
4.5 任务创建实战——从零开始
说了这么多,我们来写一个实际的任务创建代码。
// 任务句柄
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
// 任务1:LED闪烁,使用vTaskDelay
void vTask1(void *pvParameters)
{
for(;;)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 任务2:精确周期任务,使用vTaskDelayUntil
void vTask2(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(100); // 10Hz
for(;;)
{
// 采集传感器数据
uint32_t adcValue = HAL_ADC_GetValue(&hadc1);
// 处理数据
ProcessSensorData(adcValue);
// 精确延时
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
// 创建任务1:优先级2,栈大小128字
xTaskCreate(vTask1, "Task1", 128, NULL, 2, &xTask1Handle);
// 创建任务2:优先级3,栈大小256字
xTaskCreate(vTask2, "Task2", 256, NULL, 3, &xTask2Handle);
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
for(;;);
}
uxTaskGetStackHighWaterMark()查看实际使用量,再调整栈大小。
嗯,到这里,任务创建与管理的核心内容就讲完了。记住:TCB是任务的身份证,状态机决定了任务怎么切换,优先级决定了谁先跑,延时函数选对了能让你的系统更稳定。这些基础打牢了,后面讲任务间通信和同步时,你会觉得轻松很多。