【3 FreeRTOS+STM32CubeMX】【转载】从单片机到操作系统,走进FreeRTOS

转载【连载】从单片机到操作系统③——走进FreeRTOS

【1】 从单片机到操作系统①

我们熟悉的单片机编程:

这种结构基本上都是在main函数开始完成一些初始化,然后在主循环里周期性地调用一些函数。

这应该是最常见的结构了吧,学过单片机的都知道在 main 函数里面的那个“while(1)”。笔者也对这个 while(1) 印象深刻,因为它让我明白了单片机程序运行的归宿就在这。

在不考虑中断的情况下,整个单片机的最根本任务就是这个 while(1) 循环。在此称它为“主循环”,认为 main 函数及其调用的所有子函数(以及子函数再次调用的函数……)都在一个“主进程”里。

初学单片机时,大部分精力都放在单片机和各个模块的驱动上,所以在开始相当长的一段时间里采用的都是这种程序结构。而 Fun1、Fun2……这些函数完成的功能也都是比较简单的,每个函数完成一个简单的小功能,然后顺序执行就可以组合完成某个功能。

需要强调的是,这些函数虽然功能简单,但是占用 CPU 资源不一定少,比如最简单的一个独立按键扫描程序:

注意到这个程序里有一个 5ms 延时函数,在延时的这段时间里单片机运行一些无意义的指令消耗时间。在此期间其他任务得不到运行,整个进程阻塞在延时函数这个地方。并且,如果按键一直按下没有释放的话,程序将停留在 while(key==0); 处。

简单来说就是,系统一直在等你的释放,而单片机运算速度特别快,这就是占用了单片机的所有资源了,这样子,下一个任务在你不释放的时候就没办法得到运行。就必须得等到上一个任务做完了才能做下一个任务。这样子做,就是最低效率的了。

主函数顺序调用结构的特点

首先,正如它的名称是“顺序调用”,任务之间的运行顺序是固定不变的,当然不可能有优先级区别,它只适合完成那些周期性循环的工作。

单片机在这个任务运行的时候,其他任务是得不到运行的。并且如果这个任务由于某种原因卡住了,它将阻塞整个进程的运行。

任务执行的并行与否是相对而言的,要根据具体的情况。如果我们的要求不高,当然用这种简单的结构是最方便的了,但是这种简单的结构也确实存在很多不足,有很多可以改进的地方。

在此我们明确一下这种结构的特点:

1、由主循环调用的任务的执行顺序是固定的。

2、由主循环调用的任务都只能单独地运行,进入一个任务,就不能处理其他任务。

3、这些任务执行时间一般会比较长(相对后面几章改造过的任务函数而言),某一个任务里面的延时函数会造成整个进程被延时。

主循环调用任务函数的一种非常常用的结构。到目前为止,在主进程的构建方面用得非常多。有点也是有很多的。起码看起来界面好看很多,并且带有了逻辑性的东西。

如:

可以看到,主循环其实不进行任何实际功能的处理,它完成的只是调用各个任务函数。

对于比较大型复杂的系统,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<TASKS_MAX; i++) // 逐个任务时间处理  

    {  

        if (TaskComps[i].Timer) // 时间不为 0  

        {  

        TaskComps[i].Timer--; // 减去一个节拍  

            if (TaskComps[i].Timer == 0) // 时间减完了  

            {  

                TaskComps[i].Timer = TaskComps[i].ItvTime; // 恢复  

                //计时器值,从新下一次  

                TaskComps[i].Run = 1; // 任务可以运行  

            }  

        }  

    }  

}  

下面的函数就是判断什么时候该执行那一个任务了,实现任务的管理操作,应用者只需要在main()函数中调用此函数就可以了,并不需要去分别调用和处理任务函数。

void TaskProcess(void)  
{  
    u8 i;  

    for (i=0; i<TASKS_MAX; i++) // 逐个任务时间处理  
    {  
        if (TaskComps[i].Run) // 时间不为 0  
        {  
            TaskComps[i].TaskHook(); // 运行任务  
            TaskComps[i].Run = 0; // 标志清 0  
        }  
    }  
}  

void TaskDisplayClock(void)  

void TaskKeySan(void)  

void TaskDispStatus(void)  

我现在解释一下为什么能做到,定时器是一直在工作的,主程序也是一直在执行,那么当我们的时间到了,我们的代码:

  1. TaskComps[i].Run = 1; // 任务可以运行  

  2. 就是表示任务可以执行了,那么在主程序中,我们知道任务可以执行的话,我们就能直接执行了:

TaskComps[i].TaskHook(); // 运行任务  

因为这个结构体定义的

  1. void (*TaskHook)(void);    // 要运行的任务函数  

是一个指向任务的函数,那么执行这句话就是跳到要执行的任务中去了。前提是结构体已经初始化了。

到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面?

不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。。

先暂时介绍到这里。网上也很多相关教程,而且作者水平有限,如有错误请指正。

后台回复“时间片轮询法”即可获得例程代码,已经测试过的,基于stm32的测试,很简单的几句代码。

【3】从单片机到操作系统③——走进FreeRTOS

从前面的文章,我们知道,(单核)单片机某一时刻只能干一件事,会造成单片机资源的浪费,而且还有可能响应不够及时,所以,在比较庞大的程序或者是要求实时性比较高的情况下,我们可以移植操作系统。因为这种情况下操作系统比裸机方便很多,效率也高。下面,杰杰将带你们走进FreeRTOS的世界随便看看。

首先说明一下:

①   鉴于作者水平有限,学习并不深入,只是学习了皮毛而已,出错在所难免,欢迎指正。

②   以下文章说的操作系统均为FreeRTOS

③   文章参考了野火的书籍:【 野火®】  《从 0 到 1 教你写 uCOS-III 》   ——刘火良

④   参考了网络开源作者的笔记

下面正式开始本文内容。

在没有用到操作系统之前,单片机的运行是顺序执行,就是说,很多时候,单片机在执行这件事的时候,无法切换到另一件事。这就造成了资源的浪费,以及错过了突发的信号。那么,用上了操作系统的时候,很容易避免了这样的问题。

很简单,从感觉上,单片机像是同时在干多件事,为什么说像呢,因为单片机的执行速度很快,快到我们根本没办法感觉出来,但是同时做两件事是不可能的,在(单核)单片机中,因为它的硬件结构决定了CPU只能在一个时间段做一件事如:

22fb6b385628a1bb63aeb91b2a45e8ad.jpg

如这张图,都是按照顺序来执行这些事的,假设每个任务(事件)的time无限小,小到我们根本没法分辨出来,那么我们也会感觉单片机在同时做这六件事。

真相就是:所有任务都好像在执行,但实际上在任何一个时刻都只有一个任务在执行

如是加上了中断系统(Interrupt service routine 中断服务程序)的话,就可以将上图理解为下图:

1100576-20170518203520978-129299542.png

通常把程序分为两部分:前台系统和后台系统。 简单的小系统通常是前后台系统,这样的程序包括一个死循环和若干个中断服务程序:应用程序是一个无限循环,循环中调用API函数完成所需的操作,这个大循环就叫做后台系统。中断服务程序用于处理系统的异步事件,也就是前台系统。前台是中断级,后台是任务级。简单来说就是程序一直按顺序执行,有中断来了就做中断(前台)的事情。处理完中断(前台)的事情,就回到大循环(后台)继续按顺序执行。

那么问题来了,这样子的系统肯定不是好的系统,我在做第一个任务的时候想做第四个任务,根本做不到啊,其实也能做到,让程序执行的指针cp指向第四个任务就行了。但是任务一旦复杂,那么整个工程的代码的结构,可移植性,及可读性,肯定会差啦。

 FreeRTOS

         那么操作系统的移植就是不可或缺的了。什么叫RTOS?:Real Time OS,实时操作系统,强调的是实时性,就是要规定什么时间该做什么任务。那么假如同一个时刻,需要执行两个或者多个任务怎么办。那么我们可以人为地把任务划分优先级,哪个任务重要,就先做,因为前面一直强调,单片机无法同时做两件事,在某一个时刻只能做一件事。

那么FreeRTOS是怎么操作的呢?先看看FreeRTOS的内核吧:

FreeRTOS是一个可裁剪、可剥夺型的多任务内核,而且没有任务数限制。FreeRTOS提供了实时操作系统所需的所有功能,包括资源管理、同步、任务通信等。 FreeRTOS是用C和汇编来写的,其中绝大部分都是用C语言编写的,只有极少数的与处理器密切相关的部分代码才是用汇编写的,FreeRTOS结构简洁,可读性很强!RTOS的内核负责管理所有的任务,内核决定了运行哪个任务,何时停止当前任务切换到其他任务,这个是内核的多任务管理能力。

可剥夺内核顾名思义就是可以剥夺其他任务的CPU使用权,它总是运行就绪任务中的优先级最高的那个任务。

1100576-20170518205756744-313403642.png

(图片来源网络)

在FreeRTOS中,每个任务都是无限循环的,一般来说任务是不会结束运行的,也不允许有返回值,任务的结构一般都是

  1. While(1)
  2. {
  3. /****一直在循环执行*****/
  4. }

如果不需要这个任务了,那就把它删除。

移植的教程我就不写了,超级简单的,按照已有的大把教程来做就行了。(如果没有资源,可以在后台找我,我给一份移植的教程/源码)

其实FreeRTOS的运用及其简单,移植成功按照自己的意愿来配置即可,而且FreeRTOS有很多手册,虽然作者英语很差,但是我有谷歌翻译!!!哈哈哈

既然一直都说任务任务,那肯定要有任务啊,创建任务:

  1. // task. h  task.c
  2. BaseType_t xTaskCreate(      TaskFunction_t pvTaskCode,
  3.                               const char * const pcName,
  4.                               uint16_t usStackDepth,
  5.                               void *pvParameters,
  6.                               UBaseType_t uxPriority,
  7.                               TaskHandle_t *pvCreatedTask
  8.                           );

函数的原型都有,按照字面的理解

  1. TaskFunction_t     pvTaskCode               //传递进来的是任务函数
  2. const  char *         const   pcName         //传递进来的是任务Name
  3. uint16_t                usStackDepth            //传入的是堆栈的大小

在这里要说明一下,在裸机中开发,我们不管局部变量还是全局变量,反正定义了就能用,中断发生时,函数返回地址发哪里,我们也不管。但是在操作系统中,我们必须弄清楚我们的参数是怎么储存的,他们的大小是多大,就需要我们去定义这个堆栈的大小。它就是用来存放我们的这些东西的。太小,导致堆栈溢出,发生异常。(栈是单片机 RAM 里面一段连续的内存空间

因为在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间。

  1. void                    *pvParameters              //传递给任务函数的参数
  2. UBaseType_t      uxPriority                     //任务优先级
  3. TaskHandle_t     *pvCreatedTask           //任务句柄

任务句柄也是很重要的东西,我们怎么删除任务也是要用到任务句柄,其实说白了,我操作系统怎么知道你是什么任务,靠的就是任务句柄的判断,才知道哪个任务在执行,哪个任务被挂起。下一个要执行的任务是哪个等等,靠的都是任务句柄。

那么要使用这些东西,我们肯定要实现啦,下面就是实现的定义,要定义优先级,堆栈大小,任务句柄,任务函数等。

  1. //任务优先级
  2. #define LED_TASK_PRIO           2
  3. //任务堆栈大小     
  4. #define LED_STK_SIZE             50
  5. //任务句柄
  6. TaskHandle_t                               LED_Task_Handler;
  7. //任务函数
  8. void LED_Task(void *pvParameters);

创建任务后,可以开启任务调度了,然后系统就开始运行。

  1. xTaskCreate(
  2.            (TaskFunction_t )                     LED_Task,    //任务函数
  3.             (const char*    )                       "led_task",   //任务名称
  4.             (uint16_t       )                          LED_STK_SIZE, //任务堆栈大小
  5.             (void*          )                            NULL, //传递给任务函数的参数
  6.             (UBaseType_t    )                    START_TASK_PRIO, //任务优先级
  7.             (TaskHandle_t*  )                    &LED_Task_Handler//任务句柄 
  8.                      );
  9.  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是在做什么的

当然可以实现多个任务。还是很简单的。

  1. //LED任务函数
  2. void LED_Task(void *pvParameters)
  3. {
  4.     while(1)
  5.     {
  6.         LED0  =  !LED0;
  7.         vTaskDelay(1000);
  8.     }
  9. }

这就是一个简单的操作系统的概述。

下一篇,应该是讲述开启任务调度与任务切换的具体过程。

这个可以参考野火的书籍《从 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; / * <List item包含最大可能的项目值,这意味着它始终在列表的末尾,因此用作标记。*/

7listSECOND_LIST_INTEGRITY_CHECK_VALUE / * <如果configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则设置为已知值。* /

8} List_t;

列表的结构体中值得注意的是:

     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。

     列表项的pxNextpxPrevious这两个指针都指向自己本身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 ] = '\0';

2pxNewTCB->uxPriority = uxPriority;

3pxNewTCB->uxBasePriority = uxPriority;

4pxNewTCB->uxMutexesHeld = 0;

5

6vListInitialiseItem( &( pxNewTCB->xStateListItem ) );

7vListInitialiseItem( &( pxNewTCB->xEventListItem ) );

  又或者是在定时器相关的初始化中:

1pxNewTimer->pcTimerName = pcTimerName;

2pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;

3pxNewTimer->uxAutoReload = uxAutoReload;

4pxNewTimer->pvTimerID = pvTimerID;

5pxNewTimer->pxCallbackFunction = pxCallbackFunction;

6

7vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );

列表项的末尾插入

  函数:

1void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )

2{

3    ListItem_t * const pxIndex = pxList->pxIndex;

4    listTEST_LIST_INTEGRITY( pxList );

5    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

6    listGET_OWNER_OF_NEXT_ENTRY(). */

7    pxNewListItem->pxNext = pxIndex;    //  1

8    pxNewListItem->pxPrevious = pxIndex->pxPrevious;    //  2

9    /* Only used during decision coverage testing. */

10    mtCOVERAGE_TEST_DELAY();

11    pxIndex->pxPrevious->pxNext = pxNewListItem;        //  3

12    pxIndex->pxPrevious = pxNewListItem;                //  4

13    /* Remember which list the item is in. */
14    pxNewListItem->pvContainer = ( void * ) pxList;

15    ( pxList->uxNumberOfItems )++;

16}

传入的参数:

         pxList列表项要插入的列表。

         pxNewListItem要插入的列表项是什么。

         从末尾插入,那就要先知道哪里是头咯,我们在列表中的成员pxIndex就是用来遍历列表项的啊,那它指向的地方就是列表项的头,那么既然FreeRTOS中的列表很像数据结构中的双向链表,那么,我们可以把它看成一个环,是首尾相连的,那么函数中说的末尾就是列表项头的前一个,很显然其结构图应该是下图这样子的(初始化结束后pxIndex指向了xListEnd):

为什么是这样子的呢,一句句代码来解释:

一开始:

1ListItem_t * const pxIndex = pxList->pxIndex;

保存了一开始的索引列表项(xListEnd)的指向。

1pxNewListItem->pxNext = pxIndex;         //  1

新列表项的下一个指向为索引列表项,也就是绿色的箭头。

1pxNewListItem->pxPrevious = pxIndex->pxPrevious;      //  2

刚开始我们初始化完成的时候pxIndex->pxPrevious的指向为自己xListEnd,那么xNewListItem->pxPrevious的指向为xListEnd。如2紫色的箭头。

1pxIndex->pxPrevious->pxNext = pxNewListItem;             //  3

索引列表项(xListEnd)的上一个列表项还是自己,那么自己的下一个列表项指向就是指向了pxNewListItem。

1pxIndex->pxPrevious = pxNewListItem;                              //  4

这句就很容易理解啦。如图的4橙色的箭头。

插入完毕的时候标记一下新的列表项插入了哪个列表,并且将uxNumberOfItems进行加一,以表示多了一个列表项。

为什么源码要这样子写呢?因为这只是两个列表项,一个列表含有多个列表项,那么这段代码的通用性就很强了。无论原本列表中有多少个列表项,也无论pxIndex指向哪个列表项!

看看是不是按照源码中那样插入呢?

列表项的插入

 源码:

 
  1. 1void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )

  2. 2{

  3. 3ListItem_t *pxIterator;

  4. 4const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

  5. 5    listTEST_LIST_INTEGRITY( pxList );

  6. 6    listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

  7. 7    if( xValueOfInsertion == portMAX_DELAY )

  8. 8    {

  9. 9        pxIterator = pxList->xListEnd.pxPrevious;

  10. 10    }

  11. 11    else

  12. 12    {

  13. 13        for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) /*lint !e826 !e740 The mini list structure is used as the list end to save RAM.  This is checked and valid. */

  14. 14        {

  15. 15            /* There is nothing to do here, just iterating to the wanted

  16. 16            insertion position. */

  17. 17        }

  18. 18    }

  19. 19    pxNewListItem->pxNext = pxIterator->pxNext;

  20. 20    pxNewListItem->pxNext->pxPrevious = pxNewListItem;

  21. 21    pxNewListItem->pxPrevious = pxIterator;

  22. 22    pxIterator->pxNext = pxNewListItem;

  23. 23    /* Remember which list the item is in.  This allows fast removal of the

  24. 24    item later. */

  25. 25    pxNewListItem->pvContainer = ( void * ) pxList;

  26. 26    ( pxList->uxNumberOfItems )++;

  27. 27}

传入的参数:

     pxList列表项要插入的列表。

     pxNewListItem要插入的列表项是什么。

        

pxList决定了插入哪个列表,pxNewListItem中的xItemValue值决定了列表项插入列表的位置

 
  1. 1ListItem_t *pxIterator;  

  2. 2const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

  定义一个辅助的列表项pxIterator,用来迭代找出插入新列表项的位置,并且保存获取要插入的列表项pxNewListItem的xItemValue

  如果打开了列表项完整性检查,就要用户实现configASSERT(),源码中有说明。

  既然是要插入列表项,那么肯定是要知道列表项的位置了,如果新插入列表项的xItemValue是最大的话(portMAX_DELAY),就直接插入列表项的末尾。否则就需要比较列表中各个列表项的xItemValue的大小来进行排列。然后得出新列表项插入的位置。

1for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext )

  上面源码就是实现比较的过程。

与上面的从列表项末尾插入的源码一样,FreeRTOS的代码通用性很强,逻辑思维也很强。

         如果列表中列表项的数量为0,那么插入的列表项就是在初始化列表项的后面。如下图所示:

过程分析:

新列表项的pxNext指向pxIterator->pxNext,也就是指向了xListEnd(pxIterator)。

1pxNewListItem->pxNext = pxIterator->pxNext;

    而xListEnd(pxIterator)的pxPrevious指向则为pxNewListItem。

1 pxNewListItem->pxNext->pxPrevious = pxNewListItem;

     新列表项的(pxPrevious)指针指向xListEnd(pxIterator)

  pxIterator 的 pxNext 指向了新列表项

 
  1. 1pxNewListItem->pxPrevious = pxIterator;

  2. 2pxIterator->pxNext = pxNewListItem;

与从末尾插入列表项其实是一样的,前提是当前列表中列表项的数目为0。

     假如列表项中已经有了元素呢,过程又是不一样的了。原来的列表是下图这样子的:

  假设插入的列表项的xItemValue是2,而原有的列表项的xItemValue值是3,那么,按照源码,我们插入的列表项是在中间了。而pxIterator则是①号列表项。

插入后的效果:

分析一下插入的过程:

      新的列表项的pxNext指向的是pxIterator->pxNext,也就是③号列表项。因为一开始pxIterator->pxNext=指向的就是③号列表项!!

1pxNewListItem->pxNext = pxIterator->pxNext;

     而pxNewListItem->pxNext 即③号列表项的指向上一个列表项指针(pxPrevious)的则指向新插入的列表项,也就是②号列表项了。

1pxNewListItem->pxNext->pxPrevious = pxNewListItem;

     新插入列表项的指向上一个列表项的指针pxNewListItem->pxPrevious指向了辅助列表项pxIterator。很显然要连接起来嘛!

1pxNewListItem->pxPrevious = pxIterator;     

     同理,pxIterator列表项的指向下一个列表项的指针则指向新插入的列表项了pxNewListItem

1pxIterator->pxNext = pxNewListItem;

而其他没改变指向的地方不需改动。(图中的两条直线做的连接线是不需要改动的)

当插入完成的时候,记录一下新插入的列表项属于哪个列表。并且让该列表下的列表项数目加一。

 
  1. 1pxNewListItem->pvContainer = ( void * ) pxList;

  2. 2         ( pxList->uxNumberOfItems )++;

删除列表项

    源码:

 
  1. 1UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

  2. 2{

  3. 3/* The list item knows which list it is in.  Obtain the list from the list

  4. 4item. */

  5. 5List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer;

  6. 6    pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;

  7. 7    pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

  8. 8    /* Only used during decision coverage testing. */

  9. 9    mtCOVERAGE_TEST_DELAY();

  10. 10    /* Make sure the index is left pointing to a valid item. */

  11. 11    if( pxList->pxIndex == pxItemToRemove )

  12. 12    {

  13. 13        pxList->pxIndex = pxItemToRemove->pxPrevious;

  14. 14    }

  15. 15    else

  16. 16    {

  17. 17        mtCOVERAGE_TEST_MARKER();

  18. 18    }

  19. 19    pxItemToRemove->pvContainer = NULL;

  20. 20    ( pxList->uxNumberOfItems )--;

  21. 21    return pxList->uxNumberOfItems;

  22. 22}

  其实删除是很简单的,不用想都知道,要删除列表项,那肯定要知道该列表项是属于哪个列表吧,pvContainer就是记录列表项是属于哪个列表的。

  删除就是把列表中的列表项从列表中去掉,其本质其实就是把他们的连接关系删除掉,然后让删除的列表项的前后两个列表连接起来就行了,假如是只有一个列表项,那么删除之后,列表就回到了初始化的状态了。

 
  1. 1 pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;

  2. 2 pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

这两句代码就实现了将删除列表项的前后两个列表项连接起来。

按照上面的讲解可以理解这两句简单的代码啦。

  假如删除的列表项是当前索引的列表项,那么在删除之后,列表中的pxIndex就要指向删除列表项的上一个列表项了。

 
  1. 1if( pxList->pxIndex == pxItemToRemove )

  2. 2  {

  3. 3      pxList->pxIndex = pxItemToRemove->pxPrevious;

  4. 4  }

  当然还要把当前删除的列表项的pvContainer指向NULL,让它不属于任何一个列表,因为,删除的本质是删除的仅仅是列表项的连接关系其内存是没有释放掉的,假如是动态内存分配的话。

  并且要把当前列表中列表项的数目返回一下。

至此,列表的源码基本讲解完毕。

最后

大家还可以了解一下遍历列表的宏,它在list.h文件中:

 
  1. 1define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )                                        \

  2. 2{                                                                                            \

  3. 3List_t * const pxConstList = ( pxList );                                                    \

  4. 4    /* Increment the index to the next item and return the item, ensuring */                \

  5. 5    /* we don't return the marker used at the end of the list.  */                          \

  6. 6    ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                            \

  7. 7    if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )  \

  8. 8    {                                                                                       \

  9. 9        ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;                        \

  10. 10    }                                                                                       \

  11. 11    ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;                                          \

  12. 12}

  这是一个宏,用于列表的遍历,返回的是列表中列表项的pxOwner成员,每次调用这个宏(函数)的时候,其pxIndex索引会指向当前返回列表项的下一个列表项。

【6】从单片机到操作系统⑥——FreeRTOS任务切换机制详解

【连载】从单片机到操作系统⑥——FreeRTOS任务切换机制详解

大家晚上好,我是杰杰,最近挺忙的,好久没有更新了,今天周末就吐血更新一下吧!

前言

     FreeRTOS是一个是实时内核,任务是程序执行的最小单位,也是调度器处理的基本单位,移植了FreeRTOS,则避免不了对任务的管理,在多个任务运行的时候,任务切换显得尤为重要。而任务切换的效率会决定了系统的稳定性与效率。

    FreeRTOS的任务切换是干嘛的呢,rtos的实际是永远运行的是具有最高优先级的运行态任务,而那些之前在就绪态的任务怎么变成运行态使其得以运行呢,这就是我们FreeRTOS任务切换要做的事情,它要做的是找到最高优先级的就绪态任务,并且让它获得cpu的使用权,这样,它就能从就绪态变成运行态,这样子,整个系统的实时性就会很好,响应也会很好,而不会让程序阻塞卡死。

    要知道怎么实现任务切换,那就要知道任务切换的机制,在不同的cpu(mcu)中,触发的方式可能会不一样,现在是以Cortex-M3为例来讲讲任务的切换。为了大家能看懂本文,我就抛转引玉一下,引用《Cortex-M3权威指南-中文版》的部分语句(如涉及侵权,请联系杰杰删除)

SVC 和 PendSV

    SVC(系统服务调用,亦简称系统调用)和 PendSV(Pended System Call,可悬起系统调用),它们多用于在操作系统之上的软件开发中。SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就会产生一个 SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。

   另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面,SVC异常是必须立即得到响应的(若因优先级不比当前正处理的高,或是其它原因使之无法立即

响应,将上访成硬 fault——译者注),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像 SVC 那样会上访)。OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。

    如果一个发生的异常不能被即刻响应,就称它被“悬起”(pending)。不过,少数 fault异常是不允许被悬起的。一个异常被悬起的原因,可能是系统当前正在执行一个更高优先级异常的服务例程,或者因相关掩蔽位的设置导致该异常被除能。对于每个异常源,在被悬起的情况下,都会有一个对应的“悬起状态寄存器”保存其异常请求,直到该异常能够执行为止,这与传统的 ARM 是完全不同的。在以前,是由产生中断的设备保持住请求信号。现在NVIC 的悬起状态寄存器的出现解决了这个问题,即使后来设备已经释放了请求信号,曾经的中断请求也不会错失。

系统任务切换的工程分析

   在系统中正常执行的任务(假设没有外部中断IRQ),用Systick直接做上下文切换是完全没有问题的,如图:

 但是问题是几乎很少嵌入式的设备会不用其丰富的中断响应,所以,直接用systick做系统的上下文切换那是不实际的,这存在很大的风险,因为假设systick打断了一个中断(IRQ),立即做出上下文切换的话,则触犯用法 fault 异常,除了重启你没有其他办法了,这样子做出来的产品就是垃圾!!用我老板的话说就是写的什么狗屎!!!如图所示:

    那这么说这样不行那也不行,怎么办啊?请看看前面接介绍的PendSV,是不是有点豁然开朗了?PendSV 来完美解决这个问题。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

   懂了吗?就是说,只要将PendSV的优先级设为最低的,systick即使是打断了IRQ,它也不会马上进行上下文切换,而是等到IRQ执行完,PendSV 服务例程才开始执行,并且在里面执行上下文切换。过程如图所示:

任务切换的源码实现

过程差不多了解了,那看看FreeRTOS中怎么实现吧!!

FreeRTOS有两种方法触发任务切换:

  1.   一种就是systick触发PendSV异常,这是最经常使用的。

  2.  另一种是主动进行切换任务,执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换。

1

先说说第一种吧,就在systick中断中调用xPortSysTickHandler();

下面是源码:

 
  1. void xPortSysTickHandler( void )

  2. {

  3.     vPortRaiseBASEPRI();

  4.     {

  5.         /* Increment the RTOS tick. */

  6.         if( xTaskIncrementTick() != pdFALSE )

  7.         {

  8.             /* A context switch is required.  Context switching is performed in

  9.             the PendSV interrupt.  Pend the PendSV interrupt. */

  10.             portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

  11.         }

  12.     }

  13.     vPortClearBASEPRIFromISR();

  14. }

     它的执行过程是这样子的,屏蔽所有中断,因为SysTick以最低的中断优先级运行,所以当这个中断执行时所有中断必须被屏蔽。vPortRaiseBASEPRI();就是屏蔽所有中断的。而且并不需要保存本次中断的值,因为systick的中断优先级是已知的,执行完直接恢复所有中断即可。

    在xTaskIncrementTick()中会对tick的计数值进行自加,然后检查有没有处于就绪态的最优先级任务,如果有,则返回非零值,然后表示需要进行任务切换,而并非马上进行任务切换,此处要注意,它只是向中断状态寄存器bit28位写入1,只是将PendSV挂起,假如没有比PendSV更高优先级的中断,它才会进入PendSV中断服务函数进行任务切换。

 #define portNVIC_PENDSVSET_BIT        ( 1UL << 28UL )

然后解除屏蔽所有中断。

vPortClearBASEPRIFromISR();

2

 另一种方法是主动进行任务切换,不管是使用taskYIELD()还是portYIELD_FROM_ISR(),最终都会执行下面的代码:

 
  1. #define portYIELD()                                                                \

  2. {                                                                                \

  3.     /* Set a PendSV to request a context switch. */                             \

  4.     portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;                             \                                                                       

  5.     __dsb( portSY_FULL_READ_WRITE );                                            \

  6.     __isb( portSY_FULL_READ_WRITE );                                            \

  7. }

  这portYIELD()其实是一个宏定义来的。同样是向中断状态寄存器bit28位写入1,将PendSV挂起,然后等待任务的切换。

具体的任务切换源码

     一直在说怎么进行任务切换的,好像还没看到任务切换的源码啊,哎,下面来看看任务切换的真面目!!

 
  1. __asm void xPortPendSVHandler(void)

  2. {

  3.     extern uxCriticalNesting;

  4.     extern pxCurrentTCB;

  5.     extern vTaskSwitchContext;

  6.     PRESERVE8

  7.     mrs r0, psp

  8.     isb

  9.     ldr r3, =pxCurrentTCB       /* Get the location of the current TCB. */

  10.     ldr r2, [r3]

  11.     stmdb r0!, {r4-r11}         /* Save the remaining registers. */

  12.     str r0, [r2]                /* Save the new top of stack into the first member of the TCB. */

  13.     stmdb sp!, {r3, r14}

  14.     mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY

  15.     msr basepri, r0

  16.     dsb

  17.     isb

  18.     bl vTaskSwitchContext

  19.     mov r0, #0

  20.     msr basepri, r0

  21.     ldmia sp!, {r3, r14}

  22.     ldr r1, [r3]

  23.     ldr r0, [r1]                /* The first item in pxCurrentTCB is the task top of stack. */

  24.     ldmia r0!, {r4-r11}         /* Pop the registers and the critical nesting count. */

  25.     msr psp, r0

  26.     isb

  27.     bx r14

  28.     nop

  29. }

  不是我不想看,是我看到汇编就头大啊,这几天我也在看源码,实在是头大。

  找到核心的函数看看就好啦,不管那么多,有兴趣的可以研究一下中断代码,有不懂的也很欢迎你们来问我,一起研究研究,也是不错的选择。

下面是看重点的地方了:

 
  1. mov r0,             #configMAX_SYSCALL_INTERRUPT_PRIORITY

  2. msr basepri, r0

这两句代码是关闭中断的。关中断就得干活了,嘿嘿嘿~

bl vTaskSwitchContext

BL是跳转指令嘛,这个我还是有点懂的。

  调用函数vTaskSwitchContext(),寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换,然后就是打开中断,退出去了。

寻找下一个要运行任务

  是不是感觉没什么大不了的样子,如果你是这样子觉得的,可能还没学到家,赶紧去看看FreeRTOS的源码,在config.h配置文件中是不是有一个叫做硬件查找下一个运行的任务呢?configUSE_PORT_OPTIMISED_TASK_SELECTION,这个在FreeRTOS中叫做特殊方法,其实也是硬件查找啦,但是并不是每种单片机都支持的,如果是不支持的话,只能选择软件查找的方法了,就是所谓的通用方法。通用方法我就不多说了,因为我用的是STM32,他是支持硬件方法的,这样子效率更高,所以我也没必要去研究他的软件方法,假如有兴趣的小伙伴可以研读一下源码,有不懂的可以向我提问,源码如下:

 
  1. #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                            \

  2.     {                                                                                                   \

  3.     UBaseType_t uxTopPriority = uxTopReadyPriority;                                                     \

  4.                                                                                                         \

  5.         /* Find the highest priority queue that contains ready tasks. */                                \

  6.         while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )                           \

  7.         {                                                                                               \

  8.             configASSERT( uxTopPriority );                                                              \

  9.             --uxTopPriority;                                                                            \

  10.         }                                                                                               \

  11.                                                                                                         \

  12.         /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of                        \

  13.         the same priority get an equal share of the processor time. */                                  \

  14.         listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );           \

  15.         uxTopReadyPriority = uxTopPriority;                                                             \

  16.     } /* taskSELECT_HIGHEST_PRIORITY_TASK */

而硬件的方法源码则在下面:

 
  1.     #define taskSELECT_HIGHEST_PRIORITY_TASK()                                                      \

  2.     {                                                                                               \

  3.         UBaseType_t uxTopPriority;                                                                      \

  4.                                                                                                     \

  5.         /* Find the highest priority list that contains ready tasks. */                             \

  6.         portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                              \

  7.         configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );     \

  8.         listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );       \

  9.     } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

其方法是利用硬件提供的计算前导零指令CLZ,具体宏定义为:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

  静态变量uxTopReadyPriority包含了处于就绪态任务的最高优先级的信息,因为FreeRTOS运行的永远是处于最高优先级的运行态,而下个处于最高优先级的就绪态则必定会在下次任务切换的时候运行,uxTopReadyPriority使用每一位来表示任务是否处于就绪态,比如变量uxTopReadyPriority的bit0为1,则表示存在优先级为0的任务处于就绪态,bit6为1则表示存在优先级为6的任务处于就绪态。并且,由于bit0的优先级高于bit6,那么下个任务就是bit0的任务运行了(数组越低优先级越高)。由于32位整形数最多只有32位,因此使用这种特殊方法限定最大可用优先级数目为32,即优先级0~31。得到了下个处于最高优先级就绪态任务了,就调用listGET_OWNER_OF_NEXT_ENTRY来获取下一个任务的列表项,然后将该列表项的任务控制块TCB赋值给pxCurrentTCB,那么我们就得到下一个要运行的任务了。

至此,任务切换已经完成。

END

猜你喜欢

转载自blog.csdn.net/qq_40662854/article/details/82752654