FreeRTOS任务管理

目录

一、基本概念

二、任务的创建与删除

1、什么是任务

2、创建任务

示例一

示例二

3、删除任务

示例

三、任务的优先级和Tick

1、任务优先级

2、Tick

示例

3、修改优先级

四、任务状态

1、阻塞状态(Blocked)

2、暂停状态(Suspended)

3、就绪状态(Ready)

4、完整的状态转换图

五、Delay函数

1、两个Delay函数

2、示例

六、空闲函数及其钩子函数

1、介绍

2、使用钩子函数的前提


一、基本概念

 对于整个单片机程序,我们称之为 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 函数

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值