目录
一、基本概念
对于整个单片机程序,我们称之为 application,应用程序。
使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也 称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
- 喂饭:这是一个任务
- 回信息:这是另一个任务
这可以引入很多概念:
- 任务状态(State):
- 当前正在喂饭,它是 running 状态;另一个"回信息"的任务就是"not running" 状态
- "not running"状态还可以细分:
- ready:就绪,随时可以运行
- blocked:阻塞,卡住了,母亲在等待同事回信息
- suspended:挂起,同事废话太多,不管他了
- 优先级(Priority)
- 我工作生活兼顾:喂饭、回信息优先级一样,轮流做
- 我忙里偷闲:还有空闲任务,休息一下
- 厨房着火了,什么都别说了,先灭火:优先级更高
- 栈(Stack)
- 喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了
- 回信息时,我要记得刚才聊的是啥
- 做不同的任务,这些细节不一样
- 对于人来说,当然是记在脑子里
- 对于程序,是记在栈里
- 每个任务有自己的栈
- 事件驱动
- 孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口
- 协助式调度(Co-operative Scheduling)
- 你在给同事回信息
- 同事说:好了,你先去给小孩喂一口饭吧,你才能离开
- 同事不放你走,即使孩子哭了你也不能走
- 你好不容易可以给孩子喂饭了
- 孩子说:好了,妈妈你去处理一下工作吧,你才能离开
- 孩子不放你走,即使同事连发信息你也不能走
- 你在给同事回信息
二、任务的创建与删除
1、什么是任务
在 FreeRTOS 中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
- 这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个 函数
- 函数内部,尽量使用局部变量:
- 每个任务都有自己的栈
- 每个任务运行这个函数时
- 任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B 的 栈里
- 不同任务的局部变量,有自己的副本
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本
- 要防止冲突
下面是一个示例:
void ATaskFunction( void *pvParameters )
{
/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
for( ;; )
{
/* 任务的代码 */
}
/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
* NULL表示删除的是自己
*/
vTaskDelete( NULL );
/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
一般创建完任务后,会出现一个任务函数,而任务函数的实现为死循环,若你的任务函数在退出时不是一个死循环会跳到下图代码位置,则会进入一个死循环,会导致其他任务无法得以执行,正确做法是在退出任务时是一个死循环,或者不用死循环,但结尾一定需要将任务删除。
举个例子:
void Led_Test(void) { Led_Init(); for(int i=0;i<10;i++) { Led_Control(LED_GREEN, 1); mdelay(500); Led_Control(LED_GREEN, 0); mdelay(500); } }
上述代码中是任务的一个调用函数(LED闪烁),还有其他几个任务在执行,其中它是以for循环结尾,则LED闪烁10次后任务退出,其他任务正常执行,但结果不是这样的,由于其任务退出不是以死循环退出,导致程序进入另一个死循环,所有任务无法得以继续执行,所以正确的代码如下(一个任务函数如果不经过处理直接返回的话,会返回到上面红框中的错误处理函数,函数里面会关闭所有中断,并且进入一个死循环)(不用死循环的做法):
void Led_Test(void) { Led_Init(); for(int i=0;i<10;i++) { Led_Control(LED_GREEN, 1); mdelay(500); Led_Control(LED_GREEN, 0); mdelay(500); } vTaskDelete( NULL ); //将任务删除(自杀) }
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 // 任务句柄, 以后使用它来操作这个任务
};
参数说明:
参数
|
描述
|
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。
|
使用静态分配内存的函数如下:
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 // 静态分配的任务结构体的指针,用它来操作这个任务
};
相比于使用动态分配内存创建任务的函数,最后2个参数不一样:
示例一
使用动态、静态分配内存的方式,分别创建多个任务:监测遥控器键值并在LCD上显示、LED 闪烁、全彩LED渐变颜色、使用无源蜂鸣器播放音乐。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
static StackType_t puxStackColorBuffer[128];//静态任务色分配的栈大小
static StaticTask_t pxColorBuffer; // 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xColorHandle; //任务色的返回句柄,一般用来判断
static StackType_t puxStackLightBuffer[128];//静态任务光分配的栈大小
static StaticTask_t pxLightBuffer;// 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xLightHandle;//任务光的返回句柄,一般用来判断
TaskHandle_t xSoundTaskHandle;//任务句柄
BaseType_t ret;//任务声的返回句柄,一般用来判断
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); //CubeMX配置的任务
/* add threads, ... */
/* 创建任务:色 */
xColorHandle = xTaskCreateStatic(ColorLED_Test,"ColorTask",128,NULL,osPriorityNormal,puxStackColorBuffer,&pxColorBuffer);//静态创建任务
/* 创建任务:光 */
xLightHandle = xTaskCreateStatic(Led_Test,"LEDTask",128,NULL,osPriorityNormal,puxStackLightBuffer,&pxLightBuffer);//静态创建任务
/* 创建任务:声 */
extern void PlayMusic(void *params);
ret = xTaskCreate(PlayMusic,"SoundTask",128,NULL,osPriorityNormal,&xSoundTaskHandle);//动态创建任务
void StartDefaultTask(void *argument)
{
LCD_Init();
LCD_Clear();
for(;;)
{
IRReceiver_Test(); /* 影 */
}
}
示例二
我们创建 2 个任务,使用同一个函数,但是在 LCD 上打印不一样的信息。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
struct TaskPrint //创建一个结构体 结构体包含一下内容
{
uint8_t x;//横坐标
uint8_t y;//纵坐标
char name[16];//任务名字
};
static struct TaskPrint Task1 = {0, 0, "Task1"};//动态创建任务的第四个参数需要用到的参数
static struct TaskPrint Task2 = {0, 3, "Task2"};
static struct TaskPrint Task3 = {0, 6, "Task3"};
static int flag = 1;
void LcdPrintTask(void *params)
{
struct TaskPrint *data = params;//将输入的参数赋给定义的结构体
uint32_t cnt = 0;
int len;
while(1)
{
/* 打印信息 */
if(flag)
{
flag = 0;
len = LCD_PrintString(data->x,data->y,data->name);//这里的返回值是打印任务的长度
len += LCD_PrintString(len,data->y,":");//将打印名字处后打印":"的长度一起加上 得到的长度是打印完任务名字和":"后的长度
LCD_PrintSignedVal(len,data->y,cnt++);//在":"后打印计数数值
flag = 1;
}
mdelay(500);
}
}
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
/* CubeMX配置的任务被我们注释掉 其中包括了lcd初始化和清屏函数 若不使用则会导致屏幕显示混论 */
LCD_Init();
LCD_Clear();
//defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes); //CubeMX配置的任务
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* 使用三个任务调用同一个函数 */
xTaskCreate(LcdPrintTask,"task1",128,&Task1,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"task2",128,&Task2,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"task3",128,&Task3,osPriorityNormal,NULL);
这里遇到一个问题,三个任务在交叉执行后,他们的数值是不一样的,原因在于:在上述代码中,我们给创建的三个任务分配的优先级为 osPriorityNormal ,而 osPriorityNormal 存在于一个优先级就绪链表中,在代码内部,这个就绪链表为从上到下(高优先级到低优先级)不断遍历,寻找高优先级的任务 TCB 结构体并执行,由于我们创建的任务都为 osPriorityNormal (位于第24位优先级),所以遍历到24时会执行其中的任务函数,但在同一个优先级我们由创建了三个任务,于是会有一个 pxCurrentTCB指针 来判断当前哪个任务优先级更高,而在上述代码中,我们创建任务时这个 pxCurrentTCB指针就会指向该任务,所以当我们最后创建 task3 时,这个 pxCurrentTCB 指针指向 task3,此时在整个就绪链表不止这三个任务,还有另外的一个空闲任务(后续讲解),这是在启动调度器时函数会创建的,优先级位于 list[0] ,所以基本不会运行。当我们启动调度器时,pxCurrentTCB 指针指向的任务优先指向,当 task3 执行完毕后,pxCurrentTCB 指针指向下一个任务,即 task1 ,于是 task1 计数加一,当 task1 执行完毕后,pxCurrentTCB 指针又指向下一个任务,即 task2 ,如此循环。
上述代码中的 data 来自参数 pvParameters,pvParameters 来自哪里?创建任务时传入的。
- 使用 xTaskCreate 创建任务时,第 4 个参数就是 pvParameters
- 不同的任务,pvParameters 不一样
3、删除任务
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
参数
|
描述
|
pvTaskCode
|
任务句柄,使用 xTaskCreate 创建任务时可以得到一个句柄。
也可传入 NULL,这表示删除自己。
|
怎么删除任务?举个不好的例子:
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行 vTaskDelete(pvTaskCode),pvTaskCode 是自己的句柄
- 杀人:执行 vTaskDelete(pvTaskCode),pvTaskCode 是别的任务的句柄
示例
当监测到遥控器的Power按键被按下后,删除音乐播放任务。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
static StackType_t puxStackColorBuffer[128];//静态任务色分配的栈大小
static StaticTask_t pxColorBuffer; // 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xColorHandle; //任务色的返回句柄,一般用来判断
static StackType_t puxStackLightBuffer[128];//静态任务光分配的栈大小
static StaticTask_t pxLightBuffer;// 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xLightHandle;//任务光的返回句柄,一般用来判断
/* 创建任务:色 */
xColorHandle = xTaskCreateStatic(ColorLED_Test,"ColorTask",128,NULL,osPriorityNormal,puxStackColorBuffer,&pxColorBuffer);//静态创建任务
/* 创建任务:光 */
xLightHandle = xTaskCreateStatic(Led_Test,"LEDTask",128,NULL,osPriorityNormal,puxStackLightBuffer,&pxLightBuffer);//静态创建任务
void StartDefaultTask(void *argument)
{
LCD_Init();
LCD_Clear();
for(;;)
{
uint8_t dev, data;
int len;
TaskHandle_t xSoundTaskHandle = NULL;//任务句柄
BaseType_t ret;//任务声的返回句柄,一般用来判断
IRReceiver_Init();
LCD_PrintString(0,0,"Waitting Control");
while (1)
{
/* 读取红外遥控键值 */
if(IRReceiver_Read(&dev,&data) == 0)
{
/* 创建播放音乐的任务 */
if(data == 0xa8)
{
extern void PlayMusic(void *params);
if(xSoundTaskHandle == NULL)//判断任务是否存在
{
LCD_ClearLine(0,0);
LCD_PrintString(0,0,"Create task");
ret = xTaskCreate(PlayMusic,"SoundTask",128,NULL,osPriorityNormal,&xSoundTaskHandle);//动态创建任务
}
}
/* 删除播放音乐的任务 */
else if(data == 0xa2)
{
if(xSoundTaskHandle != NULL)//判断任务是否存在
{
LCD_ClearLine(0,0);
LCD_PrintString(0,0,"Delete task");
vTaskDelete(xSoundTaskHandle);
xSoundTaskHandle = NULL;
PassiveBuzzer_Control(0);
}
}
}
}
}
}
三、任务的优先级和Tick
1、任务优先级
在上个示例中,音乐播放与LED闪烁和彩色小灯任务同时进行,导致音乐播放时同个音频蜂鸣器响了很久。怎么让播放的音乐更动听?提高优先级。
优先级的取值范围是: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 会确保最高优先级的、可运行的任务,马上就能执行
- 对于相同优先级的、可运行的任务,轮流执行
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改变了, 我们也不用去修改代码。
示例
提高音乐播放任务的优先级,使用vTaskDelay进行延时。
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
static StackType_t puxStackColorBuffer[128];//静态任务色分配的栈大小
static StaticTask_t pxColorBuffer; // 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xColorHandle; //任务色的返回句柄,一般用来判断
static StackType_t puxStackLightBuffer[128];//静态任务光分配的栈大小
static StaticTask_t pxLightBuffer;// 静态分配的任务结构体的指针,用它来操作这个任务
static TaskHandle_t xLightHandle;//任务光的返回句柄,一般用来判断
/* 创建任务:色 */
xColorHandle = xTaskCreateStatic(ColorLED_Test,"ColorTask",128,NULL,osPriorityNormal,puxStackColorBuffer,&pxColorBuffer);//静态创建任务
/* 创建任务:光 */
xLightHandle = xTaskCreateStatic(Led_Test,"LEDTask",128,NULL,osPriorityNormal,puxStackLightBuffer,&pxLightBuffer);//静态创建任务
void StartDefaultTask(void *argument)
{
LCD_Init();
LCD_Clear();
for(;;)
{
uint8_t dev, data;
int len;
TaskHandle_t xSoundTaskHandle = NULL;//任务句柄
BaseType_t ret;//任务声的返回句柄,一般用来判断
IRReceiver_Init();
LCD_PrintString(0,0,"Waitting Control");
while (1)
{
/* 读取红外遥控键值 */
if(IRReceiver_Read(&dev,&data) == 0)
{
/* 创建播放音乐的任务 */
if(data == 0xa8)
{
extern void PlayMusic(void *params);
if(xSoundTaskHandle == NULL)//判断任务是否存在
{
LCD_ClearLine(0,0);
LCD_PrintString(0,0,"Create task");
ret = xTaskCreate(PlayMusic,"SoundTask",128,NULL,osPriorityNormal+1,&xSoundTaskHandle);//动态创建任务,同时提高音乐播放的优先级 同时在PlayMusic函数中将mDelay函数换成vTaskDelay函数,该函数不会参与调度
}
}
/* 删除播放音乐的任务 */
else if(data == 0xa2)
{
if(xSoundTaskHandle != NULL)//判断任务是否存在
{
LCD_ClearLine(0,0);
LCD_PrintString(0,0,"Delete task");
vTaskDelete(xSoundTaskHandle);
xSoundTaskHandle = NULL;
PassiveBuzzer_Control(0);
}
}
}
}
}
}
3、修改优先级
使用uxTaskPriorityGet来获得任务的优先级:
BaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
使用参数 xTask 来指定任务,设置为 NULL 表示获取自己的优先级。
使用vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet{
TaskHandle_t xTask,
UBaseType_t uxNewPriority };
使用参数 xTask 来指定任务,设置为 NULL 表示设置自己的优先级;
参数 uxNewPriority 表示新的优先级,取值范围是 0~(configMAX_PRIORITIES – 1)。
四、任务状态
以前我们很简单地把任务的状态分为 2 中:运行(Runing)、非运行(Not Running)。
对于非运行的状态,还可以继续细分,比如前面的FreeRTOS_08_task_priority中:
- Task3 执行 vTaskDelay 后:处于非运行状态,要过 3 秒种才能再次运行
- Task3 运行期间,Task1、Task2 也处于非运行状态,但是它们随时可以运行
- 这两种"非运行"状态就不一样,可以细分为:
- 阻塞状态(Blocked)
- 暂停状态(Suspended)
- 就绪状态(Ready)
1、阻塞状态(Blocked)
在日常生活的例子中,母亲在电脑前跟同事沟通时,如果同事一直没回复,那么母亲的 工作就被卡住了、被堵住了、处于阻塞状态(Blocked)。重点在于:母亲在等待。
在FreeRTOS_08_task_priority实验中,如果把任务3中的vTaskDelay调用注释掉,那 么任务1、任务2根本没有执行的机会,任务1、任务2被"饿死"了(starve)。
在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:
- 任务要等待某个事件,事件发生后它才能运行
- 在等待事件过程中,它不消耗 CPU 资源
- 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)
在阻塞状态的任务,它可以等待两种类型的事件:
- 时间相关的事件
- 可以等待一段时间:我等 2 分钟
- 也可以一直等待,直到某个绝对时间:我等到下午 3 点
- 同步事件:这事件由别的任务,或者是中断程序产生
- 例子 1:任务 A 等待任务 B 给它发送数据
- 例子 2:任务 A 等待用户按下按键
- 同步事件的来源有很多(这些概念在后面会细讲):
- 队列(queue)
- 二进制信号量(binary semaphores)
- 计数信号量(counting semaphores)
- 互斥量(mutexes)
- 递归互斥量、递归锁(recursive mutexes)
- 事件组(event groups)
- 任务通知(task notifications)
在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:
- 10ms 之内有数据到来:成功返回
- 10ms 到了,还是没有数据:超时返回
2、暂停状态(Suspended)
在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:
- 好烦啊,我暂停一会
- 领导说:你暂停一下
FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数 xTaskToSuspend 表示要暂停的任务,如果为 NULL,表示暂停自己。
要退出暂停状态,只能由别人来操作:
- 别的任务调用:vTaskResume
- 中断程序调用:xTaskResumeFromISR
实际开发中,暂停状态用得不多。
3、就绪状态(Ready)
这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。
4、完整的状态转换图
五、Delay函数
1、两个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 来让任务周期性地运行
2、示例
比较vTaskDelay和vTaskDelayUntil实际阻塞的时间,并在LCD上打印出来。
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;
BaseType_t preTime;
uint64_t t1, t2;
preTime = xTaskGetTickCount();
while (1)
{
/* 打印信息 */
if (g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);
len += LCD_PrintString(len, pInfo->y, ":");
LCD_PrintSignedVal(len, pInfo->y, cnt++);
g_LCDCanUse = 1;
mdelay(cnt & 0x3);
}
t1 = system_get_ns();
//vTaskDelay(500); // 500000000
vTaskDelayUntil(&preTime, 500);
t2 = system_get_ns();
LCD_ClearLine(pInfo->x, pInfo->y+2);
LCD_PrintSignedVal(pInfo->x, pInfo->y+2, t2-t1);
}
}
六、空闲函数及其钩子函数
1、介绍
空闲任务(Idle 任务)的作用之一:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一 个可以运行的任务:所 以 ,我们要提供空闲任务。在使用 vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为 0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为 0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当 然可以进入省电模式了。
空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用 vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
2、使用钩子函数的前提
在 FreeRTOS\Source\tasks.c 中,可以看到如下代码,所以前提就是:
- 把这个宏定义为 1:configUSE_IDLE_HOOK
- 实现 vApplicationIdleHook 函数