作为基于ARM7、Cortex-M3硬件开发的嵌入式工程师,我一直反对使用RTOS。不仅因为不恰当的使用RTOS会给项目带来额外的稳定性风险,更重要的是我认为绝大多数基于ARM7、Cortex-M3硬件的项目,还没复杂到使用RTOS的地步,使用状态机就足够了。
对于现代的微处理器,特别是资源相对丰富ARM7、Cortex-M3硬件来说,RTOS占用的硬件资源已经越来越可以忽略。所以在当今环境下,我们无需担心RTOS会拖累性能。相反,RTOS提供的事件驱动型设计方式,使得RTOS只是在处理实际任务时才会运行,这能够更合理的利用CPU。在实际项目中,如果程序等待一个超时事件,传统的无RTOS情况下,要么在原地一直等待而不能执行其它任务,要么使用复杂(相对RTOS提供的任务机制而言)的状态机机制。如果使用RTOS,则可以很方便的将当前任务阻塞在该事件下,然后自动去执行别的任务,这显然更方便,并且可以高效的利用CPU。处理这类事件,是我使用RTOS的最大动力,但考虑到系统的稳定性,我不得不再三权衡RTOS可能带来的一些弊端:
以上原因是我拒绝在实际项目中使用RTOS的理由,但是否使用RTOS跟是否学习RTOS完全是两码事。我认为任何嵌入式软件设计人员都应该至少学习一种RTOS,不仅是需要掌握RTOS背后的操作系统原理、学习RTOS的编程方式,更是为将来做准备。
即便我认为现在的物联网有点言过其实,但我依然看好物联网的发展前景。随着物联网的发展,未来的嵌入式产品必然更为复杂、连接性更强以及需要更丰富的用户界面。当处理这些任务时,一个好的RTOS就变得不可缺少了。
书到用时方恨少,我希望自己永远不会有这种感觉。所以从现在起,我要开始深入一个RTOS,探索它背后的原理,掌握其编程方法,避免其缺陷和陷阱,并将它安全的用在将来的项目中。
对比了许多RTOS,最终选择FreeRTOS,原因是多方面的:
学习的资料来源主要是FreeRTOS的官方网站(www.freertos.org)和源代码。FreeRTOS的创始人RichardBarry编写了大量的移植代码和配套文档,我只不过是沿着Richard Barry铺好的路前进,所以,这没什么困难的。
最后,感谢RichardBarry的付出,感谢Richard Barry的无私开源精神!
附录1:2010~2017年EEtimes杂志嵌入式市场调查报告有关RTOS使用榜截图
2010和2011年RTOS使用榜
2012和2013年RTOS使用榜
2013年和2014年RTOS使用榜
2014年和2015年RTOS使用榜
2017年RTOS使用榜
FreeRTOS可以被移植到很多不同架构的处理器和编译器。每一个RTOS移植都附带一个已经配置好的演示例程,可以方便快速启动开发。更好的是,每个演示例程都附带一个说明网页,提供如何定位RTOS演示工程源代码、如何编译演示例程、如何配置硬件平台的全部信息。
演示例程说明网页还提供基本的RTOS移植细节信息,包括如何编写FreeRTOS兼容的中断服务例程,不同架构的中断处理会稍有不同。
通过下面的简单说明,可以在几分钟内运行RTOS。
FreeRTOS具有详细的开发说明文档,可以在其官方网站上查看。首先打开官方网站,目前的网站地址是:http://www.freertos.org。在首页左侧的导航栏中,展开"Supported Devices & Demos"菜单项,单击"OfficiallySupported Demos"链接,去查看FreeRTOS支持的微控制器制造商列表。单击微控制器制造商名称,进入具体的制造商文档页面列表。
到FreeRTOS官方网站下载源码,下载包包含RTOS内核源码和官方移植演示工程。解压缩后放到合适的目录下。(如果你不想访问慢吞吞的国外网站,我在CSDN做了一个镜像,可以 点击此处 ,这篇文章中有最新的大部分FreeRTOS源码包下载链接)
每一个RTOS移植包都附带有预先配置好的演示例程 ,已经创建好了所有必须的RTOS源文件并包含了必须的RTOS头文件。推荐在提供的演示例程的基础上进行自己的FreeRTOS应用编程。
FreeRTOS下载包中包含每个处理器移植和演示例程的源码。将所有移植包放入一个下载文件中大大简化了分类处理,但是下载包中的文件数量也多的惊人!无论如何,目录结构还是非常简单的,并且FreeRTOS实时内核仅仅只有3个文件(如果需要,还有一些附加文件,比如软件定时器、事件组以及协程)。
下载包目录包含两个子目录:FreeRTOS和FreeRTOS-Plus。如下所示:
FreeRTOS-Plus目录树包含多个自述文件(Readme)。接下来本文只描述FreeRTOS内核的核心源文件和演示例程,它们又被分成两个主要的子目录,如下所示:
RTOS代码的核心包含在三个文件中:tasks.c、queue.c、list.c。这三个文件位于FreeRTOS/Source目录。在该目录下还包含三个可选的文件:timers.c、event_groups.c、croutine.c,分别实现软件定时、事件组和协程功能。
FreeRTOS/Source目录结构如下所示:
每个支持的处理器架构需要一小段与处理器架构相关的RTOS代码。这个是RTOS移植层,它位于FreeRTOS/Source/Portable/[相应编译器]/[相应CPU架构]子目录。
对于FreeRTOS,堆栈设计也属于移植层。FreeRTOS/Source/portable/MemMang目录下heap_x.c文件给出了多种堆栈方案,后续文章将会详细介绍堆栈操作。
移植层目录举例:
FreeRTOS下载包中还包含各种处理器架构和编译器的演示例程。大多数的演示例程代码对所有移植都是通用的,位于FreeRTOS/Demo/Common/Minimal目录。FreeRTOS/Demo/Common/Full目录下的是历史遗留代码,仅用于PC。
FreeRTOS/Demo目录结构如下所示:
FreeRTOS/Demo目录下剩余的子目录包含预先配置好的工程,可以用于构建个人演示例程。子目录的命名与移植平台和编译器相关。每一个RTOS移植包都有自己的说明文档。
演示例程目录举例:
根据上一节FreeRTOS源码目录结构说明的RTOS演示工程的所在的位置,打开并编译演示工程。
演示例程附带的说明网页会介绍如何配置硬件、下载程序和执行演示例程。说明网页还会提供演示例程的功能信息,这样你就可以判断演示例程执行是否正确。
FreeRTOS下载包中已经包含很多演示例程- 每一个例程都是针对于:
可以在官方网站首页左侧的树形菜单 'Supported Devices' 中找到这些例程介绍。
可惜的是不可能为所有微控制器、编译器和评估板提供演示例程。因此,官方提供的演示例程可能不完全符合你正在使用的开发平台。本章描述如何通过修改或合并官方提供的演示例程,来满足自己的开发平台需求(包括微处理器和编译器)。
修改一个现有的评估板例程,使之运行到另一个同类评估板上,通常是比较简单的,稍微复杂些的是跨编译器移植。本文介绍这两情况下的修改,只是对相似的平台有效。然而,将FreeRTOS移植到一个全新的平台、未支持的处理器架构,并不是件简单的事情。本文不讨论如何将FreeRTOS移植到一个全新平台。
本节描述如何通过修改一个官方提供的演示例程,使之运行到另一个评估板,这里两个评估板使用同系列微处理器,使用相同编译器。在这个例子中,将运行于SAM7S-EK硬件开发板上的IAR SAM7S演示例程,修改使之运行到Olimex SAM7-P64开发板。(注:两块开发板都是使用ATMEL公司的ARM7微处理器,前者使用AT91SAM7S256,后者使用AT91SAM7S64)
作为修改练习的起点,被修改的演示例程是要能使用的。因此,在未做任何修改之前,首先检查下载的演示例程能否被正确的编译。绝大多数情况下,演示例程编译后是没有任何错误和警告的。
关于演示例程所在目录,参考《FreeRTOS系列第2篇---FreeRTOS入门指南》一文的第三节。
LED灯是用来指示演示例程运行的最简单方法,因此点亮新硬件平台上的LED灯通常是最容易的。
两个不同评估板上的LED连接到相同的IO端口通常是不太可能的,因此,一些小幅度修改是必须的。
在partest.c文件中的vParTestInitialise() 函数包含IO端口的模式和方向配置。在main.c文件中的prvSetupHardware()函数包含更多的硬件初始化(比如,使能IO外设的时钟模块),可能需要根据不同的使用进行一些修改。
根据目标评估板的硬件,在上面两个函数中做必要的修改,然后写一段简单程序,来检查硬件LED是否完好。这个简单程序不使用FreeRTOS,只是为了确保硬件LED能够正常工作。因此,注释掉之前的main()函数,使用下面的例子代替:
一旦确定硬件LED可以正常工作,就可以恢复原来的main()函数。
作为入门级的多任务应用程序应该尽量的简单,LED闪烁测试程序常常担任这样的角色,可以堪比经典的“Hello Wold”。这个任务几乎在所有演示例程中都能看到,在main()函数中调用vStartLEDFlashTasks() (使用协程版本时调用vStartFlashCoRoutines())来实现。如果你使用的演示例程main()函数中并没有调用vStartLEDFlashTasks()(或vStartFlashCoRoutines()),那么需要你将FreeRTOS/Demo/Common/Minimal/Flash.c文件添加到你的工程,并在main()函数手动的增加vStartLEDFlashTasks()函数。
除了调用vStartLEDFlashTasks()外,注释掉所有用于启动一个或多个演示任务的函数。最后的main()函数仅调用三个函数:prvSetupHardware()、vStartLEDFlashTasks()和vTaskStartScheduler()。例如(基于典型的main()函数):
这是一个非常简单的应用程序,正确执行后,LED0~2(包括2)或分别按照不同的频率闪烁。
一旦简单的LED闪烁例程正确执行后,你可以恢复之前注释掉的所有的演示任务。
以下要点需牢记:
本节主要描述如何修改一个现存的工程或者按照需求合并两个现存的工程。比如,你希望使用GCC编译器创建一个STR9演示工程(demo project),并且你下载的FreeRTOS软件包中并没有GCC版本的STR9演示例程,但是FreeRTOS下载包中有IAR版本的STR9演示例程和GCC版本的STR75x演示例程。则可以通过这两个现存的工程来创 建GCC版本的STR9演示工程。可以有两种方式完成:
使用GCC版本的STR75x演示工程,修改使之符合指定的微处理器(STR9评估板上的微处理器)。
使用GCC创建一个新的工程。从IAR版本的STR9演示工程中获取文件和配置信息,使之符合GCC编译器需求。
对于一个特定平台,大多数(不是全部)硬件接口代码包含在一个叫做FreeRTOS/source/portable/[编译器]/[微控制器/port.c的文件中,和它对应的头文件是FreeRTOS/source/portable/[编译器]/[微控制器]/portmacro.h。
对于一些编译器来说,port.c和portmacro.h就是所需要的全部硬件接口代码。另一些还需要一些汇编文件,这些文件叫做portasm.s或者portasm.asm。
最后,仅对于ARM7 GCC移植,同样存在一个类似的硬件接口文件:portISR.c,portISR.c是从port.c中分离出来的,这些代码必须在ARM模式下编译,port.c中剩余的代码既可以在ARM模式下编译,也可在THUMB模式下编译。
编译器可以为嵌入式系统提供某些特定的C语言扩展。比如某个特定关键字可以标识出一个函数是中断处理服务函数。
扩展的C语言部分,是不属于标准C语言规范的。因此,编译器与编译器之间是有差别的。FreeRTOS的文件中就包含类似的非标准C语言语法,在文件夹FreeRTOS/source/portable中(上文中提到的特定微控制器硬件接口代码也在这个文件中)。此外,一些演示例程会使用到中断服务程序,这些中断服务程序并不属于FreeRTOS的一部分,并且如何定义和使用这些中断服务程序也是编译器所特定的。
C启动文件和链接脚本都属于处理器和编译器特定的。不推荐尝试从无到有的创建这些文件,应该到FreeRTOS演示工程中寻找一个合适的来修改。
要特别小心ARM7启动文件。它必须将IRQ中断服务程序入口地址配置到快速中断处理向量表或者普通中断向量表中。这两种情况,演示工程都提供了例子。
链接脚本必须正确的描述当前使用处理器的内存映射。
每一个工程通常都会定义一些宏,这些预处理宏定义了一些要被编译的特定的硬件接口代码。要包含portmacro.h文件才能识别这些宏。比如,当使用GCC编译MegaAVR硬件接口代码时,宏GCC_MEGA_AVR必须被定义;当使用IAR编译MegaAVR硬件接口代码时,宏IAR_MEGA_AVR必须被定义等等。参考演示例程工程以及FreeRTOS/source/include/portable.h文件可以查找当前工程定义了那些宏。如果预处理宏未定义,那么portmacro.h文件所在目录的路径必须被包含到预处理器的搜索路径中。
其它的编译器设置,比如优化选项,也是很关键的。可以参考提供的演示工程。
具有IDE的编译器通常具有目标微控制器选项并将它作为工程设置的一部分,所以新的工程也必须适应新的目标微控制器,同样的,如果使用到makefile文件,则makefile文件也必须更新以符合新的目标微控制器。
调用函数prvSetupTimerInterrupt()来配置系统节拍中断,这个函数可以在以下路径的文件中找到:FreeRTOS/source/portable/[compiler]/[microcontroller]/port.c
FreeRTOS内存管理一章中描述了FreeRTOS如何使用RAM,并且描述了RAM是如何分配给RTOS内核的。
如果你要将演示例程移植到一个RAM稍小的微处理器上,那么你可能需要减少configTOTAL_HEAP_SIZE的值(位于FreeRTOSConfig.h),并且减少演示例程的任务个数。可以通过简单的注释掉不需要的任务来实现。
如果你要将演示例程移植到一个ROM较小的微处理器中,那么你可能需要减少应用例程的文件数目,他们位于FreeRTOS/Demo/common文件夹目录下。同时你还要删除main函数中对他们的调用函数。
注:可能你是通过搜索引擎找到这篇文章,满怀希望的点进来,以为能解决自己移植的所有问题,但是看完后却发现本文站的角度太高,并不是特别适合对移植一无所知的你。先别急着以为本文是标题党而点踩,可能有一篇文章适合你,这篇文章以Cortex-M3硬件平台为例,详细的介绍移植过程,请点击这里。
FreeRTOS的核心源代码遵从MISRA编码标准指南。这个标准篇幅稍长,你可以在MISRA官方网站花少量钱买到,这里不再复制任何标准。
FreeRTOS源代码不符合MISRA标准的项目如下所示:
FreeRTOS可以在很多不同编译器中编译,其中的一些编译器比同类有更高级特性。因为这个原因,FreeRTOS不使用任何非C语言标准的特性或语法。一个例外情况是头文件stdint.h。在文件夹FreeRTOS/Source/include下包含一个叫做stdint.readme的文件,如果你的编译器不提供stdint类型定义,可以将stdint.readme文件重命名为stdint.h。
RTOS内核和演示例程源代码使用以下规则:
1> 变量
2> 函数
3> 宏
只有stdint.h和RTOS自己定义的数据类型可以使用,但也有例外情况,如下所示:
有三种类型会在移植层定义,它们是:
3.4风格指南
在FreeRTOS官方网站可以下载到最新版的FreeRTOS包,我这里使用的是V8.2.3版本。
下载包内的总文件数量多的令人生畏,但文件结构却很简洁。《FreeRTOS入门指南》一文的第3节详细描述了下载包文件结构,我们这里只是简单提一下。
下载包根目录下包含两个子目录:FreeRTOS和FreeRTOS-Plus。其中,FreeRTOS-Plus文件夹中包含一些FreeRTOS+组件和演示例程(组件大都收费),我们不对这个文件夹下的内容多做了解,重点说一下FreeRTOS文件夹。
FreeRTOS文件夹下包含两个子目录:Demo和Source。其中,Demo包含演示例程的工程文件,Source包含实时操作系统源代码文件。
FreeRTOS实时操作系统内核仅包含三个必要文件,此外还有三个可选文件。RTOS核心代码位于三个源文件中,分别是tasks.c、queue.c和list.c。这三个文件位于FreeRTOS/Source目录下,在同一目录下还有3个可选的文件,叫做timers.c、event_groups.c和croutine.c,分别用于软件定时器、事件组和协程。
对于支持的处理器架构,RTOS需要一些与处理器架构相关的代码。可以称之为RTOS硬件接口层,它们位于FreeRTOS/Source/Portable/[相应编译器]/[相应处理器架构]文件夹下。我们这次要移植到Cortex-M3微控制,使用Keil MDK编译器,所以需要的RTOS硬件接口代码位于:FreeRTOS\Source\portable\RVDS\ARM_CM3文件夹下。
堆栈分配也是属于硬件接口层(移植层),在FreeRTOS/Source/portable/MemMang文件夹下具有各种类型的堆栈分配方案。这里我们使用heap_1.c提供的堆栈分配方案。关于FreeRTOS的内存管理,后续《FreeRTOS内存管理》一文中会详细介绍FreeRTOS内存管理的特性和用法,《FreeRTOS内存管理分析》一文会从源码级别分析FreeRTOS内存管理的具体实现,这里不用多纠结,你也可以快速的浏览一下这两篇文章,里面或许有许多不懂的,但不要着急,先放过它们。
FreeRTOS文件夹下的Demo文件夹中还包括各种演示例程,涉及多种架构的处理器以及多种编译器。FreeRTOS/Demo/Common/Minimal文件夹下的演示例程代码中,绝大部分对所有移植硬件接口都是适用的。FreeRTOS/Demo/Common/Full文件夹下的代码属于历史遗留代码,仅用于PC移植层。
将tasks.c、queue.c和list.c这三个内核代码加入工程,将port.c和heap_1.c这两个与处理器相关代码加入工程。port.c位于FreeRTOS\Source\portable\RVDS\ARM_CM3文件夹下,heap_1.c位于FreeRTOS/Source/portable/MemMang文件夹下。
对于刚接触FreeRTOS的用户来说,最简单方法是找一个类似的Demo工程,复制该工程下的FreeRTOSConfig.h文件,在这个基础上进行修改。详细的配置说明将在后续《FreeRTOS内核配置说明》一文中给出,这里依然不必纠结。
如果你在FreeRTOSConfig.h中设置了configUSE_TICK_HOOK=1,则必须编写voidvApplicationTickHook( void )函数。该函数利用时间片中断,可以很方便的实现一个定时器功能。详见后续文章《FreeRTOS内核配置说明》有关宏configUSE_TICK_HOOK一节。
如果你在FreeRTOSConfig.h中设置了configCHECK_FOR_STACK_OVERFLOW=1或=2,则必须编写voidvApplicationStackOverflowHook( xTaskHandle pxTask, signed char *pcTaskName )函数,该函数用于检测堆栈溢出,详见后续文章《FreeRTOS内核配置说明》有关宏configCHECK_FOR_STACK_OVERFLOW一节。
为了验证你的硬件板子是否可靠的工作,首先编写一个小程序片,比如闪烁一个LED灯或者发送一个字符等等,我们这里使用UART发送一个字符。代码如下所示(假设你已经配置好了启动代码,并正确配置了UART):
如果硬件可以正常发送字符,说明硬件以及启动代码OK,可以进行下一步。
在Cortex-M3硬件下,FreeRTOS使用SysTick作为系统节拍时钟,使用SVC和PendSVC进行上下文切换。异常中断服务代码位于port.c文件中,FreeRTOS的作者已经为各种架构的CPU写好了这些代码,可以直接拿来用,需要用户做的,仅仅是将这些异常中断入口地址挂接到启动代码中。
在startup.s中,使用IMPORT关键字声明要挂接的异常中断服务函数名,然后将:
在步骤3.5中,我们为了测试硬件是是否能够工作,编写了一个发送字符的小函数,这里我们将把这个小函数作为我们第一个任务要执行的主要代码:每隔1秒钟,发送一个字符。代码如下所示:
FreeRTOS的任务以及编写格式将在后续文章《FreeRTOS任务概述》一文中详述,这里只是一个很简单的任务,先有有大体印象。这里面有一个API函数vTaskDelay(),这个函数用于延时,具体用法将在后续文章《FreeRTOS任务控制》一文中详细介绍,延时函数代码级分析将在《FreeRTOS高级篇10---系统节拍时钟分析》。这里不必在意太多的未知情况,因为后面会一点点将这些未知空间探索一遍的。
这里我们使用SysTick定时器作为系统的节拍时钟,设定每隔10ms产生一次节拍中断。由于FreeRTOS对移植做了非常多的工作,以至于我们只需要在FreeRTOSConfig.h中配置好以下两个宏定义即可:
第一个宏定义CPU系统时钟,也就是CPU执行时的频率。第二个宏定义FreeRTOS的时间片频率,这里定义为100,表明RTOS一秒钟可以切换100次任务,也就是每个时间片为10ms。
在prot.c中,函数vPortSetupTimerInterrupt()设置节拍时钟。该函数根据上面的两个宏定义的参数,计算SysTick定时器的重装载数值寄存器,然后设置SysTick定时器的控制及状态寄存器,设置如下:使用内核时钟源、使能中断、使能SysTick定时器。另外,函数vPortSetupTimerInterrupt()由函数vTaskStartScheduler()调用,这个函数用于启动调度器。
这里特别重要,因为涉及到中断优先级和中断嵌套。这里先给出基于Cortex-M3硬件(lpc177x_8x系列微控制器)的一个配置例子,在FreeRTOSConfig.h中:
后续文章《FreeRTOS内核配置说明》会详细介绍这些宏的含义,对于Cortex-M内核,后续文章《Cortex-M内核使用FreeRTOS特别注意事项》一文,会讲述这些宏与硬件的联系,那个时候你一定会清楚这些宏所定义的数字会对你的硬件产生什么影响的。现在,我们只需要知道他们很重要就足够了,没人能一口吃成胖子。
还需要在FreeRTOSConfig.h设置一些必要的宏,这些宏如下所示:
调用FreeRTOS提供的API函数来创建任务,代码如下所示:
关于详细的创建任务API函数,会在后续文章《FreeRTOS任务创建和删除》一文中介绍。
调用FreeRTOS提供的API函数来启动调度器,代码如下所示:
关于详细的开启调度器API函数,会在后续文章《FreeRTOS内核控制》一文中介绍。
此时的main函数代码如下所示:
到这里,一个最基本的FreeRTOS应用程序就已经运行起来,将硬件板子接到PC的RS232串口,可以观察到每隔一秒钟,板子都会向PC发送一个指定的字符。
回头看一下移植过程,FreeRTOS移植到Cortex-M3硬件是多么的简单,这一方面归功于FreeRTOS的设计师已经为移植做了大量工作,同时,新一代的Cortex-M3硬件也为操作系统增加了一些列便利特性,比如SysTick定时器和全新的中断及异常。
但是移植成功也只是万里长征的第一步,因为这只是最简单的应用。我们还不清楚FreeRTOS背后的机理、调度算法的面貌、甚至连信号量也都没有涉及。就本文的移植过程来看,我们也刻意忽略了很多细节,比如FreeRTOSConfig.h文件中的宏都有什么意义?改动后对RTOS有何影响?比如FreeRTOS任务API的细节、调度API的细节,再比如FreeRTOS的内存如何分配?如何进行堆栈溢出检查等等。
所以,先不要沾沾自喜,曲折的道路还远没到来呢。
接下来的很多篇文章会围绕这个最简单的移植例程做详细的讲解,要把本篇文章中刻意隐藏的细节一一拿出来。这要一直持续到我们介绍队列、信号量、互斥量等通讯机制为止。
FreeRTOS内核是高度可定制的,使用配置文件FreeRTOSConfig.h进行定制。每个FreeRTOS应用都必须包含这个头文件,用户根据实际应用来裁剪定制FreeRTOS内核。这个配置文件是针对用户程序的,而非内核,因此配置文件一般放在应用程序目录下,不要放在RTOS内核源码目录下。
在下载的FreeRTOS文件包中,每个演示例程都有一个FreeRTOSConfig.h文件。有些例程的配置文件是比较旧的版本,可能不会包含所有有效选项。如果没有在配置文件中指定某个选项,那么RTOS内核会使用默认值。典型的FreeRTOSConfig.h配置文件定义如下所示,随后会说明里面的每一个参数。
1.configUSE_PREEMPTION
为1时RTOS使用抢占式调度器,为0时RTOS使用协作式调度器(时间片)。
注:在多任务管理机制上,操作系统可以分为抢占式和协作式两种。协作式操作系统是任务主动释放CPU后,切换到下一个任务。任务切换的时机完全取决于正在运行的任务。
2.configUSE_PORT_OPTIMISED_TASK_SELECTION
某些运行FreeRTOS的硬件有两种方法选择下一个要执行的任务:通用方法和特定于硬件的方法(以下简称“特殊方法”)。
通用方法:
特殊方法:
3.configUSE_TICKLESS_IDLE
设置configUSE_TICKLESS_IDLE为1使能低功耗tickless模式,为0保持系统节拍(tick)中断一直运行。
通常情况下,FreeRTOS回调空闲任务钩子函数(需要设计者自己实现),在空闲任务钩子函数中设置微处理器进入低功耗模式来达到省电的目的。因为系统要响应系统节拍中断事件,因此使用这种方法会周期性的退出、再进入低功耗状态。如果系统节拍中断频率过快,则大部分电能和CPU时间会消耗在进入和退出低功耗状态上。
FreeRTOS的tickless空闲模式会在空闲周期时停止周期性系统节拍中断。停止周期性系统节拍中断可以使微控制器长时间处于低功耗模式。移植层需要配置外部唤醒中断,当唤醒事件到来时,将微控制器从低功耗模式唤醒。微控制器唤醒后,会重新使能系统节拍中断。由于微控制器在进入低功耗后,系统节拍计数器是停止的,但我们又需要知道这段时间能折算成多少次系统节拍中断周期,这就需要有一个不受低功耗影响的外部时钟源,即微处理器处于低功耗模式时它也在计时的,这样在重启系统节拍中断时就可以根据这个外部计时器计算出一个调整值并写入RTOS
系统节拍计数器变量中。
4.configUSE_IDLE_HOOK
设置为1使用空闲钩子(Idle Hook类似于回调函数),0忽略空闲钩子。
当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级)。对于已经删除的RTOS任务,空闲任务可以释放分配给它们的堆栈内存。因此,在应用中应该注意,使用vTaskDelete()函数时要确保空闲任务获得一定的处理器时间。除此之外,空闲任务没有其它特殊功能,因此可以任意的剥夺空闲任务的处理器时间。
应用程序也可能和空闲任务共享同个优先级。
空闲任务钩子是一个函数,这个函数由用户来实现,RTOS规定了函数的名字和参数,这个函数在每个空闲任务周期都会被调用。
要创建一个空闲钩子:
这个钩子函数不可以调用会引起空闲任务阻塞的API函数(例如:vTaskDelay()、带有阻塞时间的队列和信号量函数),在钩子函数内部使用协程是被允许的。
使用空闲钩子函数设置CPU进入省电模式是很常见的。
5.configUSE_MALLOC_FAILED_HOOK
每当一个任务、队列、信号量被创建时,内核使用一个名为pvPortMalloc()的函数来从堆中分配内存。官方的下载包中包含5个简单内存分配策略,分别保存在源文件heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c中。
仅当使用这五个简单策略之一时,宏configUSE_MALLOC_FAILED_HOOK才有意义。
如果定义并正确配置malloc()失败钩子函数,则这个函数会在pvPortMalloc()函数返回NULL时被调用。只有FreeRTOS在响应内存分配请求时发现堆内存不足才会返回NULL。
如果宏configUSE_MALLOC_FAILED_HOOK设置为1,那么必须定义一个malloc()失败钩子函数,如果宏configUSE_MALLOC_FAILED_HOOK设置为0,malloc()失败钩子函数不会被调用,即便已经定义了这个函数。malloc()失败钩子函数的函数名和原型必须如下所示:
6.configUSE_TICK_HOOK
设置为1使用时间片钩子(Tick Hook),0忽略时间片钩子。
注:时间片钩子函数(Tick Hook Function)
时间片中断可以周期性的调用一个被称为钩子函数(回调函数)的应用程序。时间片钩子函数可以很方便的实现一个定时器功能。
只有在FreeRTOSConfig.h中的configUSE_TICK_HOOK设置成1时才可以使用时间片钩子。一旦此值设置成1,就要定义钩子函数,函数名和参数如下所示:
vApplicationTickHook()函数在中断服务程序中执行,因此这个函数必须非常短小,不能大量使用堆栈,不能调用以”FromISR" 或 "FROM_ISR”结尾的API函数。
在FreeRTOSVx.x.x\FreeRTOS\Demo\Common\Minimal文件夹下的crhook.c文件中有使用时间片钩子函数的例程。
7.configCPU_CLOCK_HZ
写入实际的CPU内核时钟频率,也就是CPU指令执行频率,通常称为Fcclk。配置此值是为了正确的配置系统节拍中断周期。
8.configTICK_RATE_HZ
RTOS 系统节拍中断的频率。即一秒中断的次数,每次中断RTOS都会进行任务调度。
系统节拍中断用来测量时间,因此,越高的测量频率意味着可测到越高的分辨率时间。但是,高的系统节拍中断频率也意味着RTOS内核占用更多的CPU时间,因此会降低效率。RTOS演示例程都是使用系统节拍中断频率为1000HZ,这是为了测试RTOS内核,比实际使用的要高。(实际使用时不用这么高的系统节拍中断频率)
多个任务可以共享一个优先级,RTOS调度器为相同优先级的任务分享CPU时间,在每一个RTOS 系统节拍中断到来时进行任务切换。高的系统节拍中断频率会降低分配给每一个任务的“时间片”持续时间。
9.configMAX_PRIORITIES
配置应用程序有效的优先级数目。任何数量的任务都可以共享一个优先级,使用协程可以单独的给与它们优先权。见configMAX_CO_ROUTINE_PRIORITIES。
在RTOS内核中,每个有效优先级都会消耗一定量的RAM,因此这个值不要超过你的应用实际需要的优先级数目。
注:任务优先级
每一个任务都会被分配一个优先级,优先级值从0~ (configMAX_PRIORITIES - 1)之间。低优先级数表示低优先级任务。空闲任务的优先级为0(tskIDLE_PRIORITY),因此它是最低优先级任务。
FreeRTOS调度器将确保处于就绪状态(Ready)或运行状态(Running)的高优先级任务比同样处于就绪状态的低优先级任务优先获取处理器时间。换句话说,处于运行状态的任务永远是高优先级任务。
处于就绪状态的相同优先级任务使用时间片调度机制共享处理器时间。
10.configMINIMAL_STACK_SIZE
定义空闲任务使用的堆栈大小。通常此值不应小于对应处理器演示例程文件FreeRTOSConfig.h中定义的数值。
就像xTaskCreate()函数的堆栈大小参数一样,堆栈大小不是以字节为单位而是以字为单位的,比如在32位架构下,栈大小为100表示栈内存占用400字节的空间。
11.configTOTAL_HEAP_SIZE
RTOS内核总计可用的有效的RAM大小。仅在你使用官方下载包中附带的内存分配策略时,才有可能用到此值。每当创建任务、队列、互斥量、软件定时器或信号量时,RTOS内核会为此分配RAM,这里的RAM都属于configTOTAL_HEAP_SIZE指定的内存区。后续的内存配置会详细讲到官方给出的内存分配策略。
12.configMAX_TASK_NAME_LEN
调用任务函数时,需要设置描述任务信息的字符串,这个宏用来定义该字符串的最大长度。这里定义的长度包括字符串结束符’\0’。
13.configUSE_TRACE_FACILITY
设置成1表示启动可视化跟踪调试,会激活一些附加的结构体成员和函数。
14.configUSE_STATS_FORMATTING_FUNCTIONS (V7.5.0新增)
设置宏configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS为1会编译vTaskList()和vTaskGetRunTimeStats()函数。如果将这两个宏任意一个设置为0,上述两个函数不会被编译。
15.configUSE_16_BIT_TICKS
定义系统节拍计数器的变量类型,即定义portTickType是表示16位变量还是32位变量。
定义configUSE_16_BIT_TICKS为1意味着portTickType代表16位无符号整形,定义configUSE_16_BIT_TICKS为0意味着portTickType代表32位无符号整形。
使用16位类型可以大大提高8位和16位架构微处理器的性能,但这也限制了最大时钟计数为65535个’Tick’。因此,如果Tick频率为250HZ(4MS中断一次),对于任务最大延时或阻塞时间,16位计数器是262秒,而32位是17179869秒。
16.configIDLE_SHOULD_YIELD
这个参数控制任务在空闲优先级中的行为。仅在满足下列条件后,才会起作用。
通过时间片共享同一个优先级的多个任务,如果共享的优先级大于空闲优先级,并假设没有更高优先级任务,这些任务应该获得相同的处理器时间。
但如果共享空闲优先级时,情况会稍微有些不同。当configIDLE_SHOULD_YIELD为1时,其它共享空闲优先级的用户任务就绪时,空闲任务立刻让出CPU,用户任务运行,这样确保了能最快响应用户任务。处于这种模式下也会有不良效果(取决于你的程序需要),描述如下:
图中描述了四个处于空闲优先级的任务,任务A、B和C是用户任务,任务I是空闲任务。上下文切换周期性的发生在T0、T1…T6时刻。当用户任务运行时,空闲任务立刻让出CPU,但是,空闲任务已经消耗了当前时间片中的一定时间。这样的结果就是空闲任务I和用户任务A共享一个时间片。用户任务B和用户任务C因此获得了比用户任务A更多的处理器时间。
可以通过下面方法避免:
设置configIDLE_SHOULD_YIELD为0将阻止空闲任务为用户任务让出CPU,直到空闲任务的时间片结束。这确保所有处在空闲优先级的任务分配到相同多的处理器时间,但是,这是以分配给空闲任务更高比例的处理器时间为代价的。
17.configUSE_TASK_NOTIFICATIONS(V8.2.0新增)
设置宏configUSE_TASK_NOTIFICATIONS为1(或不定义宏configUSE_TASK_NOTIFICATIONS)将会开启任务通知功能,有关的API函数也会被编译。设置宏configUSE_TASK_NOTIFICATIONS为0则关闭任务通知功能,相关API函数也不会被编译。默认这个功能是开启的。开启后,每个任务多增加8字节RAM。
这是个很有用的特性,一大亮点。
每个RTOS任务具有一个32位的通知值,RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务可以解除任务的阻塞状态(因等待任务通知而进入阻塞状态)。相对于以前必须分别创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。更好的是,相比于使用信号量解除任务阻塞,使用任务通知可以快45%(使用GCC编译器,-o2优化级别)。
18.configUSE_MUTEXES
设置为1表示使用互斥量,设置成0表示忽略互斥量。读者应该了解在FreeRTOS中互斥量和二进制信号量的区别。
关于互斥量和二进制信号量简单说:
19.configUSE_RECURSIVE_MUTEXES
设置成1表示使用递归互斥量,设置成0表示不使用。
20.configUSE_COUNTING_SEMAPHORES
设置成1表示使用计数信号量,设置成0表示不使用。
21.configUSE_ALTERNATIVE_API
设置成1表示使用“替代”队列函数('alternative' queue functions),设置成0不使用。替代API在queue.h头文件中有详细描述。
注:“替代”队列函数已经被弃用,在新的设计中不要使用它!
22.configCHECK_FOR_STACK_OVERFLOW
每个任务维护自己的栈空间,任务创建时会自动分配任务需要的占内存,分配内存大小由创建任务函数(xTaskCreate())的一个参数指定。堆栈溢出是设备运行不稳定的最常见原因,因此FreeeRTOS提供了两个可选机制用来辅助检测和改正堆栈溢出。配置宏configCHECK_FOR_STACK_OVERFLOW为不同的常量来使用不同堆栈溢出检测机制。
注意,这个选项仅适用于内存映射未分段的微处理器架构。并且,在RTOS检测到堆栈溢出发生之前,一些处理器可能先产生故障(fault)或异常(exception)来反映堆栈使用的恶化。如果宏configCHECK_FOR_STACK_OVERFLOW没有设置成0,用户必须提供一个栈溢出钩子函数,这个钩子函数的函数名和参数必须如下所示:
参数xTask和pcTaskName为堆栈溢出任务的句柄和名字。请注意,如果溢出非常严重,这两个参数信息也可能是错误的!在这种情况下,可以直接检查pxCurrentTCb变量。
推荐仅在开发或测试阶段使用栈溢出检查,因为堆栈溢出检测会增大上下文切换开销。
任务切换出去后,该任务的上下文环境被保存到自己的堆栈空间,这时很可能堆栈的使用量达到了最大(最深)值。在这个时候,RTOS内核会检测堆栈指针是否还指向有效的堆栈空间。如果堆栈指针指向了有效堆栈空间之外的地方,堆栈溢出钩子函数会被调用。
这个方法速度很快,但是不能检测到所有堆栈溢出情况(比如,堆栈溢出没有发生在上下文切换时)。设置configCHECK_FOR_STACK_OVERFLOW为1会使用这种方法。
当堆栈首次创建时,在它的堆栈区中填充一些已知值(标记)。当任务切换时,RTOS内核会检测堆栈最后的16个字节,确保标记数据没有被覆盖。如果这16个字节有任何一个被改变,则调用堆栈溢出钩子函数。
这个方法比第一种方法要慢,但也相当快了。它能有效捕捉堆栈溢出事件(即使堆栈溢出没有发生在上下文切换时),但是理论上它也不能百分百的捕捉到所有堆栈溢出(比如堆栈溢出的值和标记值相同,当然,这种情况发生的概率极小)。
使用这个方法需要设置configCHECK_FOR_STACK_OVERFLOW为2.
23.configQUEUE_REGISTRY_SIZE
队列记录有两个目的,都涉及到RTOS内核的调试:
除了进行内核调试外,队列记录没有其它任何目的。
configQUEUE_REGISTRY_SIZE定义可以记录的队列和信号量的最大数目。如果你想使用RTOS内核调试器查看队列和信号量信息,则必须先将这些队列和信号量进行注册,只有注册后的队列和信号量才可以使用RTOS内核调试器查看。查看API参考手册中的vQueueAddToRegistry()
和vQueueUnregisterQueue()函数获取更多信息。
24.configUSE_QUEUE_SETS
设置成1使能队列集功能(可以阻塞、挂起到多个队列和信号量),设置成0取消队列集功能。
25.configUSE_TIME_SLICING(V7.5.0新增)
默认情况下(宏configUSE_TIME_SLICING未定义或者宏configUSE_TIME_SLICING设置为1),FreeRTOS使用基于时间片的优先级抢占式调度器。这意味着RTOS调度器总是运行处于最高优先级的就绪任务,在每个RTOS
系统节拍中断时在相同优先级的多个任务间进行任务切换。如果宏configUSE_TIME_SLICING设置为0,RTOS调度器仍然总是运行处于最高优先级的就绪任务,但是当RTOS
系统节拍中断发生时,相同优先级的多个任务之间不再进行任务切换。
26.configUSE_NEWLIB_REENTRANT(V7.5.0新增)
如果宏configUSE_NEWLIB_REENTRANT设置为1,每一个创建的任务会分配一个newlib(一个嵌入式C库)reent结构。
27.configENABLE_BACKWARD_COMPATIBILITY****
头文件FreeRTOS.h包含一系列#define宏定义,用来映射版本V8.0.0和V8.0.0之前版本的数据类型名字。这些宏可以确保RTOS内核升级到V8.0.0或以上版本时,之前的应用代码不用做任何修改。在FreeRTOSConfig.h文件中设置宏configENABLE_BACKWARD_COMPATIBILITY为0会去掉这些宏定义,并且需要用户确认升级之前的应用没有用到这些名字。
28.configNUM_THREAD_LOCAL_STORAGE_POINTERS****
设置每个任务的线程本地存储指针数组大小。
线程本地存储允许应用程序在任务的控制块中存储一些值,每个任务都有自己独立的储存空间,宏configNUM_THREAD_LOCAL_STORAGE_POINTERS指定每个任务线程本地存储指针数组的大小。API函数vTaskSetThreadLocalStoragePointer()用于向指针数组中写入值,API函数pvTaskGetThreadLocalStoragePointer()用于从指针数组中读取值。
比如,许多库函数都包含一个叫做errno的全局变量。某些库函数使用errno返回库函数错误信息,应用程序检查这个全局变量来确定发生了那些错误。在单线程程序中,将errno定义成全局变量是可以的,但是在多线程应用中,每个线程(任务)必须具有自己独有的errno值,否则,一个任务可能会读取到另一个任务的errno值。
FreeRTOS提供了一个灵活的机制,使得应用程序可以使用线程本地存储指针来读写线程本地存储。具体参见后续文章《FreeRTOS系列第12篇---FreeRTOS任务应用函数》。
29.configGENERATE_RUN_TIME_STATS
设置宏configGENERATE_RUN_TIME_STATS为1使能运行时间统计功能。一旦设置为1,则下面两个宏必须被定义:
举一个例子,假如我们配置了一个定时器,每500us中断一次。在定时器中断服务例程中简单的使长整形变量ulHighFrequencyTimerTicks自增。那么上面提到两个宏定义如下(可以在FreeRTOSConfig.h中添加):
30.configUSE_CO_ROUTINES
设置成1表示使用协程,0表示不使用协程。如果使用协程,必须在工程中包含croutine.c文件。
注:协程(Co-routines)主要用于资源发非常受限的嵌入式系统(RAM非常少),通常不会用于32位微处理器。
在当前嵌入式硬件环境下,不建议使用协程,FreeRTOS的开发者早已经停止开发协程。
31.configMAX_CO_ROUTINE_PRIORITIES
应用程序协程(Co-routines)的有效优先级数目,任何数目的协程都可以共享一个优先级。使用协程可以单独的分配给任务优先级。见configMAX_PRIORITIES。
32.configUSE_TIMERS
设置成1使用软件定时器,为0不使用软件定时器功能。详细描述见FreeRTOS software timers 。
33.configTIMER_TASK_PRIORITY
设置软件定时器服务/守护进程的优先级。详细描述见FreeRTOS software timers 。
34.configTIMER_QUEUE_LENGTH
设置软件定时器命令队列的长度。详细描述见FreeRTOS software timers。
35.configTIMER_TASK_STACK_DEPTH
设置软件定时器服务/守护进程任务的堆栈深度,详细描述见FreeRTOS software timers 。
36.configKERNEL_INTERRUPT_PRIORITY、configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY
这是移植和应用FreeRTOS出错最多的地方,所以需要打起精神仔细读懂。
Cortex-M3、PIC24、dsPIC、PIC32、SuperH和RX600硬件设备需要设置宏configKERNEL_INTERRUPT_PRIORITY;PIC32、RX600和Cortex-M硬件设备需要设置宏configMAX_SYSCALL_INTERRUPT_PRIORITY。
configMAX_SYSCALL_INTERRUPT_PRIORITY和configMAX_API_CALL_INTERRUPT_PRIORITY,这两个宏是等价的,后者是前者的新名字,用于更新的移植层代码。
注意下面的描述中,在中断服务例程中仅可以调用以“FromISR”结尾的API函数。
通过设置configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级级别高于configKERNEL_INTERRUPT_PRIORITY可以实现完整的中断嵌套模式。这意味着FreeRTOS内核不能完全禁止中断,即使在临界区。此外,这对于分段内核架构的微处理器是有利的。请注意,当一个新中断发生后,某些微处理器架构会(在硬件上)禁止中断,这意味着从硬件响应中断到FreeRTOS重新使能中断之间的这段短时间内,中断是不可避免的被禁止的。
不调用API的中断可以运行在比configMAX_SYSCALL_INTERRUPT_PRIORITY高的优先级,这些级别的中断不会被FreeRTOS禁止,因此不会因为执行RTOS内核而被延时。
例如:假如一个微控制器有8个中断优先级别:0表示最低优先级,7表示最高优先级(Cortex-M3和Cortex-M4内核优先数和优先级别正好与之相反,后续文章会专门介绍它们)。当两个配置选项分别为4和0时,下图描述了每一个优先级别可以和不可做的事件:
这些配置参数允许非常灵活的中断处理:
在系统中可以像其它任务一样为中断处理任务分配优先级。这些任务通过一个相应中断唤醒。中断服务例程(ISR)内容应尽可能的精简---仅用于更新数据然后唤醒高优先级任务。ISR退出后,直接运行被唤醒的任务,因此中断处理(根据中断获取的数据来进行的相应处理)在时间上是连续的,就像ISR在完成这些工作。这样做的好处是当中断处理任务执行时,所有中断都可以处在使能状态。
中断、中断服务例程(ISR)和中断处理任务是三码事:当中断来临时会进入中断服务例程,中断服务例程做必要的数据收集(更新),之后唤醒高优先级任务。这个高优先级任务在中断服务例程结束后立即执行,它可能是其它任务也可能是中断处理任务,如果是中断处理任务,那么就可以根据中断服务例程中收集的数据做相应处理。
configMAX_SYSCALL_INTERRUPT_PRIORITY接口有着更深一层的意义:在优先级介于RTOS内核中断优先级(等于configKERNEL_INTERRUPT_PRIORITY)和configMAX_SYSCALL_INTERRUPT_PRIORITY之间的中断允许全嵌套中断模式并允许调用API函数。大于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断优先级绝不会因为执行RTOS内核而延时。
运行在大于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级中断是不会被RTOS内核所屏蔽的,因此也不受RTOS内核功能影响。这主要用于非常高的实时需求中。比如执行电机转向。但是,这类中断的中断服务例程中绝不可以调用FreeRTOS的API函数。
为了使用这个方案,应用程序要必须符合以下规则:调用FreeRTOS
API函数的任何中断,都必须和RTOS内核处于同一优先级(由宏configKERNEL_INTERRUPT_PRIORITY设置),或者小于等于宏configMAX_SYSCALL_INTERRUPT_PRIORITY定义的优先级。
37.configASSERT
断言,调试时可以检查传入的参数是否合法。FreeRTOS内核代码的关键点都会调用configASSERT( x
)函数,如果参数x为0,则会抛出一个错误。这个错误很可能是传递给FreeRTOS
API函数的无效参数引起的。定义configASSERT()有助于调试时发现错误,但是,定义configASSERT()也会增大应用程序代码量,增大运行时间。推荐在开发阶段使用这个断言宏。
举一个例子,我们想把非法参数所在的文件名和代码行数打印出来,可以先定义一个函数vAssertCalled,该函数有两个参数,分别接收触发configASSERT宏的文件名和该宏所在行,然后通过显示屏或者串口输出。代码如下:
这里__FILE__和__LINE__是大多数编译器预定义的宏,分别表示代码所在的文件名(字符串格式)和行数(整形)。
这个例子虽然看起来很简单,但由于要把整形__LINE__转换成字符串再显示,在效率和实现上,都不能让人满意。我们可以使用C标准库assert的实现方法,这样函数vAssertCalled只需要接收一个字符串形式的参数(推荐仔细研读下面的代码并理解其中的技巧):
这里稍微讲解一下,由于内置宏__LINE__是整数型的而不是字符串型,把它转化成字符串需要一个额外的处理层。宏STR和和宏VAL正是用来辅助完成这个转化。宏STR用来把整形行号替换掉__LINE__,宏VAL用来把这个整形行号字符串化。忽略宏STR和VAL中的任何一个,只能得到字符串”__LINE__”,这不是我们想要的。
这里使用三目运算符’?:’来代替参数判断if语句,这样可以接受任何参数或表达式,代码也更紧凑,更重要的是代码优化度更高,因为如果参数恒为真,在编译阶段就可以去掉不必要的输出语句。
38.INCLUDE Parameters
以“INCLUDE”起始的宏允许用户不编译那些应用程序不需要的实时内核组件(函数),这可以确保在你的嵌入式系统中RTOS占用最少的ROM和RAM。
每个宏以这样的形式出现:
INCLUDE_FunctionName
在这里FunctionName表示一个你可以控制是否编译的API函数。如果你想使用该函数,就将这个宏设置成1,如果不想使用,就将这个宏设置成0。比如,对于API函数vTaskDelete():
表示希望使用vTaskDelete(),允许编译器编译该函数
表示禁止编译器编译该函数。
在阅读本文之前,有两个定义在FreeRTOSConfig.h中的宏,你必须先明白它们是什么意思,《FreeRTOS内核配置说明》一文中,讲解了这两个宏:
FreeRTOS与Cortex-M内核可谓是绝配,以至于让移植和使用FreeRTOS都变得更简单起来。根据FreeRTOS官方反馈,在Cortex-M内核上使用FreeRTOS大多数的问题点是由不正确的优先级设置引起的。这个问题也是在意料之中的,因为尽管Cortex-M内核的中断模式是非常强大的,但对于那些使用传统中断优先级架构的工程师来说,Cortex-M内核中断机制也有点笨拙(或者是说使用比较繁琐),并且违反直觉(这个主要是因为Cortex-M中断优先级数值越大代表的优先级反而越小)。本章打算描述Cortex-M的中断优先级机制,并描述怎样结合RTOS内核使用。
说明:虽然Cortex-M内核的优先级方案看上去比较复杂,但每一个官方发布的FreeRTOS 接口包(在FreeRTOSV7.2.0\FreeRTOS\Source\portable文件夹中,一般为port.c)内都会有正确配置的演示例程,可以以此为参考。
首先需要清楚有效优先级的总数,这取决于微控制器制造商怎么使用Cortex内核。所以,并不是所有的Cortex-M内核微处理器都具有相同的中断优先级级别。
Cortex-M构架自身最多允许256级可编程优先级(优先级配置寄存器最多8位,所以优先级范围从0x00~0xFF),但是绝大多数微控制器制造商只是使用其中的一部分优先级。比如,TI Stellaris Cortex-M3和Cortex-M4微控制器使用优先级配置寄存器的3个位,能提供8级优先级。再比如,NXP LPC17xx Cortex-M3微控制器使用优先级配置寄存器的5个位,能提供32级优先级。
RTOS中断嵌套方案将有效的中断优先级分成两组:一组可以通过RTOS临界区屏蔽,另一组不受RTOS影响,永远都是使能的。宏configMAX_SYSCALL_INTERRUPT_PRIORITY在FreeRTOSConfig.h中配置,定义两组中断优先级的边界。逻辑优先级高于此值的中断不受RTOS影响。最优值取决于微控制器使用的优先级配置寄存器的位数。
有必要先解释一下优先级值和逻辑优先级:在Cortex-M内核中,假如有8级优先级,我们说优先级值是0~7,但数值最大的优先级7却代表着最低的逻辑优先级。很多使用传统传统中断优先级架构的工程师会觉得这样比较绕,违反直觉。以下内容提到的优先级要仔细区分是优先级数值还是逻辑优先级。
接下来需要清楚的是,在Cortex-M内核中,一个中断的优先级数值越低,逻辑优先级却越高。比如,中断优先级为2的中断可以抢占中断优先级为5的中断,但反过来就不行。换句话说,中断优先级2比中断优先级5的优先级更高。
这是Cortex-M内核最容易让人犯错之处,因为大多数的非Cortex-M内核微控制器的中断优先级表述是与之相反的。
以“FromISR”结尾的FreeRTOS函数是具有中断调用保护的(执行这些函数会进入临界区),但是就算是这些函数,也不可以被逻辑优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断服务函数调用。(宏configMAX_SYSCALL_INTERRUPT_PRIORITY定义在头文件FreeRTOSConfig.h中)。因此,任何使用RTOSAPI函数的中断服务例程的中断优先级数值大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY宏的值。这样就能保证中断的逻辑优先级等于或低于configMAX_SYSCALL_INTERRUPT_PRIORITY。
Cortex中断默认情况下有一个数值为0的优先级。大多数情况下0代表最高级优先级。因此,绝对不可以在优先级为0的中断服务例程中调用RTOSAPI函数。
Cortex-M内核的中断优先级寄存器是以最高位(MSB)对齐的。比如,如果使用了3位来表达优先级,则这3个位位于中断优先级寄存器的bit5、bit6、bit7位。剩余的bit0~bit4可以设置成任何值,但为了兼容,最好将他们设置成1.
Cortex-M优先级寄存器最多有8位,如果一个微控制器只使用了其中的3位,那么这3位是以最高位对齐的,见下图:
某微控制器只使用了优先级寄存器中的3位,下图展示了优先级数值5(二进制101B)是怎样在优先级寄存器中存储的。如果优先级寄存器中未使用的位置1,下图也展示了为什么数值5(二进制0000 0101B)可以看成数值191(二进制1011 1111)的。
某微控制器只使用了优先级寄存器中的4位,下图展示了优先级数值5(二进制101B)是怎样在优先级寄存器中存储的。如果优先级寄存器中未使用的位置1,下图也展示了为什么数值5(二进制0000
0101B)可以看成数值95(二进制0101 1111)的。
上文中已经描述,那些在中断服务例程中调用RTOS API函数的中断逻辑优先级必须低于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY(低逻辑优先级意味着高优先级数值)。
CMSIS以及不同的微控制器供应商提供了可以设置某个中断优先级的库函数。一些库函数的参数使用最低位对齐,另一些库函数的参数可能使用最高位对齐,所以,使用时应该查阅库函数的应用手册进行正确设置。
可以在FreeRTOSConfig.h中设置宏configMAX_SYSCALL_INTERRUPT_PRIORITY和configKERNEL_INTERRUPT_PRIORITY的值。这两个宏需要根据Cortex-M内核自身的情况进行设置,要以最高有效位对齐。比如某微控制器使用中断优先级寄存器中的3位,设置configKERNEL_INTERRUPT_PRIORITY的值为5,则代码为:
宏configKERNEL_INTERRUPT_PRIORITY指定RTOS内核使用的中断优先级,因为RTOS内核不可以抢占用户任务,因此这个宏一般设置为硬件支持的最小优先级。对于Cortex-M硬件,RTOS使用到硬件的PendSV和SysTick硬件中断,在函数xPortStartScheduler()中(该函数在port.c中,由启动调度器函数vTaskStartScheduler()调用),将PendSV和SysTick硬件中断优先级寄存器设置为宏configKERNEL_INTERRUPT_PRIORITY指定的值。
有关代码如下(位于port.c):
RTOS内核使用Cortex-M内核的BASEPRI寄存器来实现临界区(注:BASEPRI为优先级屏蔽寄存器,优先级数值大于或等于该寄存器的中断都会被屏蔽,优先级数值越大,逻辑优先级越低,但是为零时不屏蔽任何中断)。这允许RTOS内核可以只屏蔽一部分中断,因此可以提供一个灵活的中断嵌套模式。
那些需要在中断调用时保护的API函数,FreeRTOS使用寄存器BASEPRI实现中断保护临界区。当进入临界区时,将寄存器BASEPRI的值设置成configMAX_SYSCALL_INTERRUPT_PRIORITY,当退出临界区时,将寄存器BASEPRI的值设置成0。很多Bug反馈都提到,当退出临界区时不应该将寄存器设置成0,应该恢复它之前的状态(之前的状态不一定是0)。但是Cortex-M
NVIC决不会允许一个低优先级中断抢占当前正在执行的高优先级中断,不管BASEPRI寄存器中是什么值。与进入临界区前先保存BASEPRI的值,退出临界区再恢复的方法相比,退出临界区时将BASEPRI寄存器设置成0的方法可以获得更快的执行速度。
RTOS内核通过写configMAX_SYSCALL_INTERRUPT_PRIORITY的值到BASEPRI寄存器的方法创建临界区。中断优先级0(具有最高的逻辑优先级)不能被BASEPRI寄存器屏蔽,因此,configMAX_SYSCALL_INTERRUPT_PRIORITY绝不可以设置成0。
本文介绍内存管理的基础知识,详细源码分析见《 FreeRTOS高级篇7---FreeRTOS内存管理分析》
FreeRTOS提供了几个内存堆管理方案,有复杂的也有简单的。其中最简单的管理策略也能满足很多应用的要求,比如对安全要求高的应用,这些应用根本不允许动态内存分配的。
FreeRTOS也允许你自己实现内存堆管理,甚至允许你同时使用两种内存堆管理方案。同时实现两种内存堆允许任务堆栈和其它RTOS对象放置到快速的内部RAM,应用数据放置到低速的外部RAM。
每当创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数有些时候能够用于完成这个任务,但是:
因此,提供一个替代的内存分配方案通常是必要的。
嵌入式/实时系统具有千差万别的RAM和时间要求,因此一个RAM内存分配算法可能仅属于一个应用的子集。
为了避免这个问题,FreeRTOS在移植层保留内存分配API函数。移植层在RTOS核心代码源文件之外(不属于核心源代码),这使得不同的应用程序可以提供适合自己的应用实现。当RTOS内核需要RAM时,调用pvPortMallo()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()函数来代替free()函数。
FreeRTOS下载包中提供5种简单的内存分配实现,本文稍后会进行描述。用户可以适当的选择其中的一个,也可以自己设计内存分配策略。
FreeRTOS提供的内存分配方案分别位于不同的源文件(heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c)之中,源文件位于下载包\FreeRTOS\Source\portable\MemMang文件夹中。其它实现方法可以根据需要增加。如果要使用FreeRTOS提供的内存堆分配方案,选中的源文件必须被正确的包含到工程文件中。
这是所有实现中最简单的一个。一旦分配内存之后,它甚至不允许释放分配的内存。尽管这样,heap_1.c还是适用于大部分嵌入式应用程序。这是因为大多数深度嵌入式(deeplyembedded)应用只是在系统启动时创建所有任务、队列、信号量等,并且直到程序结束都会一直使用它们,永远不需要删除。
当需要分配RAM时,这个内存分配方案只是简单的将一个大数组细分出一个子集来。大数组的容量大小通过FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来设置。
API函数xPortGetFreeHeapSize()返回未分配的堆栈空间总大小,可以通过这个函数返回值对configTOTAL_HEAP_SIZE进行合理的设置。
heap_1功能简介:
和方案1不同,这个方案使用一个最佳匹配算法,它允许释放之前分配的内存块。它不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。
有效的堆栈空间大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来定义。
API函数xPortGetFreeHeapSize()返回剩下的未分配堆栈空间的大小(可用于优化设置configTOTAL_HEAP_SIZE宏的值),但是不能提供未分配内存的碎片细节信息。
heap_2功能简介:
heap_2.c适用于需要动态创建任务的大多数小型实时系统(smallreal time)。
heap_3.c简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。
heap_3.c功能简介:
注:使用heap_3时,FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏定义没有作用。
这个方案使用一个最佳匹配算法,但不像方案2那样。它会将相邻的空闲内存块合并成一个更大的块(包含一个合并算法)。
有效的堆栈空间大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE来定义。
API函数xPortGetFreeHeapSize()返回剩下的未分配堆栈空间的大小(可用于优化设置configTOTAL_HEAP_SIZE宏的值),但是不能提供未分配内存的碎片细节信息。
heap_4.c功能简介:
heap_4.c还特别适用于移植层代码,可以直接使用pvPortMalloc()和 vPortFree()函数来分配和释放内存。
这个方案同样实现了heap_4.c中的合并算法,并且允许堆栈跨越多个非连续的内存区。
Heap_5通过调用vPortDefineHeapRegions()函数实现初始化,在该函数执行完成前不允许使用内存分配和释放。创建RTOS对象(任务、队列、信号量等等)会隐含的调用pvPortMalloc(),因此必须注意:使用heap_5创建任何对象前,要先执行vPortDefineHeapRegions()函数。
vPortDefineHeapRegions()函数只需要单个参数。该参数是一个HeapRegion_t结构体类型数组。HeapRegion_t在portable.h中定义,如下所示:
这个数组必须使用一个NULL指针和0字节元素作为结束,起始地址必须从小到大排列。下面的代码段提供一个例子。MSVCWin32模拟器演示例程使用了heap_5,因此可以当做一个参考例程。
应用程序可以使用任务也可以使用协程,或者两者混合使用,但是任务和协程使用不同的API函数,因此在任务和协程之间不能使用同一个队列或信号量传递数据。
通常情况下,协程仅用在资源非常少的微处理器中,特别是RAM非常稀缺的情况下。目前协程很少被使用到,因此对于协程FreeRTOS作者既没有把它删除也没有进一步开发。
所以本系列文章以后不会对协程过多描述,包括其API函数。
简而言之:使用RTOS的实时应用程序可认为是一系列独立任务的集合。每个任务在自己的环境中运行,不依赖于系统中的其它任务或者RTOS调度器。在任何时刻,只有一个任务得到运行,RTOS调度器决定运行哪个任务。调度器会不断的启动、停止每一个任务,宏观看上去就像整个应用程序都在执行。作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。为了实现这点,每个任务都需要有自己的堆栈。当任务切出时,它的执行环境会被保存在该任务的堆栈中,这样当再次运行时,就能从堆栈中正确的恢复上次的运行环境。
一个任务可为下面中的一个:
每个任务都要被指定一个优先级,从0~configMAX_PRIORITIES,configMAX_PRIORITIES定义在FreeRTOSConfig.h中。
如果某架构硬件支持CLZ(或类似)指令(计算前导零的数目,Cortex-M3是支持该指令的,从ARMv6T2才支持这个指令),并且打算在移植层使用这个特性来优化任务调度机制,需要有一些步骤,首先将FreeRTOSConfig.h中configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,并且最大优先级数目configMAX_PRIORITIES不能大于32。除此之外,configMAX_PRIORITIES可以设置为任意值,但是考虑到configMAX_PRIORITIES设置越大,RAM消耗也越大,一般设置为满足使用的最小值。
低优先级数值代表低优先级。空闲任务(idle task)的优先级为0(tskIDLE_PRIORITY)。
FreeRTOS调度器确保处于最高优先级的就绪或运行态任务获取处理器,换句话说,处于运行状态的任务,只有其中的最高优先级任务才会运行。
任何数量的任务可以共享同一个优先级。如果宏configUSE_TIME_SLICING未定义或着宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
一个任务具有以下结构:
任务函数返回为void,参数只有一个void类型指针。所有的任务函数都应该是这样。void类型指针可以向任务传递任意类型信息。
任务函数决不应该返回,因此通常任务函数都是一个死循环。
任务由xTaskCreate()函数创建,由vTaskDelete()函数删除。
空闲任务是启动RTOS调度器时由内核自动创建的任务,这样可以确保至少有一个任务在运行。空闲任务具有最低任务优先级,这样如果有其它更高优先级的任务进入就绪态就可以立刻让出CPU。
删除任务后,空闲任务用来释放RTOS分配给被删除任务的内存。因此,在应用中使用vTaskDelete()函数后确保空闲任务能获得处理器时间就很重要了。除此之外,空闲任务没有其它有效功能,所以可以被合理的剥夺处理器时间,并且它的优先级也是最低的。
应用程序任务共享空闲任务优先级(tskIDLE_PRIORITY)也是可能的。这种情况如何配置可以参考configIDLE_SHOULE_YIELD配置参数类获取更多信息。
空闲任务钩子是一个函数,每一个空闲任务周期被调用一次。如果你想将任务程序功能运行在空闲优先级上,可以有两种选择:
创建一个空闲钩子步骤如下:
通常,使用这个空闲钩子函数设置CPU进入低功耗模式。
在FreeRTOS移植到Cortex-M3硬件平台的文章中,我们已经见过任务创建API,但那篇文章的重点在于如何移植FreeRTOS,本文将重点放在任务的创建和删除API函数上面。
任务创建和删除API函数位于文件task.c中,需要包含task.h头文件。
创建新的任务并加入任务就绪列表。
如果使用FreeRTOS-MPU(在官方下载包中,为Cortex-M3内核写了两个移植方案,一个是普通的FreeRTOS移植层,还有一个是FreeRTOS-MPU移植层。后者包含完整的内存保护),那么推荐使用函数xTaskCreateRestricted()来代替xTaskCreate()。在使用FreeRTOS-MPU的情况下,使用xTaskCreate()函数可以创建运行在特权模式或用户模式(见下面对函数参数uxPriority的描述)的任务。当运行在特权模式下,任务可以访问整个内存映射;当处于用户模式下,任务仅能访问自己的堆栈。无论在何种模式下,MPU都不会自动捕获堆栈溢出,因此标准的FreeRTOS堆栈溢出检测机制仍然会被用到。xTaskCreateRestricted()函数具有更大的灵活性。
如果任务成功创建并加入就绪列表函数返回pdPASS,否则函数返回错误码,具体参见projdefs.h。
voidvTaskDelete( TaskHandle_t xTask );
从RTOS内核管理器中删除一个任务。任务删除后将会从就绪、阻塞、暂停和事件列表中移除。在文件FreeRTOSConfig.h中,必须定义宏INCLUDE_vTaskDelete 为1,本函数才有效。
注:被删除的任务,其在任务创建时由内核分配的存储空间,会由空闲任务释放。如果有应用程序调用xTaskDelete(),必须保证空闲任务获取一定的微控制器处理时间。任务代码自己分配的内存是不会自动释放的,因此删除任务前,应该将这些内存释放。
xTask:被删除任务的句柄。为NULL表示删除当前任务。
void vTaskDelay( portTickTypexTicksToDelay )
调用vTaskDelay()函数后,任务会进入阻塞状态,持续时间由vTaskDelay()函数的参数xTicksToDelay指定,单位是系统节拍时钟周期。常量portTICK_RATE_MS 用来辅助计算真实时间,此值是系统节拍时钟中断的周期,单位是毫秒。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskDelay 必须设置成1,此函数才能有效。
vTaskDelay()指定的延时时间是从调用vTaskDelay()后开始计算的相对时间。比如vTaskDelay(100),那么从调用vTaskDelay()后,任务进入阻塞状态,经过100个系统时钟节拍周期,任务解除阻塞。因此,vTaskDelay()并不适用与周期性执行任务的场合。此外,其它任务和中断活动,会影响到vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),因此会影响任务下一次执行的时间。API函数vTaskDelayUntil()可用于固定频率的延时,它用来延时一个绝对时间。
void vTaskDelayUntil( TickType_t *pxPreviousWakeTime,
const TickType_txTimeIncrement );
任务延时一个指定的时间。周期性任务可以使用此函数,以确保一个恒定的频率执行。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskDelayUntil 必须设置成1,此函数才有效。
这个函数不同于vTaskDelay()函数的一个重要之处在于:vTaskDelay()指定的延时时间是从调用vTaskDelay()之后(执行完该函数)开始算起的,但是vTaskDelayUntil()指定的延时时间是一个绝对时间。
调用vTaskDelay()函数后,任务会进入阻塞状态,持续时间由vTaskDelay()函数的参数指定,单位是系统节拍时钟周期。因此vTaskDelay()并不适用于周期性执行任务的场合。因为调用vTaskDelay()到任务解除阻塞的时间不总是固定的并且该任务下一次调用vTaskDelay()函数的时间也不总是固定的(两次执行同一任务的时间间隔本身就不固定,中断或高优先级任务抢占也可能会改变每一次执行时间)。
vTaskDelay()指定一个从调用vTaskDelay()函数后开始计时,到任务解除阻塞为止的相对时间,而vTaskDelayUntil()指定一个绝对时间,每当时间到达,则解除任务阻塞。
应当指出的是,如果指定的唤醒时间已经达到,vTaskDelayUntil()立刻返回(不会有阻塞)。因此,使用vTaskDelayUntil()周期性执行的任务,无论任何原因(比如,任务临时进入挂起状态)停止了周期性执行,使得任务少运行了一个或多个执行周期,那么需要重新计算所需要的唤醒时间。这可以通过传递给函数的指针参数pxPreviousWake指向的值与当前系统时钟计数值比较来检测,在大多数情况下,这并不是必须的。
常量portTICK_RATE_MS 用来辅助计算真实时间,此值是系统节拍时钟中断的周期,单位是毫秒。
当调用vTaskSuspendAll()函数挂起RTOS调度器时,不可以使用此函数。
UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask );
获取指定任务的优先级。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskPriorityGet必须设置成1,此函数才有效。
返回指定任务的优先级。
void vTaskPrioritySet( TaskHandle_txTask,
UBaseType_tuxNewPriority );
设置指定任务的优先级。如果设置的优先级高于当前运行的任务,在函数返回前会进行一次上下文切换。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskPrioritySet 必须设置成1,此函数才有效。
void vTaskSuspend( TaskHandle_txTaskToSuspend );
挂起指定任务。被挂起的任务绝不会得到处理器时间,不管该任务具有什么优先级。
调用vTaskSuspend函数是不会累计的:即使多次调用vTaskSuspend ()函数将一个任务挂起,也只需调用一次vTaskResume ()函数就能使挂起的任务解除挂起状态。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskSuspend必须设置成1,此函数才有效。
void vTaskResume( TaskHandle_txTaskToResume );
恢复挂起的任务。
通过调用一次或多次vTaskSuspend()挂起的任务,可以调用一次vTaskResume ()函数来再次恢复运行。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskSuspend必须置1,此函数才有效。
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume );
用于恢复一个挂起的任务,用在ISR中。
通过调用一次或多次vTaskSuspend()函数而挂起的任务,只需调用一次xTaskResumeFromISR()函数即可恢复运行。
xTaskResumeFromISR()不可用于任务和中断间的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务还没有挂起,调用xTaskResumeFromISR()函数是没有意义的,只能等下一次中断)。这种情况下,可以使用信号量作为同步机制。在文件FreeRTOSConfig.h中,宏INCLUDE_vTaskSuspend 和 INCLUDE_xTaskResumeFromISR 必须设置成1,此函数才有效。
如果恢复任务后需要上下文切换返回pdTRUE,否则返回pdFALSE。由ISR确定是否需要上下文切换。
该函数向TaskStatus_t结构体填充相关信息,系统中每一个任务的信息都可以填充到TaskStatus_t结构体数组中,数组大小由uxArraySize指定。结构体TaskStatus_t定义如下:
注意,这个函数仅用来调试用,调用此函数会挂起所有任务,直到函数最后才恢复挂起的任务,因此任务可能被挂起很长时间。在文件FreeRTOSConfig.h中,宏configUSE_TRACE_FACILITY必须设置为1,此函数才有效。
被填充的TaskStatus_t结构体数量。这个值应该等于通过调用API函数uxTaskGetNumberOfTasks()返回的值,但如果传递给uxArraySize参数的值太小,则返回0。
TaskHandle_t xTaskGetCurrentTaskHandle(void );
在文件FreeRTOSConfig.h中,宏INCLUDE_xTaskGetCurrentTaskHandle必须设置为1,此函数才有效。
返回当前任务(调用该函数的任务)的句柄。
TaskHandle_t xTaskGetIdleTaskHandle(void );
在文件FreeRTOSConfig.h中,宏INCLUDE_xTaskGetIdleTaskHandle必须设置为1,此函数才有效。
返回空闲任务句柄。空闲任务在RTOS调度器启动时自动创建。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
任务的堆栈空间会随着任务执行以及中断处理而增长或缩小。该函数可以返回任务启动后的最小剩余堆栈空间。换句话说,可以间接估算出一个任务最多需要多少堆栈空间。在文件FreeRTOSConfig.h中,宏INCLUDE_uxTaskGetStackHighWaterMark 必须设置成1,此函数才有效。
返回最小剩余堆栈空间,以字为单位。比如一个32为架构处理器,返回值为1表示有4字节堆栈空间没有使用过。如果返回值为0,则任务很可能已经发生了堆栈溢出。
eTaskState eTaskGetState( TaskHandle_txTask );
返回一个枚举类型的任务状态值。在文件FreeRTOSConfig.h中,宏INCLUDE_eTaskGetState必须设置为1,此函数才有效。
下表列出返回值和对应的任务状态。
char * pcTaskGetTaskName( TaskHandle_txTaskToQuery );
获取任务的描述内容,在文件FreeRTOSConfig.h中,宏INCLUDE_pcTaskGetTaskName必须设置成1,此函数才有效。
一个指针,指向任务描述字符串。
volatile TickType_t xTaskGetTickCount(void );
这个函数不能在ISR中调用。在ISR中用xTaskGetTickCountFromISR(),原型为volatileTickType_t xTaskGetTickCountFromISR( void )。
返回从vTaskStartScheduler函数调用后的系统时钟节拍次数。
BaseType_t xTaskGetSchedulerState( void);
获取调度器当前状态。在文件FreeRTOSConfig.h中,宏INCLUDE_xTaskGetSchedulerState或configUSE_TIMERS必须定义为1,此函数才有效。
返回值是以下常量之一(定义在task.h):taskSCHEDULER_NOT_STARTED(未启动)、taskSCHEDULER_RUNNING(正常运行)、taskSCHEDULER_SUSPENDED(挂起)。
UBaseType_t uxTaskGetNumberOfTasks(void );
获取RTOS内核当前管理的任务总数。包含所有就绪、阻塞和挂起状态的任务。对于一个删除的任务,如果它的堆栈空间还没有被空闲任务释放掉,则这个被删除的任务也含在计数值中。
返回RTOS内核当前管理的任务总数。
void vTaskList( char *pcWriteBuffer );
将每个任务的状态、堆栈使用情况等以字符的形式保存到参数pcWriteBuffer指向的区域。vTaskList()函数调用usTaskGetSystemState()函数,然后将得到的信息格式化为程序员易读的字符形式。输出的内容例子如下图所示,图中State一栏中,B表示阻塞、R表示就绪、D表示删除(等待清除内存)、S表示挂起或阻塞。
注意,调用这个函数会挂起所有任务,这一过程可能持续较长时间,因此本函数仅在调试时使用。在文件FreeRTOSConfig.h中,宏configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS必须定义为1,此函数才有效。
void vTaskGetRunTimeStats( char*pcWriteBuffer );
这个函数用于统计每个任务的运行时间。要使用这个函数必须满足一些条件,那就是必须有一个用于时间统计的定时器或计数器,这个定时器或计数器的精度要至少大于10倍的系统节拍周期。这个定时器或计数器的配置以及获取定时时间是由两个宏定义实现的,这两个宏一般在文件FreeRTOSConfig.h中定义。配置定时器或计数器的宏为portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),获取定时时间的宏为portGET_RUN_TIME_COUNTER_VALUE。实现了这两个宏定义后,还必须在文件FreeRTOSConfig.h中将宏configGENERATE_RUN_TIME_STATS和configUSE_STATS_FORMATTING_FUNCTIONS设置为1,此API函数才有效。
这个API函数调用usTaskGetSystemState()函数获取每个任务的状态信息,并把其中的运行时间格式化为程序员易读的字符形式,并将这些信息保存到参数pcWriteBuffer指向的区域。
注意,调用这个函数会挂起所有任务,这一过程可能持续较长时间,因此本函数仅在调试时使用。
以lpc17xx系列为控制为例,我们使用定时器0来作为统计基准时钟。
在文件FreeRTOSConfig.h中,设置宏configGENERATE_RUN_TIME_STATS和configUSE_STATS_FORMATTING_FUNCTIONS为1,
在文件FreeRTOSConfig.h中,定义下列代码:
可以给每个任务分配一个标签值。这个值一般用于应用程序,RTOS内核不会使用。在文件FreeRTOSConfig.h中,宏configUSE_APPLICATION_TASK_TAG必须设置为1,此函数才有效。
注:TaskHookFunction_t原型定义:typedef BaseType_t (*TaskHookFunction_t)(void * )
TaskHookFunction_txTaskGetApplicationTaskTag( TaskHandle_t xTask );
返回分配给任务的标签值。程序员定义标签值,RTOS内核通常不会访问标签值。
函数仅对高级用户使用。在文件FreeRTOSConfig.h中,宏configUSE_APPLICATION_TASK_TAG必须设置为1,此函数才有效。
返回指定任务的标签值。
可以为每个任务分配一个标签值,当这个值是一个TaskHookFunction_t类型函数指针时,相当于应用程序向任务注册了一个回调函数,而API函数xTaskCallApplicationTaskHook用来调用这个回调函数。
一般这个函数配合RTOS跟踪钩子宏使用,见12.设置任务标签值一节的用法举例。
此函数仅用于高级用户。
线程本地存储允许应用程序在任务的控制块中存储一些值,每个任务都有自己独立的储存空间。
比如,许多库函数都包含一个叫做errno的全局变量。某些库函数使用errno返回库函数错误信息,应用程序检查这个全局变量来确定发生了那些错误。在单线程程序中,将errno定义成全局变量是可以的,但是在多线程应用中,每个线程(任务)必须具有自己独有的errno值,否则,一个任务可能会读取到另一个任务的errno值。
FreeRTOS提供了一个灵活的机制,使得应用程序可以使用线程本地存储指针来读写线程本地存储。在文件FreeRTOSConfig.h中,宏configNUM_THREAD_LOCAL_STORAGE_POINTERS指定每个任务线程本地存储指针数组的大小。API函数vTaskSetThreadLocalStoragePointer()用于向指针数组中写入值,API函数pvTaskGetThreadLocalStoragePointer()用于从指针数组中读取值。
参见16.获取线程本地存储指针一节。
此函数仅用于高级用户。从线程本地存储指针数组中读取值。更详细描述见15.设置线程本地存储指针一节。
返回一个指针,这个指针存储在线程本地存储指针数组中,数组索引由参数xIndex指定。
void vTaskSetTimeOutState( TimeOut_t *const pxTimeOut );
此函数仅用于高级用户,通常与API函数xTaskCheckForTimeOut()共同使用。
任务因为等待某事件而进入阻塞状态,通常情况下任务会设置一个等待超时周期。如果在等待事件超时,任务会退出阻塞状态。想象一个这样的应用,某任务等待一个事件而进入阻塞状态,但是事件迟迟不发生,超时后任务退出阻塞状态继续执行任务。假如任务等待的事件仍然没有发生,则任务又会阻塞在该事件下。只要任务等待的事件一直不发生,这个任务进入阻塞然后超时退出阻塞,再进入阻塞的循环就会一直存在。是不是可以设定一个总超时时间,只要总阻塞时间大于这个总超时时间,则可以结束这个任务或进行相应记录?freeRTOS提供了两个API函数来完成这个功能,这就是vTaskSetTimeOutState()和xTaskCheckForTimeOut()。
vTaskSetTimeOutState()函数用于设置初始条件,之后调用xTaskCheckForTimeOut()函数检查任务总阻塞时间是否超过总超时时间,如果没有超过,则调整剩余的超时时间计数器。
参见18.超时检测。
此函数仅用于高级用户,通常与API函数vTaskSetTimeOutState共同使用。
详细描述见17.设置超时状态。
taskYIELD:用于强制上下文切换的宏。在中断服务程序中的等价版本为portYIELD_FROM_ISR,这也是个宏,其实现取决于移植层。
用于上下文切换的实际代码由移植层提供。对于Cortex-M3硬件,这个宏会引起PendSV中断。
taskENTER_CRITICAL:用于进入临界区的宏。在临界区中不会发生上下文切换。
进入临界区的实际代码由移植层提供,对于Cortex-M3硬件,先禁止所有RTOS可屏蔽中断,这可以通过向basepri 寄存器写入configMAX_SYSCALL_INTERRUPT_PRIORITY来实现。basepri寄存器被设置成某个值后,所有优先级号大于等于此值的中断都被禁止,但若被设置为0,则不关闭任何中断,0为默认值。然后临界区嵌套计数器增1。
taskEXIT_CRITICAL:用于退出临界区的宏。
退出临界区的实际代码有移植层提供,对于Cortex-M3硬件,先将临界区嵌套计数器减1,如果临界区计数器为零,则使能所有RTOS可屏蔽中断,这可以通过向basepri 寄存器写入0来实现。
taskDISABLE_INTERRUPTS:禁止所有RTOS可屏蔽中断。在调用宏taskENTER_CRITICAL进入临界区时,也会间接调用该宏禁止所有RTOS可屏蔽中断。
taskENABLE_INTERRUPTS:使能所有RTOS可屏蔽中断。在调用宏taskEXIT_CRITICAL退出临界区时,也会间接调用该宏使能所有RTOS可屏蔽中断。
void vTaskStartScheduler( void );
启动RTOS调度器,之后RTOS内核控制哪个任务执行以及何时执行。
当调用vTaskStartScheduler()后,空闲任务被自动创建。如果configUSE_TIMERS被设置为1,定时器后台任务也会被创建。
如果vTaskStartScheduler()成功执行,则该函数不会返回,直到有任务调用了vTaskEndScheduler()。如果因为RAM不足而无法创建空闲任务,该函数也可能执行失败,并会立刻返回调用处。
void vTaskEndScheduler( void );
仅用于x86硬件架构中。
停止RTOS内核系统节拍时钟。所有创建的任务自动删除并停止多任务调度。
void vTaskSuspendAll( void );
挂起调度器,但不禁止中断。当调度器挂起时,不会进行上下文切换。调度器挂起后,正在执行的任务会一直继续执行,内核不再调度(意味着当前任务不会被切换出去),直到该任务调用了xTaskResumeAll ()函数。
内核调度器挂起期间,那些可以引起上下文切换的API函数(如vTaskDelayUntil()、xQueueSend()等)决不可使用。
BaseType_t xTaskResumeAll( void );
恢复因调用vTaskSuspendAll()函数而挂起的实时内核调度器。xTaskResumeAll()仅恢复调度器,它不会恢复那些被vTaskSuspend()函数挂起的任务。
返回pdTRUE 表示恢复调度器引起了一次上下文切换,否则,返回pdFALSE。
void vTaskStepTick( TickType_txTicksToJump );
如果RTOS使能tickless空闲功能,每当只有空闲任务被执行时,系统节拍时钟中断将会停止,微控制器进入低功耗模式。当微控制器退出低功耗后,系统节拍计数器必须被调整,将进入低功耗的时间弥补上。
如果FreeRTOS移植文件中定义了宏portSUPPRESS_TICKS_AND_SLEEP()实体,则函数vTaskStepTick用于在这个宏portSUPPRESS_TICKS_AND_SLEEP()实体内部调整系统节拍计数器。函数vTaskStepTick是一个全局函数,所以也可以在宏portSUPPRESS_TICKS_AND_SLEEP()实体中重写该函数。
在文件FreeRTOSConfig.h中,宏configUSE_TICKLESS_IDLE必须设置为1,此函数才有效。
注:本文介绍任务通知的基础知识,详细源码分析见《FreeRTOS高级篇8---FreeRTOS任务通知分析》
每个RTOS任务都有一个32位的通知值,任务创建时,这个值被初始化为0。RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务可以解除阻塞状态,前提是这个阻塞事件是因等待通知而引起的。发送通知的同时,也可以可选的改变接收任务的通知值。
可以通过下列方法向接收任务更新通知:
相对于用前必须分别创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。更好的是,相比于使用信号量解除任务阻塞,使用任务通知可以快45%、使用更少的RAM(使用GCC编译器,-o2优化级别)。
使用API函数xTaskNotify()和xTaskNotifyGive()(中断保护等价函数为xTaskNotifyFromISR()和vTaskNotifyGiveFromISR())发送通知,在接收RTOS任务调用API函数xTaskNotifyWait()或ulTaskNotifyTake()之前,这个通知都被保持着。如果接收RTOS任务已经因为等待通知而进入阻塞状态,则接收到通知后任务解除阻塞并清除通知。
RTOS任务通知功能默认是使能的,可以通过在文件FreeRTOSConfig.h中设置宏configUSE_TASK_NOTIFICATIONS为0来禁止这个功能,禁止后每个任务节省8字节内存。
虽然RTOS任务通知速度更快并且占用内存更少,但它也有一些限制:
向指定任务发送指定的通知值。如果打算使用RTOS任务通知实现轻量级的二进制或计数信号量,推荐使用API函数xTaskNotifyGive()来代替本函数。
此函数不可以在中断服务例程中调用,中断保护等价函数为xTaskNotifyFromISR()。
枚举变量成员以及作用如下表所示。
参数eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知,又接收到了一个通知,则这次通知值未能更新并返回pdFALSE,否则返回pdPASS。
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify );
其实这是一个宏,本质上相当于xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement )。可以使用该API函数代替二进制或计数信号量,但速度更快。在这种情况下,应该使用API函数ulTaskNotifyTake()来等待通知,而不应该使用API函数xTaskNotifyWait()。
此函数不可以在中断服务例程中调用,中断保护等价函数为vTaskNotifyGiveFromISR()。
ulTaskNotifyTake()是专门为使用更轻量级更快的方法来代替二进制或计数信号量而量身打造的。FreeRTOS获取信号量的API函数为xSemaphoreTake(),可以使用ulTaskNotifyTake()函数等价代替。
当一个任务使用通知值来实现二进制或计数信号量时,其它任务或者中断要使用API函数xTaskNotifyGive()或者使用参数eAction为eIncrement的API函数xTaskNotify()。推荐使用xTaskNotifyGive()函数(其实是个宏,我们这里把它看作一个API函数)。另外需要注意的是,如果在中断中使用,要使用它们的中断保护等价函数:vTaskNotifyGiveFromISR()和xTaskNotifyFromISR()。
API函数xTaskNotifyTake()有两种方法处理任务的通知值,一种方法是在函数退出时将通知值清零,这种方法适用于实现二进制信号量;另外一种方法是在函数退出时将通知值减1,这种方法适用于实现计数信号量。
如果RTOS任务的通知值为0,使用xTaskNotifyTake()可以可选的使任务进入阻塞状态,直到该任务的通知值不为0。进入阻塞的任务不消耗CPU时间。
返回任务的当前通知值,为0或者为调用API函数xTaskNotifyTake()之前的通知值减1。
如果打算使用RTOS任务通知实现轻量级的二进制或计数信号量,推荐使用API函数ulTaskNotifyTake()来代替本函数。
如果接收到通知,返回pdTRUE,如果API函数xTaskNotifyWait()等待超时,返回pdFALSE。
此函数与任务通知API函数xTaskNotify()非常像,只不过此函数具有一个附加参数,用来回传任务当前的通知值,然后根据参数ulValue和eAction更新任务的通知值。
此函数不能在中断服务例程中使用,在中断服务例程中使用xTaskNotifyAndQueryFromISR()函数。
参数eAction为eSetValueWithoutOverwrite时,如果被通知任务还没取走上一个通知,又接收到了一个通知,则这次通知值未能更新并返回pdFALSE,否则返回pdPASS。
虽然这是介绍FreeRTOS系列的文章,但这篇文章偏重于命令行解释器的实现。这一方面是因为任务通知使用起来非常简单,另一方面也因为对于嵌入式程序来说,使用命令行解释器来辅助程序调试是非常有用的。程序调试是一门技术,基本上我们需要两种调试手段,一种是可以单步仿真的硬件调试器,另外一种是可以长期监视程序状态的状态输出,可以通过串口、显示屏等等手段输出异常信息或者某些关键点。这里的命令行解释器就属于后者。
本文实现的命令行解释器具有以下特性:
一个带参数的命令格式如下所示:
参数名 <参数1> <参数2> … <参数3>[回车换行符]
FreeRTOS的编码标准及风格见《FreeRTOS系列第4篇---FreeRTOS编码标准及风格指南》,但我自己的编码风格跟FreeRTOS并不相同,并且我也不打算改变我当前坚持使用的编码风格。所以在这篇或者以后的文章中可能会在一个程序中看到两种不同的编码风格,对于涉及FreeRTOS的代码,我尽可能使用FreeRTOS建议的编码风格,与FreeRTOS无关的代码,我仍然使用自己的编码风格。我可以保证,两种编码风格决不会影响程序的可读性,编写良好可读性的代码,是我一直注重并坚持的。
命令行解释器使用一个硬件串口,需要外部提供两个串口底层函数:一个是串口初始化函数init_cmd_uart(),用于初始化串口波特率、中断等事件;另一个是发送单个字符函数my_putc()。此外,命令行为串口接收中断服务程序提供函数fill_rec_buf(),用于保存接收到的字符,当收到回车换行符后,该函数向命令行分析任务发送通知。
类printf函数用来格式化输出,我一般用来辅助调试,为了方便的将调试代码从程序中去除,需要将类printf函数进行封装。我的文章《编写优质嵌入式C程序》第5.2节给出了一个完整的类printf函数实现和封装代码,最终我们使用到的类printf函数是如下形式的宏:
我们将会创建一个任务,用来分析接收到的命令,如果命令有效则调用命令实现函数。这个任务名字为vTaskCmdAnalyze()。串口接收中断用于接收命令,如果接收到回车换行符,则向任务vTaskCmdAnalyze()发送任务通知,表明已经接收到一条完整命令,任务可以去处理了。
示意框图如图3-1所示。
命令行解释器程序需要涉及两个数据结构:一个与命令有关,包括命令的名字、命令的最大参数数目、命令的回调函数类型、命令帮助信息等;另一个与分析命令有关,包括接收命令字符缓冲区、存放参数缓冲区等。
定义如下:
需要说明一下命令回调函数的参数,argc保存接收到的参数数目,cmd_arg指向参数缓冲区,目前只支持32位的整形参数,这在绝大多数嵌入式场合是足够的。
定义如下:
缓冲区的大小使用宏来定义,通过更改相应的宏定义,可以设置整条命令的最大长度、命令参数最大数目等。
本文使用的串口软件是SecureCRT,在这个软件下敲击的任何键盘字符,都会立刻通过串口硬件发送出去,这与Telnet类似。所以我们无需使用串口的FIFO,每接收到一个字符就产生一次中断。串口中断与硬件关系密切,所以命令行解释器提供了一个与硬件无关的函数fill_rec_buf(),每当串口中断接收到一个字符,就以收到的字符为参数调用这个函数。 fill_rec_buf()函数主要操作变量cmd_analyze,变量的声明原型为:
函数fill_rec_buf()的实现代码为:
命令行分析任务大部分时间都会因为等待任务通知而处于阻塞状态。当接收到一个通知后,任务首先去除命令行中的无效字符和控制字符,然后找出命令名并分析参数数目、将参数转换成十六进制数并保存到参数缓冲区中,最后检查命令名和参数是否合法,如果合法则调用命令回调函数处理本条命令。
串口软件SecureCRT支持控制字符。比如在输入一串命令的时候,发现某个字符输入错误,就要使用退格键或者左右移动键定位到错误的位置进行修改。这里的退格键和左右移动键都属于控制字符,比如退格键的键值为0x08、左移键的键值为0x1B0x5B 0x44。我们之前也说过,在软件SecureCRT中输入字符时,每敲击一个字符,该字符立刻通过串口发送给我们的嵌入式设备,也就是所有键值都会按照敲击键盘的顺序存入到接收缓冲区中,但这里面可能有我们不需要的字符,我们首先需要利用控制字符将不需要的字符删除掉。这个工作由函数get_true_char_stream()实现,代码如下所示:
接收到的命令中可能带有参数,我们需要知道参数的数目,还需要把字符型的参数转换成整形数并保存到参数缓冲区(这是因为命令回调函数需要这两个参数)。这个工作由函数cmd_arg_analyze()实现,代码如下所示:
在这个函数cmd_arg_analyze()中,调用了字符转整形函数string_to_dec()。我们只支持整形参数,这里给出一个字符转整形函数的简单实现,可以识别负号和十六进制的前缀’0x’。在这个函数中调用了三个C库函数,分别是isdigit()、isxdigit()和tolower(),因此需要包含头文件#include
我们举两个例子:第一个是不带参数的例子,输入命令后,函数返回一个“Helloworld!”字符串;第二个是带参数的例子,我们输入命令和参数后,函数返回每一个参数值。我们在讲数据结构的时候特别提到过命令回调函数的原型,这里要根据这个函数原型来声明命令回调函数。
在讲数据结构的时候,我们定义了与命令有关的数据结构。每条命令需要包括命名名、最大参数、命令回调函数、帮助等信息,这里要将每条命令组织成列表的形式。
如果要定义自己的命令,只需要按照6.3节的格式编写命令回调函数,然后将命令名、参数数目、回调函数和帮助信息按照本节格式加入到命令表中即可。
有了上面的基础,命令行分析任务实现起来就非常轻松了,源码如下:
推荐使用SecureCRT软件,这是我觉得最适合命令行交互的串口工具。此外,这个软件非常强大,除了支持串口,还支持SSH、Telnet等。对于串口,SecureCRT工具还支持文件发送协议:Xmodem、Ymodem和Zmodem。这在使用串口远程升级时很有用,可以用来发送新的程序二进制文件。我曾经使用Ymodem做过远程升级,以后有时间再详细介绍SecureCRT的Ymodem功能细节。
要用于本文介绍的命令行解释器,要对SecureCRT软件做一些设置。
选择Serial功能、设置端口、波特率、校验等,特别要注意的是不要勾选任何流控制选项,如图2-1所示。
图2-1:设置串口参数
依次点击菜单栏的“选项”---“会话选项”,在弹出的“会话选项”界面中,点击左边树形菜单的“终端”---“仿真”---“模式”,在右边的仿真模式区域选中“换行”和“新行模式”,如图2-2所示。
图2-2:设置新行模式
依次点击菜单栏的“选项”---“会话选项”,在弹出的“会话选项”界面中,点击左边树形菜单的“终端”---“仿真”---“高级”,在右边的“高级仿真”区域,选中“本地回显”,如图2-3所示。
图2-3:设置本地回显
我们通过6.3节和6.4接定义了两个命令,第一条命令的名字为”hello”,这是一个无参数命令,直接输出字符串”Hello world!”。第二条命令的名字为”arg”,是一个带参数命令,输出每个参数的值。下面对这两个命令进行测试。
设置好SecureCRT软件,输入字符”hello”后,按下回车键,设备会返回字符串”Hello world!”。如图8-1所示。
图8-1:无参数命令测试
设置好SecureCRT软件,输入字符”arg 1 2 -3 0x0a”后,按下回车键,设备会返回每个参数值。如图8-2所示。
图8-2:带参数命令测试
使用RTOS编程,为每个任务分配多大的堆栈空间就成了一项技术活:分配多了浪费系统资源,分配少了又恐怕会发生堆栈溢出。由于中断和抢占式调度器的存在,我们要估算出一个任务需要多少堆栈是非常困难的,今天我们就介绍一种方法,来获取每个任务的剩余堆栈空间。本文以NXP LPC177x_8x系列微控制器为例。
我们将这个功能做成一个命令,添加到《FreeRTOS系列第15篇---使用任务通知实现命令行解释器》一文介绍的命令解释列表中。当程序运行一段时间后,我们在SecureCRT软件中输入命令“task”后回车,能看到如图1-1所示的任务信息。这里只有两个任务,其中堆栈一列中的数字,代表对应任务剩余的堆栈空间,单位是StackType_t类型,这个类型在移植层定义,一般定义为4字节。
图1-1:任务信息
如图1-1所示,要实现堆栈使用量信息以及CPU使用率信息,必须将FreeRTOSConfig.h文件中的两个宏设置为1:
第一个宏用来使能可视化追踪功能,第二个宏用来使能运行时间统计功能。如果第二个宏设置为1,则下面两个宏必须被定义:
我们使用定时器1来产生基准时钟,定时器1初始化函数为:
定时器1被配置成每隔500微秒,TC寄存器值增一。我们将定时器1的 TC寄存器值作为基准时钟当前时间。当TC寄存器值溢出时,大概要经过24.8天,这对于我们这个应用是足够的。
在FreeRTOSConfig.h中,定义初始化基准定时器宏和获取当前时间宏:
获取每个任务的状态信息使用的是API函数uxTaskGetSystemState(),该函数定义为:
函数uxTaskGetSystemState()向TaskStatus_t结构体填充相关信息,系统中每一个任务的信息都可以填充到TaskStatus_t结构体数组中,数组大小由uxArraySize指定。结构体TaskStatus_t定义如下:
注意,这个函数仅用来调试用,调用此函数会挂起所有任务,直到函数结束后才恢复挂起的任务,因此任务可能被挂起很长时间。在文件FreeRTOSConfig.h中,宏configUSE_TRACE_FACILITY必须设置为1,此函数才有效。
由于我们不使用动态内存分配策略,所以实现定义了最大任务个数并预先分配好了存储任务状态信息的数组:
正确调用函数uxTaskGetSystemState()后,任务的信息会被放在TaskStatus_t结构体中,我们需要将这些信息格式化为容易阅读的形式,并共通过串口打印到屏幕。完成这些功能的函数叫做get_task_state(),代码如下所示:
在《FreeRTOS系列第15篇---使用任务通知实现命令行解释器》一文我们讲过了命令表,这里只需要将get_task_state()函数添加到命令列表中,命令设置为”task”,代码如下所示:
队列是主要的任务间通讯方式。可以在任务与任务间、中断和任务间传送信息。大多数情况下,队列用于具有线程保护的FIFO(先进先出)缓冲区:新数据放在队列的后面。当然,数据也可以放在队列的前面,在下一篇讲队列API函数时,会涉及到数据的存放位置。
图1-1:读写队列
图1-1所示的队列中,最多能保存5个项目,并且假设队列永远不会满。任务A使用API函数xQueueSendToBack()向队列发送数据,每次发送一个数据,新入队的数据置于上一次入队数据的后面。任务B使用API函数xQueueReceive()将数据从队列取出,先入队的数据先出队。
通常情况下,鱼和熊掌是不可兼得的,但FreeRTOS的队列用户模型管理却兼顾简单和灵活。发送到队列的消息是通过拷贝实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。FreeRTOS队列具有以下特性:
API函数允许指定阻塞时间。
每当任务企图从一个空的队列读取数据时,任务会进入阻塞状态(这样任务不会消耗任何CPU时间并且另一个任务可以运行)直到队列中出现有效数据或者阻塞时间到期。
每当任务企图向一个满的队列写数据时,任务会进入阻塞状态,直到队列中出现有效空间或者阻塞时间到期。
如果多个任务阻塞在一个队列上,那么最高优先级别的任务会第一个解除阻塞。
注:中断程序中绝不可以使用不带“FromISR”结尾的API函数!
总结一下队列的基本用法:
以上使用的API函数将在下一篇文章中介绍。
FreeRTOS为操作队列提供了非常丰富的API函数,包括队列的创建、删除,灵活的入队和出队方式、带中断保护的入队和出队等等。下面就来详细讲述这些API函数。
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
返回队列中存储的信息数目。具有中断保护的版本为uxQueueMessagesWaitingFromISR(),原型为:UBaseType_t uxQueueMessagesWaitingFromISR( const QueueHandle_t xQueue )。
UBaseType_t uxQueueSpacesAvailable( QueueHandle_t xQueue );
返回队列的空闲数目。
void vQueueDelete( QueueHandle_t xQueue );
删除队列并释放所有分配给队列的内存。
3.2参数描述
BaseType_t xQueueReset( QueueHandle_t xQueue );
将队列复位到初始状态。
FreeRTOSV7.2.0以及以后的版本总是返回pdPASS。
QueueHandle_t xQueueCreate (UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
创建新队列。为新队列分配指定的存储空间并返回队列句柄。
成功创建队列返回队列句柄,否自返回0。
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
其实是一个宏,真正被调用的函数是xQueueGenericSend()。定义这个宏是为了向后兼容那些不包含函数xQueueSendToFront()和xQueueSendToBack()宏的FreeRTOS版本。它与xQueueSendToBack()等同。
这个宏向队列尾部投递一个队列项。项目以拷贝的形式入队,而不是引用形式入队。绝不可以在中断服务例程中调用这个宏,使用带有中断保护的版本xQueueSendFromISR()来完成相同的功能。
6.2参数描述
队列项入队成功返回pdTRUE,否则返回errQUEUE_FULL。
其实是一个宏,真正被调用的函数是xQueueGenericSendFromISR()。这个宏是xQueueSend()的中断保护版本,用于中断服务程序,等价于xQueueSendToBackFromISR()。
在中断服务例程中向队列尾部投递一个队列项。
列项入队成功返回pdTRUE,否则返回errQUEUE_FULL。
其实是一个宏,真正被调用的函数是xQueueGenericSend()。这个宏等价于xQueueSend()。
向队列尾投递一个队列项。绝不可以在中断中调用这个宏,可以使用带有中断保护的版本xQueueSendToBackFromISR ()来完成相同功能。
同xQueueSend()。
同xQueueSend()。
同xQueueSend()。
其实是一个宏,真正被调用的函数是xQueueGenericSendFromISR()。这个宏是xQueueSendToBack()的中断保护版本,用于中断服务程序,等价于xQueueSendFromISR()。
在中断服务例程中向队列尾部投递一个队列项。
同QueueSendFromISR()。
同QueueSendFromISR()。
同QueueSendFromISR()。
其实是一个宏,真正被调用的函数是xQueueGenericSend()。
这个宏向队列首部投递一个队列项。绝不可以在中断服务例程中调用这个宏,可以使用带有中断保护的版本xQueueSendToFrontFromISR ()来完成相同功能。
队列项入队成功返回pdTRUE,否则返回errQUEUE_FULL。
其实是一个宏,真正被调用的函数是xQueueGenericSendFromISR()。这个宏是xQueueSendToFront ()的中断保护版本,用于中断服务程序。
列项入队成功返回pdTRUE,否则返回errQUEUE_FULL。
其实是一个宏,真正被调用的函数是xQueueGenericReceive()。
这个宏从队列中读取一个队列项并把该队列项从队列中删除。读取队列项是以拷贝的形式完成,而不是以引用的形式,因此必须提供足够大的缓冲区以便容纳队列项。参数pvBuffer指向这个缓冲区。
绝不可以在中断服务例程中调用这个宏,可以使用使用带有中断保护的版本xQueueReceiveFromISR来完成相同功能。
成功接收到列表项返回pdTRUE,否则返回pdFALSE。
从队列中读取一个队列项并把该队列项从队列中删除。功能与xQueueReceive()相同,用于中断服务函数。
成功接收到列表项返回pdTRUE,否则返回pdFALSE。
其实是一个宏,真正被调用的函数是xQueueGenericReceive()。
这个宏从队列中读取一个队列项,但不会把该队列项从队列中移除。这个宏绝不可以用在中断服务例程中,可以使用使用带有中断保护的版本xQueuePeekFromIS()来完成相同功能。
同xQueueReceive()。
同xQueueReceive()。
同xQueueReceive()。
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer,);
功能与xQueuePeek()相同,用于中断服务程序。
成功接收到列表项返回pdTRUE,否则返回pdFALSE。
void vQueueAddToRegistry(QueueHandle_t xQueue, char *pcQueueName,);
为队列分配名字并进行注册。
队列注册有两个目的,这两个目的都是为了调试RTOS内核:
队列注册仅用于调试器。
宏configQUEUE_REGISTRY_SIZE定义了可以注册的队列和信号量的最大数量。仅当你想使用可视化调试内核时,才进行队列和信号量注册。
void vQueueUnregisterQueue(QueueHandle_t xQueue);
从队列注册表中移除指定的队列。
17.2参数描述
BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue );
查询队列是否为空。这个函数仅用于ISR。
队列非空返回pdFALSE,其它值表示队列为空。
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue );
查询队列是否满,仅用于ISR。
队列没有满返回pdFALSE,其它值表示队列满。
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void * pvItemToQueue);
其实是个宏,真正被调用的函数是xQueueGenericSend()。这个宏是xQueueSendToBack()的另一个版本,向队列尾投递一个队列项,如果队列已满,则覆盖之前的队列项。一般用于只有一个队列项的队列中,如果队列的队列项超过1个,使用这个宏会触发一个断言(已经正确定义configASSERT()的情况下)。这个宏绝不可以在中断服务程序中调用,可以使用使用带有中断保护的版本xQueueOverwriteFromISR()来完成相同功能。
总是返回pdPASS。
其实是个宏,真正被调用的函数是xQueueGenericSendFromISR()。这个宏的功能与xQueueOverwrite()相同,用在中断服务程序中。
总是返回pdPASS。
FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。
我们可以把互斥量和递归互斥量看成特殊的信号量。互斥量和信号量在用法上不同:
二进制信号量既可以用于互斥功能也可以用于同步功能。
二进制信号量和互斥量非常相似,但是有一些细微差别:互斥量包含一个优先级继承机制,二进制信号量则没有这个机制。这使得二进制信号量更好的用于实现同步(任务间或任务和中断间),互斥量更好的用于实现简单互斥。本节仅描述用于同步的二进制信号量。
信号量API函数允许指定一个阻塞时间。当任务企图获取一个无效信号量时,任务进入阻塞状态,阻塞时间用来确定任务进入阻塞的最大时间,阻塞时间单位为系统节拍周期时间。如果有多个任务阻塞在同一个信号量上,那么当信号量有效时,具有最高优先级别的任务最先解除阻塞。
可以将二进制信号量看作只有一个项目(item)的队列,因此这个队列只能为空或满(因此称为二进制)。任务和中断使用队列无需关注谁控制队列---只需要知道队列是空还是满。利用这个机制可以在任务和中断之间同步。
考虑这样一种情况,一个任务用来维护外设。使用轮询的方法会浪费CPU资源并且妨碍其它任务执行。更好的做法是任务的大部分时间处于阻塞状态(允许其它任务执行),直到某些事件发生该任务才执行。可以使用二进制信号量实现这种应用:当任务取信号量时,因为此时尚未发生特定事件,信号量为空,任务会进入阻塞状态;当外设需要维护时,触发一个中断服务例程,该中断服务仅仅给出信号量(向队列写数据)。任务只是取信号,并不需要归还,中断服务只是给信号。
任务的优先级可以用于确保外设及时获得维护。还可以使用队列来代替二进制信号量。中断例程可以捕获与外设事件相关的数据并将它发往任务的队列。任务发现队列数据有效时解除阻塞,如果需要,则进行数据处理。第二种方案使得中断执行尽可能的短,其它处理过程可以在任务中实现。
注:中断程序中决不可使用无“FromISR”结尾的API函数。
注:在大部分应用场合,任务通知都可以代替二进制信号量,并且速度更快、生成的代码更少。
图1-1:中断和任务之间同步---使用信号量
如图1-1所示,程序开始运行时,信号量无效,因此任务阻塞在这个信号量下。一段时间后,一个中断发生,在中断服务程序中使用API函数xSemaphoreGiveFromISR()给出了一个信号,信号量变得有效。当退出中断服务程序后,执行上下文切换,任务解除阻塞,使用API函数xSemaphoreTake()取走信号量并执行任务。之后信号量变得无效,任务再次进入阻塞。
二进制信号量可以被认为是长度为1的队列,计数信号量则可以被认为长度大于1的队列。此外,信号量使用者不必关心存储在队列中的数据,只需关心队列是否为空。
通常计数信号量用于下面两种事件:
注:中断程序中决不可使用无“FromISR”结尾的API函数。
注:在大部分应用场合,任务通知都可以代替计数信号量,并且速度更快、生成的代码更少。
互斥量是一个包含优先级继承机制的二进制信号量。用于实现同步(任务之间或者任务与中断之间)的话,二进制信号量是更好的选择,互斥量用于简单的互锁。
用于互锁的互斥量可以充当保护资源的令牌。当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问同一资源。
互斥量和信号量使用相同的API函数,因此互斥量也允许指定一个阻塞时间。阻塞时间单位为系统节拍周期时间,数目表示获取互斥量无效时最多处于阻塞状态的系统节拍周期个数。
互斥量与二进制信号量最大的不同是:互斥量具有优先级继承机制。也就是说,如果一个互斥量(令牌)正在被一个低优先级任务使用,此时一个高优先级企图获取这个互斥量,高优先级任务会因为得不到互斥量而进入阻塞状态,正在使用互斥量的低优先级任务会临时将自己的优先级提升,提升后的优先级与与进入阻塞状态的高优先级任务相同。这个优先级提升的过程叫做优先级继承。这个机制用于确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”影响降低到最小。
在很多场合中,某个硬件资源只有一个,当低优先级任务占用该资源的时候,即便高优先级任务也只能乖乖的等待低优先级任务释放资源。这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
为什么优先级继承能够降低优先级翻转的影响呢?举个例子,现在有任务A、任务B和任务C,三个任务的优先级顺序为任务C>任务B>任务A。任务A和任务C都要使用某一个硬件资源,并且当前任务A占有该资源。
先看没有优先级继承的情况:任务C也要使用该资源,但是此时任务A正在使用这个资源,因此任务C进入阻塞,此时三个任务的优先级顺序没有发生变化。在任务C进入阻塞之后,某硬件产生了一次中断,唤醒了一个事件,该事件可以解除任务B的阻塞状态。在中断结束后,因为任务B的优先级是大于任务A的,所以任务B抢占任务A的CPU权限。那么任务C的阻塞时间就至少为:中断处理时间+任务B的运行时间+任务A的运行时间。
再看有优先级继承的情况:任务C也要使用该资源,但是此时任务A正在使用这个资源,因此任务C进入阻塞,此时由于优先级A会继承任务C的优先级,三个任务的优先级顺序发生了变化,新的优先级顺序为:任务C=任务A>任务B。在任务C进入阻塞之后,某硬件产生了一次中断,唤醒了一个事件,该事件可以解除任务B的阻塞状态。在中断结束后,因为任务A的优先级临时被提高,大于任务B的优先级,所以任务A继续获得CPU权限。任务A完成后,处于高优先级的任务C会接管CPU。所以任务C的阻塞时间为:中断处理时间+任务A的运行时间。看,任务C的阻塞时间变小了,这就是优先级继承的优势。
优先级继承不能解决优先级反转,只能将这种情况的影响降低到最小。硬实时系统在一开始设计时就要避免优先级反转发生。
图4-1 互斥量用于保护资源
如图4-1所示,互斥量用来保护资源。为了访问资源,任务必须先获取互斥量。任务A想获取资源,首先它使用API函数xSemaphoreTake()获取信号量,成功获取到信号量后,任务A就持有了互斥量,可以安全的访问资源。期间任务B开始执行,它也想访问资源,任务B也要先获得信号量,但是信号量此时是无效的,任务B进入阻塞状态。当任务A执行完成后,使用API函数xSemaphoreGive()释放信号量。之后任务B解除阻塞,任务B使用API函数xSemaphoreTake()获取并得到信号量,任务B可以访问资源。
已经获取递归互斥量的任务可以重复获取该递归互斥量。使用xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态。比如,某个任务成功获取5次递归互斥量,那么在它没有返还5次该递归互斥量之前,这个互斥量对别的任务无效。
递归互斥量可以看成带有优先级继承机制的信号量,获取递归互斥量的任务在用完后必须返还。
互斥量不可以用在中断服务程序中,这是因为:
互斥量具有优先级继承机制,只有在任务中获取或给出互斥才有意义。
中断不能因为等待互斥量而阻塞。
FreeRTOS的信号量包括二进制信号量、计数信号量、互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量)。我们可以把互斥量和递归互斥量看成特殊的信号量。
信号量API函数实际上都是宏,它使用现有的队列机制。这些宏定义在semphr.h文件中。如果使用信号量或者互斥量,需要包含semphr.h头文件。
二进制信号量、计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的;递归互斥信号量的创建、获取和释放API函数都是独立的。
SemaphoreHandle_t xSemaphoreCreateBinary( void );
这个函数用于创建一个二进制信号量。二进制信号量要么有效要么无效,这也是为什么叫做二进制的原因。
新创建的信号量处于无效状态,这意味着使用API函数xSemaphoreTake()获取信号之前,需要先给出信号。
二进制信号量和互斥量非常相似,但也有细微的区别:互斥量具有优先级继承机制,二进制信号量没有这个机制。这使得二进制信号量更适合用于同步(任务之间或者任务和中断之间),互斥量更适合互锁。
一旦获得二进制信号量后不需要恢复,一个任务或中断不断的产生信号,而另一个任务不断的取走这个信号,通过这样的方式来实现同步。
低优先级任务拥有互斥量的时候,如果另一个高优先级任务也企图获取这个信号量,则低优先级任务的优先级会被临时提高,提高到和高优先级任务相同的优先级。这意味着互斥量必须要释放,否则高优先级任务将不能获取这个互斥量,并且那个拥有互斥量的低优先级任务也永远不会被剥夺,这就是操作系统中的优先级翻转。
互斥量和二进制信号量都是SemaphoreHandle_t类型,并且可以用于任何具有这类参数的API函数中。
创建计数信号量,计数信号量通常用于以下两种情况:
NULL表示信号量创建失败,否则返回信号量句柄。
SemaphoreHandle_t xSemaphoreCreateMutex( void )
创建互斥量。可以使用API函数xSemaphoreTake()和xSemaphoreGive()访问互斥量,但是绝不可以用xSemaphoreTakeRecursive()和xSemaphoreGiveRecursive()访问。
二进制信号量和互斥量非常相似,但也有细微的区别:互斥量具有优先级继承机制,二进制信号量没有这个机制。这使得二进制信号量更适合用于同步(任务之间或者任务和中断之间),互斥量更适合互锁。
一旦获得二进制信号量后不需要恢复,一个任务或中断不断的产生信号,而另一个任务不断的取走这个信号,通过这样的方式来实现同步。
低优先级任务拥有互斥量的时候,如果另一个高优先级任务也企图获取这个信号量,则低优先级任务的优先级会被临时提高,提高到和高优先级任务相同的优先级。这意味着互斥量必须要释放,否则高优先级任务将不能获取这个互斥量,并且那个拥有互斥量的低优先级任务也永远不会被剥夺,这就是操作系统中的优先级翻转。
互斥量和二进制信号量都是SemaphoreHandle_t类型,并且可以用于任何具有这类参数的API函数中。
NULL表示信号量创建失败,否则返回信号量句柄。
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )
用于创建递归互斥量。被创建的互斥量可以被API函数xSemaphoreTakeRecursive()和xSemaphoreGiveRecursive()使用,但不可以被API函数xSemaphoreTake()和xSemaphoreGive()使用。
递归类型的互斥量可以被拥有者重复获取。拥有互斥量的任务必须调用API函数xSemaphoreGiveRecursive()将拥有的递归互斥量全部释放后,该信号量才真正被释放。比如,一个任务成功获取同一个互斥量5次,那么这个任务要将这个互斥量释放5次之后,其它任务才能获取到它。
递归互斥量具有优先级继承机制,因此任务获得一次信号后必须在使用完后做一个释放操作。
互斥量类型信号不可以用在中断服务例程中。
NULL表示互斥量创建失败,否则返回互斥量句柄。
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
删除信号量。如果有任务阻塞在这个信号量上,则这个信号量不要删除。
xSemaphore:信号量句柄
xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait)
获取信号量。信号量必须是通过API函数xSemaphoreCreateBinary()、xSemaphoreCreateCounting()和xSemaphoreCreateMutex()预先创建过的。注意,递归互斥量类型信号量不能使用该函数、不用在中断服务程序中使用该函数。
成功获取到信号量返回pdTRUE,否则返回pdFALSE。
API函数xSemaphoreTake()的另一版本,用于中断服务程序。
信号量成功获取返回pdTRUE,否则返回pdFALSE。
xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait );
获取递归互斥信号量。互斥量必须是通过API函数xSemaphoreCreateRecursiveMutex()创建的类型。
文件FreeRTOSConfig.h中的宏configUSE_RECURSIVE_MUTEXES必须设置成1,此函数才有效。
已经获取递归互斥量的任务可以重复获取该递归互斥量。使用xSemaphoreTakeRecursive() 函数成功获取几次递归互斥量,就要使用xSemaphoreGiveRecursive()函数返还几次,在此之前递归互斥量都处于无效状态。比如,某个任务成功获取5次递归互斥量,那么在它没有返还5次该递归互斥量之前,这个互斥量对别的任务无效。
成功获取递归互斥量返回pdTURE,否则返回pdFALSE。
xSemaphoreGive(SemaphoreHandle_t xSemaphore )
用于释放一个信号量。信号量必须是API函数xSemaphoreCreateBinary()、xSemaphoreCreateCounting()或xSemaphoreCreateMutex() 创建的。必须使用API函数xSemaphoreTake()获取这个信号量。
这个函数绝不可以在中断服务例程中使用,可以使用带中断保护版本的API函数xSemaphoreGiveFromISR()来实现相同功能。
这个函数不能用于使用API函数xSemaphoreCreateRecursiveMutex()所创建的递归互斥量。
信号量释放成功返回pdTRUE,否则返回pdFALSE。
释放信号量。是API函数xSemaphoreGive()的另个版本,用于中断服务程序。信号量必须是通过API函数xSemaphoreCreateBinary()或xSemaphoreCreateCounting()创建的。这里没有互斥量,是因为互斥量不可以用在中断服务程序中。
xSemaphore:信号量句柄
pxHigherPriorityTaskWoken:如果*pxHigherPriorityTaskWoken为pdTRUE,则需要在中断退出前人为的经行一次上下文切换。从FreeRTOS
V7.3.0开始,该参数为可选参数,并可以设置为NULL。
成功释放信号量返回pdTURE,否则返回errQUEUE_FULL。
xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex )
释放一个递归互斥量。互斥量必须是使用 API函数xSemaphoreCreateRecursiveMutex()创建的。文件FreeRTOSConfig.h中宏configUSE_RECURSIVE_MUTEXES必须设置成1本函数才有效。
如果递归互斥量释放成功,返回pdTRUE。
见“8 获取递归互斥量”。
TaskHandle_t xSemaphoreGetMutexHolder( SemaphoreHandle_t xMutex );
返回互斥量持有任务的句柄(如果有的话),互斥量由参数xMutex指定。
如果调用此函数的任务持有互斥量,那么可以可靠的返回任务句柄,但是如果是别的任务持有互斥量,则不总可靠。
文件FreeRTOSConfig.h中宏configUSE_MUTEXES必须设置成1本函数才有效。
返回互斥量持有任务的句柄。如果参数xMutex不是互斥类型信号量或者虽然互斥量有效但这个互斥量不被任何任务持有则返回NULL。
这是FreeRTOS基础篇的最后一篇博文,到这里我们已经可以移植、熟练使用FreeRTOS了。但如果想知道FreeRTOS背后的运行机制,这些是远远不够的,下面要走的路还会很长。要不要了解FreeRTOS背后运行机制,全凭各位的兴趣,毕竟我们即使不清楚汽车的构造细节,但只要掌握驾驶技巧也可以很好的开车的。使用RTOS也与之相似,只要我们掌握了基础篇的那些知识,我们已经可以很好的使用FreeRTOS了。探索FreeRTOS背后运行的机制,是我们对未知事件的好奇,也是我们相信理解了FreeRTOS运行机制,可以让我们更优雅、更少犯错、更举重若轻的的使用RTOS。
FreeRTOS高级篇已经开始写了,可以点击这里擦看最新的文章列表
FreeRTOSV9.0.0发行于2016.05.25,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
在FreeRTOS V9.0.0rc2版本中,xTaskCreateStatic()原型为:
在FreeRTOS V9.0.0正式版中,xTaskCreateStatic()原型为:
注:博主看过FreeRTOS V9.0.0rc2版,也看过V9.0.0正式版,发现两个版本函数xTaskCreateStatic()是相同的。
FreeRTOSV9.0.0rc2发行于2016.03.30,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV9.0.0rc1发行于2016.02.19,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.2.3发行于2015.10.16,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载 ,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.2.2发行于2015.08.12,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.2.1发行于2015.03.24,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载 ,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.2.0发行于2015.01.16,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载 ,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.1.2发行于2014.09.02,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载 ,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
FreeRTOSV8.1.1发行于2014.08.29,推荐官方网站下载。考虑到官方网站的服务器在美国,下载速度较慢,下面给出CSDN下载地址:点击下载,下载需要1个资源分,请原谅博主需要资源分增加上传权限。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章