3. FreeRTOS内核入门:任务创建、删除、挂起与恢复,任务状态机
好,咱们今天聊聊FreeRTOS里最核心的东西——任务管理。
说实话,我刚开始接触RTOS的时候,最困惑的就是任务到底是个什么玩意儿。后来在车上一跑,才真正明白:任务就是一段独立运行的代码,有自己的栈、有自己的优先级。说白了,就是让CPU在多个逻辑线程之间来回跳,看起来像同时干活。
3.1 任务状态机:五种状态,一个循环
FreeRTOS的任务状态机,我习惯把它画成一个五角星。每个角代表一种状态,箭头代表怎么跳转。
| 状态 | 英文 | 说明 |
|---|---|---|
| 运行态 | Running | CPU正在执行该任务 |
| 就绪态 | Ready | 任务可以运行,但CPU在忙别的 |
| 阻塞态 | Blocked | 任务在等某个事件(延时、信号量等) |
| 挂起态 | Suspended | 任务被主动挂起,不参与调度 |
| 删除态 | Deleted | 任务已被销毁,资源待回收 |
为什么会这样设计?你想想看,如果所有任务都在抢CPU,那不乱套了?FreeRTOS通过状态机,把任务的“活”和“等”分得清清楚楚。
核心要点:任务从创建到删除,本质上是状态在五个节点之间流转。你调用API,就是在帮任务“换挡”。
3.2 任务创建:xTaskCreate 与 xTaskCreateStatic
创建任务,最常用的是 xTaskCreate。我一般在项目里用动态创建,省心。但车规级项目里,有些客户要求静态创建——怕动态分配内存出问题。
// 动态创建示例
TaskHandle_t myTaskHandle = NULL;
void vTaskFunction(void *pvParameters) {
while(1) {
// 任务主体
vTaskDelay(pdMS_TO_TICKS(100));
}
}
BaseType_t xReturn = xTaskCreate(
vTaskFunction, // 任务函数
"MyTask", // 任务名(调试用)
256, // 栈深度(单位:字)
NULL, // 参数
2, // 优先级(0最低,configMAX_PRIORITIES-1最高)
&myTaskHandle // 任务句柄
);
if (xReturn != pdPASS) {
// 创建失败,处理错误
}
嗯,这里要注意:栈深度给多少?我踩过坑。有一次在ECU上跑一个CAN报文处理任务,给了128个字,结果跑着跑着就崩了。后来一查,是函数调用嵌套太深,栈溢出了。我的建议是:先给个保守值,比如512字,跑稳定了再调小。
个人经验:用 uxTaskGetStackHighWaterMark() 查看栈剩余空间。如果剩余不到20%,就该加栈了。
3.3 任务删除:vTaskDelete
删除任务很简单,但有个坑——你得确保任务自己把自己删了,或者别的任务删它时,它不在运行关键代码。
// 任务自己删除自己
void vTaskFunction(void *pvParameters) {
// 做一些初始化工作
// ...
// 干完活,自我了断
vTaskDelete(NULL); // NULL表示删除当前任务
}
// 别的任务删除它
void vAnotherTask(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (myTaskHandle != NULL) {
vTaskDelete(myTaskHandle);
myTaskHandle = NULL; // 记得置空,别悬空指针
}
}
我曾经在项目里犯过一个低级错误:删了任务后,没把句柄置空。结果另一个任务拿着这个野指针去操作,直接HardFault。嗯,血的教训。
警告:删除任务后,FreeRTOS会自动回收栈和TCB内存。但如果你用了静态创建,得自己手动释放资源。
3.4 任务挂起与恢复:vTaskSuspend 与 vTaskResume
挂起和阻塞不一样。阻塞是任务自己主动等,挂起是外部强制让它“闭嘴”。
// 挂起任务
vTaskSuspend(myTaskHandle);
// 恢复任务
vTaskResume(myTaskHandle);
// 从中断中恢复(注意:中断中不能用vTaskResume)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskResumeFromISR(myTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
我一般在什么场景用挂起?比如OTA升级的时候,先把所有非关键任务挂起,只留一个升级任务跑。升级完了再一个个恢复。这样能保证升级过程不被干扰。
避坑指南:我曾经在中断里直接调了 vTaskResume,结果调度器没及时切换,导致高优先级任务没立刻跑起来。后来改成 vTaskResumeFromISR 加 portYIELD 才搞定。
3.5 任务优先级与调度策略
FreeRTOS默认是抢占式调度。高优先级任务就绪了,低优先级任务立马被踢出去。但有个细节:同优先级任务之间是时间片轮转。
| 配置项 | 说明 | 我常用的值 |
|---|---|---|
| configUSE_PREEMPTION | 是否启用抢占 | 1(必须开) |
| configUSE_TIME_SLICING | 同优先级是否时间片轮转 | 1(默认开) |
| configTICK_RATE_HZ | 系统时钟节拍 | 1000(车用推荐) |
为什么车用推荐1000Hz?因为很多CAN报文周期是10ms、20ms,1000Hz的tick能精确到1ms,延时控制更准。但代价是CPU开销大一点。嗯,看你的MCU主频了,如果跑在200MHz以上,完全扛得住。
3.6 实战:一个简单的车灯控制任务
咱们来个实际的。假设你要控制一个LED灯,按一下按钮切换亮灭。用FreeRTOS实现:
TaskHandle_t xLedTaskHandle = NULL;
void vLedTask(void *pvParameters) {
uint8_t ucLedState = 0;
const uint32_t ulDebounceDelay = pdMS_TO_TICKS(50);
for(;;) {
// 等待按钮按下(假设用信号量通知)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 消抖延时
vTaskDelay(ulDebounceDelay);
// 切换LED状态
ucLedState = !ucLedState;
GPIO_WriteBit(LED_PORT, LED_PIN, ucLedState);
// 打印状态(调试用)
// printf("LED state: %d\n", ucLedState);
}
}
// 在main中创建
int main(void) {
// 硬件初始化
// ...
xTaskCreate(vLedTask, "LedTask", 128, NULL, 1, &xLedTaskHandle);
vTaskStartScheduler();
// 正常情况下不会跑到这里
while(1);
}
这个例子虽然简单,但包含了任务创建、阻塞等待、延时等核心操作。你想想看,如果把LED换成电机、把按钮换成CAN报文,是不是就活起来了?
我的习惯:每个任务都起个有意义的名字,比如"CanRxTask"、"DiagTask"。调试时用 vTaskList() 一看就知道谁在跑、谁在等。
3.7 小结
任务管理是FreeRTOS的基石。你掌握了创建、删除、挂起、恢复,再理解了状态机,基本上就能应付大部分车端应用了。
最后说一句:别怕任务多。我见过一个项目里跑了30多个任务,照样稳如老狗。关键是每个任务的栈大小要合理,优先级别乱设。嗯,今天就到这儿,下节课咱们聊队列和信号量。