详情请进入 湖南阳光电子学校 已关注:人 咨询电话:13807313137 微信号:yp941688, yp94168
嵌入式系统不只是ARM+Linux,不是只有安卓,凡是电子产品都可称为嵌入式系统。物联网行业的兴起,也提升了FreeRTOS市场占有率。本文就是介绍FreeRTOS基础及其应用,只是个人整理,可能存在问题,其目的只是简要介绍系统的基础,只能作为入门资料。
目录
一、 为什么要学习RTOS
二、 操作系统基础
三、 初识 FreeRTOS
四、 任务
五、 队列
六、 软件定时器
七、 信号量
八、 事件
九、 任务通知
十、 内存管理
十一、 通用接口
一、 为什么要学习 RTOS
进入嵌入式这个领域,入门首先接触的是单片机编程,尤其是C51 单片机来,基础的单片机编程通常都是指裸机编程,即不加入任何 RTOS(Real Time Operating System 实时操作系统)。常用的有国外的FreeRTOS、μC/OS、RTX 和国内的 RT-thread、Huawei LiteOS 和 AliOS-Things 等,其中开源且免费的 FreeRTOS 的市场占有率较高。
1.1 前后台系统在裸机系统中,所有的操作都是在一个无限的大循环里面实现,支持中断检测。外部中断紧急事件在中断里面标记或者响应,中断服务称为前台,main 函数里面的while(1)无限循环称为后台,按顺序处理业务功能,以及中断标记的可执行的事件。小型的电子产品用的都是裸机系统,而且也能够满足需求。
多任务系统的事件响应也是在中断中完成的,但是事件的处理是在任务中完成的。如果事件对应的任务的优先级足够高,中断对应的事件会立刻执行。相比前后台系统,多任务系统的实时性又被提高了。
在多任务系统中,根据程序的功能,把这个程序主体分割成一个个独立的,无限循环且不能返回的子程序,称之为任务。每个任务都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。加入操作系统后,开发人员不需要关注每个功能模块之间的冲突,重心放在子程序的实现。缺点是整个系统随之带来的额外RAM开销,但对目前的单片机的来影响不大。
学习 RTOS,一是项目需要,随着产品要实现的功能越来越多,单纯的裸机系统已经不能完美地解决问题,反而会使编程变得更加复杂,如果想降低编程的难度,就必须引入 RTOS实现多任务管理。二是技能需要,掌握操作系统,和基于RTOS的编程,实现更好的职业规划,对个人发展尤其是钱途是必不可少的。
以前一直觉得学操作系统就必须是linux,实际每个系统都有其应用场景,对于物联网行业,杀鸡焉用牛刀,小而美,且应用广泛的FreeRTOS 是首选。有一个操作系统的基础,即使后续基于其他系统开发软件,也可触类旁通,对新技术快速入门。目前接触的几款芯片都是基于FreeRTOS。
如何学习RTOS? 简单的就是在别人移植好的系统之上,看看 RTOS 里面的 API 使用说明,然后调用这些 API 实现自己想要的功能即可。完全不用关心底层的移植,这是 简单快速的入门方法。这种学习方式,如果是做产品,可以快速的实现功能,弊端是当程序出现问题的时候,如果对RTOS不够了解,会导致调试困难,无从下手。
各种RTOS内核实现方式都差不多,我们只需要深入学习其中一款就行。万变不离其宗,正如掌握了C51基础,后续换其他型号或者更高级的ARM单片机,在原理和方法上,都是有借鉴意义,可以比较快的熟悉并掌握新单片机的使用。
二、 操作系统基础 2.1 链表链表作为 C 语言中一种基础的数据结构,在平时写程序的时候用的并不多,但在操作系统里面使用的非常多。FreeRTOS 中存在着大量的基础数据结构链表和链表项的操作(list 和 list item)。FreeRTOS 中与链表相关的操作均在 list.h 和 list.c 这两个文件中实现。
链表比数组, 大优势是占用的内存空间可以随着需求扩大或缩小,动态调整。实际FreeRTOS中各种任务的记录都是依靠链表动态管理,具体的可以参考源码的任务控制块tskTCB。任务切换状态,就是将对应的链表进行操作,链表操作涉及创建和插入、删除和查找。
2.2 队列队列是一种只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。队尾放入数据,对头挤出。先进先出,称为FIFO。
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。
就绪(Ready):该任务在就绪列表中,就绪的任务已经具备执行的能力,只等待调度器进行调度,新创建的任务会初始化为就绪态。
运行(Running):该状态表明任务正在执行,此时它占用处理器,调度器选择运行的永远是处于 高优先级的就绪态任务。
阻塞(Blocked):任务当前正在等待某个事件,比如信号量或外部中断。
挂起态(Suspended):处于挂起态的任务对调度器而言是不可见的。
挂起态与阻塞态的区别,当任务有较长的时间不允许运行的时候,我们可以挂起任务,这样子调度器就不会管这个任务的任何信息,直到调用恢复任务的 接口;而任务处于阻塞态的时候,系统还需要判断阻塞态的任务是否超时,是否可以解除阻塞。
各任务运行时使用消息、信号量等方式进行通信,不能是全局变量。任务通常会运行在一个死循环中,不会退出,如果不再需要,可以调用删除任务。
2.4 临界区临界区就是一段在执行的时候不能被中断的代码段。在多任务操作系统里面,对全局变量的操作不能被打断,不能执行到一半就被其他任务再次操作。一般被打断,原因就是系统调度或外部中断。对临界区的保护控制,归根到底就是对系统中断的使能控制。在使用临界区时,关闭中断响应,对部分优先级的中断进行屏蔽,因此临界区不允许运行时间过长。为了对临界区进行控制,就需要使用信号量通信,实现同步或互斥操作。
三、 初识 FreeRTOS 3.1 FreeRTOS源码FreeRTOS 由美国的 Richard Barry 于 2003 年发布, 2018 年被亚马逊收购,改名为 AWS FreeRTOS,版本号升级为 V10,支持MIT开源协议,亚马逊收购 FreeRTOS 也是为了进入物联网和人工智能,新版本增加了物联网行业的网络协议等功能。
FreeRTOS 是开源免费的,可从官网 下载源码和说明手册。例如展锐的UIS8910使用的是V10。以FreeRTOSv10.4.1为例,含 Demo 例程,Source内核的源码,License许可文件。
3.1.1 Source 文件夹FreeRTOS/ Source 文件夹下的文件:
括FreeRTOS 的通用的头文件include和 C 文件,括任务、队列、定时器等,适用于各种编译器和处理器,是通用的。
需要特殊处理适配的在portblle文件夹,其下内容与编译器和处理器相关, FreeRTOS 要想运行在一个单片机上面,它们就必须关联在一起,通常由汇编和 C 联合编写。通常难度比较高,不过一般芯片原厂提供移植好的接口文件。这里不介绍移植的方法,因为自己也不明白。
Portblle/MemMang 文件夹下存放的是跟内存管理相关的,总共有五个 heap 文件,有5种内存动态分配方式,一般物联网产品选用 heap4.c 。
3.1.2 Demo 文件夹里面含了 FreeRTOS 官方为各个单片机移植好的工程代码,FreeRTOS 为了推广自己,会给针对不同半导体厂商的评估板实现基础功能范例, Demo下就是参考范例。
3.1.3 FreeRTOSConfig.h配置FreeRTOSConfig.h头文件对FreeRTOS 所需的功能的宏均做了定义,需要根据应用情况配置合适的参数,其作用类似MTK功能机平台的主mak文件,部分定义如下:
1.#defineconfigUSE_PREEMPTION1例如系统时钟tick等参数在就这个文件配置,具体作用可以看注释。一般情况下使用SDK不需要改动,特殊情况下咨询原厂再调整。
3.2 FreeRTOS 编码规范接触一个新平台或者SDK,明白它的编码规范,文件作用,可以提高源码阅读效率,快速熟悉其内部实现。
3.2.1 数据类型FreeRTOS针对不同的处理器,对标准C的数据类型进行了重定义。
1.#defineportCHARchar应用编码中,推荐使用的是下面这种风格。
1.typedefintint32_t;FreeRTOS 中,定义变量的时候往往会把变量的类型当作前缀,好处看到就知道其类型。
char 型变量的前缀是 c
short 型变量的前缀是 s
long 型变量的前缀是 l
复杂的结构体,句柄等定义的变量名的前缀是 x
变量是无符号型的再加前缀 u,是指针变量则加前缀 p
3.2.3 函数名函数名含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数则会加一个 prv(private)的前缀。
例如vTaskPrioritySet()函数的返回值为 void 型,在 task.c 这个文件中定义。
3.2.4 宏宏内容是由大写字母表示,前缀是小写字母,表示该宏在哪个头文件定义,如:
1.#definetaskYIELD()portYIELD()表示该宏是在task.h。
3.2.5 个人解读1、编码不缺编码规范,但是实际使用中很难完全依照标准执行,即使freeRTOS源码也是如此。
2、关于函数或者宏定义中带文件名的作用,使用Source Insight 编辑代码,该前缀的意义不大。
3、规则是活的,只要所有人都按一个规则执行,它就是标准。
3.3 FreeRTOS应用开发关于freeRTOS的应用开发,主要是任务的创建和调度,任务间的通信与同步,涉及队列、信号量等操作系统通用接口。结合应用需求,涉及定时器、延时、中断控制等接口。
特别说明,有些功能的实现方式有多种形式,只针对常用方式进行说明,例如task的创建,只说明动态创建方式,因为很少使用静态方式。
四、 任务 4.1 创建任务xTaskCreate()使用动态内存的方式创建一个任务。
1.ret=xTaskCreate((TaskFunction_t)master_task_main,/*任务入口函数*/(1)创建任务就是软件运行时的一个while(1)的入口,一般阅读其他代码,找到这个函数,再跟踪到任务入口函数,学习基于freeRTOS系统的代码,首先就是找到main和这个接口。
(1):任务入口函数,即任务函数的名称,需要我们自己定义并且实现。
(2):任务名字,字符串形式, 大长度由 FreeRTOSConfig.h 中定义的 configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,只是方便调试。
(3):任务堆栈大小,单位为字, 4 个字节,这个要注意,否则系统内存紧缺。
(4):任务入口函数形参,不用的时候配置为 0 或者NULL 即可。
(5) :任务的优先级,在 FreeRTOS 中,数值越大优先级越高,0 代表 低优先级。基于其SDK开发,可将自定义的所有业务功能task设为同一个优先级,按时间片轮询调度。
(6):任务控制块指针,使用动态内存的时候,任务创建函数 xTaskCreate()会返回一个指针指向任务控制块,也可以设为NULL,因为任务句柄后期可以不使用。
4.2 开启调度当任务创建成功后处于就绪状态(Ready),在就绪态的任务可以参与操作系统的调度。操作系统任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是 vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由FreeRTOS 管理,此时才是真正进入实时操作系统中的第一步。
vTaskStartScheduler开启调度时,顺便会创建空闲任务和定时器任务。
FreeRTOS 为了任务启动和任务切换使用了三个异常:SVC、PendSV 和SysTick。
SVC(系统服务调用,亦简称系统调用)用于任务启动。
PendSV(可挂起系统调用)用于完成任务切换,它是可以像普通的中断一样被挂起的,它的 大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样产生的PendSV 中断就不会打断其他中断的运行。
SysTick 用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次 SysTick 中断,下一个任务将获得一个时间片。
FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度。相同优先级的任务采用时间片轮转方式进行调度(也就是分时调度),时间片轮转调度仅在当前系统中无更高优先级就绪任务存在的情况下才有效。
4.3 启动方式FreeRTOS有两种启动方式,效果一样,看个人喜好。
第一种:main 函数中将硬件初始化, RTOS 系统初始化,所有任务的创建完成, 后一步开启调度。目前看到的几个芯片SDK都是这种方式。
第二种:main 函数中将硬件和 RTOS 系统先初始化好,只创建一个任务后就启动调度器,然后在这个任务里面创建其它应用任务,当所有任务都创建成功后,启动任务再把自己删除。
4.4 任务创建源码分析xTaskCreate()创建任务。
1.BaseType_txTaskCreate(TaskFunction_tpxTaskCode,申请任务控制块内存,检查配置参数,初始化,将任务信息加入到就绪链表,等待调度。前面链表部分提到,freeRTOS的任务信息都是使用链表记录,在task.c有
1.PRIVILEGED_DATAstaticList_tpxReadyTasksLists[configMAX_PRIORITIES];//就绪分别记录就绪态、阻塞态和挂起的任务,其中阻塞态有2个,是因为特殊考虑,时间溢出 的问题,实际开发单片机项目计时超过24h的可以借鉴。其中pxReadyTasksLists链表数组,其下标就是任务的优先级。
4.5 任务调度源码分析
创建完任务的时候,vTaskStartScheduler开启调度器,空闲任务、定时器任务也是在开启调度函数中实现的。
为什么要空闲任务?因为 FreeRTOS一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing),并且空闲任务不可以被挂起与删除,空闲任务的优先级是 低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权。这些都是系统必要的东西,也无需自己实现。
1.voidvTaskStartScheduler(void)FreeRTOS 系统中的每一个任务都有多种运行状态,具体如下:
任务挂起函数
vTaskSuspend()挂起指定任务,被挂起的任务绝不会得到 CPU 的使用权
vTaskSuspendAll()将所有的任务都挂起任务恢复函数
vTaskResume()任务恢复就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。xTaskResumeFromISR() 专门用在中断服务程序中。无论通过调用一次或多次vTaskSuspend()函数而被挂起的任务,也只需调用一次恢复即可解挂 。
任务删除函数 vTaskDelete()用于删除任务。当一个任务可以删除另外一个任务,形参为要删除任 务创建时返回的任务句柄,如果是删除自身, 则形参为 NULL。
4.7 任务使用注意点1、中断服务函数是不允许调用任何会阻塞运行的接口。一般在中断服务函数中只做标记事件的发生,然后通知任务,让对应任务去执行相关处理 。
2、将紧急的处理事件的任务优先级设置偏高一些。
3、空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务,永远不会挂起空闲任务,不应该陷入死循环。
4、创建任务使用的内存不要过多,按需申请。如果浪费太多,后续应用申请大空间可能提示内存不足。
五、 队列 5.1 队列的概念队列用于任务间通信的数据结构,通过消息队列服务,任务或中断服务将消息放入消息队列中。其他任务或者自身从消息队列中获得消息。实现队列可以在任务与任务间、中断和任务间传递信息。队列操作支持阻塞等待,向已经填满的队列发送数据或者从空队列读出数据,都会导致阻塞,时间自定义。消息队列的运作过程具如下:
xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的句柄。队列句柄其实就是一个指向队列数据结构类型的指针。
1.master_queue=xQueueCreate(50,sizeof(task_message_struct_t));创建队列,占用50个单元,每个单元为sizeof(task_message_struct_t)字节,和 malloc比较类似。其 终使用的函数是 xQueueGenericCreate(),后续信号量等也是使用它创建,只是 后的队列类型不同。
申请内存后,xQueueGenericReset再对其进行初始化,队列的结构体xQUEUE成员:
1.typedefstructQueueDefinition/*Theoldnamingconventionisusedtopreventbreakingkernelawaredebuggers.*/