FreeRTOS【2】任务、优先级知识重点
目录
- 写在前面
- 一.什么是任务
- 二.创建任务
- 三.使用任务参数
- 四.任务的删除
- 五.任务优先级和 Tick
- 任务优先级
- Tick
- 修改优先级
- 六.任务状态
- 七.Delay 函数
- 八.空闲任务及其钩子函数
写在前面
这篇文章只写关键的知识。
一.什么是任务
在 FreeRTOS 中,任务就是一个函数,原型如下
void ATaskFunction( void *pvParameters );
注意点:
这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部,尽量使用局部变量:
-每个任务都有自己的栈 - 每个任务运行这个函数时
- 任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B
的栈里 - 不同任务的局部变量,有自己的副本
- 任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本。
- 要防止冲突。
二.创建任务
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 ); // 任务句柄, 以后使用它来操作这个任
务
参数名 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个 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(失败原因只有内存不足)。注意:文档里都说失败时返回值是 pdFAIL,这不对。pdFAIL 是 0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 是-1。 |
三.使用任务参数
多个任务可以使用同一个函数,怎么体现它们的差别?
- 栈不同。
- 创建任务时可以传入不同的参数。
本质是就是不同的任务,只不过两个任务之间执行的逻辑是一样的。
四.任务的删除
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数名 | 描述 |
---|---|
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。也可传入NULL,这表示删除自己。 |
怎么删除任务?
- 自杀: vTaskDelete(NULL)。
- 被杀:别的任务执行 vTaskDelete(pvTaskCode), pvTaskCode 是自己的句
柄 。 - 杀人:执行 vTaskDelete(pvTaskCode), pvTaskCode 是别的任务的句柄。
五.任务优先级和 Tick
任务优先级
优先级的取值范围是: 0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
FreeRTOS 的调度器可以使用 2 种方法来快速找出优先级最高的、可以运行的任务。
使用不同的方法时, configMAX_PRIORITIES 的取值有所不同。
- 通用方法
使用 C 函数实现,对所有的架构都是同样的代码。对 configMAX_PRIORITIES 的取值
没有限制。但是 configMAX_PRIORITIES 的取值还是尽量小,因为取值越大越浪费内存,
也浪费时间。
configUSE_PORT_OPTIMISED_TASK_SELECTION 被定义为 0、或者未定义时,使用此方法。 - 架构相关的优化的方法
架构相关的汇编指令,可以从一个 32 位的数里快速地找出为 1 的最高位。使用这些
指令,可以快速找出优先级最高的、可以运行的任务。使用这种方法时,
configMAX_PRIORITIES 的取值不能超过 32。
configUSE_PORT_OPTIMISED_TASK_SELECTION 被定义为 1 时,使用此方法。
FreeRTOS 会确保最高优先级的、可运行的任务,马上就能执行
对于相同优先级的、可运行的任务,轮流执行
Tick
人有心跳,心跳间隔基本恒定。
FreeRTOS 中也有心跳,它使用定时器产生固定间隔的中断。这叫 Tick、滴答,比如
每 10ms 发生一次时钟中断。
注意,基于 Tick 实现的延时并不精确,比如 vTaskDelay(2)的本意是延迟 2 个 Tick 周
期,有可能经过 1 个 Tick 多一点就返回了。
使用 vTaskDelay 函数时,建议以 ms 为单位,使用 pdMS_TO_TICKS 把时间转换为 Tick。
这样的代码就与 configTICK_RATE_HZ 无关,即使配置项 configTICK_RATE_HZ 改变了,
我们也不用去修改代码。
修改优先级
- 获取优先级
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用参数 xTask 来指定任务,设置为 NULL 表示获取自己的优先级。
- 修改优先级
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
使 用 参 数 xTask 来 指 定 任 务 , 设 置 为 NULL 表 示 设 置 自 己 的 优 先 级 ;
参数 uxNewPriority 表示新的优先级,取值范围是 0~(configMAX_PRIORITIES – 1)。
六.任务状态
- 阻塞状态(Blocked)
任务处于想执行但是执行不了,需要契机才能运行,以依托与外部条件的。
- 契机是什么?:
- 时间
- 等待一段时间,比如2min
- 等到固定的时间
- 同步事件:这事件由别的任务,或者是中断程序产生
- 例子 1:任务 A 等待任务 B 给它发送数据
- 例子 2:任务 A 等待用户按下按键
- 同步事件的来源:
- 队列(queue)
- 二进制信号量(binary semaphores)
- 计数信号量(counting semaphores)
- 互斥量(mutexes)
- 递归互斥量、递归锁(recursive mutexes) 事件组(event groups)
- 任务通知(task notifications)
- 时间
- 暂停状态(Suspended)
FreeRTOS 中的任务也可以进入暂停状态,暂停就是冻结了这个任务,或者隐藏了这个任务。
唯一的方法是通过 vTaskSuspend 函数。函
数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数 xTaskToSuspend 表示要暂停的任务,如果为 NULL,表示暂停自己。
要退出暂停状态,只能由别人来操作:
- 别的任务调用: vTaskResume
- 中断程序调用: xTaskResumeFromISR
实际开发中,暂停状态用得不多。
- 就绪状态(Ready)
随时响应的状态。
状态轮转图:
七.Delay 函数
很多人看Delay函数,望文生义,感觉就是简单的一个延时函数,其实不然,操作系统作为一个整体,各个任务交替执行井然有序的前提是需要一个delay来调节任务的节拍。
Delay类似操作系统世界的交通警察,该停的时候停,该动的时候动。
有两个 Delay 函数:
- vTaskDelay:至少等待指定个数的 Tick Interrupt 才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
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 来让任务周期性地运行。
八.空闲任务及其钩子函数
==一个良好的程序,它的任务都是事件
驱动的:平时大部分时间处于阻塞状态。==有可能我们自己创建的所有任务都无法执行,但
是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用
vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为 0:它不能阻碍用户任务运行.
- 空闲任务要么处于就绪态, 要么处于运行态,永远不会阻塞.
空闲任务的优先级为 0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马
上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)
了空闲任务,这是由调度器实现的。
要注意的是:如果使用 vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执
行,否则就无法释放被删除任务的内存。
钩子函数:
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环
每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都
停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。 - 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,
当然可以进入省电模式了。
空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。
如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
使用钩子函数的前提:
- 宏定义为 1: configUSE_IDLE_HOOK
- 实现 vApplicationIdleHook 函数