✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
个人主页:@rivencode的个人主页
系列专栏:玩转FreeRTOS
推荐一款模拟面试、刷题神器,从基础到大厂面试题点击跳转刷题网站进行注册学习
赶紧行动起来,面试通过率!!!
今天正式开启FreeRTOS应用讲解,本文将采用,先讲解原理然后分析FreeRTOS的源码,最后进行实验,由浅入深讲解FreeRTOS的应用,本文将详细阐述任务的多种状态以及任务状态之间如何切换,多种调度策略,以及空闲任务的作用,主要方式就是深入理解FreeRTOS的源码,才能深刻理解任务如何管理,本质上就是几个函数,几个链表,任务在这几条链表中挂来挂去就实现了任务的状态的切换。
创建任务参考:《FreeRTOS-实现任务调度器》已经详细阐述了任务创建函数的内部实现,在这里主要是对上创建任务的细节进行补充,以及对比静态与动态创建任务的差别。
什么是任务?
简单来说任务FreeRTOS中就是一个无限循环无法返回的函数,在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务,一个任务负责项目的一块功能,每个任务都有一个独立的栈(而这个栈是谁分配的?从哪里分配? 与我们裸机用的一个主栈有什么区别? 解答这些疑问接着往下看)。
而且一个任务函数需要满足一下要求:
什么叫做静态创建任务,所谓静态指的是任务所需要的内存(任务的栈,任务的任务结构体(TCB) )是静态分配的,啥意思呢?,用全局变量来定义任务的栈(全局数组),任务结构体(全局的结构体变量),大家都知道全局变量的生命周期直到程序结束才会结束,也就是说任务所用到的内存在程序运行起来之后就无法回收了。
1)定义空闲任务与静态任务的栈
要想使用静态任务创建函数必须configSUPPORT_STATIC_ALLOCATION宏(支持静态分配的宏)定义为1。
当configSUPPORT_STATIC_ALLOCATION==1,在启动调度器函数中,会使用静态的方法创建空闲任务。
什么是空闲任务? 有什么作用?
空闲任务:顾名思义就是CPU空闲的时候执行的任务,当整个系统都没有就绪任务的时候,系统必须保证有一个任务在运行(空闲任务的优先级是最低的),空闲任务的作用是清理一些被删除的任务(内存清理),执行钩子函数等等作用,后面会讲到。
多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM(内存中) ,其实就像我们裸机分配栈一样,栈就是一块空闲的空间只是存在一些使用的特性而已,除了任务分配的栈之外,还有一个主栈(就是在我们启动文件里面定义的栈)供FreeRTOS内核使用以及中断服务函数使用,而任务中的函数就使用该任务的栈。
这里有一个误区:
不要认为栈就只能有一个,栈只是一个特殊的内存,特殊在哪里?,无非就是栈的那几个特性:1.从高地址向低地址生长(先使用高地址的空间)2.满足先进后出,所以栈可以随便找一个空闲的内存当做栈,一般是一个全局数组(甚至是堆区中的全局数组(对,你没有听错在堆区划分一块区域当做任务的栈就是后面要讲的动态创建任务)),关键点在什么地方?
ARM架构中有两个栈指针,一个主栈指针MSP,任务栈指针PSP,当在任务函数中运行时,PSP会去维护这个任务栈(指向该任务的栈),此时任务函数用的局部变量等就能在自己任务栈中分配内存了,当切换任务的时候PSP也会指向正在运行的任务的栈,保证任务使用自己的任务栈,而MSP是维护FreeRTOS系统与中断函数的栈,也就是说FreeRTOS系统与中断函数所用的栈是主栈,当进入中断时使用MSP使用主栈。
关于ARM结构的知识非常多,也非常重要,关于栈与堆的区别,操作系统中的双堆栈机制(MSP,PSP)详情:
《FreeRTOS-ARM架构与程序的本质》
《FreeRTOS-ARM架构深入理解》
如何确定你需要多大的任务栈空间
1.主要根据你的程序中使用了多少局部变量,还有任务函数调用关系(只要找到调用关系最深的调用链(函数调用时也要保存返回地址到栈中))来估计栈的大小。
2.根据SRAM的大小,栈的大小肯定不能超过内存(SRAM)的大小,因为SRAM还要包括堆的大小,全局变量,静态变量(static定义).
主要就是用估计法多给一点不要这么小气,当然精确的方法可以看编译后生成的反汇编文件(后面会出一篇文件详细阐述程序的编译链接过程)
2)定义任务控制块
而在多任务系统中,任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,而这个任务块其实就是一个任务结构体,这个任务结构体包括任务的所有信息,比如任务的栈指针,任务名称,任务的形参,任务的优先级,任务的状态等。有了这个任务控制块,FreeRTOS 对任务的全部操作都可以通过这个任务控制块来实现。
任务说简单点就是一个无限循环且不带返回值的 C 函数,只不过这个函数有自己的任务栈,而且函数不能返回,返回了代表发生了错误。
4)静态创建任务函数
函数返回值:
xTaskCreateStatic函数返回初始化好的任务控制块指针,我们就可以利用这个指针来更改已创建任务的优先级、删除已创建的任务等操作
1).初始化任务控制块各个成员(pcTaskName,pxTopOfStack,xStateListItem,xEventListItem,uxPriority)
2).伪造任务现场(重点)
将函数指针(pc),函数参数(r0),以及其余伪造所有的CPU寄存器数值保存在任务栈中,等启动调度器时,启动第一个任务恢复现场,将该任务的函数地址放入PC寄存器,函数参数放入R0寄存器,立即运行该任务。
《FreeRTOS-实现任务调度器》中详细阐述了任务现场伪造的过程,以及任务调度器的实现。
在《FreeRTOS-时间片与任务阻塞的实现》详细讲解了就绪态到阻塞态的相互转换,本篇文章主要阐述悬起态,再系统讲解这种状态的切换(看到后面吧)
5)启动任务调度器
当任务创建好后,是处于任务就绪(Ready),在就绪态的任务可以参与操作系统的调度。在任务调度器中还需创建空闲任务,在操作系统中任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由FreeRTOS 管理
vTaskStartScheduler()主要是创建定时器任务(当然需要定义configUSE_TIMERS宏为1,这里先不讲,因为定时器任务可有可无),主要是创建空闲任务,空闲任务也只是一个普通的任务,它的创建也有两种方式一种静态一种是动态创建。
当我们静态创建任务的时候将configSUPPORT_STATIC_ALLOCATION宏设置为1了,所以空闲任务得静态创建。
vApplicationGetIdleTaskMemory函数得用户自己实现,为创建空闲任务传递栈以及任务控制块。
创建空闲任务:
利用vApplicationGetIdleTaskMemory函数得到用户创建的空闲任务的栈和TCB,创建空闲任务。
至于空闲任务的作用,查看空闲任务函数一看便知,不过牵扯的知识点过多留到文章后面讲解,先讲创建任务。
启动第一个任务:
在vTaskStartScheduler()启动调度器函数中,调用prvStartFirstTask()去启动第一个任务(是创建任务中优先级最高的任务pxCurrentTCB)
启动第一个任务就是去启动pxCurrentTCB指向的创建任务中最高优先级的任务
《FreeRTOS-实现任务调度器》中详细阐述了如何启动第一个任务
实验:创建静态任务
实验效果:
使用模拟器,来调试代码,结果成功启动第一个任务,而且一直运行该任务打印3。
什么是动态创建任务:所谓动态就是使用类似于malloc函数去堆上分配内存(为任务的栈与TCB分配空间)。
这一下是堆一下又是栈不会搞懵嘛?
所谓的堆与栈一样也只不过是一块空闲的内存(SRAM),只不过栈与堆的用法不同,堆上的空间由我们程序员来使用malloc函数来分配,想要多大就要多大,但是必须也由程序员使用free函数来释放空间,如果你用完了忘记释放的话就会导致内存泄漏,而栈空间的使用完全自动的,进入函数创建函数栈帧退出函数自动释放栈帧,堆的生长方式是从低地址向高地址(先使用低地址的空间)
关于栈和堆的概念,函数栈帧的概念就不过多阐述了,请参考《FreeRTOS-ARM架构与程序的本质》
动态内存有什么好处?
《动态内存分配及动态顺序表的实现》
FreeRTOS 做法是在 SRAM 里面定义一个大数组,也就是堆内存,供 FreeRTOS 的动态内存分配函数malloc使用,在第一次使用的时候,系统会将定义的堆内存进行初始化,这些代码在 FreeRTOS 提供的内存管理方案中实现(heap_1.c、heap_2.c、heap_4.c ,后面会专门出一篇文章阐述,现在只要知道在FreeRTOS 所谓的动态内存只不过是一个全局数组,只不过这个数组的空间可以反复利用,因为动态动态有使用就有回收(malloc使用与free回收)
动态分配任务的栈和任务控制块
在动态创建任务函数中xTaskCreate,动态分配任务的栈和任务控制块,也就是从堆(大数组)上分配一块空间作为任务的栈和任务的TCB
2)定义任务函数
创建两个任务
函数返回值:
成功:pdPASS;
失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)
最后与静态创建任务一样去启动任务
讲了这么多,其实本质的区别就是静态创建需要自己定义栈和TCB,而动态创建任务是malloc函数分配的栈和TCB。
而二者的区别就在于:静态创建的任务即使任务被删除了,它的栈和TCB所占的内存空间不能被回收,而动态创建的任务结束后会自动被操作系统回收(其实就是调用了free函数释放内存)
那为什么动态创建任务怎么方便为啥还要静态创建任务?
其实就是动态创建会有内存不够的可能,当然几率也比较小,其实你想怎么做就怎么做存在即合理。
这也就是为什么动态创建任务函数xTaskCreate返回值是是否创建任务成功,而静态创建任务函数xTaskCreateStatic返回的是任务控制块的指针,而动态创建任务函数xTaskCreate是靠我自己传入的任务控制块的指针,在xTaskCreate内部会让这个指针指向初始化完成的任务控制块。
因为他们的优先级都是1,事先已经配置了支持时间片轮转
《FreeRTOS-时间片与任务阻塞的实现》
所以3个任务轮流执行,虽然微观上看起来是轮流执行,如果让每个任务轮流指向1ms,而让我们肉眼宏观上来看,三个任务像是在同时执行(因为你能分辨1ms的时间嘛?)
在任务被添加到就绪链表之前,pxCurrentTCB会被更新指向创建任务中优先级最高的任务,而三个任务恰好优先级都是1,pxCurrentTCB会指向任务3,再启动第一个函数就是pxCurrentTCB指向的任务3
实验:使用同一个函数创建多个任务
实验结果:
还是两个任务交替执行。
还是提两个问题
关于类型的概念,不清楚的请参考:《C语言深度解剖之数据到底在内存中如何存储》
2.两个任务使用一个函数为什么不会冲突
原因就在于:每个任务都有一个独立的栈,自己使用自己的栈空间,所以函数内的局部变量有两个份,分别保存在两个任务栈中,互不影响
任务具有:
运行态(Running)
阻塞状态(Blocked)
暂停状态(Suspended)
就绪状态(Ready)
四种状态
而任务处于是什么状态在FreeRTOS是如何体现的?
一句话就是将任务插入各类链表
在上图的链表在后面会一一详细讲解,将任务的状态的切换彻底理解
处于就绪、挂起、延时的任务,都会被挂接到各自的链表中。调度器就是通过把任务TCB中的状态列表项xStateListItem和事件列表项xEventListItem挂接到不同的链表来表示任务的不同状态。
当某个任务处于就绪态时,调度器就将这个任务TCB的xStateListItem列表项(从某个延时/悬起链表中移除)挂接到就绪列表。事件列表项xEventListItem也一样,当队列满的情况下,任务因入队操作而阻塞时,就会将xEventListItem挂接到队列的等待入队链表上。
阻塞态与挂起态的任务有何区别?
阻塞态的任务是在等待某个事件的发生,当该事件发生的时候,任务就会被恢复到就绪链表中(当然如果恢复的任务比当前运行的任务优先级高,则恢复的任务会立马抢占CPU成为运行态的任务),而挂起的任务(当任务有较长的时间不允许运行的时候,我们可以挂起任务)是暂停任务,除非我们主动调用vTaskResume()或 vTaskResumeFromISR()函数才可以恢复任务。
其实说了这么多,如果不去阅读FreeRTOS有些问题还是会似懂非懂,等你看到后面分析源码一切都会豁然开朗。
1.在调度器运行的时候(有任务正在运行),在运行的任务中创建一个优先级更高的任务则该任务会抢占正在运行的低优先级的任务。
2.刚从阻塞态恢复就绪态的任务,如果该任务优先级高于正在运行的任务则发送任务调度进行抢占。
当然阻塞的任务也包括等待同步事件的任务,任务恢复如果先级高于正在运行的任务则发送任务调度进行抢占。
3.刚从挂起态恢复就绪态的任务(从挂起链表到就绪链表),如果该任务优先级高于正在运行的任务则发送任务调度进行抢占。
补充几点:
1.FreeRTOS系统中同一时间只能有一个任务处于运行态(running),所以像那些vTaskDelay、 vTaskSuspend、vTaskResume等可以改变任务状态函数必然是在运行中的任务中调用的(参数传入NULL表示要操作当前正在运行的任务,也可以传入其他函数的hanle(该任务控制块的指针)操作其他任务)
2.所有状态的任务都可以调用vTaskSuspend()函数进入挂起状态,进入挂入态的任务就像与世隔绝一般断开所有连接挂入悬起链表中,只有别的正在运行的任务调用vTaskResume(传入任务hanle),将该挂起的任务从悬起链表放入就绪链表之中。
3.调度器只会在就绪链表中选择最高优先级的任务运行,当然也会检测是否有任务延时到期,等到的某个事件(就会将这个任务从阻塞态变成就绪态添加至就绪链表),所以挂起的任务就完全屏蔽了一样
任务的阻塞态与就绪态的相互转化,支持时间片,实现延时链表
请参考:《FreeRTOS-时间片与任务阻塞的实现》
今天我们主要阐述,任务如何进入挂起态,如何恢复?
所谓挂起任务:不能得到执行的任务(得不到CPU的使用权)
运行中的任务可以通过调用 vTaskSuspend()函数都可以将处于任何状态的任务挂起(传入NULL挂起自己),只能通过vTaskResume()恢复该任务。
想要支持任务挂起必须INCLUDE_vTaskSuspend宏设置为1支持挂起
话不多说直接上vTaskSuspend()函数的源码。
1.参数xTaskToSuspend传入NULL说明要挂起当前任务
2.任务被挂起前的状态可能是(运行态、就绪态 :处于就绪链表),也可能是阻塞态(延时链表、事件等待链表),挂起之后需要从这些链表中移除,然后将任务插入悬起链表。
清除位图的操作代表该优先级的链表为空。详情参考:《FreeRTOS-时间片与任务阻塞的实现》
3.重置下一个任务的解除阻塞时间,防止挂起的任务刚好是要从延时链表恢复(延时到期)的任务
4.当被挂起的任务为当前任务此时需分为两种情况:
1).调度器已启动
说明被挂起的任务是正在运行的任务,所以要发生一次调度,切换任务.
2).调度器未启动
此时没有任务在运行,现在处于创建任务时期
看注释吧:
挂起的任务要想恢复只能靠调用vTaskResume()函数进行任务的恢复,任务恢复就是让挂起的任务重新进入就绪状态(添加至就绪链表),恢复的任务会保留挂起前的状态信息(包括挂起前的优先级),如果恢复的任务的优先级大于当前正在运行的任务则发生一个任务切换,进行抢占,恢复的任务变成运行态。
直接上vTaskResume()函数的源码:
代码比较简单:
总结一句话就是恢复挂起任务:将任务从悬起链表中移除,然后添加到就绪链表中,最后判断恢复的任务的优先级是否大于当前任务的优先级,大于则切换任务。
实验思路:
任务1中:先记录当前xTickCount的值,等到xTickCount+10在挂起任务3进入挂起态,到xTickCount+20恢复任务3.
任务2中:调用vTaskDelay(10),让任务2进入阻塞态
首先明白一点什么是调度器?
简单来说就是任务的切换
挂起调度器的本质是什么?
为了暂停任务的切换,任务切换的本质是什么:在支持任务抢占的情况下,当更高优先级的任务被恢复或者被创建,会抢占低优先级的任务。(当然支持时间片的情况下优先级的任务会轮流执行咱先不讨论这个)
最后一个关键问题为什么需要挂起调度器?
其实我们挂起调度器的根本原因就是去保护临界资源,什么是临界资源:(比如一些全局的变量),全部的变量所以任务中/中断中能都可以去使用,如果我们不去保护这些全局变量会发生什么情况?
当我们在一个正在执行的任务函数中去使用一个全局变量(临界资源),此时如果一个更高优先级的任务(恢复或者被创建), 则会抢占该正在执行的任务,如果这个更高优先级的任务里也去修改这个全局变量(临界资源),则等回到原来任务时(更高优先级任务主动进入阻塞状态),这个全局变量的值已经被修改了,则后面会乱套了。
(最低优先级的中断都比最高优先级的任务的优先级的要高,因为啥呢所谓任务优先级操作系统人为指定的,再高优先级的任务也只是一个应用程序,必须给中断/异常(更加紧急的事物让步))
当我们在一个正在执行的任务函数中去使用一个全局变量(临界资源),此时一个中断来临,会打断正在执行的任务转而去执行中断服务函数,如果在中断服务函数中修改了这个全局变量(临界资源),等回到任务运行时,这个全局变量的值已经被修改了,则后面一样会乱套了。
顺着上面的思路就可以提出两种解决方案
1.挂起调度器:当我们挂起调度器中,操作系统不在进行任务切换也就不存在高优先级的任务抢占的情况。
2.关中断:当我们关闭了中断(当然不可能是全部关闭,关闭部分中断),中断就不会打断任务的执行了,当然某些低优先级的中断使用了某些临界资源也可以通过关中断(关闭一些优先级的较高的中断)来保护临界资源不被修改。
关中断也可以实现挂起调度器的作用?
为啥这么说:原因挂起调度器的本质是阻止任务切换,而任务切换是PendSV中断中进行的,关中断也会将PendSV中断屏蔽,则关中断也会阻止任务的切换。
在ARM中有提供几个指令快速开关中断:
在FreeRTOS中使用的是BASEPRI寄存器屏蔽高于某个中断号(中断号越高优先级的越低)的中断全部屏蔽,以后会专门出一篇文章来阐述
调用 xTaskResumeAll()函数可以挂起任务调度器,而且该函数可以嵌套可以重复挂起调度器,但是恢复调度器的时候调用了多少次的 vTaskSuspendAll()就要调用多少次xTaskResumeAll()进行恢复。
任务调度器被挂起时:操作系统停止任务的切换,也就是说让当前任务独享CPU,这样任务其实就不能来打断当前任务(当前任务就可以随便使用临界资源了),这样就很好的实现的临界资源的保护。
我们一直说挂起任务调度器但是任务调度器是如何被挂起的呢,或者直白一点如果取阻止任务切换的呢?
接下来看好叭您
当我们当前任务要去使用某些临界资源的时候只需要调用vTaskSuspendAll()函数就能挂起任务调度器。
你也看到了vTaskSuspendAll()函数非常简单,就是让全局变量uxSchedulerSuspended自增1
uxSchedulerSuspended默认为0表示调度器未被挂起,当调用vTaskSuspendAll()函数让uxSchedulerSuspended自增1,则uxSchedulerSuspended大于零表示调度器被挂起。
为什么uxSchedulerSuspended大于零就能挂起任务调度器或者说怎么让操作系统不切换任务呢?
这就要源头开始讲了,首先我们要知道任务是如何切换的,是在哪里执行的切换?
《FreeRTOS-实现任务调度器》
《FreeRTOS-时间片与任务阻塞的实现》
在上面两篇文章中已经讲过:FreeRTOS启动一个Systick中断作为系统的时基,比如1ms发生一次Systick中断然后执行中断服务函数,而在Systick中断会调用xTaskIncrementTick()函数,判断是否需要进行任务的切换(1.如果支持抢占:如果有更高优先级的任务被恢复,则会发生一次任务切换2.如果支持时间片:在没有更高优先级的任务来抢占的时候,同优先级任务会交替执行,每次进入中断都会切换任务),而如果需要任务切换,则会悬起PendSV中断,在其他没有其他中断运行的时候,则执行PendSV中断(在中断中优先级最低)切换。
关于 SCV中断,Systick中断请参考:
《FreeRTOS-ARM架构深入理解》
以及PendSV中如何切换任务请参考:
《FreeRTOS-实现任务调度器》
任务调度的过程(Systick中断,xTaskIncrementTick()时基更新函数,任务切换的过程)已经在上述文章中详细阐述过,这里面再补充一些细节关于Systick中断服务函数内部细节看看到底变量uxSchedulerSuspended是如何挂起与恢复调度器的。
SysTick中断服务函数主要调用了xTaskIncrementTick()函数,当xTaskIncrementTick()函数返回为真(pdTRUE)时才调用 taskYIELD()执行任务切换。
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/* 如果调度器正常运行,该函数的作用:
1.更新系统时基xTickCount(每中断一次就会调用xTaskIncrementTick加1)
2.检查是否有延时到期的任务(如果有则将任务从延时链表->就绪链表中)
3.判断是否要切换任务(需要切换返回pdTURE不需要返回pdFALSE
4.如果使能了钩子函数,则会执行钩子函数 */
/* 如果调度器被挂起了则上述事情1,2,3都不会做,4会做,永远返回pdFALSE表示不切换任务 */
/* 重点来了uxSchedulerSuspended为pdFALSE(0),才会执行下面系列操作 */
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* 任务调度器未被挂起 */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/* 更新系统时基 */
xTickCount = xConstTickCount;
/* 如果计数器溢出(为0),交换延时列表指针和溢出延时列表指针 */
if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
{
taskSWITCH_DELAYED_LISTS();
}
/* 看是否有延时任务到期,任务按照唤醒时间从小到大存储在延时链表中
任务链表中第一个任务未到期,其它任务一定没有到期 */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ; ; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* 如果延时链表为空,设置xNextTaskUnblockTime为最大值 */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* 如果延时链表不为空,获取延时链表第一个任务的到期时间,如果唤醒时间到期
,延时链表中第一个任务要被移除阻塞状态成为就绪态 */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* 直到延时链表中所有唤醒时间到期的任务溢出
才跳出循环 */
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deemed easier to understand with multiple breaks. */
}
/* 从延时链表中删除到期任务 */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/* 如果是因为等待事件而阻塞则将到期任务从事件列表中删除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
}
/* 将解除阻塞的任务添加至就绪链表 */
prvAddTaskToReadyList( pxTCB );
/* 如果使能了任务抢占 */
#if ( configUSE_PREEMPTION == 1 )
{
/* 如果解除阻塞的任务优先级大于当前任务,触发任务切换切换标志 */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* 如果支持抢占又支持时间片 */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
/* 同优先级的任务轮流执行(前提是这些任务优先级最高) */
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
/* 支持钩子函数 */
#if ( configUSE_TICK_HOOK == 1 )
{
/* 执行钩子函数 */
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook();
}
}
#endif /* configUSE_TICK_HOOK */
/* 这个等下 解释 */
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* configUSE_PREEMPTION */
}
/* 如果uxSchedulerSuspended==pdFLASH(0)
任务调度器被挂起*/
else
{
/* uxPendedTicks用于统计调度器挂起期间,系统节拍中断次数
当调用恢复调度器函数时,会执行uxPendedTicks次本函数(xTaskIncrementTick()):
恢复系统节拍中断计数器,如果有任务阻塞到期,则删除阻塞状态
*/
++xPendedTicks;
/* 钩子函数 */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
总结:
如果调度器正常运行,xTaskIncrementTick函数的作用:
1.更新系统时基xTickCount(每中断一次就会调用xTaskIncrementTick加1)
2.检查是否有延时到期的任务(如果有则将任务从延时链表->就绪链表中)
3.判断是否要切换任务(需要切换返回pdTURE不需要返回pdFALSE
4.如果使能了钩子函数,则会执行钩子函数
关于xTaskIncrementTick的细节参考:
《FreeRTOS-时间片与任务阻塞的实现》
1.所以当调度器被挂起本质是停止任务切换:而在xTaskIncrementTick直接屏蔽代码不让切换任务。
2.调用vTaskSuspendAll()就要调用多少次挂起调度器就要调用多少次xTaskResumeAll()才能恢复调度器。
当然既然要挂起调度器,不让任务切换,当然也要从源头下手,从任务切换的PendSV中断服务函数入手从根源停止任务切换
任务切换(PendSV中断服务函数)的过程请参考:《FreeRTOS-实现任务调度器》
总结:
调度器被挂起时,如果有中断等调用了PendSV中断服务函数想要去切换任务,但PendSV中断服务函数中并不会去切换任务,而只会将xYieldPending标志置1,等待调度器恢复的时候在xTaskIncrementTick函数中判断xYieldPending如果为1切换任务
xYieldPending就是用于任务调度器挂起时又刚好需要发生一次任务切换,只能先将xYieldPending置1等到任务调度器恢复时,由xTaskIncrementTick函数判断xYieldPending是否挂起调度器期间有需要切换任务,以后会进行补充。
当任务调度器被挂起的时候,就绪链表不能被访问,所以在调度器挂起期间有(某个在等待同步事件的任务等到了该事件就应该从阻塞态变成就绪态)但是调度器挂起无法挂起就绪链表则先将任务挂起到xPendingReadyList链表中等到调度器恢复时,再将xPendingReadyList链表任务一一添加到就绪链表中
要恢复调度器就需要调用 xTaskResumeAll()函数,调用vTaskSuspendAll()就要调用多少次挂起调度器就要调用多少次xTaskResumeAll()才能恢复调度器
当我们不需要该任务时可以利用vTaskDelete()删除该任务,形参为要删除任务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。
1.当删除一个任务时,那这个任务在操作系统中就完全消失了,所以需要将任务挂入的(就绪,阻塞,挂起和事件链表)中移除。
2.任务删除也要删除任务的信息,而存储任务信息的就是任务结构体TCB,所以我们只需要释放任务的结构体TCB的内存(前提任务是动态创建的,TCB与任务的栈都是动态分配的也就是说可以回收)
3.任务的栈中还保存着任务函数那些局部变量等,任务删除一样也要释放任务的栈
当一个正在运行的任务调用了vTaskDelete(NULL),并传入参数为NULL,问题来了,任务能自己删除自己嘛?
先说结论:肯定不行
为什么呢:原因就在于任务要彻底删除必须的将任务的TCB与任务的栈的空闲都释放,但是矛盾来了正在运行的任务中调用vTaskDelete(NULL)函数,而vTaskDelete(NULL)是一个函数,它本身需要用到栈,而这个栈就是该正在运行的任务的栈,所以删除任务需要删除栈而调用vTaskDelete()函数需要栈,所以任务不能调用vTaskDelete()函数来删除自身,只能是删除其他的任务。
拿紧接着上面的问题,又有一个问题,任务要删除自身调用vTaskDelete(NULL),怎么办呢?
则只会将任务的从(就绪,阻塞,挂起和事件链表)中移除,而任务的TCB与栈会交给空闲任务去释放,这样才算完成任务的删除。
#if ( INCLUDE_vTaskDelete == 1 )
void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t * pxTCB;
/* 关中断 */
taskENTER_CRITICAL();
{
/* 获取要删除的任务控制块,如果 xTaskToDelete为NULL则删除任务自身 */
pxTCB = prvGetTCBFromHandle( xTaskToDelete );
/* 将任务从就绪链表或者延时链表移除 */
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 清除位图:如果该优先级的就绪链表中无任务则清除 */
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
/* 如果当前任务在等待事件,那么将任务从事件列表中移除 */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
uxTaskNumber++;
if( pxTCB == pxCurrentTCB )
{
/* 任务正在删除自身,则不能在任务本身内完成删除,
所以会将任务放在等待删除的链表中,等切换到空闲任务时
空闲任务会将会等待删除的链表的任务释放掉控制块
和释放的堆栈的内存 */
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) );
/* 递增uxDeletedTasksWaitingCleanUp,
以便空闲任务知道有多少任务要被删除,
则空闲任务会将这些任务统统真正的删除
:释放任务的TCB与栈的内存 */
++uxDeletedTasksWaitingCleanUp;
/* 任务删除钩子函数 */
portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending );
}
else
{
/* 如果删除的不是自身而是其他任务
则可以彻底删除要删除的任务
当前任务数减一,uxCurrentNumberOfTasks
是全局变量用于记录当前的任务数量 */
--uxCurrentNumberOfTasks;
/* 删除任务控制块与任务的栈 */
prvDeleteTCB( pxTCB );
/* 重置下一个任务的解除阻塞时间,防止下一个解除阻塞的
任务刚好是被删除的任务 */
prvResetNextTaskUnblockTime();
}
}
/* 开中断 */
taskEXIT_CRITICAL();
/* 如删除的是当前的任务,则需要立马发起一次任务切换 */
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
configASSERT( uxSchedulerSuspended == 0 );
/* 任务切换 */
portYIELD_WITHIN_API();
}
}
}
1.如果想要使用任务删除函数vTaskDelete() 则 必 须 在FreeRTOSConfig.h 中将宏定义 INCLUDE_vTaskDelete 配置为 1。
为什么要怎么麻烦搞怎么多宏?
有些嵌入式设备内存小,FreeRTOS支持代码裁剪,将一些不需要的功能代码可以删除。
一段一段代码来分析一下叭,其实注释已经非常清晰了一看就懂了。
1.将要删除的任务从(就绪链表 或 延时链表 或 悬起链表 或 事件链表)移除,则任务不处于任务状态,所以任务删除能将任务状态下的任务删除。
2.当任务调用vTaskDelete(NULL),删除自身时,并不能完全删除任务,只能将任务从(就绪链表 或 延时链表 或 悬起链表 或 事件链表)移除,则任务不处于任务状态,则任务就不会参与调度了,但是并没有释放任务的TCB与任务的栈的内存,则需要借助空闲任务来删除,如果是删除其他任务那就是彻底将其删除。
3.如删除的是当前的任务,则需要立马发起一次任务切换
FreeRTOS中必须时时刻刻需要一个任务正在运行,当我们自己创建的所有应用任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务:
1.空闲任务优先级为0:它不能阻碍用户任务运行
2.空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞(如何空闲任务都阻塞了,FreeRTOS中就可能一个任务都没有运行,这个情况是绝对不允许的)
空闲任务的作用?
下面就是空闲任务函数:
1.调用prvCheckTasksWaitingTermination()函数,查看是否有任何任务已自行删除,如果是则空闲任务负责释放已删除任务的 TCB 和堆栈
prvCheckTasksWaitingTermination()函数将所有待删除(那些调用vTaskDelete(NULL)删除自身的任务),一一释放任务的TCB与任务栈的内存。
2.空闲任务礼让同优先级为0的用户任务
1).配置configIDLE_SHOULD_YIELD为1(前提正常抢占configUSE_PREEMPTION==1),则空闲任务会礼让同优先级的用户任务,也就是礼让优先级与空闲任务同为0的用户任务。
2).如果不支持抢占的话(configUSE_PREEMPTION==0),空闲任务一定会礼让同优先级的用户任务
如果你要问为啥是上面这种设计,那你得问设置操作系统的程序员了
什么是空闲任务礼让用户任务(优先级与空闲任务优先级一样为0),
礼让是什么意思?
如果有一个优先级为0的用户任务,还有一个操作自带的优先级为0的空闲任务,如果系统支持时间片的话,则用户任务与空闲任务轮流执行,假设系统时钟为1ms,则两个任务各自运行1ms。
但如果空闲任务礼让的话:则空闲任务只执行一清理函数prvCheckTasksWaitingTermination()后,主动请求已出任务切换,主动让出CPU给用户任务,如果不礼让的话:因为空闲任务函数本身也是一个死循环,可能在1ms的时间已经执行了几遍函数体(包括会运行用户定义的钩子函数(如果启用了钩子函数)),而礼让只会执行一遍空闲任务函数体。
上面说的有点绕了其实就是空闲任务礼让用户的任务的话:空闲任务只执行一遍函数体(可能时间远远没有1ms,如果是不礼让情况空闲任务与用户任务要轮流执行1ms的),就马上自动放弃CPU资源,主动进行一个任务切换到用户任务运行。
3.如设置了configUSE_IDLE_HOOK == 1正常空闲任务钩子函数,这个钩子函数由用户来编写
一般在项目中延时阻塞用的非常频繁,因为有着优先级不同的任务,低优先级的任务想要得到运行高优先级的任务必须进入阻塞或者挂起的状态,所以高优先级的任务一定会有阻塞的情况(可以调用vTaskDelay()或者vTaskDelayUntil(),将CPU使用权让给低优先级的任务)。
vTaskDelay函数的形参为延时时间,单位为一个tick,一个tick就是系统节拍周期也就是Systick中断周期,假设周期为1ms,传入1那就是延时1ms,进入延时的任务会变成阻塞态(则其他任务就得以运行),并添加至延时链表中,在Systick中断服务函数中,检测任务延时是否到期,如果到期就将任务从延时链表移至就绪链表,如果该恢复的任务优先级的大于正在运行任务的优先级则立马发送抢占(大概就是这么一个过程)。
绝对延时与绝对延时有什么区别?
1.相对延时vTaskDelay()
上图中的do something任务本身代码执行时间可能不一样,还有一种情况让任务的执行时间不一样,那就是在调用vTaskDelay()函数前当前任务被高优先级任务抢占了当前任务或者进入了中断,进而影响到当前任务的下一次执行的时间。
所以使用vTaskDelay()函数只能保证,从调用vTaskDelay()函数开始到延时结束这一段时间是固定了,而任务不一定能周期执行。
绝对延时是指:可以较为精确的周期运行任务
任务以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是固定的。
如果要理解vTaskDelayUntil()函数的实现一定一定要看下图:
当然同样的:上图中的do something任务本身代码执行时间可能不一样,还有一种情况让任务的执行时间不一样,那就是在调用vTaskDelayUntil()函数前当前任务被高优先级任务抢占了当前任务或者进入了中断,但是使用了vTaskDelayUntil()函数就能周期执行任务。
相对延时vTaskDelay(),与绝对延时vTaskDelayUntil()的理论讲得非常详细了
后面直接看源码咯
关于vTaskDelay(),与Systick中断服务函数,在《FreeRTOS-时间片与任务阻塞的实现》已经详细阐述,这里将不再赘述
直接看注释就好啦。
#if ( INCLUDE_vTaskDelay == 1 )
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* 任务延时时间xTicksToDelay必须大于0
不然直接强制进行一个任务切换 */
if( xTicksToDelay > ( TickType_t ) 0U )
{
/* 关调度器 */
vTaskSuspendAll();
{
/* 将任务添加到延时链表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
/* 开调度器 */
xAlreadyYielded = xTaskResumeAll();
}
/* 如果xTaskResumeAll()函数未进行任务切换
则强制任务切换(当前任务都阻塞了肯定要进行切换任务
当然如果传入的参数<0一样强制切换任务)*/
if( xAlreadyYielded == pdFALSE )
{
/* 任务切换 */
portYIELD_WITHIN_API();
}
}
#endif /* INCLUDE_vTaskDelay */
在vTaskDelay()函数主要是调用了prvAddCurrentTaskToDelayedList(),将要延时的任务按照任务的唤醒时间从小到大排序插入延时链表,任务进入阻塞状态,(如果任务唤醒时间溢出了则需要插入溢出延时链表)
static void prvAddCurrentTaskToDelayedList( TickType_t xTicksToWait,
const BaseType_t xCanBlockIndefinitely )
{
TickType_t xTimeToWake;
/* 获取系统时基计数器xTickCount的值 */
const TickType_t xConstTickCount = xTickCount;
/* 将任务从就绪链表移除 */
if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
/* 如果对应优先级的链表中无任务,则清除对应优先级的位图 */
portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
}
#if ( INCLUDE_vTaskSuspend == 1 )
{
if( ( xTicksToWait == portMAX_DELAY ) && ( xCanBlockIndefinitely != pdFALSE ) )
{
/* 如果延时时间xTicksToWait为最大值portMAX_DELAY
则将任务添加至悬起链表,将任务挂起(相当于死等事件发生) */
listINSERT_END( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 计算任务唤醒时间,可能这个时间可能会溢出但是后面会处理好 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 将任务唤醒时间设置为任务结点的辅助排序值 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
/* 唤醒时间溢出,则将此任务插入溢出链表中 */
if( xTimeToWake < xConstTickCount )
{
/* 插入溢出链表 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 任务唤醒时间未溢出,因此任务插入当前正常延时链表 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 更新xNextTaskUnblockTime的值,确保xNextTaskUnblockTime是
延时链表中第一个要唤醒任务的唤醒时间 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
}
}
#else /* INCLUDE_vTaskSuspend */
{
/* 当不支持挂起任务时 */
/* 计算任务唤醒时间,可能这个时间可能会溢出但是后面会处理好 */
xTimeToWake = xConstTickCount + xTicksToWait;
/* 将任务唤醒时间设置为任务结点的辅助排序值 */
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
/* 唤醒时间溢出,则将此任务插入溢出链表中 */
if( xTimeToWake < xConstTickCount )
{
/* 插入溢出链表 */
vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
}
else
{
/* 任务唤醒时间未溢出,因此任务插入当前正常延时链表 */
vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
/* 更新xNextTaskUnblockTime的值,确保xNextTaskUnblockTime是
延时链表中第一个要唤醒任务的唤醒时间 */
if( xTimeToWake < xNextTaskUnblockTime )
{
xNextTaskUnblockTime = xTimeToWake;
}
}
/* Avoid compiler warning when INCLUDE_vTaskSuspend is not 1. */
( void ) xCanBlockIndefinitely;
}
#endif /* INCLUDE_vTaskSuspend */
}
SysTick中断服务函数主要调用了xTaskIncrementTick()函数,当xTaskIncrementTick()函数返回为真(pdTRUE)时才调用 taskYIELD()执行任务切换。
关于更新时基xTaskIncrementTick()函数的作用看注释就好了,里面的难点主要是两条延时链表的相互转化详情查看:《FreeRTOS-时间片与任务阻塞的实现》
vTaskDelayUntil()函数的作用就是实现任务的周期性执行,在前面已经讲解的原理,我们直接看vTaskDelayUntil()函数是如何实现的。
vTaskDelayUntil()函数:
参数:
1.pxPreviousWakeTime: 上一次被唤醒的时间
2.xTimeIncrement:任务周期时间
表示任务要阻塞到(pxPreviousWakeTime + xTimeIncrement)
返回值:
判断:传入的xTimeIncrement任务周期时间是否合理
什么意思:任务运行的时间不能超出任务周期时间
直接上源码咯:
#if ( INCLUDE_xTaskDelayUntil == 1 )
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
/* 获取任务开始延时的时间点 */
const TickType_t xConstTickCount = xTickCount;
/* 计算任务被唤醒时间xTimeToWake */
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* pxPreviousWakeTime是上一次任务的唤醒的时间,然后
任务执行的一段时间然后xConstTickCount时间进入xTaskDelayUntil函数
所以理论上xConstTickCount > *pxPreviousWakeTime,则下面
表示xConstTickCount溢出了 */
if( xConstTickCount < *pxPreviousWakeTime )
{
/* xTimeToWake < *pxPreviousWakeTime说明xTimeToWake也溢出了
,则xTimeToWake与xConstTickCount同时溢出则相当于
没有溢出,xTimeToWake > xConstTickCount表明
周期性延时时间大于任务主体代码的执行时间*/
if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
}
else
{
/* 滴答时间xConstTickCount未溢出 */
/* 只是任务唤醒xTimeToWake时间溢出 或者 xTimeToWake与xConstTickCount都未溢出
保证周期性延时时间大于任务主体代码的执行时间*/
if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
}
/* 更新上一次的唤醒时间 */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/*prvAddCurrentTaskToDelayedList()函数需要的是阻塞时间
而不是唤醒时间,因此减去当前的滴答计数。 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
}
xAlreadyYielded = xTaskResumeAll();
/* 如果xTaskResumeAll未准备切换任务
则强制切换任务,因为任务都阻塞了那必须切换 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
/* 判断周期性延时时间是否大于任务主体代码的执行时间
如果小于返回pdFALSE说明参数(任务周期设置不合理)pdFALSE */
return xShouldDelay;
}
#endif /* INCLUDE_xTaskDelayUntil */
想要理解vTaskDelayUntil函数的实现:自己画画图,理解下面几个变量关系:
xTimeIncrement:任务周期时间。
pxPreviousWakeTime:上一次唤醒任务的时间点。
xTimeToWake:本次要唤醒任务的时间点。
xConstTickCount:进入延时的时间点。
其次就是溢出的概念请参考:《FreeRTOS-时间片与任务阻塞的实现》
1. 获取任务开始延时的时间点,以及计算任务被唤醒的时间,我们只需要关注任务在何时唤醒不需要关注任务究竟会延时多久(原因就是任务本身的执行时间就不固定而又因为:任务的周期=任务执行的时间+任务延时的时间,所以任务延时的时间也是不固定的),但是我们可以确保任务周期是固定的,即任务在两次唤醒之间的时间间隔是固定的。
2.其实下面这几个判断就是为了保证一件事:
任务运行时间不得大于任务的周期,如果大于了任务都没有执行完,那还咋实现任务的周期运行啊,即要保证: 当前进入延时的时间点 xConstTickCount不得大于下一次任务唤醒的时间xTimeToWake
3.函数内部自动更新pxPreviousWakeTime确保任务周期运行
4.如果满足任务运行时间不得大于任务的周期,则将任务添加至延时链表(不过这个函数所要的参数的任务要延时的时间而不是唤醒时间,在prvAddCurrentTaskToDelayedList函数内部自然会计算任务的唤醒时间)
最后一小段看注释叭
学FreeRTOS就是要深入底层学习,研究它的功能代码实现,这样才能彻底,征服FreeRTOS,学FreeRTOS另外一个最重要的基础就是链表了,毫不夸张的说那几条链表贯穿了整个FreeRTOS系统,最后关于本文还有几个实验日后补充进去。
结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题
点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)