3. 任务管理与调度实战:多任务LED闪烁、任务参数传递、空闲任务与钩子函数
各位同学,大家好。今天我们进入实战环节。说实话,很多初学者学FreeRTOS,看了不少理论,但一上手写代码就懵了。任务怎么创建?参数怎么传?空闲任务到底干嘛用的?
别急,这一章我们全部搞定。我会带着你,从零开始写一个多任务LED闪烁的Demo。你跟着敲一遍,保证对任务管理有全新的理解。
3.1 多任务LED闪烁:第一个实战项目
我们先定个小目标:创建两个任务,分别控制两个LED以不同频率闪烁。一个500ms,一个1000ms。听起来简单,但这里面包含了任务创建、优先级、延时阻塞等核心概念。
先看代码框架:
void vTaskLED1(void *pvParameters)
{
for(;;)
{
GPIO_SetBits(LED1_GPIO_PORT, LED1_PIN);
vTaskDelay(500 / portTICK_PERIOD_MS);
GPIO_ResetBits(LED1_GPIO_PORT, LED1_PIN);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
void vTaskLED2(void *pvParameters)
{
for(;;)
{
GPIO_SetBits(LED2_GPIO_PORT, LED2_PIN);
vTaskDelay(1000 / portTICK_PERIOD_MS);
GPIO_ResetBits(LED2_GPIO_PORT, LED2_PIN);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
嗯,这里要注意:vTaskDelay的参数单位是系统时钟节拍。如果你配置的时钟节拍是1000Hz,那么vTaskDelay(500)就是延时500ms。我习惯用portTICK_PERIOD_MS这个宏来做转换,代码可读性更好。
然后在main函数里创建任务:
int main(void)
{
// 硬件初始化
LED_GPIO_Config();
xTaskCreate(vTaskLED1, "LED1", 128, NULL, 1, NULL);
xTaskCreate(vTaskLED2, "LED2", 128, NULL, 1, NULL);
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
两个任务优先级相同,都是1。FreeRTOS的调度器会采用时间片轮转的方式,让它们交替运行。你想想看,每个任务运行一个时间片,然后切换到另一个,看起来就像两个LED在同时闪烁。
3.2 任务参数传递:让代码更灵活
上面的代码有个问题:两个任务函数几乎一模一样,只是延时不同。能不能写一个通用的任务函数,通过参数来区分?
当然可以。这就是任务参数传递的用武之地。
我们定义一个结构体,包含LED的引脚信息和闪烁频率:
typedef struct
{
uint16_t usLEDPin;
uint16_t usDelayMs;
} LED_Config_t;
// 定义两个LED的配置
LED_Config_t xLED1Config = {LED1_PIN, 500};
LED_Config_t xLED2Config = {LED2_PIN, 1000};
然后写一个通用的任务函数:
void vTaskLED(void *pvParameters)
{
LED_Config_t *pxConfig = (LED_Config_t *)pvParameters;
for(;;)
{
GPIO_SetBits(LED_GPIO_PORT, pxConfig->usLEDPin);
vTaskDelay(pxConfig->usDelayMs / portTICK_PERIOD_MS);
GPIO_ResetBits(LED_GPIO_PORT, pxConfig->usLEDPin);
vTaskDelay(pxConfig->usDelayMs / portTICK_PERIOD_MS);
}
}
创建任务时,把结构体指针传进去:
xTaskCreate(vTaskLED, "LED1", 128, (void *)&xLED1Config, 1, NULL);
xTaskCreate(vTaskLED, "LED2", 128, (void *)&xLED2Config, 1, NULL);
你看,代码瞬间精简了。而且以后要增加第三个LED,只需要定义一个新的配置结构体,再创建一个任务就行。
3.3 空闲任务与钩子函数
当系统中没有其他任务需要运行时,FreeRTOS会运行空闲任务。这个任务优先级最低,永远不能被阻塞或挂起。
空闲任务到底在干嘛?说白了,它就是在「吃时间片」。但它的存在有两个重要意义:
- 回收任务资源:当任务被删除时,空闲任务负责释放任务占用的内存。
- 进入低功耗模式:在空闲任务中,可以让CPU进入睡眠,省电。
我们可以通过空闲任务钩子函数,在空闲任务每次运行时插入自定义代码。比如统计CPU利用率、进入低功耗模式等。
启用钩子函数需要两步:
- 在FreeRTOSConfig.h中设置
configUSE_IDLE_HOOK为1 - 实现钩子函数
vApplicationIdleHook
示例:
void vApplicationIdleHook(void)
{
// 进入低功耗模式
__WFI();
}
嗯,这里要注意:钩子函数中不能调用任何可能阻塞的API,比如vTaskDelay。因为空闲任务本身不能被阻塞。我见过有人在这里加延时,结果系统直接卡死。
3.4 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
3.5 避坑指南与经验总结
做这个实战项目,有几个坑我帮你提前踩了:
- 任务栈溢出:如果LED不闪烁,或者系统跑飞,先检查栈大小。我一般会在FreeRTOSConfig.h中开启栈溢出检测钩子。
- 参数指针失效:如果传局部变量地址,任务还没运行,局部变量就没了。参数必须用全局变量或动态分配的内存。
- 钩子函数阻塞:不要在空闲任务钩子里调用vTaskDelay或等待信号量。空闲任务不能被阻塞,否则系统会崩溃。
- 优先级反转:虽然这个Demo里优先级相同,但实际项目中要注意优先级设计。高优先级任务如果一直占用CPU,低优先级任务可能永远得不到执行。
好了,这一章的内容就到这里。你跟着把代码敲一遍,跑起来,然后试着改改参数,比如让一个LED闪烁频率变成200ms,另一个变成800ms。动手试试,感受一下任务调度的魅力。
记住,理论是地图,实战才是真正的驾驶。多写代码,多调试,你很快就能掌握FreeRTOS的任务管理。
公众号:蓝海资料掘金营,微信deep3321