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()来删除任务,那么钩子函数要非常高效地执 行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

浙公网安备 33010602011771号