注:该文章为记录本人学习的过程与总结和提炼,若有疏忽在所难免,也欢迎各位读者可以指正,评论,一起交流,共同进步。
目录
一、导论
在此请各位考虑一下,为什么单片机需要操作系统呢?裸机不也能实现很多的功能了吗,何必还需要一个占用内存资源大小,增加学习难度的RTOS呢?这里就要引出裸机和RTOS最本质的区别了,RTOS的全称为(Real-Time-Operating-System)也就是实时操作系统。
什么是实时操作系统?
实时操作系统是优化后用于嵌入式/实时应用程序的操作系统 。其主要目标在于确保对事件作出及时和确定的响应。此类事件可以来自外部,如限位开关被触发,也可以来自内部,如接收到字符。
使用实时操作系统,软件应用程序可以被编写为一组独立任务 。每个任务都被分配了优先级,而实时操作系统负责 确保正在运行的任务是能够运行的最高优先级任务。 任务何时可能无法运行的示例包括任务何时在等待外部事件发生, 或任务何时在等待一个固定时段。
重点在于实时,有了实时操作系统之后,我们便能创建多个线程并分配不同的优先级(任务),通过任务调度器,以确保我们所需要让一些高优先级的线程得到执行的保证。RTOS 通常比通用操作系统体积更小、重量更轻,因此 RTOS 非常适用于 内存、计算和功率受限的设备。
二、概述
(1) 起源
FreeRTOS诞生于2003年,由英国工程师Richard Barry主导开发,旨在为资源受限的嵌入式设备提供一个开源、免费的实时操作系统。其名称中的“Free”不仅代表免费,更强调其灵活性和开放性。随着物联网(IoT)和边缘计算的兴起,FreeRTOS逐渐成为嵌入式领域的主流选择,并于2017年被亚马逊收购,进一步扩展了其在云连接设备中的应用。
(2) 概念
FreeRTOS 是一款市场领先的嵌入式系统, RTOS 支持 40 多种处理器架构,内存占用小,执行时间快,具有尖端的 RTOS 功能和库,包括对称多处理 (SMP)、具有 IPv6 支持的线程安全 TCP 堆栈以及与云服务的无缝集成。它是开源的,并得到了积极的支持和维护。
(3) 特性与优势
①轻量高效:内核仅占用6-12KB ROM和数百字节RAM,适用于低功耗微控制器(如STM32、ESP32)。
②硬实时性:基于优先级抢占式调度,确保关键任务在精确时间内完成。
③可移植性强:支持40+处理器架构,代码高度模块化,便于移植到不同硬件平台。
④丰富组件:提供任务管理、队列、信号量、软件定时器等核心功能,并集成TCP/IP、文件系统等扩展库。
⑤开源生态:采用MIT许可证,拥有活跃的开发者社区和丰富的第三方工具支持。
三、FreeRTOS基础知识及扫盲
什么是实时内核?
实时操作系统可以为应用程序编写者提供许多资源,包括 TCP/IP 堆栈、文件系统等 。内核是操作系统中负责任务管理、任务间通信和同步的部分。 FreeRTOS 是一种实时内核。
什么是实时调度器?
实时调度器和实时内核有时可互换使用。具体而言, 实时调度器是 RTOS 内核中负责决定应执行哪个任务的部分 。
什么是调度方式?
FreeRTOS一共支持三种调度方式:
①抢占式调度:主要是针对优先级不同的任务,每个任务都有自己的优先级,高优先级任务可以抢占低优先级的任务。
②时间片调度:主要是针对优先级相同的任务,当有多个任务处于相同优先级时,任务调度器就会在每个系统时钟节拍到的时候切换一次任务。
③协程式调度:轮询任务,但官方已经发表声明不再开发该调度了(基本不用)
什么是任务状态?
FreeRTOS中任务共存在四种状态:
①运行态:当前正在被执行的任务,该任务处于运行态。
②就绪态:该任务已经准备好了,等待被执行,但是还未执行,则该任务处于就绪态。
③阻塞态:某个任务通过调用系统的延时函数或是等待外部事件发生,该任务就处于阻塞态。
④挂起态:类似暂停了某个任务,需要使用FreeRTOS中自带的挂起函数和解挂函数实现。
注意:①仅只有就绪态的任务可以被转变为运行态②有任务想被执行必须进入就绪态
四种状态中,除了运行态,其他三个状态都有对应的任务状态列表分别是如下图:
调度器会在所有处于就绪列表的任务中,选择优先级最高的任务来执行。
四、FreeRTOS任务相关
1.概述
FreeRTOS中的线程(也被称为任务),是用来执行相关内容的线程函数,打个比方,比如需要制作一款智能手表,那么可以大致划分为以下几个线程(任务):
①显示线程
②通信线程
③USB线程
④蓝牙线程
⑤WIFI线程
⑥传感器线程
⑦电池管理线程
还有更多的线程可以根据具体情况创建,目的是为了解为什么要创建线程(任务),满足某一个或某几个特定单一的需求就可以创建有关这个需求的线程,尽可能通过分配不同的线程,分层次创建线程,减少耦合也就是互相之间的影响。
2.任务相关API函数
FreeRTOS官方提供了动态和静态两种方式来创建任务,其不同是动态创建又FreeRTOS系统自己所管理的堆中去分配相应的内存,静态创建需要用户手动实现,空闲任务,软件定时器任务(如果有)的堆栈分配及创建的任务堆栈分配。首先介绍动态创建任务
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);
configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,才可使用此 RTOS API 函数。
参数:
-
pvTaskCode
指向任务入口函数的指针。 任务通常以无限循环的形式实现;实现任务的函数绝不能尝试返回或退出。但是,任务可以 自行删除。
-
pcName
任务的描述性名称。此参数主要用于方便调试,但也可用于 获取任务句柄。任务名称的最大长度 由 FreeRTOSConfig.h 中的 configMAX_TASK_NAME_LEN 定义。
-
uxStackDepth
要分配用作任务堆栈的字数(不是字节数!)。例如,堆栈宽度为 32 位,uxStackDepth 为 400, 则将分配 1600 字节用作任务堆栈。堆栈深度与堆栈宽度的乘积不得超过 size_t 类型变量所能包含的最大值。
-
pvParameters
作为参数传递给所创建任务的值。如果 pvParameters 设置为某变量的地址, 则在创建的任务执行时,该变量必须仍然存在, 因此,不能传递堆栈变量的地址。
-
uxPriority
创建的任务将以该指定优先级执行。
-
pxCreatedTask
用于将句柄传递至由 xTaskCreate() 函数创建的任务。pxCreatedTask 是可选参数, 可设置为 NULL。
返回:
- 如果任务创建成功,则返回 pdPASS,
- 否则返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。
用法示例
/* 任务 */
void vTaskCode( void * pvParameters )
{
while(1)
{
//需要执行的代码
}
}
/* 创建任务的函数*/
void vOtherFunction( void )
{
BaseType_t xReturned;
TaskHandle_t xHandle = NULL; /* TaskHandle_t为任务句柄,大家可以理解为身份证,
每一个任务都有自己的任务控制块 */
/* 创建任务 */
xReturned = xTaskCreate(
vTaskCode, /* 任务函数名指针 */
"NAME", /* 任务的名字 */
STACK_SIZE, /* 堆栈大小(以字为单位,32位操作系统为1字4字节,) */
( void * ) 1, /* 传入任务的参数,无则填NULL*/
tskIDLE_PRIORITY,/* 任务优先级设置,范围具体可以在FreeRTOSconfig.h中设置*/
&xHandle ); /* 传入创建句柄地址 */
if( xReturned == pdPASS )
{
/* 任务如果被创建则会进入这里,并调用下面的函数删除任务 */
vTaskDelete( xHandle );
}
}
接下来是任务删除函数
/*
* @brief : 用于删除任务
* @para : TaskHandle_t xTask,带入任务句柄
* @retval: void
*/
void vTaskDelete( TaskHandle_t xTask );
静态函数在这里不做详细介绍,更多的使用的是动态函数创建任务即可,静态需要自己手动分配内存和释放,同时还要手动分配空闲任务和软件定时器任务的堆栈,相较于初学者会比较困难,如果有需要可以参考本文档最下面的参考资料,FreeRTOS官方文档中的xTaskCreateStatic()API。
3.任务的挂起和恢复
在前面介绍任务状态时有说道,任务可以处于挂起态,本章节的目的就是详细的介绍任务是如何进入挂起态并且如何恢复的。
//带入的参数为任务的句柄
void vTaskSuspend() //挂起任务
void vTaskResume() //恢复被挂起任务
BaseType_t xTaskResumeFromISR() //在中断中恢复被挂起的任务,有返回值
/*返回值:pdTRUE 代表任务需要进行任务切换(由于抢占式调度,
*被恢复的任务优先级可能高于当前正在被执行的任务优先级高,
*此时需要手动调用PendSV中断进行一次任务切换)
*返回值:pdFALSE 代表任务不需要进行任务切换
*/
五、中断管理
这个部分的内容需要对ARM Cortex-M的内核架构有一定的基础理解分,也是其内核和任务切换的关键,本章节可以慢慢吸收和消化,同时也要多借助官方的资料和ARM的内核指南进行参考学习。
1.中断介绍
对于ARM Cortex-M的内核,共有8位宽的中断优先级配置寄存器来配置中断的优先级,但是STM32只用到了中断优先级配置寄存器的高八位[7:4],也就是2的四次方共16级,同时STM32又将优先级分为了抢占式优先级和子优先级,接下来介绍一下这俩个优先级的区别:
①抢占式优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断
②子优先级:当同时发生具有相同抢占式优先级的两个中断时,子优先级高(数值小)的中断先执行
对于中断优先级分组设置,一共有五种分配方式,对应中断优先级分组的五组:
建议将所有优先级配置为抢占式优先级可以方便FreeRTOS管理这些中断
2.中断相关寄存器配置
在Cortex-M系列的单片机中,有三个系统中断优先级配置寄存器为SHPR1,SHPR2.SHPR3,通过这三个寄存器的配置可以将PendSV和SysTick中断的优先级设置为最低优先级,这俩个设置为最低优先级可以保证其他硬件中断的实时性和任务调度的需求。
还有一个寄存器是BASEPRI,通过这个寄存器,可以屏蔽低优先级中断,保护RTOS的调度器和关键操作,同时高优先级中断不受影响,确保实时时间可以及时响应,如果要在中断中调用FreeRTOS提供的API函数那么需要在RTOS所管理的中断范围内,且需要注意API是否是带有FromISR字样。
六、临界段代码保护
1、什么是临界段
临界段代码也叫临界区,是指那些必须完整运行,不能被打断的代码片段。运行时临界段代码时需要关闭中断,当处理完临界段代码以后再打开中断。
2、适用什么场合
- 外设 :需要严格按照时序初始化的外设,如IIC、SPI等。
- 系统 :系统自身需求,如任务切换过程等。
- 用户 :用户需求,如我们写的任务创建任务。
3、什么可以打断当前程序的运行
中断、任务调度。
4.临界段代码保护函数介绍(掌握)
4.1、临界段代码保护函数
任务级临界区调用格式示例:
中断级临界区调用格式示例:
4.2、临界段代码保护函数使用特点
- 成对使用。
- 支持嵌套。
- 尽量保持临界段耗时短。
七、任务调度器的挂起和恢复
1.任务调度器挂起和恢复函数
和上述六的任务级临界区调用使用范例是差不多的,可以参考一下
2、任务调度器挂起和恢复的特点
- 与临界区不一样的是,挂起任务调度器,未关闭中断。
- 它仅仅是防止任务之间的资源争夺,中断照样可以直接响应。
- 挂起任务调度器的方式,适用于临界区位于任务与任务之间;既不用去延时中断响应,又可以做到临界区的安全。
3、挂起任务调度器:vTaskSuspendAll()
调用一次挂起调度器,该变量uxSchedulerSuspended就加一 ,变量uxSchedulerSuspended的值,将会影响Systick触发PendSV中断,即影响任务调度。
4、恢复任务调度器:xTaskResumeAll()
调用一次恢复调度器,该变量uxSchedulerSuspended就减一 ,如果uxSchedulerSuspended等于0,则允许调度 。
- 当任务数量大于0时,恢复调度器才有意义,如果没有一个已创建的任务就无意义。
- 移除等待就绪列表中的列表项,恢复至就绪列表,直到xPendingReadyList列表为空。
- 如果恢复的任务优先级比当前正在执行任务优先级更高,则将xYieldPending赋值为pdTRUE,表示需要进行一次任务切换。
- 在调度器被挂起的期间内,是否有丢失未处理的滴答数。 xPendedCounts是丢失的滴答数,有则调用xTasklncrementTickf() 补齐弄失的滴答数。
- 判断是否允许任务切换。
- 返回任务是否已经切换;已经切换返回pdTRUE;反之返回pdFALSE。
待更新,本文会更完所有FreeRTOS为止!!
本文参考资料,特别感谢以下:
1.FreeRTOS官方文档:FreeRTOS 文档 - FreeRTOS™
2.正点原子FreeRTOS开发指南