
一、操作系统介绍1.1 什么是裸机开发裸机开发指的是在没有操作系统OS或者其他高级软件支持的情况下直接在裸机硬件上进行软件开发的过程。在裸机开发中开发者需要直接面对硬件层面的操作和控制亲自管理 CPU、内存以及 I/O 资源而不依赖于任何操作系统提供的抽象层或者服务。我们前面学习的 STM32 单片机代码都属于裸机开发。1.2 什么是操作系统一个计算机系统可以大致分为三个部分硬件Hardware、操作系统Operating System、应用程序Application Programs。硬件包含了芯片、存储空间、输入输出等设备为整个系统提供了基础的计算资源。操作系统是一个控制程序作为硬件和应用程序之间的桥梁主要是和硬件打交道负责协调分配计算资源和内存资源给不同的应用程序使用并防止系统出现故障。面对来自不同应用程序的大量且互相竞争的资源请求操作系统通过一个调度算法和内存管理算法尽可能把资源公平且有效率地分配给不同的程序。应用程序则通过调用操作系统提供的 API 接口获得相应资源完成指定的任务。操作系统从整体上分为两大类通用操作系统和实时操作系统。1.3 通用操作系统通用操作系统包括 Linux、Windows、macOS 等主流的操作系统。这些操作系统大家每天都在使用功能也十分强大只是它们有时为了保障系统的流畅运行就不能保证每个程序都能实时响应在易用性和实时性之间有所取舍。而且单片机有限的片上资源也不足以支撑通用操作系统的运行。1.4 实时操作系统实时操作系统RTOSReal 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 版本下载。2GitHub 下载GitHub 地址Releases · FreeRTOS/FreeRTOS · GitHub现在 FreeRTOS 已经将源码迁移到 GitHub 上可以直接下载。3.1.2 源码结构介绍1源码整体结构名称描述FreeRTOSFreeRTOS 内核FreeRTOS-PlusFreeRTOS 组件一般我们会选择使用第三方的组件tools工具GitHub-FreeRTOS-HomeFreeRTOS 的 GitHub 仓库链接Quick_Start_Guide快速入门指南官方文档链接Upgrading-to-FreeRTOS-xxx升级到指定 FreeRTOS 版本官方文档链接History.txtFreeRTOS 历史更新记录其他其他2FreeRTOS 文件夹结构名称描述DemoFreeRTOS 演示例程支持多种芯片架构、多种型号芯片LicenseFreeRTOS 相关许可SourceFreeRTOS 源码最重要的文件夹Test公用以及移植层测试代码3Source 文件夹结构名称描述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 文件夹里根据编译器、内核等实际环境对应选取。4portable 文件夹结构FreeRTOS 操作系统归根到底是一个软件层面的东西需要跟硬件联系在一起portable 文件夹里面的东西就是连接桥梁。由于我们使用 MDK 开发因此这里只重点介绍其中的部分移植文件。名称描述Keil指向 RVDS 文件夹RVDS不同内核芯片的移植文件MemMang内存管理相关文件Keil 文件夹里只有一个 See-also-the-RVDS-directory.txt意思是让我们看 RVDS 文件夹。1RVDS 文件夹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 文件对应的头文件主要是一些数据类型和宏定义。2MemMang 文件夹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_xTaskGetSchedulerState13.2.4 修改 stm32f1xx_it.c1引入头文件/* Private includes ----------------------------------------------------------*//* USER CODE BEGIN Includes */#includeFreeRTOS.h#includetask.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/Portable3.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_BITSMCU 使用的优先级位数STM32 一般为 4。configLIBRARY_LOWEST_INTERRUPT_PRIORITY最低中断优先级一般填 15。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITYFreeRTOS 可管理的中断最高优先级优先级数值大于等于该值的中断才能调用 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_tBaseType_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_tUBaseType_t是BaseType_t的无符号版本主要用于表示任务优先级、队列长度等不可能为负数的量。/* portmacro.h 中的典型定义 */ typedef unsigned long UBaseType_t;例如任务创建函数xTaskCreate()的优先级参数、uxTaskPriorityGet()的返回值使用的都是UBaseType_t类型。3.5.4 StackType_tStackType_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 变量变量名以变量类型为前缀前缀使用小写字母cchar 类型sshort 类型llong 类型xBaseType_t 及其他非标准类型如结构体、句柄uunsigned 无符号类型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 源码时可以快速判断一个标识符的类型、所属模块和大致用途对后续分析内核实现非常有帮助。