一、操作系统介绍
1.1 什么是裸机开发
裸机开发指的是在没有操作系统(OS)或者其他高级软件支持的情况下,直接在裸机硬件上进行软件开发的过程。在裸机开发中,开发者需要直接面对硬件层面的操作和控制,亲自管理 CPU、内存以及 I/O 资源,而不依赖于任何操作系统提供的抽象层或者服务。
我们前面学习的 STM32 单片机代码都属于裸机开发。
1.2 什么是操作系统
一个计算机系统可以大致分为三个部分:硬件(Hardware)、操作系统(Operating System)、应用程序(Application Programs)。
- 硬件:包含了芯片、存储空间、输入输出等设备,为整个系统提供了基础的计算资源。
- 操作系统:是一个控制程序,作为硬件和应用程序之间的桥梁,主要是和硬件打交道,负责协调分配计算资源和内存资源给不同的应用程序使用,并防止系统出现故障。面对来自不同应用程序的大量且互相竞争的资源请求,操作系统通过一个调度算法和内存管理算法尽可能把资源公平且有效率地分配给不同的程序。
- 应用程序:则通过调用操作系统提供的 API 接口获得相应资源,完成指定的任务。
操作系统从整体上分为两大类:通用操作系统和实时操作系统。
1.3 通用操作系统
通用操作系统包括 Linux、Windows、macOS 等主流的操作系统。这些操作系统大家每天都在使用,功能也十分强大,只是它们有时为了保障系统的流畅运行,就不能保证每个程序都能实时响应,在易用性和实时性之间有所取舍。而且单片机有限的片上资源也不足以支撑通用操作系统的运行。
1.4 实时操作系统
实时操作系统(RTOS,Real Time Operating System)中,实时(Real Time)指的是任务(Task)或者说实现一个功能的线程(Thread)必须在给定的时间(Deadline)内完成。
人们总有种误解,认为如果能堆砌更多的处理器核心数目、更高的处理器频率、更大的内存、更快的总线速度,系统就一定能达到实时性。然而事与愿违,强大的计算能力并不能保证系统的实时性。举一个简单的例子,比如汽车中的安全气囊,在传感器检测到汽车发生碰撞后,安全气囊需要在 30ms 内完全打开,不然司机和乘客的人身安全将受到极大的威胁。倘若车载 ECU 有很强大的计算能力,但是如果因为要执行其他复杂计算任务或者任务调度的问题,导致对汽车异常状态的监测和安全气囊的响应时间超过了规定的时间,系统实时性将无法得到保障,从而导致系统失效和人员伤亡,这将会是非常严重的问题。
为了保障这些实时任务能在给定的时间内完成,需要一个实时系统对这些任务进行调度和管理。一个实时操作系统能尽力保障每个任务的运行时间在规定时间内完成,这包括:
- 对中断和内部异常的处理
- 对安全相关的事件的处理
- 任务调度机制等
正所谓术业有专攻,在嵌入式领域中,嵌入式实时操作系统(RTOS)可以更合理、更有效地利用 CPU 的资源,简化应用软件的设计,缩短系统开发时间,从而更好地保证系统的实时性和可靠性。
目前比较流行的实时操作系统包括黑莓 QNX、FreeRTOS、uCOS、RT-Thread 等。
1.3FreeRTOS 简介
RTOS(实时操作系统)是指一类系统,如 FreeRTOS、uC/OS、RTX、RT-Thread 等,都是 RTOS 类操作系统。FreeRTOS 是所有实时操作系统中最受欢迎的一款。
1.3.1 FreeRTOS 发展历史
- FreeRTOS 由美国的 Richard Barry 于 2003 年发布。
- FreeRTOS 于 2017 年被亚马逊收购,改名为 AWS FreeRTOS。
1.3.2 FreeRTOS 优势
FreeRTOS 是市场领先的面向微控制器和小型微处理器的实时操作系统(RTOS),与世界领先的芯片公司合作开发,现在每 170 秒下载一次。FreeRTOS 通过 MIT 开源许可免费分发,包括一个内核和一组不断丰富的 IoT 库,适用于所有行业领域。FreeRTOS 的构建突出可靠性和易用性。
FreeRTOS 是一款受欢迎、广泛应用于嵌入式系统的 RTOS,其开源、轻量级、可移植的特点使其成为许多嵌入式开发者的首选,主要优势如下:
- 开源和免费:FreeRTOS 是一款开源的 RTOS,采用 MIT 许可证发布,可以免费使用、修改和分发。
- 轻量级设计:FreeRTOS 注重轻量级设计,适用于资源受限的嵌入式系统,不占用过多内存和处理器资源。
- 广泛应用:FreeRTOS 在嵌入式领域得到广泛应用,包括工业自动化、医疗设备、消费电子产品、汽车电子等。
- 多平台支持:FreeRTOS 的设计注重可移植性,可以轻松地移植到不同的硬件平台,支持多种处理器架构。
- 丰富的功能:提供了多任务调度、任务通信、同步等功能,适用于复杂的嵌入式应用场景。
1.3.3 FreeRTOS 特点
官网:FreeRTOS™ - FreeRTOS™,并且支持中文。
- 任务调度:FreeRTOS 通过任务调度器管理多个任务,支持不同优先级的任务,实现任务的有序执行。
- 任务通信和同步:提供了队列、信号量等机制,支持任务之间的通信和同步,确保数据的安全传递。
- 内存管理:提供简单的内存管理机制,适用于嵌入式环境,有效利用有限的内存资源。
- 定时器和中断处理:支持定时器功能,能够处理中断,提供了可靠的实时性能。
- 开发社区:拥有庞大的用户社区,开发者可以在社区中获取支持、解决问题,并分享经验。
- 可移植性:设计注重可移植性,可以轻松地移植到不同的硬件平台,提高了代码的重用性。
二、FreeRTOS 基础知识
2.1 多任务处理
内核是操作系统的核心组件。诸如 Linux 这样的操作系统采用的内核,看似允许用户同时访问计算机。很明显,多个用户可以同时执行多个程序。每个执行程序都是受操作系统控制的任务(或线程)。如果一个操作系统能够以这种方式执行多个任务,则可称其为多任务操作系统。
使用多任务操作系统可以简化原本复杂的软件应用程序的设计:
- 操作系统的多任务处理和任务间通信功能允许将复杂的应用程序分割成一组更小、更易于管理的任务。
- 通过分割,您可以更轻松地执行软件测试、分解团队内部工作以及复用代码。
- 复杂的时序和排序细节可以从应用程序代码中移除,由操作系统负责。
即使单核处理器一次只能执行一项任务,多任务操作系统也可以通过任务之间的快速切换制造并发执行的假象。下图展示了与时间相关的三项任务的执行模式:任务名称采用颜色编码,并写在左手边;时间从左向右移动,彩色线条显示了在任何特定时间正在执行的任务。上方展示了所感知的并发执行模式,下方展示了实际的多任务执行模式。
2.2 任务调度
一个处理器核心在某一时刻只能运行一个任务,如在各个任务之间迅速切换,这样看起来就像多个任务在同时运行。操作系统中任务调度器的责任就是决定在某一时刻要执行哪个任务。
调度器是内核中负责决定在任何特定时间应执行哪些任务的部分。内核可以在任务生命周期内多次挂起并且稍后恢复一个任务。调度策略是调度器用来决定在任何时间点执行哪个任务的算法。
FreeRTOS 默认使用固定优先级的抢占式调度策略,对同等优先级的任务执行时间片轮询调度:
- 抢占式调度:FreeRTOS 采用抢占式调度方式,允许更高优先级的任务在任何时刻抢占正在执行的低优先级任务。这确保了高优先级任务能够及时响应,并提高了系统的实时性。
- 时间片轮询:在相同优先级的任务之间,FreeRTOS 采用时间片轮转策略。每个任务执行一个时间片,如果有其他同优先级的任务等待执行,则切换到下一个任务。这有助于公平地分配 CPU 时间。
但是并不是说高优先级的任务会一直执行,导致低优先级的任务无法得到执行。如果高优先级任务等待某个资源(延时或等待信号量等)而无法执行,调度器会选择执行其他就绪的高优先级任务。
2.3 任务状态
FreeRTOS 中任务共存在 4 种状态:
- 运行态:当任务实际执行时,它被称为处于运行状态。如果运行 RTOS 的处理器只有一个内核,那么在任何给定时间内都只能有一个任务处于运行状态。注意在 STM32 中,同一时间仅一个任务处于运行态。
- 就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态),但目前没有执行的任务,因为同等或更高优先级的不同任务已经处于运行状态。
- 阻塞态:如果任务当前正在等待延时或外部事件,则该任务被认为处于阻塞状态。
- 挂起态:类似暂停,调用函数
vTaskSuspend()进入挂起态,需要调用解挂函数vTaskResume()才可以进入就绪态。
只有就绪态可转变成运行态,其他状态的任务想运行,必须先转变成就绪态。转换关系如下:
这四种状态中,除了运行态,其他三种任务状态的任务都有其对应的任务状态列表:
- 就绪列表:
pxReadyTasksLists[x],其中 x 代表任务优先级数值。 - 阻塞列表:
pxDelayedTaskList。 - 挂起列表:
xSuspendedTaskList。
列表类似于链表,后面章节会专门介绍。
以就绪列表为例。如果在 32 位的硬件中,会保存一个 32 位的变量,代表 0-31 的优先级。当某个位,置一时,代表所对应的优先级就绪列表有任务存在。
如果有多个任务优先级相同,会连接在同一个就绪列表上。
调度器总是在所有处于就绪列表的任务中,选择具有最高优先级的任务来执行。
2.4 FreeRTOS 的滴答
休眠时,RTOS 任务将指定需要“唤醒”的时间。阻塞时,RTOS 任务可以指定希望等待的最长时间。
FreeRTOS 实时内核通过滴答计数变量测量时间。定时器中断(RTOS 滴答中断)以严格的时间精度增加滴答数——允许实时内核以所选择的定时器中断频率的分辨率来测量时间。
每次滴答数增加时,实时内核必须检查是否现在是解除阻塞或唤醒任务的时间。在滴答 ISR 期间唤醒或解除阻塞的任务的优先级可能高于被中断任务的优先级。
2.5 上下文切换
1)什么是上下文切换
当一个任务执行时,它会利用处理器/微控制器寄存器,并像其他程序一样访问 RAM 和 ROM。这些资源(处理器寄存器、堆栈等)一起组成了任务执行上下文。
一个任务是一段有顺序的代码——它不知道什么时候会被内核挂起(换出或换入)或恢复(换入或换入),甚至不知道什么时候自己被挂起或恢复过。
一个任务在即将执行将两个处理器寄存器内包含的数值相加之前被挂起。当该任务被挂起时,其他任务会执行,还可能会修改处理器寄存器的数值。恢复时,该任务不会知道处理器寄存器已经被修改过了——如果它使用经修改过的数值,那么求和会得到一个错误的数值。
为了防止这种类型的错误,任务在恢复时必须有一个与挂起之前相同的上下文。通过在任务挂起时保存任务的上下文,操作系统内核负责确保上下文保持不变。任务恢复时,其保存的上下文在执行之前由操作系统内核恢复。
保存被挂起的任务的上下文和恢复被恢复的任务的上下文的过程被称为上下文切换:
- 将 TaskA 在相应的处理器寄存器中的上下文保存到其任务堆栈中。
- 将 TaskB 的上下文从其任务堆栈中恢复到相应的处理器寄存器中。
2)什么时候进行上下文切换
在需要切换任务的时候进行上下文切换,真正执行上下文切换是在 PendSV 的 ISR 中处理的。使用 PendSV 是因为其可以手动触发,并且可以在其他更高中断优先级的 ISR 中来进行设置,比较灵活。具体触发操作是将中断控制和状态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。FreeRTOS 会将 PendSV 设置为最低中断优先级,避免任务切换影响到其他正常的 ISR。
在 FreeRTOS 中有以下几个情况会触发 PendSV 异常产生切换:
- RTOS 滴答中断:会处理就绪列表,判断是否要切换任务(包括抢占式、时间片轮转)。
- 任务执行完毕:主动调用任务切换函数进行强制切换。
2.6 空闲任务
RTOS 调度器启动时,自动创建空闲任务,以确保始终存在一个能够运行的任务。
空闲任务以最低优先级创建,以确保如果有更高的优先级应用程序任务处于准备就绪状态,空闲任务则不使用任何 CPU 时间。
空闲任务负责释放被删除的任务的内存。
三、FreeRTOS 移植
3.1 FreeRTOS 源码结构介绍
3.1.1 获取源码
1)官网下载
官网地址:FreeRTOS™ - FreeRTOS™,这里我们选择当前最新的分发包 202212.01 版本下载。
2)GitHub 下载
GitHub 地址:Releases · FreeRTOS/FreeRTOS · GitHub,现在 FreeRTOS 已经将源码迁移到 GitHub 上,可以直接下载。
3.1.2 源码结构介绍
1)源码整体结构
| 名称 | 描述 |
|---|---|
| FreeRTOS | FreeRTOS 内核 |
| FreeRTOS-Plus | FreeRTOS 组件,一般我们会选择使用第三方的组件 |
| tools | 工具 |
| GitHub-FreeRTOS-Home | FreeRTOS 的 GitHub 仓库链接 |
| Quick_Start_Guide | 快速入门指南官方文档链接 |
| Upgrading-to-FreeRTOS-xxx | 升级到指定 FreeRTOS 版本官方文档链接 |
| History.txt | FreeRTOS 历史更新记录 |
| 其他 | 其他 |
2)FreeRTOS 文件夹结构
| 名称 | 描述 |
|---|---|
| Demo | FreeRTOS 演示例程,支持多种芯片架构、多种型号芯片 |
| License | FreeRTOS 相关许可 |
| Source | FreeRTOS 源码,最重要的文件夹 |
| Test | 公用以及移植层测试代码 |
3)Source 文件夹结构
| 名称 | 描述 |
|---|---|
| include | 包含了 FreeRTOS 的头文件 |
| portable | 包含 FreeRTOS 移植文件:与编译器相关、Keil 编译环境 |
| croutine.c | 协程相关文件 |
| event_groups.c | 事件相关文件 |
| list.c | 列表相关文件 |
| queue.c | 队列相关文件 |
| stream_buffer.c | 流式缓冲区相关文件 |
| tasks.c | 任务相关文件 |
| timers.c | 软件定时器相关文件 |
include 文件夹和 .c 文件是通用的头文件和 C 文件,这两部分的文件适用于各种编译器和处理器,是通用的。其中移植必需的是 tasks.c、list.c、queue.c、portable 文件夹,其他 .c 文件根据需要选取。portable 文件夹里根据编译器、内核等实际环境对应选取。
4)portable 文件夹结构
FreeRTOS 操作系统归根到底是一个软件层面的东西,需要跟硬件联系在一起,portable 文件夹里面的东西就是连接桥梁。由于我们使用 MDK 开发,因此这里只重点介绍其中的部分移植文件。
| 名称 | 描述 |
|---|---|
| Keil | 指向 RVDS 文件夹 |
| RVDS | 不同内核芯片的移植文件 |
| MemMang | 内存管理相关文件 |
Keil 文件夹里只有一个 See-also-the-RVDS-directory.txt,意思是让我们看 RVDS 文件夹。
(1)RVDS 文件夹
RVDS 文件夹包含了各种处理器相关的文件夹。FreeRTOS 是一个软件,单片机是一个硬件,FreeRTOS 要想运行在一个单片机上面,它们就必须关联在一起。
关联还是得通过写代码来关联,这部分关联的文件叫接口文件,通常由汇编和 C 联合编写。这些接口文件都是跟硬件密切相关的,不同的硬件接口文件是不一样的,但都大同小异。编写这些接口文件的过程我们就叫移植,移植的过程通常由 FreeRTOS 和 MCU 原厂的人来负责,移植好的这些接口文件就放在 RVDS 这个文件夹的目录下。
FreeRTOS 为我们提供了 Cortex-M0、M3、M4 和 M7 等内核的单片机的接口文件,根据 MCU 的内核选择对应的接口文件即可。其实准确来说,不能够叫移植,应该叫使用官方的移植,因为这些跟硬件相关的接口文件,RTOS 官方都已经写好了,我们只是使用而已。
以 ARM_CM3 这个文件夹为例,里面只有 port.c 与 portmacro.h 两个文件:
- port.c 文件:里面的内容是由 FreeRTOS 官方的技术人员为 Cortex-M3 内核的处理器写的接口文件,里面核心的上下文切换代码是由汇编语言编写而成,对技术员的要求比较高,我们只是使用的话只需拷贝过来用即可。
- portmacro.h 文件:port.c 文件对应的头文件,主要是一些数据类型和宏定义。
(2)MemMang 文件夹
MemMang 文件夹下存放的是跟内存管理相关的,总共有五个 heap 文件以及一个 readme 说明文件。
这五个 heap 文件在移植的时候必须使用一个,因为 FreeRTOS 在创建内核对象的时候使用的是动态分配内存,而这些动态内存分配的函数则在这几个文件里面实现,不同的分配算法会导致不同的效率与结果,后面在内存管理中我们会讲解每个文件的区别,由于现在是初学,所以我们选用heap_4.c即可。
3.2 FreeRTOS 在基于 HAL 库项目中移植步骤
3.2.1 目录添加源码文件
在例程的根路径下,新建 FreeRTOS 文件夹,并且在里面新建 portable 和 source 两个空文件夹。
拷贝 FreeRTOS 源码的 Source 文件夹的 7 个 .c 文件到例程的 source 文件夹。
拷贝 FreeRTOS 源码 portable 文件夹下的 Keil、RVDS、MemMang 到例程的 portable 文件夹下。
其中例程的 MemMang 可只保留heap_4.c,
例程的 RVDS 可只保留 ARM_CM3(对应我们的芯片内核)。
拷贝 FreeRTOS 源码 include 文件夹到例程的 FreeRTOS 文件夹下。
FreeRTOSConfig.h 文件是 FreeRTOS 的工程配置文件,因为 FreeRTOS 是可以裁剪的实时操作内核,应用于不同的处理器平台,用户可以通过修改这个 FreeRTOS 内核的配置头文件来裁剪 FreeRTOS 的功能,所以我们把它拷贝一份放在 user 这个文件夹下面。
在源码 ..\FreeRTOS\Demo 文件夹下面找到 CORTEX_STM32F103_Keil 这个文件夹,找到 FreeRTOSConfig.h 文件,然后拷贝到我们工程下的 Core/Inc 文件夹下即可,等下我们需要对这个文件进行修改。
3.2.2 工程添加源码文件
工程新建 Group FreeRTOS/Source 和 FreeRTOS/Portable。
FreeRTOS/Source 添加 .c 文件,
FreeRTOS/Portable 添加 port.c 和 heap_4.c 文件,
并添加配置头文件。
添加头文件。
FreeRTOS 的源码已经添加到开发环境的组文件夹下面,编译的时候需要为这些源文件指定头文件的路径,不然编译会报错。FreeRTOS 的源码里面只有 include 和 RVDS\ARM_CM3 这两个文件夹下面有头文件,只需要将这两个头文件的路径在开发环境里面指定即可。
同时我们还将 FreeRTOSConfig.h 这个头文件拷贝到了工程根目录下的 Core/Inc 文件夹下,这个路径本身就在开发环境里面(放其他路径也可以,就是一个 .h 文件)。
3.2.3 系统配置文件修改
FreeRTOSConfig.h 中添加如下 3 个配置:
#definexPortPendSVHandlerPendSV_Handler
#definevPortSVCHandlerSVC_Handler
#defineINCLUDE_xTaskGetSchedulerState1
3.2.4 修改 stm32f1xx_it.c
1)引入头文件
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include"FreeRTOS.h"
#include"task.h"
/* USER CODE END Includes */
2)注释掉 2 个函数
// void SVC_Handler(void)
// {
// }
// void PendSV_Handler(void)
// {
// }
3)添加 SysTick 时钟中断服务函数
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
externvoidxPortSysTickHandler(void);
/* USER CODE END PV */
voidSysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
/* USER CODE BEGIN SysTick_IRQn 1 */
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
/* USER CODE END SysTick_IRQn 1 */
}
注意:HAL 本身和 FreeRTOS 都默认依赖 SysTick,可能出现卡死的问题。为了保险起见,可以考虑在 SYS 选择 HAL 时钟源的时候换成其他的,并且中断优先级设为较高,比如 1。
3.3 FreeRTOS 在基于寄存器项目中移植步骤
3.3.1 目录添加源码文件
在例程的根路径下,新建 FreeRTOS 文件夹,并且在里面新建 portable 和 source 两个空文件夹。
拷贝 FreeRTOS 源码的 Source 文件夹的 7 个 .c 文件到例程的 source 文件夹。
拷贝 FreeRTOS 源码 portable 文件夹下的 Keil、RVDS、MemMang 到例程的 portable 文件夹下。
其中例程的 MemMang 可只保留 heap_4.c,例程的 RVDS 可只保留 ARM_CM3(对应我们的芯片内核)。
拷贝 FreeRTOS 源码 include 文件夹到例程的 FreeRTOS 文件夹下。
FreeRTOSConfig.h 文件是 FreeRTOS 的工程配置文件,因为 FreeRTOS 是可以裁剪的实时操作内核,应用于不同的处理器平台,用户可以通过修改这个 FreeRTOS 内核的配置头文件来裁剪 FreeRTOS 的功能,所以我们把它拷贝一份放在 user 这个文件夹下面。
3.3.2 工程添加源码文件
工程新建 Group FreeRTOS/Source 和 FreeRTOS/Portable
3.3.3 系统配置文件修改
FreeRTOSConfig.h 中添加如下 3 个配置:
#define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler #define INCLUDE_xTaskGetSchedulerState 13.3.4 main.c 中添加如下代码
FreeRTOS 使用滴答定时器来实现系统时基,需要实现滴答定时器的中断,并在中断中添加下面的代码:
extern void xPortSysTickHandler(void); void SysTick_Handler(void) { if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xPortSysTickHandler(); } }3.4 系统配置文件说明
FreeRTOSConfig.h是 FreeRTOS 的工程配置文件,作用是对 FreeRTOS 的功能进行配置和裁剪,以及 API 函数的使能等。由于 FreeRTOS 是一个可裁剪的实时内核,需要适配不同的处理器平台和不同的应用场景,因此几乎所有的功能开关、资源大小、优先级数量等都在这个头文件中通过宏定义来配置。
官网中文说明:FreeRTOS 内核配置文档。
整体的配置项可以分为三类:
- INCLUDE开头:一般是“INCLUDE_函数名”,函数的使能,1表示可用,0表示禁用。
- config开头:FreeRTOS的一些功能配置,比如基本配置、内存配置、钩子配置、中断配置等。
- 其他配置:PendSV宏定义、SVC宏定义。
下面按类别说明常用的配置项。
3.4.1 基础与调度相关配置
| 配置项 | 说明 |
|---|---|
configUSE_PREEMPTION | 为 1 时使用抢占式调度,为 0 时使用协作式调度。一般设置为 1。 |
configUSE_TIME_SLICING | 为 1 时,同等优先级的任务按时间片轮转调度。 |
configUSE_PORT_OPTIMISED_TASK_SELECTION | 为 1 时使用硬件指令(如 Cortex-M 的 CLZ)查找最高优先级任务,效率更高,但受最大优先级数量限制。 |
configTICK_RATE_HZ | 系统节拍频率,单位为 Hz。常用 1000,即 1ms 一个滴答。 |
configMAX_PRIORITIES | 可用的最大任务优先级数量,数值越大占用内存越多。 |
configMINIMAL_STACK_SIZE | 空闲任务使用的最小堆栈大小,单位为字(word),不是字节。 |
configTOTAL_HEAP_SIZE | 使用动态内存分配时,FreeRTOS 堆的总大小,单位为字节。 |
configMAX_TASK_NAME_LEN | 任务名的最大长度,包含结束符。 |
configUSE_16_BIT_TICKS | 为 1 时滴答计数为 16 位,为 0 时为 32 位。STM32 一般设置为 0。 |
configIDLE_SHOULD_YIELD | 为 1 时空闲任务会让出 CPU 给同优先级的其他就绪任务。 |
3.4.2 功能裁剪相关配置
| 配置项 | 说明 |
|---|---|
configUSE_MUTEXES | 为 1 时启用互斥信号量,并支持优先级继承。 |
configUSE_RECURSIVE_MUTEXES | 为 1 时启用递归互斥信号量。 |
configUSE_COUNTING_SEMAPHORES | 为 1 时启用计数型信号量。 |
configUSE_QUEUE_SETS | 为 1 时启用队列集。 |
configUSE_TASK_NOTIFICATIONS | 为 1 时启用任务通知功能,效率高且占用资源少。 |
configUSE_TIMERS | 为 1 时启用软件定时器,需要同时配置定时器任务相关参数。 |
configUSE_TRACE_FACILITY | 为 1 时启用可视化跟踪调试相关的结构体和函数。 |
configUSE_STATS_FORMATTING_FUNCTIONS | 为 1 时启用vTaskList()、vTaskGetRunTimeStats()等格式化统计函数。 |
configCHECK_FOR_STACK_OVERFLOW | 堆栈溢出检测方式,0 为不检测,1 和 2 为两种检测方法,需要实现vApplicationStackOverflowHook()。 |
configUSE_MALLOC_FAILED_HOOK | 为 1 时启用内存申请失败钩子函数vApplicationMallocFailedHook()。 |
configUSE_IDLE_HOOK | 为 1 时启用空闲任务钩子函数vApplicationIdleHook()。 |
configUSE_TICK_HOOK | 为 1 时启用滴答定时器钩子函数vApplicationTickHook()。 |
3.4.3 中断与优先级相关配置
| 配置项 | 说明 |
|---|---|
configPRIO_BITS | MCU 使用的优先级位数,STM32 一般为 4。 |
configLIBRARY_LOWEST_INTERRUPT_PRIORITY | 最低中断优先级,一般填 15。 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | FreeRTOS 可管理的中断最高优先级,优先级数值大于等于该值的中断才能调用 FreeRTOS 的 API。 |
configKERNEL_INTERRUPT_PRIORITY | 内核中断(PendSV、SysTick)使用的优先级,由上面两个宏换算得到。 |
configMAX_SYSCALL_INTERRUPT_PRIORITY | 同上,由configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY换算得到。 |
注意:Cortex-M 的中断优先级数值越小优先级越高,而 FreeRTOS 中任务优先级数值越大优先级越高,两者方向相反,配置时不要混淆。
3.4.4 移植相关的宏定义
除了功能配置,FreeRTOSConfig.h中还需要把 FreeRTOS 的中断处理函数映射到启动文件中的中断向量名,否则会出现重复定义或中断无法进入的问题:
#define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler #define xPortSysTickHandler SysTick_Handler如果使用 HAL 库,SysTick_Handler已经在stm32f1xx_it.c中定义,此时不要重复定义,而是在该函数中调用xPortSysTickHandler(),具体做法见 3.2.4 节。
3.4.5 运行时统计配置
如果需要查看各任务的 CPU 占用率,需要开启运行时统计功能,并提供一个高精度的计时基准(通常是一个定时器):
#define configGENERATE_RUN_TIME_STATS 1 #define configUSE_TRACE_FACILITY 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1 /* 需要用户自己实现的两个宏 */ #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats() extern uint32_t FreeRTOSRunTimeTicks; #define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks其中ConfigureTimeForRunTimeStats()用于初始化统计用的定时器,FreeRTOSRunTimeTicks需要在定时器中断中不断累加。统计定时器的频率建议为系统节拍频率的 10 到 100 倍,以保证统计精度。
例如使用 TIM3 作为统计时基,可以在btim.c中实现:
#include "btim.h" uint32_t FreeRTOSRunTimeTicks = 0; void ConfigureTimeForRunTimeStats(void) { /* TIM3 初始化,计数频率 10kHz,即每 100us 计一次数 */ BTIM_TIM3_Init(10000 - 1, 72 - 1); } /* TIM3 中断服务函数 */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { FreeRTOSRunTimeTicks++; TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }配置完成后,在任务中调用vTaskGetRunTimeStats()即可把各任务的运行时间和占用百分比格式化到缓冲区中,再通过串口打印出来。
3.4.6 配置时的常见注意事项
- 堆大小要留有余量:
configTOTAL_HEAP_SIZE太小会导致创建任务或队列失败,可以先用较大的值调试,稳定后再逐步压缩。 - 优先级数量不宜过多:
configMAX_PRIORITIES每增加一个,就绪列表数组就多一项,占用更多 RAM。 - 中断优先级要匹配:调用了 FreeRTOS API 的中断,其优先级数值必须大于等于
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY,否则会触发断言。 - 调试阶段打开钩子函数:建议开启堆栈溢出检测和内存申请失败钩子,便于尽早发现问题。
- 发布时裁剪功能:不用的功能(如软件定时器、事件组、协程)可以关闭,减小代码体积和 RAM 占用。
3.5 FreeRTOS 数据类型
FreeRTOS 针对每个移植(port)定义了四种基本数据类型,分别是
TickType_t、BaseType_t、UBaseType_t和StackType_t。这些类型在portmacro.h中通过typedef定义,其具体宽度与处理器架构和编译环境相关,这样可以让内核代码在不同平台上保持可移植性。
3.5.1 TickType_t
如果configUSE_16_BIT_TICKS设置为非零(true),则将TickType_t定义为无符号的 16 位类型;如果configUSE_16_BIT_TICKS设置为零(false),则将TickType_t定义为无符号的 32 位类型。
这个类型的变量通常用来表示系统节拍计数器的值。FreeRTOS 系统中,每隔一段时间会进行一次滴答定时器中断,节拍计数器随之加一,因此TickType_t也常用于表示延时时间、超时时间等以节拍为单位的量。
/* portmacro.h 中的典型定义 */ #if( configUSE_16_BIT_TICKS == 1 ) typedef uint16_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffff #else typedef uint32_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffffffffUL #endif注意:
configUSE_16_BIT_TICKS为 1 时,节拍计数器最大只能计到 65535,在 1kHz 节拍下约 65 秒就会溢出,因此 STM32 等 32 位平台一般设置为 0。
3.5.2 BaseType_t
BaseType_t是 FreeRTOS 中最常用的数据类型,通常定义为处理器架构最高效的位宽类型。在 32 位处理器上一般定义为 32 位有符号类型,在 16 位处理器上则定义为 16 位有符号类型。
该类型主要用于表示函数的返回值,例如pdPASS、pdFAIL、pdTRUE、pdFALSE等,也常用于表示逻辑判断结果。
/* portmacro.h 中的典型定义 */ typedef long BaseType_t; #define pdFALSE ( ( BaseType_t ) 0 ) #define pdTRUE ( ( BaseType_t ) 1 ) #define pdPASS ( pdTRUE ) #define pdFAIL ( pdFALSE )3.5.3 UBaseType_t
UBaseType_t是BaseType_t的无符号版本,主要用于表示任务优先级、队列长度等不可能为负数的量。
/* portmacro.h 中的典型定义 */ typedef unsigned long UBaseType_t;例如任务创建函数xTaskCreate()的优先级参数、uxTaskPriorityGet()的返回值,使用的都是UBaseType_t类型。
3.5.4 StackType_t
StackType_t用于定义任务堆栈中每个存储单元的类型,其宽度与处理器位宽一致。在 32 位处理器上通常定义为 32 位无符号类型,在 16 位处理器上则定义为 16 位无符号类型。
/* portmacro.h 中的典型定义 */ typedef unsigned long StackType_t;需要注意的是,任务堆栈大小以“字(word)”为单位,而不是字节。例如在 32 位平台上,configMINIMAL_STACK_SIZE为 128 时,实际占用的 RAM 为 128 × 4 = 512 字节。
3.5.5 四种数据类型对比
| 类型 | 符号性 | 典型宽度(32 位平台) | 常见用途 |
|---|---|---|---|
TickType_t | 无符号 | 32 位(受configUSE_16_BIT_TICKS控制) | 节拍计数、延时时间、超时时间 |
BaseType_t | 有符号 | 32 位 | 函数返回值、逻辑判断结果 |
UBaseType_t | 无符号 | 32 位 | 任务优先级、队列长度、计数 |
StackType_t | 无符号 | 32 位 | 任务堆栈单元 |
3.6 FreeRTOS 命名规则
FreeRTOS 的源码遵循一套统一的命名规则,熟悉这套规则后,即使不看文档也能大致判断出变量、函数和宏的类型与作用。
3.6.1 变量
变量名以变量类型为前缀,前缀使用小写字母:
c:char 类型s:short 类型l:long 类型x:BaseType_t 及其他非标准类型(如结构体、句柄)u:unsigned 无符号类型p:指针
例如xTaskHandle表示一个任务句柄,ucQueueStorage表示无符号字符类型的队列存储区,pxReadyTasksLists表示指向就绪列表的指针。
3.6.2 函数
函数名由“返回值类型前缀 + 所在文件名 + 功能描述”三部分组成,例如vTaskDelay():
v:表示这个函数的返回值是 voidTask:表示这个函数定义在 task.c 文件中Delay:表示函数的功能是延时
再如xQueueReceive()表示返回BaseType_t类型、定义在 queue.c 中、功能为接收队列数据;pvPortMalloc()表示返回 void 指针、属于移植层、功能为内存分配。
3.6.3 宏
(1)宏具有明确的描述性,并使用完整的单词(无缩写,但普遍接受的缩写除外)。
(2)宏以定义宏的文件为前缀,前缀为小写。例如,在FreeRTOSConfig.h中定义的宏以config为前缀,如configUSE_PREEMPTION;在task.c中定义的宏以task为前缀,如taskSCHEDULER_NOT_STARTED;在portmacro.h中定义的宏以port为前缀,如portMAX_DELAY。
(3)除前缀外,所有宏均使用大写字母书写,并使用下划线来分隔单词。
提示:掌握这套命名规则后,阅读 FreeRTOS 源码时可以快速判断一个标识符的类型、所属模块和大致用途,对后续分析内核实现非常有帮助。