转载
【连载】从单片机到操作系统③——走进FreeRTOS
【1】 从单片机到操作系统①
我们熟悉的单片机编程:
![](data/attach/1904/nsvdnuz6qqpyiqtwss5sq2f59hs492c8.jpg)
这种结构基本上都是在main函数开始完成一些初始化,然后在主循环里周期性地调用一些函数。
这应该是最常见的结构了吧,学过单片机的都知道在 main 函数里面的那个“while(1)”。笔者也对这个 while(1) 印象深刻,因为它让我明白了单片机程序运行的归宿就在这。
在不考虑中断的情况下,整个单片机的最根本任务就是这个 while(1) 循环。在此称它为“主循环”,认为 main 函数及其调用的所有子函数(以及子函数再次调用的函数……)都在一个“主进程”里。
在
初学单片机时,大部分精力都放在单片机和各个模块的驱动上,所以在开始相当长的一段时间里采用的都是这种程序结构。而 Fun1、Fun2……这些函数完成的功能也都是比较简单的,每个函数完成一个简单的小功能,然后顺序执行就可以组合完成某个功能。
需要强调的是,这些函数虽然功能简单,但是占用 CPU 资源不一定少,比如最简单的一个独立按键扫描程序:
![](data/attach/1904/7h5zssqx1ho7e4usend1nnfih668ihgj.jpg)
注意到这个程序里有一个 5ms 延时函数,
在延时的这段时间里单片机运行一些无意义的指令消耗时间。在此期间其他任务得不到运行,整个进程阻塞在延时函数这个地方。并且,如果按键一直按下没有释放的话,程序将停留在 while(key==0); 处。
简单来说就是,系统一直在等你的释放,而单片机运算速度特别快,这就是占用了单片机的所有资源了,这样子,下一个任务在你不释放的时候就没办法得到运行。就必须得等到上一个任务做完了才能做下一个任务。这样子做,就是最低效率的了。
主函数顺序调用结构的特点
首先,正如它的名称是“顺序调用”,任务之间的运行顺序是固定不变的,当然不可能有优先级区别,它只适合完成那些周期性循环的工作。
单片机在这个任务运行的时候,其他任务是得不到运行的。并且如果这个任务由于某种原因卡住了,它将阻塞整个进程的运行。
任务执行的并行与否是相对而言的,要根据具体的情况。如果我们的要求不高,当然用这种简单的结构是最方便的了,但是这种简单的结构也确实存在很多不足,有很多可以改进的地方。
在此我们明确一下这种结构的特点:
1、由主循环调用的任务的执行顺序是固定的。
2、由主循环调用的任务都只能单独地运行,进入一个任务,就不能处理其他任务。
3、这些任务执行时间一般会比较长(相对后面几章改造过的任务函数而言),某一个任务里面的延时函数会造成整个进程被延时。
主循环调用任务函数的一种非常常用的结构。到目前为止,在主进程的构建方面用得非常多。有点也是有很多的。起码看起来界面好看很多,并且带有了逻辑性的东西。
如:
![](data/attach/1904/trjkw58hf4vx3sfm0v245rp882bo249w.jpg)
可以看到,主循环其实不进行任何实际功能的处理,它完成的只是调用各个任务函数。
对于比较大型复杂的系统,main 函数的主循环里根本不放要实际处理的代码,而是把所有任务函数归到一起,根据选择进入相应的任务函数,当处理完该任务之后又会回到主循环,由主循环再次分配任务。
此时主循环的作用就是调配任务(当然用来调配任务的主循环本身也是一个最基本的任务),而在被调配的任务里面可能还会再次被该任务调配的子任务。
再来看看被调用的任务函数,这些函数已经不只是完成一些简单功能了,它并不是执行一些固定操作后返回,每个任务函数都有自己的一套控制逻辑,并且“不那么容易返回”。
这些任务函数同属于一个进程,但是同一时刻只有一个可以运行。当进入某个函数时,可以说进程被这个函数阻塞,其他函数得不到运行。但这也就是我们需要的效果,因为每个函数都有自己的一套控制逻辑,完全不需要考虑其他界面函数。而在函数退出时,可以由该函数本身指定下一个要进入的函数,或者本来就是由于外部修改了 FlagPage 变量才导致该函数退出的。
这种程序结构特别适合于多种“界面”的功能。一般情况下,主进程不会停留在主循环里,而是偶尔退出到主循环重新分配下一个将要进入的函数,大部分时间会停留在某个界面函数里。
此外,这些函数之间有一些公共变量,这些变量的作用就是被各个函数使用,甚至用于函数间通信,辅助完成这些函数之间的逻辑结构的构建。比如 1.1 节中的那个重要的 FlagPage变量,这个标志变量就指明了当前工作于哪种工作模式下,任何函数(包括中断进程中的函数)都可以通过改变此变量来切换工作模式。
也有一些与函数对应的用于完成特定功能的变量。比如用于数码管或者显示屏显示的现存,这些显存是有特定用处的,一般其他函数不会使用(但确实是公共变量,是可以被使用的)。
将它们明确分类一下,整个系统都有哪些东西呢:
1、整体的程序框架是由各个界面函数和少数关键的全局变量构建起来的。这是构成系统的主体框架。
2、每个界面函数在完成特定功能时,会携带一些为自己服务的“私有的”变量和函数。
3、为整个框架服务的还有一些常用的变量和函数,它们完成的是一些通用功能,可以把它们理解为“库函数”。
以上都是对一个进程的结构的讨论,并没有涉及到中断。
【2】 从单片机到操作系统②
从前面的一章内容
【连载】从单片机到操作系统① 我们没有引入中断系统的概念,都是让单片机一直以要做的事来进行死循环,无法很快速应对突发情况。
下面,我将引入中断系统的概念。
下面是来自百度百科的解释:
中断系统:中断装置和中断处理程序统称为中断系统。
中断系统是计算机的重要组成部分。实时控制、故障自动处理、计算机与外围设备间的数据传送往往采用中断系统。中断系统的应用大大提高了计算机效率。
不同的计算机其硬件结构和软件指令是不完全相同的,因此,中断系统也是不相同的。计算机的中断系统能够加强CPU对多任务事件的处理能力。中断机制是现代计算机系统中的基础设施之一,它在系统中起着通信网络作用,以协调系统对各种外部事件的响应和处理。中断是实现多道程序设计的必要条件。 中断是CPU对系统发生的某个事件作出的一种反应。 引起中断的事件称为中断源。中断源向CPU提出处理的请求称为中断请求。发生中断时被打断程序的暂停点称为断点。CPU暂停现行程序而转为响应中断请求的过程称为中断响应。处理中断源的程序称为中断处理程序。CPU执行有关的中断处理程序称为中断处理。而返回断点的过程称为
中断返回。中断的实现实行软件和硬件综合完成,硬件部分叫做硬件装置,软件部分称为软件处理程序。
画重点:要有中断装置及要处理中断程序。这样的完整的系统才是中断系统,那么对于单片机来说,什么是中断装置呢。我们从简单51单片机可以知道,它有5个中断源,那么这5个中断源就是中断装置,因为它能产生中断信号,让CPU知道。对于更强的芯片,会有更多的中断源,先不介绍。顾名思义,我们作为一个程序员,写程序是家常便饭啦,处理中断程序,那么程序里面要干什么就是由我们自己实现的了。
那么,中断系统又跟我们的程序有啥关系呢?如何提高程序的效率?
这才是今天的重点!!!!
有了中断,亦或者说有了定时器的中断,我们想怎么干就怎么干(有点虚吹了)。当然啦,按照常理,我们肯定希望任务以我们的想法去做,就好比说,让第一个LED以100ms的频率闪烁一次,LED2以200ms的频率闪烁一次,还有其他任务要每1ms执行一次,那么我们总不能用延时吧,
因为delay是会一直占用CPU的资源,其他任务就得不到运行啦。(PS:CPU的占用是我以前一直没考虑的问题,还有就是这个自编的delay()函数)这样子,跟我们想象的肯定不一样,那么,如何去实现我们想的呢。
我们可以使用定时器,当时间到了才让CPU干活。使用一个定时器作为“心跳时钟”(不知道解释对不对,如有错误,请指正),以1ms的周期定时。产生定时器中断。你们我们就能知道从时间啦,那么我们就能以时间为轨道,让CPU在什么时间执行什么任务。
int main()
{
/***各种初始化***/
while(1)
{
if(time_100ms >= TIME_100MS)
{
time_100ms = 0;
LED1_Task();
}
if(time_200ms >= TIME_200MS)
{
time_200ms = 0;
LED2_Task();
}
if(time_10ms >= TIME_10MS)
{
time_10ms = 0;
XXX_Task();
}
}
return 0;
}
-
不知道你们是否看出这份代码是有很多不足的地方!(如果看不出,可以后台私聊我,给你们讲解一下)基于此,假如多人问的话,我下一篇文章会讲解一下下。
所以,我会引入新的概念,基于时间的 “时间片轮询法”,我们的很多小型单片机无法使用RTOS的,你们时间片轮询法就很适合啦。
时间片轮询法,在很多书籍中有提到,而且有很多时候都是与操作系统一起出现,也就是说很多时候是操作系统中使用了这一方法。不过我们这里要说的这个时间片轮询法并不是挂在操作系统下,而是在前后台程序中使用此法。也是本文要详细说明和介绍的方法。
时间片轮询法:其实就是模拟系统内核,对 CPU 时间片进行分配,
如果有空闲的时间片以及正在等待的作业,就将时间片分配给那个正在等待的作业;这是个帮助实现同步时间调度的程序,需要低层硬件的支持(定时器中断)。它本身利用定时器(TIM),使一个特殊的变量“Time_Num”从0开始随时间增长(1/ms),一旦达到了指定的最大值,又回归到零,如此往复……
任何一段循环的程序可以通过初始化的结构体或者宏来间接检查自己是否在允许的时间片内,如果此时不被允许执行,就跳过这段程序。这段源代码的意义就在于实现简单实用的同步时间调度。
异步任务可能引起“竞争条件”等一些复杂的问题(FPGA中常常看到),如果只需要一个简单的方案就可以解决问题,那么同步编程仍然是最好的选择,这时如果再需要一个简单算法来调度若干个密集型同步任务,那么这个方法正好可以派上用场!
注意事项
1. 任务的划分:任务一定要划分的非常合理,尽量做到任务的相对独立;
2. 任务的优先:一定要注意任务优先级的设计,把需要及时处理的任务排到任务的最前面;
3. 任务的执行:任务的执行一定要尽量的快,一定要保证在毫秒级,否则任务还没执行完,其他任务都再等,就到不到实时系统的要求,也谈不上多任务了;
4. 时间的划分:时间片的划分是整个系统的关键,一定要保证任务在需要执行的时候能够进入该执行的任务中,否则就不能实现真正的时间片轮询了。
1.任务的划分
任务的划分并不难,你需要先全面的了解你的项目是要实现什么功能,把其划分成多个功能模块,每一个模块就是一个任务,每一个任务对应一个函数。
例如一个时钟产品,一般由:按键、显示、时间、闹铃、菜单(设置/查询等)等组成。那么我们可以把其划分成5个任务。
2. 任务的优先
同样通过以上事例来说明任务优先级,可能划分的方法有很多种,而且看不出很大的区别,这里只是随便举个例子:
A. 时间,这里的时间就是从时钟芯片中获取时间;
B. 闹铃,获取时间后应该首先判断是否是设置的闹铃时间,如果是就进行闹铃提示,否则,退出执行下一个任务;
C. 显示,显示时间,如果有闹铃,则显示闹铃标志;
D. 按键,判断是否有按键,如果有就进入相应的操作;
E. 菜单,通过按键进入相应的菜单,如果没有按键,就不执行菜单任务直接退出。
这就是整个时钟产品需要实现的整个过程,任务之间的通讯已经任务之间的相互制约都是通过全局变量实现的,例如进入时间设置等时,就没有有必要实现时间的读取,闹铃的判断,以及时间的显示。这时只需要执行按键任务以及菜单任务即可,直至退出为止。这里需要说明的是不执行的任务是在判断任务执行情况后不具体执行任务代码,并不是一直在菜单程序中死等等,直至菜单退出。因为那样的话就不是真正的多任务级了,也谈不上时间片了。
3. 任务的执行
任务的执行一定要尽量的快,一定不能因为某个任务需要等等特殊的东西,而影响的其他任务,也不能在任务中调用大的延时函数,一定要保证任务的运行速度,要知道每一个任务的具体执行时间。例如上例中,绝对不能因为等等按键的释放而导致其他任务的不运行。那么怎么消抖呢?这个方法有很多,你可要通过利用两次按键任务是时间实现消抖,例如第一按键后,你做个标志,表示有键,但是不执行菜单,可要通过第二次进入按键任务判断,是否是按键的按键,还是误按,这种情况下就必须要保证按键任务的运行时间在消抖也许的时间内容,例如20ms。
4. 时间的划分
时间片的划分尤为重要,需要保证每一任务都能在该执行的时间内运行。就以时钟事例来说,显示和获取时钟一般一秒一次就可以了,如果你有时钟冒号“:”的显示,那么1秒必须执行两次以上才能保证显示的正常。当然在系统允许的情况下可以尽量多允许几次,但一定最低的允许次数。像按键可以使用20ms作为任务的时间片,因为一般按键的消抖时间为20ms,那么时间片划分为20ms完全可以保证即不漏掉按键,也不会误读按键。
实现流程:(简单实现3个任务)
使用1个定时器,可以是任意的定时器,这里不做特殊说明,下面假设有3个任务,那么我们应该做如下工作:
1 初始化定时器,这里假设定时器的定时中断为1ms(当然你可以改成10ms,这个和操作系统一样,中断过于频繁效率就低,中断太长,实时性差)。
2 设计一个结构体:
typedef和define具体的详细区别
// 任务结构
typedef struct _TASK_COMPONENTS
{
uint8 Run; // 程序运行标记:0-不运行,1运行
uint8 Timer; // 计时器
uint8 ItvTime; // 任务运行间隔时间
void (*TaskHook)(void); // 要运行的任务函数
} TASK_COMPONENTS; // 任务定义
-
并且把任务结构体进行初始化
static TASK_COMPONENTS TaskComps[] =
{
{0, 600, 600, TaskDisplayClock}, // 显示时钟
{0, 200, 200, TaskKeySan}, // 按键扫描
{0, 300, 300, TaskDispStatus}, // 显示工作状态
};
-
任务运行标志出来,此函数就相当于中断服务函数,需要在定时器的中断服务函数中调用此函数,这里独立出来,并于移植和理解。
在中断中处理这个任务:此函数就相当于中断服务函数,需要在定时器的中断服务函数中调用此函数
void TaskRemarks(void)
{
u8 i;
for (i=0; i
下面的函数就是判断什么时候该执行那一个任务了,实现任务的管理操作,应用者只需要在main()函数中调用此函数就可以了,并不需要去分别调用和处理任务函数。
void TaskProcess(void)
{
u8 i;
for (i=0; i
void TaskDisplayClock(void)
void TaskKeySan(void)
void TaskDispStatus(void)
我现在解释一下为什么能做到,定时器是一直在工作的,主程序也是一直在执行,那么当我们的时间到了,我们的代码:
-
TaskComps[i].Run = 1; // 任务可以运行
-
就是表示任务可以执行了,那么在主程序中,我们知道任务可以执行的话,我们就能直接执行了:
TaskComps[i].TaskHook(); // 运行任务
因为这个结构体定义的
-
void (*TaskHook)(void); // 要运行的任务函数
是一个指向任务的函数,那么执行这句话就是跳到要执行的任务中去了。前提是结构体已经初始化了。
到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面?
不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。。
先暂时介绍到这里。网上也很多相关教程,而且作者水平有限,如有错误请指正。
后台回复“时间片轮询法”即可获得例程代码,已经测试过的,基于stm32的测试,很简单的几句代码。
【3】从单片机到操作系统③——走进FreeRTOS
从前面的文章,我们知道,(单核)单片机某一时刻只能干一件事,会造成单片机资源的浪费,而且还有可能响应不够及时,所以,在比较庞大的程序或者是要求实时性比较高的情况下,我们可以移植操作系统。因为这种情况下操作系统比裸机方便很多,效率也高。下面,杰杰将带你们走进FreeRTOS的世界随便看看。
首先说明一下:
① 鉴于作者水平有限,学习并不深入,只是学习了皮毛而已,出错在所难免,欢迎指正。
② 以下文章说的操作系统均为FreeRTOS
③ 文章参考了野火的书籍:【 野火®】 《从 0 到 1 教你写 uCOS-III 》 ——刘火良
④ 参考了网络开源作者的笔记
下面正式开始本文内容。
在没有用到操作系统之前,单片机的运行是顺序执行,就是说,很多时候,单片机在执行这件事的时候,无法切换到另一件事。这就造成了资源的浪费,以及错过了突发的信号。那么,用上了操作系统的时候,很容易避免了这样的问题。
很简单,从感觉上,单片机像是同时在干多件事,为什么说像呢,因为单片机的执行速度很快,快到我们根本没办法感觉出来,但是同时做两件事是不可能的,在(单核)单片机中,因为它的硬件结构决定了CPU只能在一个时间段做一件事如:
如这张图,都是按照顺序来执行这些事的,假设每个任务(事件)的time无限小,小到我们根本没法分辨出来,那么我们也会感觉单片机在同时做这六件事。
真相就是:所有任务都好像在执行,但实际上在任何一个时刻都只有一个任务在执行
如是加上了中断系统(Interrupt service routine 中断服务程序)的话,就可以将上图理解为下图:
通常把程序分为两部分:前台系统和后台系统。 简单的小系统通常是前后台系统,这样的程序包括一个死循环和若干个中断服务程序:应用程序是一个无限循环,循环中调用API函数完成所需的操作,这个大循环就叫做后台系统。中断服务程序用于处理系统的异步事件,也就是前台系统。前台是中断级,后台是任务级。简单来说就是程序一直按顺序执行,有中断来了就做中断(前台)的事情。处理完中断(前台)的事情,就回到大循环(后台)继续按顺序执行。
那么问题来了,这样子的系统肯定不是好的系统,我在做第一个任务的时候想做第四个任务,根本做不到啊,其实也能做到,让程序执行的指针cp指向第四个任务就行了。但是任务一旦复杂,那么整个工程的代码的结构,可移植性,及可读性,肯定会差啦。
FreeRTOS
那么操作系统的移植就是不可或缺的了。什么叫RTOS?:Real Time OS,实时操作系统,强调的是实时性,就是要规定什么时间该做什么任务。那么假如同一个时刻,需要执行两个或者多个任务怎么办。那么我们可以人为地把任务划分优先级,哪个任务重要,就先做,因为前面一直强调,单片机无法同时做两件事,在某一个时刻只能做一件事。
那么FreeRTOS是怎么操作的呢?先看看FreeRTOS的内核吧:
FreeRTOS是一个可裁剪、可剥夺型的多任务内核,而且没有任务数限制。FreeRTOS提供了实时操作系统所需的所有功能,包括资源管理、同步、任务通信等。 FreeRTOS是用C和汇编来写的,其中绝大部分都是用C语言编写的,只有极少数的与处理器密切相关的部分代码才是用汇编写的,FreeRTOS结构简洁,可读性很强!RTOS的内核负责管理所有的任务,内核决定了运行哪个任务,何时停止当前任务切换到其他任务,这个是内核的多任务管理能力。
可剥夺内核顾名思义就是可以剥夺其他任务的CPU使用权,它总是运行就绪任务中的优先级最高的那个任务。
(图片来源网络)
在FreeRTOS中,每个任务都是无限循环的,一般来说任务是不会结束运行的,也不允许有返回值,任务的结构一般都是
- While(1)
- {
- /****一直在循环执行*****/
- }
如果不需要这个任务了,那就把它删除。
移植的教程我就不写了,超级简单的,按照已有的大把教程来做就行了。(如果没有资源,可以在后台找我,我给一份移植的教程/源码)
其实FreeRTOS的运用及其简单,移植成功按照自己的意愿来配置即可,而且FreeRTOS有很多手册,虽然作者英语很差,但是我有谷歌翻译!!!哈哈哈
既然一直都说任务任务,那肯定要有任务啊,创建任务:
- // task. h task.c
- BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
- const char * const pcName,
- uint16_t usStackDepth,
- void *pvParameters,
- UBaseType_t uxPriority,
- TaskHandle_t *pvCreatedTask
- );
函数的原型都有,按照字面的理解
- TaskFunction_t pvTaskCode //传递进来的是任务函数
- const char * const pcName //传递进来的是任务Name
- uint16_t usStackDepth //传入的是堆栈的大小
在这里要说明一下,在裸机中开发,我们不管局部变量还是全局变量,反正定义了就能用,中断发生时,函数返回地址发哪里,我们也不管。但是在操作系统中,我们必须弄清楚我们的参数是怎么储存的,他们的大小是多大,就需要我们去定义这个堆栈的大小。它就是用来存放我们的这些东西的。太小,导致堆栈溢出,发生异常。(栈是单片机 RAM 里面一段连续的内存空间)
因为在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间。
- void *pvParameters //传递给任务函数的参数
- UBaseType_t uxPriority //任务优先级
- TaskHandle_t *pvCreatedTask //任务句柄
任务句柄也是很重要的东西,我们怎么删除任务也是要用到任务句柄,其实说白了,我操作系统怎么知道你是什么任务,靠的就是任务句柄的判断,才知道哪个任务在执行,哪个任务被挂起。下一个要执行的任务是哪个等等,靠的都是任务句柄。
那么要使用这些东西,我们肯定要实现啦,下面就是实现的定义,要定义优先级,堆栈大小,任务句柄,任务函数等。
- //任务优先级
- #define LED_TASK_PRIO 2
- //任务堆栈大小
- #define LED_STK_SIZE 50
- //任务句柄
- TaskHandle_t LED_Task_Handler;
- //任务函数
- void LED_Task(void *pvParameters);
创建任务后,可以开启任务调度了,然后系统就开始运行。
- xTaskCreate(
- (TaskFunction_t ) LED_Task, //任务函数
- (const char* ) "led_task", //任务名称
- (uint16_t ) LED_STK_SIZE, //任务堆栈大小
- (void* ) NULL, //传递给任务函数的参数
- (UBaseType_t ) START_TASK_PRIO, //任务优先级
- (TaskHandle_t* ) &LED_Task_Handler//任务句柄
- );
- vTaskStartScheduler(); //开启任务调度
这个创建任务的函数 xTaskCreate 是有返回值的,其返回值的类型是BaseType_t。
我们在描述中看看:
// @return pdPASS if the task was successfully created and added to a readylist, otherwise an error code defined in the file
projdefs.h
我们其实可以在任务调度的时候判断一下返回值是否为pdPASS从而知道任务创是否建成功。并且打印一个信息作为调试。因为后面使用信号量这些的时候都要知道信号量是否创建成功,使得代码健壮一些。免得有隐藏的bug。
然后就是具体实现我们的任务LED_Task是在做什么的
当然可以实现多个任务。还是很简单的。
- //LED任务函数
- void LED_Task(void *pvParameters)
- {
- while(1)
- {
- LED0 = !LED0;
- vTaskDelay(1000);
- }
- }
这就是一个简单的操作系统的概述。
下一篇,应该是讲述开启任务调度与任务切换的具体过程。
这个可以参考野火的书籍《从 0 到 1 教你写 uCOS-III》
【4】FreeRTOS创建任务&开启调度详解
https://blog.csdn.net/jiejiemcu/article/details/80463911
开始今天的内容之前,先补充一下上篇文章【连载】从单片机到操作系统③——走进FreeRTOS的一点点遗漏的知识点。
1
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
2 const char * const pcName,
3 uint16_t usStackDepth,
4 void *pvParameters,
5 UBaseType_t uxPriority,
6 TaskHandle_t *pvCreatedTask
7 );
8创建任务中的堆栈大小问题,在task.h中有这样子的描述:
9/**
10* @param usStackDepth The size of the task stack specified as the number of variables the stack * can hold - not the number of bytes. For example, if the stack is 16 bits wide and
11* usStackDepth is defined as 100, 200 byteswill be allocated for stack storage.
12*/
当任务创建时,内核会分为每个任务分配属于任务自己的唯一堆栈。usStackDepth 值用于告诉内核为它应该分配多大的栈空间。
这个值指定的是栈空间可以保存多少个字(word) ,而不是多少个字节(byte)。
文档也有说明,如果是16位宽度的话,假如usStackDepth = 100;那么就是200个字节(byte)。
当然,我用的是stm32,32位宽度的, usStackDepth=100;那么就是400个字节(byte)。
好啦,补充完毕。下面正式开始我们今天的主题。
我自己学的是应用层的东西,很多底层的东西我也不懂,水平有限,出错了还请多多包涵。
其实我自己写文章的时候也去跟着火哥的书看着底层的东西啦,但是本身自己也是不懂,不敢乱写。所以,这个《从单片机到操作系统》系列的文章,我会讲一点底层,更多的是应用层,主要是用的方面。
按照一般的写代码的习惯,在main函数里面各类初始化完毕了,并且创建任务成功了,那么,可以开启任务调度了。
1int main(void)
2{
3 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
4 Delay_Init(); //延时函数初始化
5 Uart_Init(115200); //初始化串口
6 LED_Init(); //初始化LED
7 KEY_Init();
8 //创建开始任务
9 xTaskCreate((TaskFunction_t )start_task, //任务函数
10 (const char* )"start_task", //任务名称
11 (uint16_t )START_STK_SIZE, //任务堆栈大小
12 (void* )NULL, //传递给任务函数的参数
13 (UBaseType_t )START_TASK_PRIO, //任务优先级
14 (TaskHandle_t* )&StartTask_Handler); //任务句柄
15 vTaskStartScheduler(); //开启任务调度
16}
来大概看看分析一下创建任务的过程,虽然说会用就行,但是也是要知道了解一下的。
注意:下面说的创建任务均为xTaskCreate(动态创建)而非静态创建。
1pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );
2/*lint !e961 MISRA exception as the casts are only redundant for some ports. */
3 if( pxStack != NULL )
4 {
5 /* Allocate space for the TCB. */
6 pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
7 /*lint !e961 MISRA exception as the casts are only redundant for some paths. */
8 if( pxNewTCB != NULL )
9 {
10 /* Store the stack location in the TCB. */
11 pxNewTCB->pxStack = pxStack;
12 }
13 else
14 {
15 /* The stack cannot be used as the TCB was not created. Free
16 it again. */
17 vPortFree( pxStack );
18 }
19 }
20 else
21 {
22 pxNewTCB = NULL;
23 }
24 }
首先是利用pvPortMalloc给任务的堆栈分配空间,if( pxStack != NULL )如果内存申请成功,就接着给任务控制块申请内存。pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );同样是使用pvPortMalloc();如果任务控制块内存申请失败则释放 之前已经申请成功的任务堆栈的内存vPortFree( pxStack );
然后就初始化任务相关的东西,并且将新初始化的任务控制块添加到列表中prvAddNewTaskToReadyList( pxNewTCB );
最后返回任务的状态,如果是成功了就是pdPASS,假如失败了就是返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
1prvInitialiseNewTask( pxTaskCode,
2 pcName,
3 ( uint32_t ) usStackDepth,
4 pvParameters,
5 uxPriority,
6 pxCreatedTask,
7 pxNewTCB,
8 NULL );
9 prvAddNewTaskToReadyList( pxNewTCB );
10 xReturn = pdPASS;
11 }
12 else
13 {
14 xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
15 }
16 return xReturn;
17 }
18// 相关宏定义
19#define pdPASS ( pdTRUE )
20#define pdTRUE ( ( BaseType_t ) 1 )
21/* FreeRTOS error definitions. */
22#define errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ( -1 )
具体的static void prvInitialiseNewTask(()实现请参考FreeRTOS的tasks.c文件的767行代码。具体的static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )实现请参考FreeRTOS的tasks.c文件的963行代码。
因为这些是tasks.c中的静态的函数,仅供xTaskCreate创建任务内部调用的,我们无需理会这些函数的实现过程,当然如果需要请自行了解。
创建完任务就开启任务调度了:
1vTaskStartScheduler(); //开启任务调度
在任务调度里面,会创建一个空闲任务(我们将的都是动态创建任务,静态创建其实一样的)
1xReturn = xTaskCreate( prvIdleTask,
2 "IDLE", configMINIMAL_STACK_SIZE,
3 ( void * ) NULL,
4 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
5 &xIdleTaskHandle );
6/*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
7 }
8相关宏定义:
9#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )
10#ifndef portPRIVILEGE_BIT
11 #define portPRIVILEGE_BIT ( ( UBaseType_t ) 0x00 )
12#endif
13#define configUSE_TIMERS 1
14 //为1时启用软件定时器
从上面的代码我们可以看出,空闲任务的优先级是tskIDLE_PRIORITY为0,也就是说空闲任务的优先级最低。当CPU没事干的时候才执行空闲任务,以待随时切换优先级更高的任务。
如果使用了软件定时器的话,我们还需要创建定时器任务,创建的函数是:
1#if ( configUSE_TIMERS == 1 )
2 BaseType_t xTimerCreateTimerTask( void )
3
然后还要把中断关一下
1portDISABLE_INTERRUPTS();
至于为什么关中断,也有说明:
1/* Interrupts are turned off here, toensure a tick does not occur
2before or during the call toxPortStartScheduler(). The stacks of
3the created tasks contain a status wordwith interrupts switched on
4so interrupts will automatically getre-enabled when the first task
5starts to run. */
6/ *中断在这里被关闭,以确保不会发生滴答
7在调用xPortStartScheduler()之前或期间。堆栈
8创建的任务包含一个打开中断的状态字
9因此中断将在第一个任务时自动重新启用
10开始运行。*/
那么如何打开中断呢????这是个很重要的问题
别担心,我们在SVC中断服务函数里面就会打开中断的
看代码:
1__asm void vPortSVCHandler( void )
2{
3 PRESERVE8
4 ldr r3, =pxCurrentTCB /* Restore the context. */
5 ldrr1, [r3] /* UsepxCurrentTCBConst to get the pxCurrentTCB address. */
6 ldrr0, [r1] /* Thefirst item in pxCurrentTCB is the task top of stack. */
7 ldmiar0!, {r4-r11} /* Pop theregisters that are not automatically saved on exception entry and the criticalnesting count. */
8 msrpsp, r0 /*Restore the task stack pointer. */
9 isb
10 movr0, #0
11 msr basepri, r0
12 orrr14, #0xd
13 bxr14
14}
1msr basepri, r0
就是它把中断打开的。看不懂没所谓,我也不懂汇编,看得懂知道就好啦。
1xSchedulerRunning = pdTRUE;
任务调度开始运行
1/* If configGENERATE_RUN_TIME_STATS isdefined then the following
2macro must be defined to configure thetimer/counter used to generate
3the run time counter time base. */
4portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
如果configGENERATE_RUN_TIME_STATS使用时间统计功能,这个宏为1,那么用户必须实现一个宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();用来配置一个定时器或者计数器。
来到我们的重点了,开启任务调度,那么任务到这了就不会返回了。
1if( xPortStartScheduler() != pdFALSE )
2 {
3 /*Should not reach here as if the scheduler is running the
4 functionwill not return. */
5 }
然后就能开启第一个任务了,感觉好难是吧,我一开始也是觉得的,但是写了这篇文章,觉得还行吧,也不算太难,可能也是在查看代码跟别人的书籍吧,写东西其实还是蛮好的,能加深理解,写过文章的人就知道,懂了不一定能写出来,所以,我还是很希望朋友们能投稿的。杰杰随时欢迎。。。
开始任务就按照套路模板添加自己的代码就好啦,很简单的。
先创建任务:
1 xTaskCreate((TaskFunction_t )led0_task,
2 (const char* )"led0_task",
3 (uint16_t )LED0_STK_SIZE,
4 (void* )NULL,
5 (UBaseType_t )LED0_TASK_PRIO,
6 (TaskHandle_t* )&LED0Task_Handler);
7 //创建LED1任务
8 xTaskCreate((TaskFunction_t )led1_task,
9 (const char* )"led1_task",
10 (uint16_t )LED1_STK_SIZE,
11 (void* )NULL,
12 (UBaseType_t )LED1_TASK_PRIO,
13 (TaskHandle_t* )&LED1Task_Handler);
创建完任务就开启任务调度:
1 vTaskStartScheduler(); //开启任务调度
然后具体实现任务函数:
1//LED0任务函数
2void led0_task(void *pvParameters)
3{
4 while(1)
5 {
6 LED0=~LED0;
7 vTaskDelay(500);
8 }
9}
10//LED1任务函数
11void led1_task(void *pvParameters)
12{
13 while(1)
14 {
15 LED1=0;
16 vTaskDelay(200);
17 LED1=1;
18 vTaskDelay(800);
19 }
20}
好啦,今天的介绍到这了为止,后面还会持续更新,敬请期待哦~
【5】从单片机到操作系统⑤——FreeRTOS列表&列表项的源码解读
【连载】从单片机到操作系统⑤——FreeRTOS列表&列表项的源码解读
FreeRTOS列表&列表项的源码解读
第一次看列表与列表项的时候,感觉很像是链表,虽然我自己的链表也不太会,但是就是感觉很像。
在FreeRTOS中,列表与列表项使用得非常多,是FreeRTOS的一个数据结构,学习过数据结构的同学都知道,数据结构能使我们处理数据更加方便快速,能快速找到数据,在FreeRTOS中,这种列表与列表项更是必不可少的,能让我们的系统跑起来更加流畅迅速。
言归正传,FreeRTOS中使用了大量的列表(List)与列表项(Listitem),在FreeRTOS调度器中,就是用到这些来跟着任务,了解任务的状态,处于挂起、阻塞态、还是就绪态亦或者是运行态。这些信息都会在各自任务的列表中得到。
看任务控制块(tskTaskControlBlock)中的两个列表项:
ListItem_t xStateListItem; / * <任务的状态列表项目引用的列表表示该任务的状态(就绪,已阻止,暂停)。*/
ListItem_t xEventListItem; / * <用于从事件列表中引用任务。*/
一个是状态的列表项,一个是事件列表项。他们在创建任务就会被初始化,列表项的初始化是根据实际需要来初始化的,下面会说。
FreeRTOS列表&列表项的结构体
既然知道列表与列表项的重要性,那么我们来解读FreeRTOS中的list.c与list.h的源码吧。从头文件lsit.h开始,看到定义了一些结构体:
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE / * <如果configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则设置为已知值。* /
configLIST_VOLATILE TickType_t xItemValue; / * <正在列出的值。在大多数情况下,这用于按降序对列表进行排序。 * /
struct xLIST_ITEM * configLIST_VOLATILE pxNext; / * <指向列表中下一个ListItem_t的指针。 * /
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; / * <指向列表中前一个ListItem_t的指针。 * /
void * pvOwner; / * <指向包含列表项目的对象(通常是TCB)的指针。因此,包含列表项目的对象与列表项目本身之间存在双向链接。 * /
void * configLIST_VOLATILE pvContainer; / * <指向此列表项目所在列表的指针(如果有)。 * /
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE / * <如果configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则设置为已知值。* /
};
typedef struct xLIST_ITEM ListItem_t; / *由于某种原因,lint希望将其作为两个单独的定义。 * /
列表项结构体的一些注意的地方:
xItemValue 用于列表项的排序,类似1—2—3—4
pxNext 指向下一个列表项的指针
pxPrevious 指向上(前)一个列表项的指针
这两个指针实现了类似双向链表的功能
pvOwner 指向包含列表项目的对象(通常是任务控制块TCB)的指针。因此,包含列表项目的对象与列表项目本身之间存在双向链接。
pvContainer 记录了该列表项属于哪个列表,说白点就是这个儿子是谁生的。。。
同时定义了一个MINI的列表项的结构体,MINI列表项是删减版的列表项,因为很多时候不需要完全版的列表项。就不用浪费那么多内存空间了,这或许就是FreeRTOS是轻量级操作系统的原因吧,能省一点是一点。MINI列表项:
1struct xMINI_LIST_ITEM
2{
3 listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
4 configLIST_VOLATILE TickType_t xItemValue;
5 struct xLIST_ITEM * configLIST_VOLATILE pxNext;
6 struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
7};
8typedef struct xMINI_LIST_ITEM MiniListItem_t;
再定义了一个列表的结构体,可能看到这里,一些同学已经蒙了,列表与列表项是啥关系啊,按照杰杰的理解,是类似父子关系的,一个列表中,包含多个列表项,就像一个父亲,生了好多孩子,而列表就是父亲,列表项就是孩子。
1typedef struct xLIST
2{
3listFIRST_LIST_INTEGRITY_CHECK_VALUE / * <如果configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则设置为已知值。* /
4configLIST_VOLATILE UBaseType_t uxNumberOfItems;
5ListItem_t * configLIST_VOLATILE pxIndex; / * <用于遍历列表。 指向由listGET_OWNER_OF_NEXT_ENTRY()调用返回的后一个列表项。*/
6MiniListItem_t xListEnd; / *
列表的结构体中值得注意的是:
uxNumberOfItems 是用来记录列表中列表项的数量的,就是记录父亲有多少个儿子,当然女儿也行~。
pxIndex 是索引编号,用来遍历列表的,调用宏listGET_OWNER_OF_NEXT_ENTRY()之后索引就会指向返回当前列表项的下一个列表项。
xListEnd 指向的是最后一个列表项,并且这个列表项是MiniListItem属性的,是一个迷你列表项。
列表的初始化
函数:
1void vListInitialise( List_t * const pxList )
2{
3 pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /*lint The mini list structure is used as the list end to save RAM. This is checked and valid. */
4 pxList->xListEnd.xItemValue = portMAX_DELAY;
5 pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); /*lint The mini list structure is used as the list end to save RAM. This is checked and valid. */
6 pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );/*lint The mini list structure is used as the list end to save RAM. This is checked and valid. */
7 pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
8 listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
9 listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
10}
将列表的索引指向列表中的xListEnd,也就是末尾的列表项(迷你列表项)
列表项的xItemValue数值为portMAX_DELAY,也就是0xffffffffUL,如果在16位处理器中则为0xffff。
列表项的pxNext与pxPrevious这两个指针都指向自己本身xListEnd。
初始化完成的时候列表项的数目为0个。因为还没添加列表项嘛~。
列表项的初始化
函数:
1void vListInitialiseItem( ListItem_t * const pxItem )
2{
3 /* Make sure the list item is not recorded as being on a list. */
4 pxItem->pvContainer = NULL;
5 /* Write known values into the list item if
6 configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
7 listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
8 listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
9}
只需要让列表项的pvContainer指针指向NULL即可,这样子就使得列表项不属于任何一个列表,因为列表项的初始化是要根据实际的情况来进行初始化的。
例如任务创建时用到的一些列表项初始化:
1pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '