FreeRTOS(1):任务管理

1、任务创建与删除

1.1 什么是任务

在 FreeRTOS 中,任务就是一个函数

void ATaskFunction( void *pvParameters );

注意:

1)这个函数不能返回 

2)同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数

3)函数内部,尽量使用局部变量

◼每个任务都有自己的栈

◼ 每个任务运行这个函数时

   ◆ 任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B 的 栈里

  ◆ 不同任务的局部变量,有自己的副本

◼ 函数使用全局变量、静态变量的话

  ◆ 只有一个副本:多个任务使用的是同一个副本

  ◆ 要防止冲突

void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
 /* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
 /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
 * NULL表示删除的是自己
 */
vTaskDelete( NULL );
 
 /* 程序不会执行到这里, 如果执行到这里就出错了 *

1.2 创建任务

创建任务时可以使用 2 个函数:动态分配内存、静态分配内存。

使用动态分配内存的函数如下:

BaseType_t xTaskCreate( 
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

 参数说明:

参数
描述
pxTaskCode
函数指针,可以简单地认为任务就是一个 C 函数。 它稍微特殊一点:永远不退出,或者退出时要调用 "vTaskDelete(NULL)"
pcName
任务的名字,FreeRTOS 内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN
usStackDepth
每个任务都有自己的栈,这里指定栈大小。 单位是 word,比如传入 100,表示栈大小为 100 word,也就是 400 字 节。 最大值为 uint16_t 的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。
pvParameters
调用 pvTaskCode 函数指针时用到:pvTaskCode(pvParameters)
uxPriority
优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate 会把它调整为 (configMAX_PRIORITIES – 1)
pxCreatedTask  用来保存 xTaskCreate 的输出结果:task handle。 以后如果想操作这个任务,比如修改它的优先级,就需要这个 handle。 如果不想使用该 handle,可以传入 NULL。
返回值 成功:pdPASS; 失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存 不足)

使用静态分配内存的函数如下:

TaskHandle_t xTaskCreateStatic ( 
 TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
 StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);

 参数说明:

参数 描述
 pxTaskCode
函数指针,可以简单地认为任务就是一个 C 函数。 它稍微特殊一点:永远不退出,或者退出时要调用 "vTaskDelete(NULL)"
pcName 任务的名字,FreeRTOS 内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN
ulStackDepth
每个任务都有自己的栈,这里指定栈大小。 单位是 word,比如传入 100,表示栈大小为 100 word,也就是 400 字 节。 最大值为 uint16_t 的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。
pvParameters
调用 pvTaskCode 函数指针时用到:pvTaskCode(pvParameters)
uxPriority
优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate 会把它调整为 (configMAX_PRIORITIES – 1)
puxStackBuffer 静态分配的栈内存,比如可以传入一个数组, 它的大小是 usStackDepth*4。
pxTaskBuffer 静态分配的 StaticTask_t 结构体的指针
返回值 成功:返回任务句柄; 失败:NULL

 

1.3 任务的删除

void vTaskDelete( TaskHandle_t xTaskToDelete );c参数

参数:

参数 描述
pvTaskCode
任务句柄,使用 xTaskCreate 创建任务时可以得到一个句柄。 也可传入 NULL,这表示删除自己。

怎么删除任务?举个不好的例子:

⚫ 自杀:vTaskDelete(NULL)

⚫ 被杀:别的任务执行 vTaskDelete(pvTaskCode),pvTaskCode 是自己的句柄

⚫ 杀人:执行 vTaskDelete(pvTaskCode),pvTaskCode 是别的任务的句柄

2、任务优先级和Tick

2.1 任务优先级

 在学习调度方法之前,你只要初略地知道:

⚫ FreeRTOS 会确保最高优先级的、可运行的任务,马上就能执行

⚫ 对于相同优先级的、可运行的任务,轮流执行

这无需记忆,就像我们举的例子:

⚫ 厨房着火了,当然优先灭火

⚫ 喂饭、回复信息同样重要,轮流做

 2.2 Tick

 对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。

"一会"怎么定义?

人有心跳,心跳间隔基本恒定。

FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms 发生一次时钟中断。

如下图:

  ⚫ 假设 t1、t2、t3 发生时钟中断

  ⚫ 两次中断之间的时间被称为时间片(time slice、tick period)

  ⚫ 时间片的长度由 configTICK_RATE_HZ 决定,假设 configTICK_RATE_HZ 为 100,那么时间片长度就是 10ms

 

 相同优先级的任务怎么切换呢?请看下图:

⚫ 任务 2 从 t1 执行到 t2

⚫ 在 t2 发生 tick 中断,进入 tick 中断处理函数:

  ◼ 选择下一个要运行的任务

  ◼ 执行完中断处理函数后,切换到新的任务:任务 1

⚫ 任务 1 从 t2 执行到 t3

⚫ 从图中可以看出,任务运行的时间并不是严格从 t1,t2,t3 哪里开始

 

 有了 Tick 的概念后,我们就可以使用 Tick 来衡量时间了,比如:

vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms
// 还可以使用pdMS_TO_TICKS宏把ms转换为tick
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms

  注意,基于Tick实现的延时并不精确,比如vTaskDelay(2)的本意是延迟2个Tick周期, 有可能经过1个Tick多一点就返回了。

 

 使用 vTaskDelay 函数时,建议以 ms 为单位,使用 pdMS_TO_TICKS 把时间转换为 Tick。 这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了, 我们也不用去修改代码。

 2.3 修改优先级

使用uxTaskPriorityGet来获得任务的优先级:

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

 使用参数 xTask 来指定任务,设置为 NULL 表示获取自己的优先级。

 

使用vTaskPrioritySet 来设置任务的优先级:

void vTaskPrioritySet( TaskHandle_t xTask,
 UBaseType_t uxNewPriority );

3、任务状态

 以前我们很简单地把任务的状态分为 2 中:运行(Runing)、非运行(Not Running)。 对于非运行的状态,还可以继续细分:

⚫ 阻塞状态(Blocked)

⚫ 暂停状态(Suspended)

⚫ 就绪状态(Ready)

 

 3.1 阻塞状态(Blocked)

在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的 工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在等待。

在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:

⚫ 任务要等待某个事件,事件发生后它才能运行

⚫ 在等待事件过程中,它不消耗 CPU 资源

⚫ 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)

3.2 暂停状态(Suspended)

FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数 xTaskToSuspend 表示要暂停的任务,如果为 NULL,表示暂停自己。

 

 

要退出暂停状态,只能由别人来操作:

⚫ 别的任务调用:vTaskResume

⚫ 中断程序调用:xTaskResumeFromISR

实际开发中,暂停状态用得不多。

3.3 就绪状态(Ready)

 这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。

3.4 Delay函数

 有两个 Delay 函数:

⚫ vTaskDelay:至少等待指定个数的 Tick Interrupt 才能变为就绪状态

⚫ vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。

这 2 个函数原型如下:

void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
* xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
* 单位都是Tick Count
*/
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
 const TickType_t xTimeIncrement );

下面画图说明:

⚫ 使用 vTaskDelay(n)时,进入、退出 vTaskDelay 的时间间隔至少是 n 个 Tick 中断

⚫ 使用 xTaskDelayUntil(&Pre, n)时,前后两次退出 xTaskDelayUntil 的时间 至少是 n 个 Tick 中断

  ◼ 退出 xTaskDelayUntil 时任务就进入的就绪状态,一般都能得到执行机会

  ◼ 所以可以使用 xTaskDelayUntil 来让任务周期性地运行  

 

 3.5 空闲任务及其钩子函数

   空闲任务(Idle 任务)的作用之一:释放被删除的任务的内存。

  除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱 动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调 度 器 必 须 能 找 到 一 个 可 以 运 行 的 任 务 : 所 以 , 我 们 要 提 供 空 闲 任 务 。 在 使 用 vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:

⚫ 空闲任务优先级为 0:它不能阻碍用户任务运行

⚫ 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

  空闲任务的优先级为 0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上 被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空 闲任务,这是由调度器实现的。

  要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会 执行,否则就无法释放被删除任务的内存。

 

  我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循 环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

⚫ 执行一些低优先级的、后台的、需要连续执行的函数

⚫ 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停 止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。

⚫ 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当 然可以进入省电模式了。 

⚫ 空闲任务的钩子函数的限制:

  ⚫ 不能导致空闲任务进入阻塞状态、暂停状态

  ⚫ 如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执 行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

 

posted @ 2023-11-01 20:55  xsgcumt  阅读(166)  评论(0)    收藏  举报