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在同时闪烁。

小提示:任务栈大小128是字(word)为单位,不是字节。在32位处理器上,128字 = 512字节。如果栈太小,任务可能会崩溃。我一般先给256字,稳定后再优化。

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,只需要定义一个新的配置结构体,再创建一个任务就行。

核心要点:任务参数是一个void指针,可以指向任何数据类型。但要注意,参数指向的内存必须在任务运行期间一直有效。如果传局部变量的地址,任务还没运行,变量就销毁了,那就会出问题。我曾经犯过这个错,排查了半天...

3.3 空闲任务与钩子函数

当系统中没有其他任务需要运行时,FreeRTOS会运行空闲任务。这个任务优先级最低,永远不能被阻塞或挂起。

空闲任务到底在干嘛?说白了,它就是在「吃时间片」。但它的存在有两个重要意义:

  • 回收任务资源:当任务被删除时,空闲任务负责释放任务占用的内存。
  • 进入低功耗模式:在空闲任务中,可以让CPU进入睡眠,省电。

我们可以通过空闲任务钩子函数,在空闲任务每次运行时插入自定义代码。比如统计CPU利用率、进入低功耗模式等。

启用钩子函数需要两步:

  1. 在FreeRTOSConfig.h中设置 configUSE_IDLE_HOOK 为1
  2. 实现钩子函数 vApplicationIdleHook

示例:

void vApplicationIdleHook(void)
{
    // 进入低功耗模式
    __WFI();
}

嗯,这里要注意:钩子函数中不能调用任何可能阻塞的API,比如vTaskDelay。因为空闲任务本身不能被阻塞。我见过有人在这里加延时,结果系统直接卡死。

警告:空闲任务钩子函数执行时间要尽量短。如果钩子函数执行时间过长,会影响其他任务的实时性。我一般只放几条指令,比如进入低功耗、喂看门狗。

3.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

任务管理与调度实战知识体系 多任务LED闪烁 • 任务创建 xTaskCreate • 任务函数无限循环 • vTaskDelay 延时 • 时间片轮转调度 • 相同优先级交替运行 核心:任务是独立执行流 任务参数传递 • void *pvParameters • 结构体封装配置 • 指针类型转换 • 内存生命周期管理 • 代码复用与解耦 核心:参数必须持久有效 空闲任务与钩子 • 空闲任务自动创建 • 优先级最低 • 回收已删除任务 • 钩子函数配置 • 低功耗/统计CPU 核心:钩子中不能阻塞 三者共同构成任务管理基础

3.5 避坑指南与经验总结

做这个实战项目,有几个坑我帮你提前踩了:

  • 任务栈溢出:如果LED不闪烁,或者系统跑飞,先检查栈大小。我一般会在FreeRTOSConfig.h中开启栈溢出检测钩子。
  • 参数指针失效:如果传局部变量地址,任务还没运行,局部变量就没了。参数必须用全局变量或动态分配的内存。
  • 钩子函数阻塞:不要在空闲任务钩子里调用vTaskDelay或等待信号量。空闲任务不能被阻塞,否则系统会崩溃。
  • 优先级反转:虽然这个Demo里优先级相同,但实际项目中要注意优先级设计。高优先级任务如果一直占用CPU,低优先级任务可能永远得不到执行。

好了,这一章的内容就到这里。你跟着把代码敲一遍,跑起来,然后试着改改参数,比如让一个LED闪烁频率变成200ms,另一个变成800ms。动手试试,感受一下任务调度的魅力。

记住,理论是地图,实战才是真正的驾驶。多写代码,多调试,你很快就能掌握FreeRTOS的任务管理。


公众号:蓝海资料掘金营,微信deep3321