欢迎来到我的博客

FreeRTOS应用基础(二)——基本任务使用

这篇文章实际上是和上一篇文章一起写的,今天才整理了一下。下一篇或许遥遥无期吧,随心随笔。

  本文主要介绍了FreeRTOS任务的基本概念与使用,此部分为FreeRTOS的最基础单元,也是RT:OS的基础构成部分。 此部分需要达到以下目标:

  1. 了解堆栈和内存管理;
  2. 理解任务的概念和状态;
  3. 熟练掌握任务的创建、删除和配置等操作;

内存管理

  栈.(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等,函数结束,内存自动释放,栈向下增长,即内存地址由高到低。
  堆(heao):一般为程序员自主分配的空间,如malloc()分配的内存即为这种,需手动分配和释放,堆向上增长,即内存地址由低到高。;
  以STM32F103为例:
在这里插入图片描述

  栈的大小,一般默认为0x00000400,即1K的大小(0x400/1024) 在C语言的库函数中,有mallc、free等函数,但是在FreeRTOS中,它们不适用:

  • 不适合用在资源紧缺的嵌入式系统中
  • 这些函数的实现过于复杂、占据的代码空间太大
  • 并非线程安全的(thread-safe)
  • 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
  • 内存碎片化
  • 使用不同的编译器时,需要进行复杂的配置
  • 有时候难以调试

因此一个内存管理模块是单片机程序架构和RTOS的刚需部分
FreeRTOS自带5种内存管理文件:

文件 优点 缺点
heap_1.c 分配简单,时间确定 只分配、不回收
heap_2.c 动态分配、最佳匹配 碎片、时间不定
heap_3.c 调用标准库函数 速度慢、时间不定
heap_4.c 相邻空闲内存可合并 可解决碎片问题、时间不定
heap_5.c 在heap_4基础上支持分隔的内存块 可解决碎片问题、时间不定

内存的动态管理属于C语言的知识范畴,在RTOS中,内存管理方式的选择也很重要,但仅从入门使用角度来说,可以暂时放在后续学习;

任务概念

  什么是任务?在操作系统中,有时候也把任务称为线程。个人理解,任务(task)即单片机要做的事,在代码中,任务就是一个函数;
  在裸机程序中,对于简单的方式,经常采用查询方式,即一件事完成后,再去完成另一件事,按照顺序执行,这种执行导致当有紧急情况时,可能会得不到处理。对于更复杂的程序,为了能够去及时执行紧急任务,于是便产生了中断(ISR)。
  查询+中断方式能够解决大部分裸机上的应用,但随着工程的复杂,裸机的程序可能会变得越来越复杂,中断得复杂和嵌套可能会带来难以解决得问题。
  RTOS相对于裸机得最大优点便是其可以通过调度实现多任务管理,即由调度器来决定当前任务得执行,即便对于大部分单片机而言,同一时刻只能执行一个任务,但通过调度算法可以实现不同时刻,多个任务复杂有序的执行,从而达到多任务效果(看起来所有任务同时运行)。

任务状态

  FreeRTOS支持以下四种任务状态:

  • 运行状态——Running
    即任务在当前时刻处于实际执行状态;
  • 阻塞状态——Blocked
    由于某些原因,任务需要满足某些条件时,才能够运行的状态,一般由信号量、消息队列、事件标志等驱动。
  • 暂停状态——Suspended
    即将任务挂起,挂起后,这个任务不再执行,只有调用函数才能将其恢复。
  • 就绪状态——Ready
    即处于能够运行,但由于优先级问题还没有运行的任务状态;
    四种任务的状态转换图如下:
    在这里插入图片描述

任务优先级、tick和延时

优先级和tick

  • 在FreeRTOS中,可设置0~confingMAX_PRIORITIES-1范围内的优先级,数值越大,优先级越高。
  • FreeRTOS会确保最高优先级的、可运行的任务,立马执行;
  • 对于相同优先级的、可运行的任务,轮流执行;

  FreeRTOS的系统心跳(系统节拍)来源于Systick系统定时器产生的周期性中断,如下图
在这里插入图片描述

  • t1、t2、t3为发射中断的时刻

  • t1和t2之间的时间称为时间片

  • 时间片的长度由confingTICK_RATE_HZ决定,如设置为1000,则时间片长度为1ms,1/1000;
    对于相同优先级的任务,可以从下图分析:
    在这里插入图片描述

    从图中也可以看出,

  • 任务切换时,需要运行中断处理函数,中断运行也需要时间,故任务运行的时间并不是严格从t1、t2、t3哪里开始的;

delay

  FreeRTOS中,有两个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

  对于vTaskDelay来说,基于tick的延时并不精确,vTaskDelay(2)本意是延迟2个tick周期,但可能一个多Tick就返回了。

/* 假设configTICK_RATE_HZ=100, Tick周期时10ms, 那么等待2个Tick,也就是等待20ms */
vTaskDelay(2); 

/* 还可以使用pdMS_TO_TICKS宏把ms转换为tick */
vTaskDelay(pdMS_TO_TICKS(100)); 		// 等待100ms

  使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。

xTaskDelayUntil(&Pre, n)

  xTaskDelayUntil可以实现相较于vTaskDelay()更加精准的延时,一般用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性的执行时),采用绝对时间维持,具体实现过程可参考
FreeRTOS学习笔记——精准延时_anobodykey的博客-CSDN博客
https://blog.csdn.net/anobodykey/article/details/42042965
  使用delay后,会使当前任务立刻进入阻塞态,此时在一个tick内,会继续下一个任务。

任务基本操作

任务函数

  在FreeRTOS中,任务就是一个函数,原型为:

void ATaskFunction( void *pvParameters );

  注意:

  • 函数无返回值;
  • 同一个函数可以创建多个任务,即多个任务可以采用同一个函数;
  • 函数内部,尽量使用局部变量
    • 每个任务都有自己的栈;
  • 有多个任务运行同一个函数时
    • 任务A的局部变量放在任务A的栈中、任务B的局部变量放在任务B的栈中;
    • 不如任务的局部变量,有自己的副本
  • 若函数内部使用全局变量、静态变量的话
    • 则多个任务使用的为同一个副本
    • 使用过程中需要放在冲突

  以下为一个任务函数的模板:

void ATaskFunction( void *pvParameters )
{
    /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
    int32_t lVariableExample = 0;
    /* 任务函数通常实现为一个无限循环 */
    while(1)
    {
    /* 任务的代码 */
    }
    /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
    * NULL表示删除的是自己
    */
    vTaskDelete( NULL );
    /* 程序不会执行到这里, 如果执行到这里就出错了 */
}

创建任务

创建任务的函数原型为:

BaseType_t xTaskCreate( 
    TaskFunction_t pxTaskCode, // 函数指针, 任务函数
    const char * const pcName, // 任务的名字
    const configSTACK_DEPTH_TYPE usStackDepth, /*任务栈 大小,单位 word ,也就是 4 字节*/
    void * const pvParameters, // 调用任务函数时传入的参数
    UBaseType_t uxPriority, // 优先级
    TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务

  详细参数说明如下:

参数 解释
pvTaskCode 函数指针,可以简单地认为任务就是一个C函数。
它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)"
pcName pcName任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。
长度为:configMAX_TASK_NAME_LEN
usStackDepth 每个任务都有自己的栈,这里指定栈大小,
单位 word ,也就是 4 字节,最大值为uint16_t的最大值。
大部分时候,只能估计栈的大小,精确的办法是看反汇编码。
pvParameters 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters)
uxPriority 优先级范围:0~(configMAX_PRIORITIES – 1),
数值越大优先级越高,
如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1)
pxCreatedTask 用来保存xTaskCreate的输出结果:task handle。
以后如果想操作这个任务,比如修改它的优先级,就需要这个handle。
如果不想使用该handle,可以传入NULL。即TCB_t
返回值 成功:pdPASS;失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足);
注意:文档里都说失败时返回值是pdFAIL,这不对。
pdFAIL是0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY是-1。

  通过实验发现,可以使用同一个函数和同一个句柄可以多次创造任务,此时任务的运行会发生重叠,同时如果此时采用删除这个句柄所创造的任务,只能删除一个运行的任务(如果你创建了两个同样的,删除后还会剩一个),若再次使用这个句柄删除任务,则会直接发生硬件错误(怀疑是访问内存越界)。故创造任务时,还需自行判断任务存在状态后再执行。

空闲任务和钩子函数

  空闲任务用于在用户任务都处于阻塞态时运行,通过空闲任务,可以使系统得到短暂的休息,同时可以在空闲任务中更好的实现低功耗,用户可以在空闲任务中实现睡眠,停机等低功耗措施。
  空闲任务需要注意:

  • 在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务;
  • 空闲任务的优先级为0,不能阻碍用户任务运行;
  • 空闲任务不会处于阻塞,要么处于就绪态、要么处于运行态;
  • 当删除任务时,需要通过执行空闲任务,去释放该任务的内存;注意:此处的空闲任务指的是内核自带的空闲任务,用户无需特意去定义,只需在删除后,提供一段tick延时,确保系统可以自动释放内存即可,除非采用静态创建的任务,需要用户自己手动定义删除内存,教程里并未强调这一点,

  空闲任务内可添加钩子函数(Idle Task Hook Functions),即用于空闲任务执行时的函数,作用如下:

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

空闲任务的钩子函数的限制:

  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

  要使用空闲任务的钩子函数,首先需要将以下宏定义打开,可以在FreeRTOSconfig.h中配置

#define configUSE_IDLE_HOOK			0

  其次需要定义以下函数:

void vApplicationIdleHook( void );

相关代码可以在task.c中看到:
在这里插入图片描述

删除任务

  删除任务需要使用以下函数

//配置宏定义
#define INCLUDE_vTaskDelete 1
/*
参数:
pvTaskCode:任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。
可传入以下值:
自杀: vTaskDelete(NULL),即当前正在执行的任务;
被杀:别的任务执行vTaskDelete(pvTaskCode) ,pvTaskCode是自己的句柄;
杀人:执行vTaskDelete(pvTaskCode) ,pvTaskCode是别的任务的句柄
*/
void vTaskDelete( TaskHandle_t xTaskToDelete );

  在执行删除函数后,任务的内存并没有被立刻释放,故调用这个函数时,一定要确保在删除前,空闲任务可以执行,将要删除的任务内存释放;

挂起任务

  任务可通过以下函数进入Suspended状态:

//使用此函数需要先配置宏定义
#define INCLUDE_vTaskSuspend 1

/*
xTaskToSuspend:任务句柄,NULL则代表挂起当前正在执行的任务,即自己挂起自己;
*/
void vTaskSuspend (TaskHandle_t xTaskToSuspend /* 任务 句柄 */

恢复任务

非中断:

//使用此函数需要先配置宏定义,和挂起一样
#define INCLUDE_vTaskSuspend 1

/*
xTaskToSuspend:需要恢复的任务句柄;
*/
void vTaskResume (TaskHandle_t xTaskToResume /* 任务 句柄 */

  此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,
中断:
  中断服务程序中使用的 xTaskResumeFromISR(),以后缀FromISR结尾。

//配置宏定义
#define INCLUDE_xResumeFromISR 1
void vTaskResumeFromISR (TaskHandle_t xTaskToResume /* 任务 句柄 */

  如果用户打算采用这个函数实现中断与任务的同步,要注意一种情况,如果此函数的调用优先于函数vTaskSuspend被调用,那么此次同步会丢失,这种情况下建议使用信号量来实现同步。 此函数是用于中断服务程序中调用的,故不可以在任务中使用此函数,任务中使用的是vTaskResume。

实验验证

  问题:在一般的freertos程序中,该如何管理任务的创建和删除?如何避免重复创建或删除任务?
功能
可抢占时间片轮转
任务一:LED1快速闪烁
任务二:LED2间隔1s闪烁

key1:任务一,正常时挂起,再按一下恢复
key2:任务二,正常时挂起,再按一下恢复

key3:任务一,存在时删除,不存在则创建
WK_UP:任务二,正常时挂起,再按一下恢复
通过串口查看各任务状态

此处采用的按键消抖,通过正点原子的key.h和delay文件实现:

#include "main.h"
#include "includes.h"

TaskHandle_t StartTask_Handler;
TaskHandle_t xHandleTask1;
TaskHandle_t xHandleTask2;
/*
*********************************************************************************************************
*	函 数 名: vTaskLED1
*	功能说明: 
*	形    参: 无
*	返 回 值: 无
*	优 先 级:  1
*	说    明:每隔1s闪烁一次
*********************************************************************************************************
*/
void vTaskLED1(void *pvParameters)
{
	while(1)
	{
		LED_On(1);
		vTaskDelay(pdMS_TO_TICKS(200));
		LED_Off(1);
		vTaskDelay(pdMS_TO_TICKS(200));
	}
}	
/*
*********************************************************************************************************
*	函 数 名: vTaskLED2
*	功能说明: 
*	形    参: 无
*	返 回 值: 无
*	优 先 级:  2
*	说    明:1s亮1s灭
*********************************************************************************************************
*/
static void vTaskLED2(void *pvParameters)
{
	while(1)
	{
		LED_Toggle(2);
		vTaskDelay(pdMS_TO_TICKS(1000));
	}
}	

/*
*********************************************************************************************************
*	函 数 名: vTaskKey
*	功能说明: 按键扫描函数
*	形    参: 无参
*	返 回 值: 无
*	优 先 级:  3
*	说    明:
*********************************************************************************************************
*/
static void vTaskKey(void *pvParameters)
{
	int key;
	int task_pass;//创建任务是否成功标志位
	static int task1_state=1;//0:不存在,1:存在且正常运行,2:挂起
	static int task2_state=1;
	while(1)
	{
		key = KEY_Scan(0);
		switch(key)
		{
			case 1://任务一:正常时挂起,再按一下恢复
			{
				if(task1_state==1)
				{
				    vTaskSuspend (xHandleTask1);
				    task1_state=2;
				    printf("任务LED1挂起成功\r\n");
				}
				else if(task1_state==2)
				{
				    vTaskResume (xHandleTask1);
				    printf("任务LED1已恢复\r\n");
				    task1_state=1;
				}
				break;
			};
			
			case 2://任务二:正常时挂起,再按一下恢复
			{
				if(task2_state==1)
				{
				    vTaskSuspend (xHandleTask2);
				    task2_state=2;
				    printf("任务LED2挂起成功\r\n");
				}
				else if(task2_state==2)
				{
				    vTaskResume(xHandleTask2);
				    printf("任务LED2已恢复\r\n");
				    task2_state=1;
				}
				break;
			};
			
			case 3://任务一:存在时删除,不存在则创建
			{
				if(task1_state)
				{
				    vTaskDelete (xHandleTask1);
				    printf("任务LED1删除\r\n");
				    task1_state = 0;
				}	
				else
				{
				    task_pass = xTaskCreate( vTaskLED1, /* 任务函数*/
				        "vTask1", /* 任务名*/
				        512, /*任务栈 大小,单位 word ,也就是 4 字节*/
				        NULL, /* 任务参数*/
				        1, /* 任务优先级*/
				        &xHandleTask1); /* 任务句柄*/
				    task1_state = 1;
					
					if(task_pass == 1)
					    printf("任务LED1创建成功\r\n");
					else
				    	printf("任务LED1创建失败\r\n");
					
				}
				break;
						
			};
			case 4://删除任务二
			{
				if(task2_state)
				{
					vTaskDelete (xHandleTask2);
					printf("任务LED2删除\r\n");
					task2_state = 0;
				}	
				else
				{
					task_pass = xTaskCreate( vTaskLED2, /* 任务函数*/
					"vTask2", /* 任务名*/
					512, /*任务栈 大小,单位 word ,也就是 4 字节*/
					NULL, /* 任务参数*/
					1, /* 任务优先级*/
					&xHandleTask2); /* 任务句柄*/
					task2_state = 1;
					if(task_pass == 1)
						printf("任务LED2创建成功\r\n");
					else
						printf("任务LED2创建失败\r\n");
				}
				break;
			}								
		}
		vTaskDelay(10);//确保系统的空闲任务有空运行
	}
}
void Task_Creat(void *pvParameters)
{
	
	xTaskCreate( vTaskKey, /* 任务函数*/
				"vTaskKEY", /* 任务名*/
				512, /*任务栈 大小,单位 word ,也就是 4 字节*/
				NULL, /* 任务参数*/
				1, /* 任务优先级*/
				NULL);/* 任务句柄*/
	xTaskCreate( vTaskLED1, /* 任务函数*/
					"vTask1", /* 任务名*/
					512, /*任务栈 大小,单位 word ,也就是 4 字节*/
					NULL, /* 任务参数*/
					1, /* 任务优先级*/
					&xHandleTask1); /* 任务句柄*/
	xTaskCreate( vTaskLED2, /* 任务函数*/
					"vTask2", /* 任务名*/
					512, /*任务栈 大小,单位 word ,也就是 4 字节*/
					NULL, /* 任务参数*/
					2, /* 任务优先级*/
					&xHandleTask2); /* 任务句柄*/	
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区
}
int main()
{

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	//设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	delay_init();	
	Key_Init();
	LED_Init();
	Uart_Init();
	printf("start\r\n");
	/* 创建开始任务*/
	xTaskCreate( Task_Creat, /* 任务函数*/
					"vTaskStart", /* 任务名*/
					512, /*任务栈 大小,单位 word ,也就是 4 字节*/
					NULL, /* 任务参数*/
					1, /* 任务优先级*/
					&StartTask_Handler); /* 任务句柄*/	
	/* 启动调度,开始执行任务*/
	vTaskStartScheduler();	

	while(1);
	
}
posted @ 2023-06-11 10:57  cloudraysun  阅读(958)  评论(0)    收藏  举报