freertos任务管理

任务调度

任务被创建后,它可能正在运行,可能暂停运行,任务有状态之分是由于调度器的存在,调度器需要决定哪些任务可以去运行,于是在FreeRTOS中任务具有4种状态,分别是就绪态、运行态、阻塞态和挂起态,它们之间的转化关系如下:

image.png

  • 4个状态的含义如下:
  1. 就绪态:已经可以运行,等待调度器的切入
  2. 运行态:正在占用CPU运行
  3. 阻塞态:等待某个事件的到来,定时或者同步
  4. 挂起态:退出调度系统,调度器不可见,只能使用vTaskSuspend()挂起和vTaskResume()唤醒后进入就绪态

第5种是僵尸态,指在任务被删除后,其TCB控制块扔保留一段时间,等待内核检查和回收资源,在内核没有处理之前,任务其实并没有被完全删除,但是再也不能被调度器调度,这称为僵尸态,在Linux下的进程是存在僵尸态的,而从FreeRTOS的API中就可以看出,FreeRTOS的任务也存在僵尸态。

  • tick时钟和调度器
    调度器本身也是一段程序,任务需要调度器安排执行顺序,那么调度器本身就需要被执行,这个执行由一个称为心跳时钟(tick)的中断触发,tick时钟的频率需要在FreeRTOSConfig.h文件中配置,单位是HZ,比如本例程中配置configTICK_RATE_HZ为1000,那么中断频时间是1000/1000 = 1ms,每隔1ms,FreeRTOS就会进入tick中断,触发调度器进行工作,在所有任务优先级相同的情况下,每个任务执行1ms然后切换到下一个任务执行1ms一次循环,这叫做任务的时间片流转
1
2
#define configTICK_RATE_HZ				( ( TickType_t ) 1000 )
#define configUSE_TIME_SLICING 1 //时间片轮转

调度器被触发后,会根据事先设定好的调度算法进行工作,FreeRTOS使用的调度算法有优先级抢占式调度和协作式调度:

优先级抢占式调度算法,给每一个任务分配一个优先级,调度器每次都选择优先级最高的任务执行,如果优先级相同,就采用时间片轮流执行,每个任务执行一个时间片后切出,再切入下一个任务。
协作式调度算法,只可能在运行态任务进入阻塞态或是运行态任务显式调用taskYIELD()主动让出CPU时,才会进行上下文切换。调度算法的配置也是在FreeRTOSConfig.h中进行。

1
#define configUSE_PREEMPTION			1	//0-协作调度,1-抢占式调度

任务优先级

在优先级相同的情况下任务可以通过时间片流转来达到所有任务都同时运行的效果,但是当采用抢占式调度时,任务会优先运行高优先级任务然后当高优先级任务运行完成后,在执行低优先级任务。但是当高优先级的任务一直在运行,没有结束或暂停,那么就会使得低优先级的任务没有机会执行调度。所以需要适当的阻塞高优先级的任务来保证低优先级的任务有时间进行任务调度。
但是注意一般通过定时器定时的延时函数如HAL_Delay()是不具备阻塞任务功能的,此类函数是通过while死等定时器计数来实现的。要想阻塞任务可以使用freertos提供的延时函数。
可以主动阻塞任务的两个延时函数:
vTaskDelay():至少等待指定个数的Tick Interrupt才能变为就绪状态
vTaskDelayUntil():等待到指定的绝对时刻,才能变为就绪态。

当然任务的优先级是可以实时获取和动态改变的。

1
2
3
4
if (uxTaskPriorityGet(stask1ctr) == uxTaskPriorityGet(taskled2))
{
vTaskPrioritySet(taskled2,configMAX_PRIORITIES-2); //优先级设置到最高 并且taskled2的延时非阻塞,所以其他任务无法执行。
}

静态任务创建

在FreeRTOS中,创建一个静态的任务可以提高系统的性能并且减少堆栈空间的使用。这是因为静态任务分配是在编译时完成的,而不是在运行时动态分配,这样可以避免动态内存分配可能带来的碎片问题。

下面是如何在FreeRTOS中创建一个静态任务的基本步骤:

  1. 定义任务堆栈区
    在创建任务之前,你需要为任务分配一个固定大小的堆栈区域。这个区域通常是通过定义一个足够大的数组来实现的。

    1
    uint8_t taskStack[ configMINIMAL_STACK_SIZE ]; // configMINIMAL_STACK_SIZE 是配置文件中的常量,定义了最小的堆栈大小。
  2. 定义任务控制块(TCB)
    静态任务需要一个任务控制块(Task Control Block),这个块是FreeRTOS用来管理和调度任务的数据结构。

    1
    2
    StaticTask_t xTaskBuffer; // StaticTask_t 是用于存储任务控制块的类型。
    StackType_t *pxTopOfStack;
  3. 初始化任务控制块
    使用prvGetTaskMemoryArray()函数初始化任务控制块和堆栈顶部指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pxTopOfStack = ( StackType_t * )taskStack;
    #if( configUSE_STATIC_ALLOCATION == 1 )
    {
    prvGetTaskMemoryArray( pxTopOfStack, &xTaskBuffer, configMINIMAL_STACK_SIZE );
    }
    #else
    {
    pxTopOfStack = ( StackType_t * )taskStack + ( configMINIMAL_STACK_SIZE / sizeof( StackType_t ) ) - 1;
    }
  4. 创建任务
    使用xTaskCreateStatic()函数创建任务,并传入之前定义的任务控制块和堆栈。

    1
    2
    3
    4
    5
    6
    7
    8
    xTaskCreateStatic(
    TaskFunction, /* 任务函数 */
    "TaskName", /* 任务的名字 */
    configMINIMAL_STACK_SIZE, /* 堆栈大小 */
    NULL, /* 传递给任务函数的参数 */
    tskIDLE_PRIORITY + 1, /* 任务优先级 */
    pxTopOfStack, /* 堆栈的顶部地址 */
    &xTaskBuffer); /* 指向任务控制块的指针 */

请注意,在实际编写代码时,需要根据你的FreeRTOS版本以及具体的硬件平台进行相应的调整。此外,configUSE_STATIC_ALLOCATION宏需要在FreeRTOSConfig.h文件中定义为1,以启用静态内存分配。

上述代码仅作为一个示例,实际使用时需要根据具体的实现细节调整代码,特别是prvGetTaskMemoryArray()函数的使用,因为该函数在某些FreeRTOS版本中可能不存在或有不同的实现方式。

动态任务创建

在FreeRTOS中创建动态任务涉及到使用xTaskCreate()函数来动态地分配任务所需的内存。以下是创建动态任务的基本步骤及示例代码:

  1. 定义任务函数

首先,你需要定义一个任务函数。这个函数将作为任务的主要执行体。

1
2
3
4
5
6
7
8
9
10
11
12
void vTaskFunction(void *pvParameters)
{
// 这里是任务的主体逻辑
while(1)
{
// 执行任务的具体工作
printf("Hello from the dynamic task!\n");

// 延迟一段时间,让其他任务有机会执行
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒
}
}
  1. 创建任务

接下来,在你的应用程序中创建任务。使用xTaskCreate()函数来创建一个新的任务实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建动态任务
xTaskHandle xTaskHandle; // 用于保存新创建任务的句柄
xTaskCreate(
vTaskFunction, // 任务函数
"Dynamic Task", // 任务名字
configMINIMAL_STACK_SIZE, // 堆栈大小
NULL, // 传递给任务函数的参数
1, // 任务优先级
&xTaskHandle); // 用于保存任务句柄的变量

// 启动调度器
vTaskStartScheduler();

  1. 参数说明
  • vTaskFunction: 这是你定义的任务函数的名称。
  • "Dynamic Task": 给任务命名,便于调试和区分。
  • configMINIMAL_STACK_SIZE: 为任务分配的堆栈大小。你可以根据实际情况调整这个值。
  • NULL: 传递给任务函数的参数。如果需要传递参数,可以替换为实际的参数。
  • 1: 任务的优先级。数值越大,优先级越高。需要注意的是,优先级的选择应该考虑到系统的整体任务调度策略。
  • &xTaskHandle: 一个指向xTaskHandle类型的变量的指针,用于接收新创建任务的句柄。
  1. 注意事项
  • 确保在调用xTaskCreate()之前已经正确配置了FreeRTOS,并且在FreeRTOSConfig.h中设置了正确的配置选项。
  • vTaskStartScheduler()会启动FreeRTOS的调度器,之后所有的任务都将由调度器管理。
  • 动态任务的堆栈是在运行时动态分配的,因此如果系统中有大量任务或者频繁创建和删除任务,可能会导致内存碎片问题。在这种情况下,考虑使用静态任务分配或者增加堆栈空间。
  1. 可以不使用任务句柄来创建动态任务
1
xTaskCreate(taskled2_app, "led2", 128, NULL, 3, NULL); // 无控制句柄也可运行

任务传参

可以通过在创建任务时传入参数,在任务运行时使用入参从而运行相应的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 传入参数 的生命周期要一直在任务运行期间保持
uint8_t a = 1;
uint8_t b = 2;

// 静态创建任务,内存由用户手动开辟释放
stask1ctr = xTaskCreateStatic(
taskled1_app, // 任务进入函数
"led2", // 任务名字
stask1size, // 堆栈大小
&a, // 传入参数,没有就传入NULL
3, // 任务优先级
stask1buff, // 静态堆栈buff
&stask1 // 静态任务控制buff
);
xTaskCreate(taskled2_app, "led1", 128, &b, 3, &taskled1);

// 通过传入的参数来控制不同led翻转
void taskled1_app(void *p)
{
while (1) // 每个运行函数必须有死循环
{
uint8_t *is = (uint8_t *)p; // 参数强制转换
if (*is == 1)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
vTaskDelay(1000);
}
else if (*is == 2)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
vTaskDelay(1500);
}
}
}

任务堆栈相互独立

不同任务可以使用相同的进入函数 因为每个任务的堆栈是独立的 所以虽然调用的函数一样,但是传入的参数和运行空间都是独立的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  stask1ctr = xTaskCreateStatic(
taskled1_app, // 任务进入函数
"led2", // 任务名字
stask1size, // 堆栈大小
&a, // 传入参数,没有就传入NULL
3, // 任务优先级
stask1buff, // 静态堆栈buff
&stask1ctrbuff // 静态任务控制buff
);
xTaskCreate(taskled1_app /*任务进入函数相同*/, "led1", 128, &b, 3, &taskled1);
// 通过传入的参数来控制不同led翻转
void taskled1_app(void *p)
{
uint16_t j = 0;
while (1) // 每个运行函数必须有死循环
{
uint8_t *is = (uint8_t *)p; // 参数强制转换
if (*is == 1)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
vTaskDelay(1000);
printf("led2:%d\r\n",j++);
}
else if (*is == 2)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
vTaskDelay(1500);
printf("led1:%d\r\n",j++);
}

}
}

可以观察到led1间隔1.5s翻转,led2间隔1s翻转。并且在串口输出的累计值,来自同一的变量j 但是j并没有被两个任务同时累加,而是在各自任务的执行频率下依次累加,所以表明两个任务的堆栈是相互独立的。
image.png

任务休眠和唤醒

使用 vTaskSuspend()使任务进入挂起态,该任务不在被调度器调度运行,使用 vTaskResume()使任务进入就绪态,该任务会被调度器调度运行。

1
2
3
4
5
if (/*GetKey() == KEY0*/ get_button_event(&MKEY0) == SINGLE_CLICK)
{
vTaskSuspend(taskled1); // 唤醒任务
vTaskResume(stask1ctr); // 暂停任务
}

任务删除与回收

使用 vTaskDelete()使任务进入僵尸态,进入僵尸态的任务将无法变成其他状态,动态创建的任务等待空闲任务来回收内存,而静态创建的任务不会回收内存。

1
2
3
4
5
else if (/*GetKey() == KEY2*/  get_button_event(&MKEY2) == SINGLE_CLICK)
{
vTaskDelete(taskled1); // 只能删除动态创建任务后内存会被空闲任务释放
vTaskDelete(stask1ctr); // 删除静态创建任务后内存不会被释放,容易造成内存泄漏
}

这里是这个文章freertos的总仓库 github https://github.com/freedom413/FreeRtosDemo.git