FreeRTOS任务管理

内容参考于百问网freertos

RTOS管理任务是通过一个链表这个链表的每个节点都是一个任务的TCB结构体(task control block 任务控制块)

FreeRTOS任务管理

1.任务创建与删除

1.1什么是任务?

任务就是一个函数,这个函数里执行的是一个死循环,如果任务不是死循环在任务退出之间任务需要“自杀”否则会出问题。

这个函数不能有返回值(void)

任务函数书写:

void ATaskFunction( void *pvParameters )
{
	/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
	int32_t lVariableExample = 0;
	
    /* 任务函数通常实现为一个无限循环 */
	for( ;; )
	{
		/* 任务的代码 */
	}

    /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己
     * NULL表示删除的是自己
     */
	vTaskDelete( NULL );
    
    /* 程序不会执行到这里, 如果执行到这里就出错了 */
}

虽然我们写了任务函数,我们还需要把它让RTOS执行起来

1.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 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。所以起名字只要表明特征就OK
usStackDepth每个任务都有自己的栈,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)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 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 // 静态分配的任务结构体的指针(TCB),用它来操作这个任务
);

相比于使用动态分配内存创建任务的函数,最后2个参数不一样:

参数描述
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)
puxStackBuffer静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。
pxTaskBuffer静态分配的StaticTask_t结构体的指针
返回值成功:返回任务句柄; 失败:NULL

动态创建任务不需要给任务说这个栈是多大,不用提前创建栈,任务TCB结构体,需要创建函数返回接收值,任务句柄

静态创建任务需要提前创建一个buffer来告诉任务这个栈为多大,还需要创建任务TCB结构体,创建的变量都是全局静态的 ,创建任务句柄(接收需要句柄)

1.2.1创建任务步骤

需要用到的函数以及需要创建的变量

动态创建任务:

//1、一般创建独立任务在这个函数中创建,也可以在任务中创建任务
void MX_FREERTOS_Init(void)
{
//接收动态任务的返回值
BaseType_t ret;
//定义一个任务句柄,x是结构体变量的前缀
TaskHandle_t xSoundTaskHandle


//2、我们使用动态创建任务
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal,&xSoundTaskHandle);
//其中
//ret是BaseType_t(long)类型,可以用于判断任务是否创建成功
//MyTask任务地址也就是任务函数名
//"myfirsttask"起的任务名
//128这个任务分配栈的大小具体大小128*数据类型大小看图
//NULL,调用函数传入参数为空
//osPriorityNormal采用系统默认优先级24
//&xSoundTaskHandle,任务的句柄地址。

}

函数栈大小配置: 

任务优先级配置:

静态创建:

static StackType_t g_pucStackOfLightTask[128];//静态创建任务栈的空间buffer
static StaticTask_t g_TCBofLightTask;//静态任务TCB结构体
static TaskHandle_t xLightTaskHandle;//静态任务句柄


void MX_FREERTOS_Init(void) {

/* 创建任务: 光 */
  xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);


}
与动态创建不同的是
1、g_pucStackOfLightTask 栈buffer地址
2、xLightTaskHandle任务TCB结构体

这样我们就是用动态创建和静态创建分别创建了一个任务。

1.2.2用一个函数创建多个任务

多个任务运行同一个函数,互不影响,因为他们都有自己的栈,这次我们创建任务使用函数传参(pvParameters)

我们使用LCD打印每个任务运行的次数,我们使用动态来创建三个任务

struct  DisplayInfo {
    int x;
    int y;
    const char *str;
};
//定义三个全局静态参数作为任务输入
//显示任务巡行次数需要x,y,任务名
static struct TaskPrintInfo g_Task1Info = {0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info = {0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info = {0,6,"Task3"};
static int g_LCDCanUse=1;//这个全局变量用于保护任务函数void LcdPrintTask(void* params)防止在运行过程中被切换。这个方法大概率可行但不是万无一失,后面会将同步互斥。


/*使用同一个函数创建不同的任务*/
xTaskCreate(LcdPrintTask,"Task1",128,&g_Task1Info,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"Task2",128,&g_Task2Info,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"Task3",128,&g_Task3Info,osPriorityNormal,NULL);

//任务函数
void LcdPrintTask(void* params)
{
	struct TaskPrintInfo* pInfo=params;
	uint32_t cnt=0;
	int len=0;
		while(1)
		{
			if(g_LCDCanUse==1)//进入让g_LCDCanUse=0,就算中途备切换出去别的任务也不能进来
			{
			g_LCDCanUse=0;	
			len = LCD_PrintString(pInfo->x,pInfo->y,pInfo->name);//返回打印字符串的长度
			len +=	LCD_PrintString(len,pInfo->y,":");
			LCD_PrintSignedVal(len,pInfo->y,cnt);
			cnt++;
			g_LCDCanUse=1;//没有这个延时在这块恰好被切换的概率太低了。
			}
			/*打印信息*/
			mdelay(500);//没有这个延时任务基本一直在运行第一个任务,其他任务很难进入while()通过分析代码可以知道.
		}

}

1.3任务删除

1.3.1任务自杀

        任务自杀就是任务自己删除自己

vTaskDelete(VULL);//在任务中调用这一句就实现任务自杀
1.3.2任务他杀

        在别的任务中删除任务

vTaskDelete(传入被杀任务的句柄);//任务他杀
1.3.3在任务中创建和删除新任务注意的问题

        1、频繁的删除和创建任务会使内存碎片化,有可能再创建任务的时候就会失败。

        2、在任务中创建新的任务,新任务的栈也是独立的,RTOS每个任务的栈都是独立的

        3、对于删除任务我们最好使用自杀,自己去判断是否需要删除的条件,并在自己内部处理删除的后事。(比如蜂鸣器发声删除任务后他还会一直持续一个音调发声我们需要关掉它。)

2.任务优先级与Tick

2.1任务优先级

优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高一共57个优先级0-56。

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.2Tick

FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时钟中断。

  • 假设t1、t2、t3发生时钟中断
  • 两次中断之间的时间被称为时间片(time slice、tick period)
  • 时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就是10ms

 任务运行的时间并不是严格从t1,t2,t3哪里开始,而是根据Tick时间片时间到了就会切换任务。

有了Tick的概念后,我们就可以使用Tick来衡量时间了,比如:

vTaskDelay(2);  // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms

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

使用vTaskDelay函数时,建议以ms为单位,使用pdMS_TO_TICKS把时间转换为Tick。

这样的代码就与configTICK_RATE_HZ无关,即使配置项configTICK_RATE_HZ改变了,我们也不用去修改代码。

2.3修改优先级

使用uxTaskPriorityGet来获得任务的优先级:

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

使用参数xTask来指定任务,设置为NULL表示获取自己的优先级。

使用vTaskPrioritySet 来设置任务的优先级:

void vTaskPrioritySet( TaskHandle_t xTask,
                       UBaseType_t uxNewPriority );

使用参数xTask来指定任务,设置为NULL表示设置自己的优先级;

参数uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。

 3.任务状态

3.1就绪状态(ready)

就绪状态就是任务满足了运行的条件,只差轮到它运行了。

3.2运行状态(runing)

正在跑的任务。

3.3阻塞状态(Blocked)

在FreeRTOS_04_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到了,还是没有数据:超时返回

3.4暂停状态(Suspended)

FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。

要退出暂停状态,只能由别人来操作:

  • 别的任务调用:vTaskResume
  • 中断程序调用:xTaskResumeFromISR

实际开发中,暂停状态用得不多

3.5完整的任务转换图

转换过程需要的函数参考上文

4.两个Delay函数

有两个Delay函数:

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
  • xTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。

这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来让任务周期性地运行

这两个函数我举个例子吧 :

vTaskDelay(n)这个n就是需要等待多长时间,放学等待小美十分钟。

xTaskDelayUntil(&Pre, n)设定n个时间片为一个周期,每过一个周期运行一次,从上一个唤醒时间开始到下一次唤醒为一个周期,也就是它会获取当前xTaskGetTickCount,Pre=xTaskGetTickCount(),然后根据过了几n个Tick中断再次ready任务。

5.FreeRTOS任务调度机制

5.1需要了解的基本内容

        1、优先级高的任务会抢占低优先级任务,同等优先级任务轮流运行。

        2、所有新创建新的任务会直接进入就绪状态。

        3、同级优先级任务最后创建的任务最先运行(这个和就绪链表中有个PrCurrentTCB指针有关这个我们之后会讲到)

       我们每创建一个新任务这个PrCurrentTCB指针就会指向它,当我们启用调度器,PrCurrentTCB指向最后一个创建的任务,所以最后创建的任务最先运行。

        4、Tick中断里会做三件事:

                ·cont++时间基准

                ·判断阻塞链表里的任务是否阻塞结束,如果结束将阻塞任务放在就绪链表里

                ·发起调度,从就绪链表的最高优先级链表开始向最低优先级链表遍历,如果链表不为空那就执行对应优先级链表里就绪的任务。

        5、FreeRTOS任务有四个状态,只需要记录三个状态runing不需要被记录,因此有三种链表来放这三种不同状态的任务

 5.2就绪链表讲解

它是数组但是每个数组成员都是一个链表

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ] = {0},configMAX_PRIORITIES最大为56-1一共有56个优先级,优先级数越大优先级越高

pxReadyTasksLists[0]->它存放优先级为0的任务,在FreeRTOS中默认创建了一个优先级为0的任务

 在pxReadyTasksLists[24]->存放优先级为24的任务

 在pxReadyTasksLists[25]->存放优先级为25任务

5.3调度机制

我们假设pxReadyTasksLists[24]放了任务1,2,3   

pxReadyTasksLists[25]放了任务4

第一种:当任务4不放弃CPU资源时任务1,2,3就会被“饿死”。

第二种:在没有创建任务4的时候任务1,2,3轮流运行,当创建好任务4后任务4直接抢占CPU资源运行。当任务4进入阻塞状态后,会将任务4放入阻塞链表,任务1,2,3,轮流运行,轮流过程每次进入Tick中断都会遍历状态链表,如果任务4阻塞时间到那就将它放在就绪链表,在去遍历就绪链表优先级高的任务先执行。

6.空闲任务及其钩子函数

我们知道在freertos中系统在启动调度器会自动创建一个空闲任务

删除任务后需要回收被删任务的栈,TCB,这个就叫“收尸”

6.1空闲任务的作用

1、我们删除任务有两种一种是自杀,一种是他杀,他杀任务的“收尸工作”由杀死这个任务的任务来收尸,自杀的任务谁来收尸呢?由空闲任务来收尸。

因为空闲任务的优先级为0,因此我们要么不使用自杀,要么其他任务要阻塞否则自杀任务的栈无法回收,就会导致我们之后创建任务失败(在任务里创建任务然后创建的任务自杀几下内存就没了)。

良好的编程习惯

·事件驱动

·延时函数,不要使用死循环(要让任务进入阻塞,使空闲任务有执行的机会)

2、非死循环任务在退出函数前必须自杀否子整个系统都会宕机。

我们来细细的品味一下。

 

 我们创建任务的时候传入任务函数地址,当这个任务是一个有限循环时他就会退出,就会到prvTaskExitError()函数。具体什么代码细节我没搞清,我们只要记住任务函数退出就会宕机。

6.2空闲钩子函数

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
  • 空闲任务的钩子函数的限制:
  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。

6.23使用钩子函数的前提

在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:

  • 把这个宏定义为1:configUSE_IDLE_HOOK
  • 实现vApplicationIdleHook函数

 

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值