任务管理
3.1任务的概念
对于整个单片机程序,我们称之为application,应用程序。 1
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
3.2 任务创建与删除
3.2.1 什么是任务
在FreeRTOS中,任务就是一个函数,原型如下:
1 | void ATaskFunction( void *pvParameters ); |
要注意以下几点:
- 这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部,尽量使用局部变量
- 每个任务都有自己的栈
- 每个任务运行这个函数时
- 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
- 不同任务的局部变量,有自己的副本
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本
- 要防止冲突(后续会讲)
下面是一个示例:
1 | void ATaskFunction( void *pvParameters ) |
3.2.2 创建任务
创建任务时使用的函数如下:
1 | BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数 |
参数说明:
参数 | 描述 |
---|---|
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。 |
*在已经内置FreeRTOS的系统中,无需启用调度器,直接在需要的位置创建任务即可
3.2.4 任务的删除
删除任务时使用的函数如下:
1 | void vTaskDelete( TaskHandle_t xTaskToDelete ); |
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
3.3任务优先级和系统Tick
3.3.1 任务优先级
优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。
FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。
在学习调度方法之前,你只要初略地知道:
- FreeRTOS会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
3.3.2 Tick
对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。
“一会”怎么定义?
人有心跳,心跳间隔基本恒定。
FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。
有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:
1 | vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms |
注意,基于Tick实现的延时并不精确,比如vTaskDelay(2)的本意是延迟2个Tick周期,有可能经过1个Tick多一点就返回了。
使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。
这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。
3.3.4 修改优先级
使用uxTaskPriorityGet来获得任务的优先级:
1 | UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ); |
使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
1 | void vTaskPrioritySet( TaskHandle_t xTask, |
使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;
参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。
3.4 任务状态
以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分:
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
3.4.1 阻塞状态(Blocked)
在实际产品中,我们不会让一个任务一直运行,而是使用”事件驱动”的方法让它运行:
- 任务要等待某个事件,事件发生后它才能运行
- 在等待事件过程中,它不消耗CPU资源
- 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)
在阻塞状态的任务,它可以等待两种类型的事件:
- 时间相关的事件
- 可以等待一段时间:我等2分钟
- 也可以一直等待,直到某个绝对时间:我等到下午3点
- 同步事件:这事件由别的任务,或者是中断程序产生
- 例子1:任务A等待任务B给它发送数据
- 例子2:任务A等待用户按下按键
- 同步事件的来源有很多(这些概念在后面会细讲):
- 队列(queue)
- 二进制信号量(binary semaphores)
- 计数信号量(counting semaphores)
- 互斥量(mutexes)
- 递归互斥量、递归锁(recursive mutexes)
- 事件组(event groups)
- 任务通知(task notifications)
在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:
10ms之内有数据到来:成功返回
10ms到了,还是没有数据:超时返回
3.4.2 暂停状态(Suspended)
FreeRTOS中的任务可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:
1 | void vTaskSuspend( TaskHandle_t xTaskToSuspend ); |
参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。
要退出暂停状态,只能由别人来操作:
别的任务调用:vTaskResume
中断程序调用:xTaskResumeFromISR
实际开发中,暂停状态用得不多。
3.4.3 就绪状态(Ready)
这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。
3.4.4 完整的状态转换图
3.5 Delay函数
3.5.1 两个Delay函数
有两个Delay函数:
- vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
- vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。
这2个函数原型如下:
1 | void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给Tick */ |
- 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
- 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会所以可以使用xTaskDelayUntil来让任务周期性地运行