HAL库版FreeRTOS(上)

目录

FreeRTOS 简介

初识FreeRTOS

什么是FreeRTOS?

首先看一下FreeRTOS 的名字,可以分为两部分:“Free”和“RTOS”,“Free”就是免费的、
自由的、不受约束的意思,“RTOS”全称是Real Time Operating System,中文名就是实时操作系统,要注意的是,RTOS 并不是值某一特定的操作系统,而是指一类操作系统,例如,µC/OS,FreeRTOS,RTX,RT-Thread 等这些都是RTOS 类的操作系统。因此,从FreeRTOS 的名字中就能看出,FreeROTS 是一款免费的实时操作系统。

操作系统是允许多个任务“同时运行”的,操作系统的这个特性被称为多任务。然而实际
上,一个CPU 核心在某一时刻只能运行一个任务,而操作系统中任务调度器的责任就是决定在某一时刻CPU 究竟要运行哪一个任务,任务调度器使得CPU 在各个任务之间来回切换并处理任务,由于切换处理任务的速度非常快,因此就给人造成了一种同一时刻有多个任务同时运行的错觉。

操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分
配同样的运行时间,时间到了就切换到下一个任务,Unix 操作系统就是这样的。RTOS 的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的。在实时环境中,要求操作系统必须实时地对某一个事件做出响应,因此任务调度器的行为必须是可预测的。像FreeRTOS这种传统的RTOS 类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。

FreeRTOS 是众多RTOS 类操作系统中的一种,FreeRTOS 十分的小巧,可以在资源有限的
微控制器中运行,当然了,FreeRTOS 也不仅仅局限于在微控制器中使用。就单从文件数量上来看FreeRTOS 要比µC/OS 少得多。

为什么选择FreeRTOS?

  • 1.免费。这是很重要的一点,因为在做产品的时候是要考虑产品的成本的,显而易见的FreeRTOS 操作系统就是一个很好的选择,当然了,也可以选择其他免费的RTOS 操作系统。
  • 2.简单。µC/OS 操作系统系统相比要少很多,核心代码只有9000行左右。
  • 3.使用广泛。许多半导体厂商和软件厂商都在其产品中使用了FreeRTOS 操作系统。比如,许多的半导体厂商都会在其产品的SDK 包中使用FreeRTOS 操作系统,尤其是涉及Wi-Fi、蓝牙等这些带协议栈的芯片或模块;著名的GUI 设计软件库TouchGFX 在其软件的应用例程中使用了FreeRTOS 操作系统;ST 公司也在其STM32Cube 生态系统中加入了对FreeRTOS 操作系统的支持。
  • 4.资料齐全。在FreeRTOS 操作系统的官网(https://www.freertos.org/)上,提供了大量的FreeRTOS 操作系统的相关文档及例程源码。但是美中不足的是,提供的文档都是英文文档,查看这些资料要求有一定的英语功底。
  • 5.可移植性强。FreeRTOS 操作系统支持多种不同架构的不同型号的处理器,比如STM32系列的F1、F4、F7 和H7 等都可以移植FreeRTOS,这极大的方便了我们学习和使用FreeRTOS操作系统。

FreeRTOS 的特点

FreeRTOS 操作系统是一个功能强大的RTOS 操作系统,并且能够根据需求进行功能裁剪,
以满足各种环境的要求,FreeRTOS 的特点如下图所示:

在这里插入图片描述

商业许可

FreeRTOS 采用了MIT 开源许可,这允许将FreeRTOS 操作系统用于商业应用,并且不需
要公开源代码。此外,FreeRTOS 还衍生出了另外两个操作系统:OpenRTOS 和SafeRTOS,其中OpenRTOS 使用了和FreeRTOS 相同的代码,只是OpenRTOS 受商业授权保护,OpenRTOS的商业许可和FreeRTOS 的MIT 开源许可对比如下表所示:

在这里插入图片描述

SafeRTOS 同样是FreeRTOS 的衍生版本,SafeRTOS 符合工业、医疗、汽车和其他国际安
全标准的严格要求,具有更高的安全性。

查找资料

在笔者编写此教程的时候,FreeRTOS 内核的最新版本是V10.4.6,本文和本文配套的例程源码都是基于FreeRTOS 内核的V10.4.6 这个版本。因此获取FreeRTOS最权威、最实时的资料,FreeRTOS 官网是最好的地方,FreeRTOS 的官网网址是https://www.freertos.org/,打开后如下图所示:

在这里插入图片描述

底下的两个按钮,分别是“Download FreeRTOS”和“Getting Started”,通过
“Download FreeRTOS”就能够下载到最新发布的FreeRTOS,而右侧的“Getting Started”就是在FreeRTOS 官网查看在线资料的入口。通过点击“Getting Started”,再点击“Getting started with the FreeRTOS kernel”底下的“Learn More”就能够查看到有关FreeRTOS 内核的在线资料文档了。同时在页面的左侧可以看到FreeRTOS 在线资料的导航栏,如下图所示:

在这里插入图片描述

从上图可以看到,FreeRTOS 的官网提供了大量的在线资料,其中包括了入门FreeRTOS、
FreeRTOS 的官方书籍、FreeRTOS 内核的相关内容、开发文档、次要文档、FreeRTOS 支持的设备、FreeRTOS API 参考手册、FreeRTOS 的授权说明。“Developer Docs”和“Secondary Docs”即“开发文档”和“次要文档”是FreeRTOS 官方提供的FreeRTOS 在线文档。在FreeRTOS API参考手册中详细地介绍了FreeRTOS 中各个API 的使用说明,包括API 函数的参数说明、返回值说明以及API 用法举例,任意打开一个创建任务的API 函数,如下图所示:

在这里插入图片描述

在这个页面下方,就可以看到“xTaskCreate”这个API 函数详细的用法说明和用法举例,
用法举例的代码如下所示:

/* 被创建的任务*/
void vTaskCode(void * pvParameters) {
        /* 确保传入的参数是1 */
        configASSERT(((uint32_t) pvParameters) == 1);
        for (;;) {
            /* 任务代码*/
        }
    }
    /* 用来创建任务的函数*/
void vOtherFunction(void) {
    BaseType_t xReturned;
    TaskHandle_t xHandle = NULL;
    /* 创建任务*/
    xReturned = xTaskCreate(
        vTaskCode, /* 任务函数*/
        "NAME", /* 任务名*/
        STACK_SIZE, /* 任务堆栈大小,单位:字*/ (void * ) 1, /* 传递给任务函数的参数*/
        tskIDLE_PRIORITY, /* 任务优先级*/ & xHandle); /* 任务句柄*/
    if (xReturned == pdPASS) {
        /* 任务创建完成,使用任务句柄来删除任务*/
        vTaskDelete(xHandle);
    }
}

FreeRTOS 官方文档

FreeRTOS 官方的文档和教程怎么样呢?点击刚刚导航栏中的“FreeRTOS Books”,就能够看到FreeRTOS 的官方文档和配套的源代码,如下图所示:

在这里插入图片描述

从上图可以看到,FreeRTOS 官方提供了两份PDF 文档和一份文档配套的源代码,其中一
份PDF 是FreeRTOS 的教程指南,另一份PDF 是FreeRTOS 的参考手册。相比于uC/OS,
FreeRTOS 官方提供的文档确实有点少。

FreeRTOS 官方还提供了两份在线文档,就是刚刚提到的“Developer Docs”和“Secondary Docs”,以“Developer Docs”为例,在导航栏中点击Developer Docs,就能看到文档的目录,如下图所示:
在这里插入图片描述

Cortex-M 架构资料

本书是以正点原子的STM32 系列板卡为例,讲解FreeRTOS,在FreeRTOS 的移植,任务
切换的原理中会涉及芯片结构的相关知识,因此需要了解ARM Cortex-M 架构的相关知识。有本详细地介绍了ARM Cortex-M3 和ARM Cortex-M4 的书籍叫做《The Definitive Guide to ARM Cortex M3 and Cortex M4 Processors, 3rd Edition》,这本书的中文翻译版本为《ARM Cortex-M3与Cortex-M4 权威指南(第3 版)》,这本书对ARM Cortex-M3 和ARM Cortex-M4 作了非常详细的介绍,强烈建议想深入了解ARM Cortex 的读者阅读此书,英文原版可以在ARM 官方免费下载。此书的中文版本如下图所示:
在这里插入图片描述
后面的学习中涉及到ARM Cortex-M 架构的知识均参考自这本书。

FreeRTOS 源码初探

FreeRTOS 源码下载

编写此教程的时候,FreeRTOS 最新发布的版本是v202112.00,FreeRTOS 内核的最新版本是V10.4.6,这里要注意的是FreeRTOS 和FreeRTOS 内核是两个不同的东西,FreeRTOS 包含了FreeRTOS 内核以及其他的一些FreeRTOS 组件,v202112.00 版本的FreeRTOS 对应的FreeRTOS 内核版本就是V10.4.6,本教程主要讲解的是FreeRTOS 内核,因此本教程以及本教程配到的例程源码全部基于FreeRTOS 内核的V10.4.6 这个版本。

在FreeRTOS 官网https://www.freertos.org/的主页点击“Download FreeRTOS”,即可进入到FreeRTOS 的下载页面,如下图所示:

在这里插入图片描述

从上图可以看到,FreeRTOS 提供两个版本的FreeRTOS 下载链接,分别为FreeRTOS 和
FreeRTOS LTS,其中FreeRTOS LTS 是FreeRTOS Long Time Support,这个版本的FreeRTOS 会受官方长期的支持和维护,如果是做产品的话,当然优先选择FreeRTOS LTS,但是就我们学习而言,当让是选择最新的发布版本FreeRTOS,因此点击图1.3.1.1 中绿色的Download 按钮进行FreeRTOS 的下载。当然,也可以通过Github 下载,或单独下载V10.4.6 版本的FreeRTOS 内核,这里为了方便读者理解,下载包含了FreeRTOS 组件的FreeRTOS v202112.00。下载解压后得到的文件,如下图所示:

在这里插入图片描述

上图展示的就是FreeRTOS 的根目录。各子文件和子文件的的描述如下表所示:

在这里插入图片描述

组件包括TCP、MQTT、UDP等等,我们一般使用第三方的组件,不使用RTOS自带的组件,所以组件文件夹我们用不到。

重点关注FreeRTOS 内核的文件夹,如下图所示:

在这里插入图片描述
上图展示的就是FreeRTOS 内核的根目录。接下来就开始介绍FreeRTOS 内核中的文件。

FreeRTOS 文件预览

  1. Demo 文件夹
    Demo 文件夹里面就是FreeRTOS 的演示工程,打开以后如下图所示:
    在这里插入图片描述

限于篇幅,图1.3.2.1 仅展示了Demo 文件夹中的部分演示工程。从Demo 文件夹中可以看出,FreeRTOS 支持多种芯片架构的多种不同型号的芯片,其中就包括了ST 的F1、F4、F7 和H7 系列的相关FreeRTOS 演示工程,这对于入门学习FreeRTOS 是十分有帮助的,在学习移植FreeRTOS 的过程中就可以参考这些演示工程。

  1. License 文件夹
    License 文件夹中包含了FreeRTOS 的相关许可信息,如果是要使用FreeRTOS 做产品的话,就得仔细地看看这个文件夹中的内容。

  2. Source 文件夹
    这个文件夹中的内容就是FreeRTOS 的源代码了,这就是学习和使用FreeRTOS 的重中之重,Source 文件夹打开后如下图所示:

在这里插入图片描述

图1.3.2.2 中的文件就是FreeRTOS 的源文件了。可以看到,就文件数量而言,FreeRTOS 的文件数量相对与µC/OS 而言少了不少。Source 文件夹中各文件和文件夹的描述如下表所示:

在这里插入图片描述

portable文件夹

FreeRTOS 操作系统归根到底是一个软件层面的东西,那FreeRTOS 是如何跟硬件联系在一起的呢?portable 文件夹里面的东西就是连接软件层面的FreeRTOS 操作系统和硬件层面的芯片的桥梁。打开protable 文件夹后,可以看到FreeRTOS 针对不同的芯片架构和不同的编译器提供了不同的移植文件,由于本文是使用MDK 开发正点原子的STM32 系列板卡,因此这里只重点介绍其中的部分移植文件,如下图所示:

在这里插入图片描述

首先来看一下Keil 文件夹,打开Keil 文件夹后可以看到,Keil 文件夹中之后一个文件,文
件名为:“See-also-the-RVDS-directory.txt”,看文件名就知道要转到RVDS 文件夹了。接下来打开RVDS 文件夹,如下图所示:

在这里插入图片描述
从图1.3.2.5 中可以看出,FreeRTOS 提供了ARM Cortex-M0、ARM Cortex-M3、ARM Cortex-M3、ARM Cortex-M7 等内核芯片的移植文件,这里就不再深入了,下文讲解到FreeRTOS 移植部分的时候再进行详细分析。

最后再来看一下图1.3.2.4 中的MemMang 文件夹,打开MemMang 文件夹后如下图所示:

在这里插入图片描述

MemMang 中的文件是FreeRTOS 提供的用于内存管理的文件,从图1.3.2.6 中可以看到,MemMang 文件夹中包含了五个C 源文件,这五个C 源文件对应了五种内存管理的方法。这里暂不对FreeRTOS 提供的内存管理进行深究,下文讲解到FreeRTOS 内存管理的时候再进行详细分析。

FreeRTOS 移植

移植前准备

在开始移植FreeRTOS 之前,需要提前准备好一个用于移植FreeRTOS 的基础工程,和
FreeRTOS 的源码。

  1. 基础工程
    由于本文的后续一些实验当中需要用到LED、LCD、定时器、串口、内存管理等外设及功能,因此就以正点原子标准例程-HAL 库版本的内存管理的实验工程为基础工程进行FreeRTOS的移植。由于内存管理实验例程的BSP 文件夹中可能不包含定时器的驱动文件,因此如果内存管理试验力撑的BSP 文件夹如果不包含TIMER 文件夹的话,需要从定时器相关实验的BSP 文件夹中拷贝一份TIMER 到FreeRTOS 移植基础工程当中。

  2. FreeRTOS 源码
    本教程所使用的FreeRTOS 内核源码的版本V10.4.6,即FreeRTOS v202112.00。在第一章中已经详细的介绍了如何从FreeRTOS 的官网获取FreeRTOS 的源码,同样的,开发板资料盘中也提供了本教程所使用的FreeRTOS 源码,即FreeRTOS v202112.00(FreeRTOS 内核V10.4.6),路径为:软件资料→FreeRTOS 学习资料→FreeRTOSv202112.00.zip。

添加FreeRTOS 文件

在准备好基础工程和FreeRTOS 的源码后,接下来就可以开始进行FreeRTOS 的移植了。

  1. 添加FreeRTOS 源码

在基础工程的Middlewares 文件夹中新建一个FreeRTOS 子文件夹,如下图所示:

在这里插入图片描述

图2.1.2.1 中的FreeRTOS 就是新建的文件夹,这里要说明的是图2.1.2.1 中的其他文件夹为
内存管理实验工程中原本就存在的,对于正点原子的不同STM32 开发板图2.1.2.1 中的文件可能有所不同,但只需新建一个FreeRTOS 子文件即可。

接着就需要将FreeRTOS 的源代码添加到刚刚新建的FreeRTOS 子文件中了。将FreeRTOS
内核源码的Source 文件夹下的所有文件添加到工程的FreeRTOS 文件夹中,如下图所示:

在这里插入图片描述

图2.1.2.2 中各文件和文件夹的描述在1.3.2 小节中已经说明,对于在正点原子的STM32 系
列开发板上移植FreeRTOS,portable 文件夹中的文件只需要使用到图1.3.2.3 中的三个文件夹,其余用不到的文件,可以删除。

  1. 将文件添加到工程

打开基础工程,新建两个文件分组,分别为Middlewares/FreeRTOS_CORE 和
Middlewares/FreeRTOS_PORT,如下图所示:

在这里插入图片描述

Middlewares/FreeRTOS_CORE 分组用于存放FreeRTOS 的内核C 源码文件,将“1. 添加
FreeRTOS 源码”步骤中的FreeRTOS 目录下所有的FreeRTOS 的内核C 源文件添加到
Middlewares/FreeRTOS_CORE 分组中。

Middlewares/FreeRTOS_PORT 分组用于存放FreeRTOS 内核的移植文件,需要添加两个文件到这个分组,分别为heap_x.c 和port.c。

首先是heap_x.c,在路径FreeRTOS/portable/MemMang 下有五个C 源文件,这五个C 源文
件对应了五种FreeRTOS 提供的内存管理算法,读者在进行FreeRTOS 移植的时候可以根据需求选择合适的方法,具体这五种内存管理的算法,在后续FreeRTOS 内存管理章节会具体分析,这里就先使用heap_4.c,将heap_4.c 添加到Middlewares/FreeRTOS_PORT 分组中。

接着是port.c,port.c 是FreeRTOS 这个软件与MCU 这个硬件连接的桥梁,因此对于正点原子的STM32 系列不同的开发板,所使用的port.c 文件是不同的。port.c 文件的路径在FreeRTOS/portable/RVDS 下。进入到FreeRTOS/portable/RVDS,可以看到FreeRTOS 针对不同的MCU 提供了不同的port.c 文件,具体正点原子的STM32 系列开发板与不同port.c 的对应关系如下表所示:

在这里插入图片描述

只需将开发板芯片对应的port.c 文件添加到Middlewares/FreeRTOS_PORT 分组中即可。
将所有FreeRTOS 相关的所需文件添加到工程后,如下图所示:

在这里插入图片描述

  1. 添加头文件路径

接下来添加FreeRTOS 源码的头文件路径,需要添加两个头文件路径,毋庸置疑,其中一
个头文件路径就是FreeRTOS/include,另外一个头文件路径为port.c 文件的路径,根据表2.1.2.4中不同类型开发板与port.c 文件的对应关系进行添加即可。

添加完成后如下图所示(这里以正点原子的STM32F1 系列开发板为例,其他类型的开发板类似):

在这里插入图片描述

  1. 添加FreeRTOSConfig.h 文件

FreeRTOSConfig.h 是FreeRTOS 操作系统的配置文件,FreeRTOS 操作系统是可裁剪的,用户可以根据需求对FreeRTOS 进行裁剪,裁剪掉不需要用到的FreeRTOS 功能,以此来节约MCU中寸土寸金的内存资源。那么FreeRTOSConfig.h 文件从哪里来呢?主要有三个途径:

(1) FreeRTOSConfig.h 获取途径一第一种途径就是用户自行编写,用户可以根据自己的需求编写FreeRTOSConfig.h 对FreeRTOS 操作系统进行裁剪。FreeRTOS 官网的在线文档中就详细地对FreeRTOSConfig.h 中各个配置项进行了描述,网页链接:https://www.freertos.org/a00110.html。当然,对于FreeRTOS 新手来说,不建议自行编写。

(2) FreeRTOSConfig.h 获取途径二
第二种途径就是FreeRTOS 内核的演示工程,在“1.3.2 FreeRTOS 文件预览”这一小节中,
介绍了Demo 文件夹,Demo 文件夹中包含了FreeRTOS 官方提供的演示工程,在这些演示工程当中就包含了每个演示工程对应的FreeRTOSConfig.h 文件,需要注意的是,有些演示工程使用的是老版本的FreeRTOS,因此部分演示工程的FreeRTOSConfig.h 文件并不能够很好的适用于新版本的FreeRTOS。任意打开其中一个演示工程,如下图所示:

在这里插入图片描述

读者可以在Demo 文件夹中找到与自己所使用芯片相似的演示工程中的FreeRTOSConfig.h
文件,并根据自己的需求,稍作修改。

(3) FreeRTOSConfig.h 获取途径三
第三种途径,也是笔者推荐的。可以从本套教程配套例程“FreeRTOS 移植实验”的User 子
文件夹下找到FreeRTOSConfig.h 文件,这个文件就是参考FreeRTOS 官网中对FreeRTOSConfig.h文件的描述,并针对正点原子的STM32 系列开发板编写的。

这里要说明的是,本套教程是用于学习FreeRTOS 的,因此在FreeRTOSConfig.h 文件中并没有对FreeRTOS 的功能作过多的裁剪,大部分的功能都保留了,只不过在后续的部分实验中还需要对FreeRTOSConfig.h 文件作相应的修改,以满足实验的需求。
本教程就以途径三进行讲解,只需将本套教程配套例程“FreeRTOS 移植实验”User 子文件
夹下的FreeRTOSConfig.h 文件添加到基础工程的User 子目录下即可。这里要注意的是,正点原子的STM32 系列开发板对应的FreeRTOSConfig.h 文件是不通用的,具体原因在后续分析FreeRTOSConfig.h 文件的时候会具体地讲解。

修改SYSTEM文件(sys.c、usart.c、delay.c)

SYSTEM 文件夹中的文件一开始是针对µC/OS 编写的,因此使用FreeRTOS 的话,就需要作相应的修改。SYSTEM 文件夹中一共需要修改三个文件,分别是sys.h、usart.c、delay.c。

  1. sys.h 文件
    sys.h文件的修改很简单,在sys.h文件中使用了宏SYS_SUPPORT_OS 来定义是否支持OS,因为要支持FreeRTOS,因此应当将宏SYS_SUPPORT_OS 定义为1,具体修改如下所示:
/**
* SYS_SUPPORT_OS用于定义系统文件夹是否支持OS
* 0,不支持OS
* 1,支持OS
*/
#define SYS_SUPPORT_OS 1
  1. usart.c 文件
    usart.c 文件的修改也很简单,一共有两个地方需要修改,首先就是串口的中断服务函数,原本在使用µC/OS 的时候,进入和退出中断需要添加OSIntEnter()和OSIntExit()两个函数,这是µC/OS 对于中断的相关处理机制,而FreeRTOS 中并没有这种机制,因此将这两行代码删除,修改后串口的中断服务函数如下所示:
    正点原子STM32F1 系列:
void USART_UX_IRQHandler(void) {
    HAL_UART_IRQHandler( & g_uart1_handle); /* 调用HAL库中断处理公用函数*/
    while (HAL_UART_Receive_IT( & g_uart1_handle, (uint8_t * ) g_rx_buffer,
            RXBUFFERSIZE) != HAL_OK) /* 重新开启中断并接收数据*/ {
        /* 如果出错会卡死在这里*/
    }
}

正点原子STM32F4/F7/H7 系列:

void USART_UX_IRQHandler(void) {
    uint32_t timeout = 0;
    uint32_t maxDelay = 0x1FFFF;
    HAL_UART_IRQHandler( & g_uart1_handle); /* 调用HAL库中断处理公用函数*/
    timeout = 0;
    while (HAL_UART_GetState( & g_uart1_handle) != HAL_UART_STATE_READY) /* 等待就绪*/ {
        timeout++; /* 超时处理*/
        if (timeout > maxDelay) {
            break;
        }
    }
    timeout = 0;
    /* 一次处理完成之后,重新开启中断并设置RxXferCount为1 */
    while (HAL_UART_Receive_IT( & g_uart1_handle, (uint8_t * ) g_rx_buffer,
            RXBUFFERSIZE) != HAL_OK) {
        timeout++; /* 超时处理*/
        if (timeout > maxDelay) {
            break;
        }
    }
}

接下来usart.c 要修改的第二个地方就是导入的头文件,因为在串口的中断服务函数当中已
经删除了µC/OS 的相关代码,并且也没有使用到FreeRTOS 的相关代码,因此将usart.c 中包含的关于OS 的头文件删除,要删除的代码如下所示:

/* 如果使用os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用*/
#endif
  1. delay.c 文件
    接下来修改SYSTEM 文件夹中的最后一个文件——delay.c,delay.c 文件需要改动的地方比较多,大致可分为三个步骤:删除适用于µC/OS 但不适用于FreeRTOS 的相关代码、添加FreeRTOS 的相关代码、修改部分内容。

(1) 删除适用于µC/OS 但不适用于FreeRTOS 的相关代码

一共需要删除1 个全局变量、6 个宏定义、3 个函数,这些要删除的代码在使用µC/OS 的
时候会使用到,但是在使用FreeRTOS 的时候无需使用,需要删除的代码如下所示:

/* 定义g_fac_ms变量, 表示ms延时的倍乘数,
 * 代表每个节拍的ms数, (仅在使能os的时候,需要用到)
 */
static uint16_t g_fac_ms = 0;
/*
 * 当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
 * 首先是3个宏定义:
 * delay_osrunning :用于表示OS当前是否正在运行,以决定是否可以使用相关函数
 * delay_ostickspersec :用于表示OS设定的时钟节拍,
 * delay_init将根据这个参数来初始化systick
 * delay_osintnesting :用于表示OS中断嵌套级别,因为中断里面不可以调度,
 * delay_ms使用该参数来决定如何运行
 * 然后是3个函数:
 * delay_osschedlock :用于锁定OS任务调度,禁止调度
 * delay_osschedunlock :用于解锁OS任务调度,重新开启调度
 * delay_ostimedly :用于OS延时,可以引起任务调度.
 *
 * 本例程仅作UCOSII和UCOSIII的支持,其他OS,请自行参考着移植
 */
/* 支持UCOSII */
#
ifdef OS_CRITICAL_METHOD
/* OS_CRITICAL_METHOD定义了
 * 说明要支持UCOSII
 */
# define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行*/ 
# define delay_ostickspersec OS_TICKS_PER_SEC /* OS时钟节拍,即每秒调度次数*/ 
# define delay_osintnesting OSIntNesting /* 中断嵌套级别,即中断嵌套次数*/ 
# endif
/* 支持UCOSIII */
# ifdef CPU_CFG_CRITICAL_METHOD
/* CPU_CFG_CRITICAL_METHOD定义了
 * 说明要支持UCOSIII
 */
# define delay_osrunning OSRunning /* OS是否运行标记,0,不运行;1,在运行*/ 
# define delay_ostickspersec OSCfg_TickRate_Hz /* OS时钟节拍,即每秒调度次数*/ 
# define delay_osintnesting OSIntNestingCtr /* 中断嵌套级别,即中断嵌套次数*/ 
# endif
/**
 * @brief us级延时时,关闭任务调度(防止打断us级延迟)
 * @param 无
 * @retval 无
 */
static void delay_osschedlock(void) {
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
        OS_ERR err;
        OSSchedLock( & err); /* UCOSIII的方式,禁止调度,防止打断us延时*/ 
#else /* 否则UCOSII */
        OSSchedLock(); /* UCOSII的方式,禁止调度,防止打断us延时*/ 
#endif
}
    /**
     * @brief us级延时时,恢复任务调度
     * @param 无
     * @retval 无
     */
static void delay_osschedunlock(void) {
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
        OS_ERR err;
        OSSchedUnlock( & err); /* UCOSIII的方式,恢复调度*/
#else /* 否则UCOSII */
        OSSchedUnlock(); /* UCOSII的方式,恢复调度*/ 
#endif
}
    /**
     * @brief us级延时时,恢复任务调度
     * @param ticks: 延时的节拍数
     * @retval 无
     */
static void delay_ostimedly(uint32_t ticks) {
#ifdef CPU_CFG_CRITICAL_METHOD
    OS_ERR err;
    OSTimeDly(ticks, OS_OPT_TIME_PERIODIC, & err); /* UCOSIII延时采用周期模式*/ 
#else
    OSTimeDly(ticks); /* UCOSII延时*/ 
#endif
}

(2) 添加FreeRTOS 的相关代码

只需要在delay.c 文件中使用extern 关键字导入一个FreeRTOS 函数——
xPortSysTickHandler()即可,这个函数是用于处理FreeRTOS 系统时钟节拍的,本教程是使用SysTick 作为FreeRTOS 操作系统的心跳,因此需要在SysTick 的中断服务函数中调用这个函数,因此将代码添加到SysTick 中断服务函数之前,代码修改如下:

extern void xPortSysTickHandler(void);
/**
* @brief systick中断服务函数,使用OS时用到
* @param ticks: 延时的节拍数
* @retval 无
*/
void SysTick_Handler(void)
{
	/* 代码省略*/
}

(3) 修改部分内容
最后要修改的内容包括两个,分别是包含头文件和4 个函数。
首先来看需要修改的4 个函数,分别是SysTick_Handler()、delay_init()、delay_us()和
delay_ms()。

(a) SysTick_Handler()
这个函数是SysTick 的中断服务函数,需要在这个函数中重复调用上个步骤中导入的函数
xPortSysTickHandler(),代码修改后如下所示:

/**
 * @brief systick中断服务函数,使用OS时用到
 * @param ticks: 延时的节拍数
 * @retval 无
 */
void SysTick_Handler(void) {
    HAL_IncTick();
    /* OS开始跑了,才执行正常的调度处理*/
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
        xPortSysTickHandler();
    }
}

(b) delay_init()
函数delay_init() 主要用于初始化SysTick 。这里要说明的是,在后续调用函数
vTaskStartScheduler()(这个函数在下文中讲解到FreeRTOS 任务调度器的时候会具体分析)的时候,FreeRTOS 会按照FreeRTOSConfig.h 文件的配置对SysTick 进行初始化,因此delay_init()函数初始化的SysTick 主要使用在FreeRTOS 开始任务调度之前。函数delay_init()要修改的部分主要为SysTick 的重装载值以及删除不用的代码,代码修改如下:
正点原子STM32F1 系列:

void delay_init(uint16_t sysclk) {#
    if SYS_SUPPORT_OS
    uint32_t reload;#
    endif
    SysTick - > CTRL = 0;
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
    g_fac_us = sysclk / 8;#
    if SYS_SUPPORT_OS
    reload = sysclk / 8;
    /* 使用configTICK_RATE_HZ计算重装载值
     * configTICK_RATE_HZ在FreeRTOSConfig.h中定义
     */
    reload *= 1000000 / configTICK_RATE_HZ;
    /* 删除不用的g_fac_ms相关代码*/
    SysTick - > CTRL |= 1 << 1;
    SysTick - > LOAD = reload;
    SysTick - > CTRL |= 1 << 0;#
    endif
}

正点原子STM32F4/F7/H7 系列:

void delay_init(uint16_t sysclk) {#
    if SYS_SUPPORT_OS
    uint32_t reload;#
    endif
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
    g_fac_us = sysclk;#
    if SYS_SUPPORT_OS
    reload = sysclk;
    /* 使用configTICK_RATE_HZ计算重装载值
     * configTICK_RATE_HZ在FreeRTOSConfig.h中定义
     */
    reload *= 1000000 / configTICK_RATE_HZ;
    /* 删除不用的g_fac_ms相关代码*/
    SysTick - > CTRL |= SysTick_CTRL_TICKINT_Msk;
    SysTick - > LOAD = reload;
    SysTick - > CTRL |= SysTick_CTRL_ENABLE_Msk;#
    endif
}

可以看到在正点原子STM32 系列开发板的标准例程源码中,STM32F1 系列的函数
delay_init()将SysTick 的时钟频率设置为CPU 时钟频率的1/8,而STM32F4/F7/H7 系列的函数delay_init()则将SysTick 的时钟频率设置为与CPU 相同的时钟频率,由于FreeRTOS 在配置SysTick 时,并不会配置SysTick 的时钟源,因此这将导致正点原子STM32F1 系列与正点原子STM32F4/F7/H7 系列的FreeRTOSConfig.h 文件有所差异,并且也只有这一点存在差异,这是读者在使用正点原子提供的FreeRTOSConfig.h 文件时需要注意的地方。

© delay_us()
函数delay_us()用于微秒级的CPU 忙延时,原本的函数delay_us()延时的前后加入了自定义
函数delay_osschedlock()和delay_osschedunlock()用于锁定和解锁µC/OS 的任务调度器,以此来让延时更加准确。在FreeRTOS 中可以不用加入这两个函数,但是要注意的是,这会让函数delay_us()的微秒级延时的精度有所下降,函数delay_us()修改后的代码如下所示:

void delay_us(uint32_t nus) {
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload = SysTick - > LOAD;
    /* 删除适用于µC/OS用于锁定任务调度器的自定义函数*/
    ticks = nus * g_fac_us;
    told = SysTick - > VAL;
    while (1) {
        tnow = SysTick - > VAL;
        if (tnow != told) {
            if (tnow < told) {
                tcnt += told - tnow;
            } else {
                tcnt += reload - tnow + told;
            }
            told = tnow;
            if (tcnt >= ticks) {
                break;
            }
        }
    }
    /* 删除适用于µC/OS用于解锁任务调度器的自定义函数*/
}

(d) delay_ms()
函数delay_ms()用于毫秒级的CPU 忙延时,原本的函数delay_ms()会判断µC/OS 是否运
行,如果µC/OS 正在运行的话,就使用µC/OS 的OS 延时进行毫秒级的延时,否则就调用函数delay_us()进行毫秒级的CPU 忙延时。在FreeRTOS 中,可以将函数delay_ms()定义为只进行CPU 忙延时,当需要OS 延时的时候,调用FreeRTOS 提供的OS 延时函数vTaskDelay()(在下文讲解FreeRTOS 时间管理的时候会对此函数进行分析)进行系统节拍级延时,函数delay_ms()修改后的代码如下所示:

void delay_ms(uint16_t nms) {
    uint32_t i;
    for (i = 0; i < nms; i++) {
        delay_us(1000);
    }
}

(e) 包含头文件
根据上述步骤的修改,delay.c 文件中使用到了FreeRTOS 的相关函数,因此就需要在delay.c文件中包含FreeRTOS 的相关头文件,并且移除掉原本存在的µC/OS 相关头文件。先看一下修改前delay.c 文件中包含的µC/OS 相关的头文件:

/* 添加公共头文件( ucos需要用到) */
#include "includes.h"

修改成如下内容:

/* 添加公共头文件(FreeRTOS 需要用到) */
#include "FreeRTOS.h"
#include "task.h"

至此,SYSTEM 文件夹针对FreeRTOS 的修改就完成了。

修改中断相关文件

在FreeRTOS 的移植过程中会这几到三个重要的中断,分别是FreeRTOS 系统时基定时器
的中断(SysTick 中断)、SVC 中断、PendSV 中断(SVC 中断和PendSV 中断在下文讲解FreeRTOS中断和FreeRTOS 任务切换的时候会具体分析),这三个中断的中断服务函数在HAL 库提供的文件中都有定义,对于正点原子不同的STM32 开发板,对应了不同的文件,具体对应关系如下表所示:

在这里插入图片描述

其中,SysTick 的中断服务函数在delay.c 文件中已经定义了,并且FreeRTOS 也提供了SVC
和PendSV 的中断服务函数,因此需要将HAL 库提供的这三个中断服务函数注释掉,这里采用宏开关的方式让HAL 库中的这三个中断服务函数不加入编译,使用的宏在sys.h 中定义,因此还需要导入sys.h 头文件,请读者按照表2.1.4.1 找到对应的文件进行修改,修改后的代码如下所示:

/* 仅展示修改部分,其余代码未修改、不展示*/
/* Includes --------------------------------------*/
/* 导入sys.h头文件*/
#include "./SYSTEM/SYS/sys.h"
    /**
     * @brief This function handles SVCall exception.
     * @param None
     * @retval None
     */
    /* 加入宏开关*/
#if (!SYS_SUPPORT_OS)
    void SVC_Handler(void) {}
#endif
/**
 * @brief This function handles PendSVC exception.
 * @param None
 * @retval None
 */
/* 加入宏开关*/
#if (!SYS_SUPPORT_OS)
    void PendSV_Handler(void) {}
#endif
/**
 * @brief This function handles SysTick Handler.
 * @param None
 * @retval None
 */
/* 加入宏开关*/
#if (!SYS_SUPPORT_OS)
    void SysTick_Handler(void) {
        HAL_IncTick();
    }
#endif

最后,也是移植FreeRTOS 要修改的最后一个地方,FreeRTOSConfig.h 文件中有如下定义:

#define configPRIO_BITS __NVIC_PRIO_BITS

对于这个宏定义,在下文讲解到ARM Corten-M 和FreeRTOS 中断的时候会具体分析。可
以看到,这个宏定义将configPRIO_BITS 定义成__NVIC_PRIO_BITS,而__NVIC_PRIO_BITS在HAL 库中有相关定义,对于正点原子不同的STM32 开发板,__NVIC_PRIO_BITS 定义在不同的文件中,具体的对应关系如下表所示:

在这里插入图片描述
请读者按照表2.1.4.2 找到对应的文件进行修改。虽然不同类型的开发板对应的文件不同,
但是__NVIC_PRIO_BITS 都被定义成了相同的值,如下所示:

#define __NVIC_PRIO_BITS 4U

这个值是正确的,但是如果将__NVIC_PRIO_BITS 定义成4U 的话,在编译FreeRTOS 工程的时候,Keil 会报错,具体的解决方法就是将4U 改成4,代码修改后如下所示:

#define __NVIC_PRIO_BITS 4

到此为止,FreeRTOS 就移植完毕了,整体来说难度并不是很高,但是作为FreeRTOS 的初
学者,有些读者可能不明白移植过程中涉及到的一些修改步骤,对于这个问题,笔者建议耐心跟着本教程的步骤完成,在后续的将讲解中,会一一的为读者解决这些问题,学习本来就是一个循序渐进的过程,不能想着要一口吃成大胖子。

可选步骤(建议完成)

这个步骤是可选的,但是笔者强烈建议读者完成,因为在后续实验中会使用到,并且规范
工程。本小节可分为3 个小部分,分别为修改工程目标名、移除USMART 调试组件、添加定时器驱动。

  1. 修改工程目标名称
    本教程是以标准例程-HAL 库版本的内存管理实验工程为基础工程,内存管理实验工程的工程目标名为“MALLOC”,为了规范工程,笔者建议将工程目标名修改为“FreeRTOS”或根据读者的实际场景进行修改,修改如下图所示:

在这里插入图片描述
2. 移除USMART 调试组件

由于本教程并未使用到USMART 调试组件,因此建议将USMART 调试组件从工程中移
除,如果读者需要使用USMART 调试组件的话,也可选择保留,移除USAMRT 调试组建后工程文件分组如下图所示(这里以正点原子的STM32F1 系列开发板为例,其他开发板类似):

在这里插入图片描述
4. 添加定时器驱动
由于在后续的实验中需要使用到STM32 的基本定时器外设,因此需要向工程中添加定时
器的相关驱动文件,读者也可在后续实验需要用到定时器的时候再进行添加。将定时器的相关驱动文件添加到工程的Drivers/BSP 文件分组中,如下图所示(这里以正点原子的STM32F1 系列开发板为例,其他开发板类似):

在这里插入图片描述

图2.1.5.3 是针对正点原子战舰开发板,对于其他正点原子开发板都是类似的,只需要将
btim.c 文件添加到Drivers/BSP 文件分组中即可。这里要注意的是,标准例程-HAL 版本的内存管理实验工程中并没有定时器的相关驱动文件,读者可以在标准例程-HAL 版本中定时器的相关实验工程中复制定时器的驱动文件到本教程FreeRTOS 移植的实验工程中,这点在2.1.1 小节中也有提到。

添加应用程序

移植好FreeRTOS 之后,当然要测试一下移植是否成功。在本步骤中,一共需要修改1 个
文件并添加2 个文件,修改的1 个文件为main.c,添加的2 个文件为freertos_demo.c 和
freertos_demo.h。对于main.c 主要是在main()函数中完成一些硬件的初始化,最后调用
freertos_demo.c 文件中的freertos_demo()函数。而freertos_demo.c 则是用于编写FreeRTOS 的相
关应用程序代码。

  1. main.c
    由于正点原子的STM32 系列开发板众多,这里以正点原子战舰开发板为例为读者进行演示,读者可以根据自己所移植的目标开发板在本教程的配套例程源码的《FreeRTOS 移植实验》中查看对应的main.c 文件。正点原子战舰开发板《FreeRTOS 移植实验》中的main.c 文件如下所示:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/SRAM/sram.h"
#include "./MALLOC/malloc.h"
#include "freertos_demo.h"
int main(void) {
    HAL_Init(); /* 初始化HAL库*/
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72); /* 延时初始化*/
    usart_init(115200); /* 串口初始化为115200 */
    led_init(); /* 初始化LED */
    lcd_init(); /* 初始化LCD */
    key_init(); /* 初始化按键*/
    sram_init(); /* SRAM初始化*/
    my_mem_init(SRAMIN); /* 初始化内部SRAM内存池*/
    my_mem_init(SRAMEX); /* 初始化外部SRAM内存池*/
    freertos_demo(); /* 运行FreeRTOS例程*/
}

可以看到,在main.c 文件中只包含了一个main()函数,main()函数主要就是完成了一些外
设的初始化,如串口、LED、LCD、按键等,并在最后调用了函数freertos_demo()。

  1. freertos_demo.c
#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"

/******************************************************************************************************/
/*FreeRTOS配置*/

/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO 1                   /* 任务优先级 */
#define START_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            StartTask_Handler;  /* 任务句柄 */
void start_task(void *pvParameters);        /* 任务函数 */

/* TASK1 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_PRIO      2                   /* 任务优先级 */
#define TASK1_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task1Task_Handler;  /* 任务句柄 */
void task1(void *pvParameters);             /* 任务函数 */

/* TASK2 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK2_PRIO      3                   /* 任务优先级 */
#define TASK2_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task2Task_Handler;  /* 任务句柄 */
void task2(void *pvParameters);             /* 任务函数 */

/******************************************************************************************************/

/* LCD刷屏时使用的颜色 */
uint16_t lcd_discolor[11] = {WHITE, BLACK, BLUE, RED,
                             MAGENTA, GREEN, CYAN, YELLOW,
                             BROWN, BRRED, GRAY};

/**
 * @brief       FreeRTOS例程入口函数
 * @param       无
 * @retval      无
 */
void freertos_demo(void)
{
    lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
    lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
    lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
    
    xTaskCreate((TaskFunction_t )start_task,            /* 任务函数 */
                (const char*    )"start_task",          /* 任务名称 */
                (uint16_t       )START_STK_SIZE,        /* 任务堆栈大小 */
                (void*          )NULL,                  /* 传入给任务函数的参数 */
                (UBaseType_t    )START_TASK_PRIO,       /* 任务优先级 */
                (TaskHandle_t*  )&StartTask_Handler);   /* 任务句柄 */
    vTaskStartScheduler();
}

/**
 * @brief       start_task
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

/**
 * @brief       task1
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void task1(void *pvParameters)
{
    uint32_t task1_num = 0;
    
    while(1)
    {
        lcd_clear(lcd_discolor[++task1_num % 14]);                      /* 刷新屏幕 */
        lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
        lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
        lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
        LED0_TOGGLE();                                                  /* LED0闪烁 */
        vTaskDelay(1000);                                               /* 延时1000ticks */
    }
}

/**
 * @brief       task2
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void task2(void *pvParameters)
{
    float float_num = 0.0;
    
    while(1)
    {
        float_num += 0.01f;                         /* 更新数值 */
        printf("float_num: %0.4f\r\n", float_num);  /* 打印数值 */
        vTaskDelay(1000);                           /* 延时1000ticks */
    }
}

对于freertos_demo.c 文件,这里先简单的介绍一下这个文件的代码结构,这个文件的代码
结构可分为6 个部分,分别是包含头文件、FreeRTOS 相关配置、全局变量及自定义函数、应用程序入口函数、开始任务入口函数、其他任务入口函数,接下来分别地介绍以上这几个部分的代码。

(1) 包含头文件
包含的头文件分成两个部分,分别为FreeRTOS 头文件和其他头文件,如下所示:

#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
/*FreeRTOS********************************************************************/
#include "FreeRTOS.h"
#include "task.h"
/*****************************************************************************/

(2) FreeRTOS 相关配置
FreeRTOS 的配置主要包括所创建FreeRTOS 任务的相关定义(任务优先级、任务堆栈大小、任务句柄、任务函数)以及FreeRTOS 相关变量(信号量、事件、列表、软件定时器等)的定义,如下所示:

/*FreeRTOS配置*/
/* START_TASK 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define START_TASK_PRIO 1 /* 任务优先级*/ 
# define START_STK_SIZE 128 /* 任务堆栈大小*/
TaskHandle_t StartTask_Handler; /* 任务句柄*/
void start_task(void * pvParameters); /* 任务函数*/
/* TASK1 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define TASK1_PRIO 2 /* 任务优先级*/ 
# define TASK1_STK_SIZE 128 /* 任务堆栈大小*/
TaskHandle_t Task1Task_Handler; /* 任务句柄*/
void task1(void * pvParameters); /* 任务函数*/
/* TASK2 任务配置
 * 包括: 任务句柄任务优先级堆栈大小创建任务
 */
#define TASK2_PRIO 3 /* 任务优先级*/ 
# define TASK2_STK_SIZE 128 /* 任务堆栈大小*/
TaskHandle_t Task2Task_Handler; /* 任务句柄*/
void task2(void * pvParameters); /* 任务函数*/
/*****************************************************************************/

(3) 全局变量及自定义函数
这部分主要用来定义全局变量及自定义的函数,如下所示:

/* LCD刷屏时使用的颜色*/
uint16_t lcd_discolor[11] = { WHITE, BLACK, BLUE, RED,
								MAGENTA, GREEN, CYAN, YELLOW,
								BROWN, BRRED, GRAY};

(4) 应用程序入口函数
这部分就是函数freertos_demo(),函数freertos_demo()一开始就是在LCD 上显示一些具体
实验相关的信息,然后创建开始任务,最后开启FreeRTOS 系统任务调度,如下所示:

/**
 * @brief FreeRTOS例程入口函数
 * @param 无
 * @retval 无
 */
void freertos_demo(void) {
    lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
    lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
    lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
    xTaskCreate((TaskFunction_t) start_task, /* 任务函数*/ (const char * )
        "start_task", /* 任务名称*/ (uint16_t) START_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) START_TASK_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & StartTask_Handler); /* 任务句柄*/
    vTaskStartScheduler();
}

(5) 开始任务入口函数
这部分就是开始任务的入口函数,开始任务主要用于创建或初始化特定实验中使用到的一
些硬件外设和FreeRTOS 相关的软件(信号量、事件、列表、软件定时器等)以及创建其他用于实验演示的任务,如下所示:

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t) task2, (const char * )
        "task2", (uint16_t) TASK2_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK2_PRIO, (TaskHandle_t * ) & Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

(6) 其他任务入口函数
这部分就包含了实验中用于演示的任务入口函数,如下所示:

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            lcd_clear(lcd_discolor[++task1_num % 14]); /* 刷新屏幕*/
            lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
            lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
            lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
            LED0_TOGGLE(); /* LED0闪烁*/
            vTaskDelay(1000); /* 延时1000ticks */
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    float float_num = 0.0;
    while (1) {
        float_num += 0.01 f; /* 更新数值*/
        printf("float_num: %0.4f\r\n", float_num); /* 打印数值*/
        vTaskDelay(1000); /* 延时1000ticks */
    }
}

以上就是freertos_demo.c 文件的代码结构,本教程配套的实验例程都会按着这个代码结构
来进行实验代码的编写,建议读者先熟悉这个代码结构。

  1. freertos_demo.h
#ifndef __FREERTOS_DEMO_H
#define __FREERTOS_DEMO_H

void freertos_demo(void);

#endif

freertos_demo.h 这个文件很简单,就是将函数freertos_demo()导出给其他C 源文件调用。

使用AC6 编译工程(扩展)

首先说明,本教程配套的实验工程全部采用AC5 进行开发。
AC 是“ARM Compiler”的简称,是用于编译ARM 处理器代码的编译工具链,AC 后面的
5 和6 表示的是AC 的版本号,在Keil MDK 中集成了AC5 和AC6 编译工具链,这里简单地对
比一下AC5 和AC6 的差异,如下表所示:

在这里插入图片描述

虽然AC6 的编译速度比较快,但是AC5 在各方面的兼容性都比AC6 要好,因此笔者推荐
新手读者使用AC5。当然读者可以自由地选择使用AC5 或AC6,正点原子的源码都是支持AC6的,只不过在Keil MDK 的选项配置方面有些差异,具体差异如下表所示:

在这里插入图片描述
通过以上配置正点原子的裸机程序已经能够正常使用AC6 编译工具进行编译了,但是对于
FreeRTOS 还需要稍作修改,需要修改两个地方,分别是FreeRTOS 的port.c 文件和头文件路径。

  1. FreeRTOS 的port.c 文件
    在2.1.2 小节中,往工程添加port.c 文件的路径为FreeRTOS/portable/RVDS,这是在使用AC5 编译工具的前提下,如果使用AC6 编译工具,那么port.c 文件的路径应该改为
    FreeRTOS/portable/GCC,对于正点原子不同的STM32 开发板,只要根据表2.1.2.4 将对应的port.c 文件添加到工程中即可。
  2. 头文件路径
    在2.1.2 小节中,添加了一个以port.c 文件路径为路径的头文件路径,由于使用AC6 编译工具时,使用了别的port.c 文件,因此需要将原来以port.c 文件路径为路径的头文件路径按照表2.1.2.4 修改为新的port.c 文件的路径为路径的头文件路径,修改后如下图所示(这里以正点原子的STM32F1 系列开发板为例,其他类型的开发板类似):
    在这里插入图片描述
    这里要注意的是,在切换了Keil MDK 编译工具之后,最好完整地重新编译一遍工程,即Rebuild all target files 而不是Build Target,以免出现一些莫名的问题。

FreeRTOS 移植实验

功能设计

  1. 例程功能
    本实验主要用于验证FreeRTOS 移植是否成功,本实验设计了三个任务,这三个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程2 FreeRTOS 移植实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. 程序解析
    在2.1.6 小节中已经对整体的代码结构作了说明,这里主要解析task1 任务和task2 任务,而具体的任务创建、任务调度等FreeRTOS 的相关知识,在下文会具体分析。
    (1) task1 任务
    task1 任务的入口函数代码如下所示:
/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint32_t task1_num = 0;
    while (1) {
        lcd_clear(lcd_discolor[++task1_num % 14]); /* 刷新屏幕*/
        lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
        lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
        lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
        LED0_TOGGLE(); /* LED0闪烁*/
        vTaskDelay(1000); /* 延时1000ticks */
    }
}

可以看到,task1 任务比较简单,在一个while (1)循环中,每间隔1000 个ticks 就刷新一次
屏幕背景,并控制LED0 闪烁。

(2) task2 任务

/**
 * @brief task2
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task2(void * pvParameters) {
    float float_num = 0.0;
    while (1) {
        float_num += 0.01 f; /* 更新数值*/
        printf("float_num: %0.4f\r\n", float_num); /* 打印数值*/
        vTaskDelay(1000); /* 延时1000ms */
    }
}

可以看到,task2 任务也很简单,在一个while (1)循环中,每间隔1000 个ticks 就进行一次
浮点运算,并将运算结果通过串口输出。

本实验所设计的任务都比较简单,并没有具体实际的意义,只是为了验证FreeRTOS 移植
的成功与否,因此本实验所涉及的一些函数包括函数的用法,读者暂时无需深究,后面的内容中会有详细的讲解。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示,
并且LCD 屏幕的背景颜色和板载的LED0 每间隔1000 个ticks 就切换一次状态(根据
FreeRTOSConfig.h 文件的相关配置,1000 个ticks 大致相当于1000ms 的时间,具体配置和换算的方法,在下文讲解到FreeRTOS 时间管理的时候会具体分析)。

在这里插入图片描述

同时通过串口调试助手可以看到,串口每间隔1000 个ticks 就输出一次浮点计算的结果,
如下图所示:

在这里插入图片描述

FreeRTOS 系统配置

在实际的应用场景中使用FreeRTOS,需要考虑各方面的因素,例如所使用的芯片架构、芯
片的Flash 和RAM 的大小。为了使FreeRTOS 适用于各种各样的场景,FreeRTOS 被设计成可配置和裁剪的,本章就来详细地讲解如何配置和裁剪FreeRTOS。

FreeRTOSConfig.h 文件

针对FreeRTOSConfig.h 文件,在FreeRTOS 官方的在线文档中有详细的说明,网址为:
https://www.freertos.org/a00110.html。

FreeRTOS 使用FreeRTOSConfig.h 文件进行配置裁剪。FreeRTOSConfig.h 文件中有几十个配置项,这使得用户能够很好地配置和裁剪FreeRTOS。虽然有很多的配置项,但是FreeRTOS的初学者也不要觉得学习FreeRTOS 是一件很难的事,正点原子针对正点原子的STM32 系列开发板,以“能用的都用上”的原则,编写并为读者提供了FreeRTOSConfig.h 文件,读者可参照2.1.2 小节中“FreeRTOSConfig.h 获取途径三”获取正点原子编写的FreeRTOSConfig.h 文件编写适用于自己实际工程的FreeRTOSConfig.h 文件,以配置并裁剪FreeRTOS。

FreeRTOSConfig.h 文件中的配置项可分为三大类:“config”配置项、“INCLUDE”配置项
其他配置项,下面就为读者详细地讲解这三类配置项。

“config”配置项(功能配置和裁剪)

“config”配置项按照配置的功能分类,可分为十类,分别为基础配置项、内存分配相关定
义、钩子(回调)函数相关定义、运行时间和任务状态统计相关定义、协程相关定义、软件定时器相关定义、中断嵌套行为配置、断言、FreeRTOS MPU 特殊定义和ARMv8-M 安全侧端口相关定义。

基础配置项

这部分包含FreeRTOS 的一些基础配置项,如下表所示

在这里插入图片描述
在这里插入图片描述

  1. configUSE_PREEMPTION
    此宏用于设置系统的调度方式。当宏configUSE_PREEMPTION 设置为1 时,系统使用抢占式调度;当宏configUSE_PREEMPTION 设置为 0 时,系统使用协程式调度。抢占式调度和协程式调度的区别在于,协程式调度是正在运行的任务主动释放CPU 后才能切换到下一个任务,任务切换的时机完全取决于正在运行的任务。协程式的优点在于可以节省开销,但是功能比较有限,现在的MCU 性能都比较强大,建议使用抢占式调度。

  2. configUSE_PORT_OPTIMISED_TASK_SELECTION
    FreeRTOS 支持两种方法来选择下一个要执行的任务,分别为通用方法和特殊方法。
    当宏configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为0 时,使用通用方法。通用方法是完全使用C 实现的软件算法,因此支持所用硬件,并且不限制任务优先级的最大值,但效率相较于特殊方法低。
    当宏configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为1 时,使用特殊方法。特殊方法的效率相较于通用方法高,但是特殊方法依赖于一个或多个特定架构的汇编指令(一般是类似计算前导零[CLZ]的指令),因此特殊方法并不支持所有硬件,并且对任务优先级的最大值一般也有限制,通常为32。

  3. configUSE_TICKLESS_IDLE
    当宏configUSE_TICKLESS_IDLE 设置为1 时,使能tickless 低功耗模式;设置为0 时,tick 中断则会移植运行。tickless 低功耗模式并不适用于所有硬件。

  4. configCPU_CLOCK_HZ
    此宏应设置为CPU 的内核时钟频率,单位为Hz。

  5. configSYSTICK_CLOCK_HZ
    此宏应设置为SysTick 的时钟频率,当SysTick 的时钟源频率与内核时钟频率不同时才可以定义,单位为Hz。

  6. configTICK_RATE_HZ
    此宏用于设置FreeRTOS 系统节拍的中断频率,单位为Hz。

  7. configMAX_PRIORITIES
    此宏用于定义系统支持的最大任务优先级数量,最大任务优先级数值为
    configMAX_PRIORITIES-1。

  8. configMINIMAL_STACK_SIZE
    此宏用于设置空闲任务的栈空间大小,单位为word。

  9. configMAX_TASK_NAME_LEN
    此宏用于设置任务名的最大字符数。

  10. configUSE_16_BIT_TICKS
    此宏用于定义系统节拍计数器的数据类型,当宏configUSE_16_BIT_TICKS 设置为1 时,系统节拍计数器的数据类型为16 位无符号整形;当宏configUSE_16_BIT_TICKS 设置为0 时,系统节拍计数器的数据类型为32 为无符号整型。

  11. configIDLE_SHOULD_YIELD
    当宏configIDLE_SHOULD_YIELD 设置为1 时,在抢占调度下,同等优先级的任务可抢占空闲任务,并延用空闲任务剩余的时间片。

  12. configUSE_TASK_NOTIFICATIONS
    当宏configUSE_TASK_NOTIFICATIONS 设置为1 时,开启任务通知功能。包括信号量、事件标志组和消息邮箱。当开启任务通知功能后,每个任务将多占用8 字节的内存空间。

  13. configTASK_NOTIFICATION_ARRAY_ENTRIES
    此宏用于定义任务通知数组的大小。

  14. configUSE_MUTEXES
    此宏用于使能互斥信号量,当宏configUSE_MUTEXS 设置为1 时,使能互斥信号量;当宏configUSE_MUTEXS 设置为0 时,则不使能互斥信号量。

  15. configUSE_RECURSIVE_MUTEXES
    此宏用于使能递归互斥信号量,当宏configUSE_RECURSIVE_MUTEXES 设置为1 时,使能递归互斥信号量;当宏configUSE_RECURSIVE_MUTEXES 设置为0 时,则不使能递归互斥信号量。

  16. configUSE_COUNTING_SEMAPHORES
    此宏用于使能计数型信号量,当宏configUSE_COUNTING_SEMAPHORES 设置为1 时,使能计数型信号量;当宏configUSE_COUNTING_SEMAPHORES 设置为0 时,则不使能计数型信号量。

  17. configUSE_ALTERNATIVE_API
    此宏在FreeRTOS V9.0.0 之后已弃用。

  18. configQUEUE_REGISTRY_SIZE
    此宏用于定义可以注册的队列和信号量的最大数量。此宏定义仅用于调试使用。

  19. configUSE_QUEUE_SETS
    此宏用于使能队列集,当宏configUSE_QUEUE_SETS 设置为1 时,使能队列集;当宏
    configUSE_QUEUE_SETS 设置为0 时,则不使能队列集。

  20. configUSE_TIME_SLICING
    此宏用于使能时间片调度,当宏configUSE_TIMER_SLICING 设置为1 且使用抢占式调度时,使能时间片调度;当宏configUSE_TIMER_SLICING 设置为0 时,则不使能时间片调度。

  21. configUSE_NEWLIB_REENTRANT
    此宏用于为每个任务分配一个NewLib 重入结构体,当宏
    configUSE_NEWLIB_REENTRANT 设置为1 时,FreeRTOS 将为每个创建的任务的任务控制块中分配一个NewLib 重入结构体。

  22. configENABLE_BACKWARD_COMPATIBILITY
    此宏用于兼容FreeRTOS 老版本的API 函数。

  23. configNUM_THREAD_LOCAL_STORAGE_POINTERS
    此宏用于在任务控制块中分配一个线程本地存储指着数组,当此宏被定义为大于0 时,
    configNUM_THREAD_LOCAL_STORAGE_POINTERS 为线程本地存储指针数组的元素个数;
    当宏configNUM_THREAD_LOCAL_STORAGE_POINTERS 为0 时,则禁用线程本地存储指针数组。

  24. configSTACK_DEPTH_TYPE
    此宏用于定义任务堆栈深度的数据类型,默认为uint16_t。

  25. configMESSAGE_BUFFER_LENGTH_TYPE
    此宏用于定义消息缓冲区中消息长度的数据类型,默认为size_t。

内存分配相关定义

这部分是于FreeRTOS 内存分配相关的配置项,如下表所示:

在这里插入图片描述

  1. configSUPPORT_STATIC_ALLOCATION
    当宏configSUPPORT_STSTIC_ALLOCATION 设置为1 时,FreeRTOS 支持使用静态方式管理内存,此宏默认设置为0。如果将configSUPPORT_STATIC_ALLOCATION 设置为1,用户还需要提供两个回调函数:vApplicationGetIdleTaskMemory() 和
    vApplicationGetTimerTaskMemory(),更详细的内容请参考第六章的“静态创建与删除任务实验”。
  2. configSUPPORT_DYNAMIC_ALLOCATION
    当宏configSUPPORT_DYNAMIC_ALLOCATION 设置为1 时,FreeRTOS 支持使用动态方式管理内存,此宏默认设置为1。
  3. configTOTAL_HEAP_SIZE
    此宏用于定义用于FreeRTOS 动态内存管理的内存大小,即FreeRTOS 的内存堆(FreeRTOS堆中可用的RAM总量),单位为Byte。
  4. configAPPLICATION_ALLOCATED_HEAP
    此宏用于自定义FreeRTOS 的内存堆,当宏configAPPLICATION_ALLOCATED_HEAP 设置为1 时,用户需要自行创建FreeRTOS 的内存堆,否则FreeRTOS 的内存堆将由编译器进行分配。利用此宏定义,可以使用FreeRTOS 动态管理外扩内存。
  5. configSTACK_ALLOCATION_FROM_SEPARATE_HEAP
    此宏用于自定义动态创建和删除任务时,任务栈内存的申请与释放函数pvPortMallocStack()
    和vPortFreeStack(),当宏configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 设置为1 是,用户需提供pvPortMallocStack()和vPortFreeStack()函数。

钩子(回调)函数相关定义

这部分是FreeRTOS 中一些钩子函数的相关配置项,如下表所示:

在这里插入图片描述

  1. configUSE_IDLE_HOOK
    此宏用于使能使用空闲任务钩子函数,当宏configUSE_IDLE_HOOK 设置为1 时,使能使用空闲任务钩子函数,用户需自定义相关钩子函数;当宏configUSE_IDLE_HOOK 设置为0 时,则不使能使用空闲任务钩子函数。
  2. configUSE_TICK_HOOK
    此宏用于使能使用系统时钟节拍中断钩子函数,当宏configUSE_TICK_HOOK 设置为1 时,使能使用系统时钟节拍中断钩子函数,用户需自定义相关钩子函数;当宏
    configUSE_TICK_HOOK 设置为0 时,则不使能使用系统时钟节拍中断钩子函数。
  3. configCHECK_FOR_STACK_OVERFLOW
    此宏用于使能栈溢出检测,当宏configCHECK_FOR_STACK_OVERFLOW 设置为1 时,使用栈溢出检测方法一;当宏configCHECK_FOR_STACK_OVERFLOW 设置为2 时,栈溢出检测方法二;当宏configCHECK_FOR_STACK_OVERFLOW 设置为0 时,不使能栈溢出检测。
  4. configUSE_MALLOC_FAILED_HOOK
    此宏用于使能使用动态内存分配失败钩子函数,当宏
    configUSE_MALLOC_FAILED_HOOK 设置为1 时,使能使用动态内存分配失败钩子函数(比如打印一些提示信息),用户需自定义相关钩子函数;当宏configUSE_MALLOC_FAILED_HOOK 设置为0 时,则不使能使用动态内存分配失败钩子函数。
  5. configUSE_DAEMON_TASK_STARTUP_HOOK
    此宏用于使能使用定时器服务任务首次执行前的钩子函数,当宏
    configUSE_DEAMON_TASK_STARTUP_HOOK 设置为1 时,使能使用定时器服务任务首次执行前的钩子函数,此时用户需定义定时器服务任务首次执行的相关钩子函数;当宏configUSE_DEAMON_TASK_STARTUP_HOOK 设置为0 时,则不使能使用定时器服务任务首次执行前的钩子函数。

运行时间和任务状态统计相关定义

这部分是与运行时间和任务状态统计相关的配置项,如下表所示:
在这里插入图片描述

  1. configGENERATE_RUN_TIME_STATS
    此宏用于使能任务运行时间统计功能,当宏configGENERATE_RUN_TIME_STATS 设置为1 时,使能任务运行时间统计功能,此时用户需要提供两个函数,一个是用于配置任务运行时间统计功能的函数portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),一般是完成定时器的初始化,另一个函数是portGET_RUN_TIME_COUNTER_VALUE(),该函数用于获取定时器的计时值;当宏configGENERATE_RUN_TIME_STATS 设置为0 时,则不使能任务运行时间统计功能。
  2. configUSE_TRACE_FACILITY
    此宏用于使能可视化跟踪调试,当宏configUSE_TRACE_FACILITY 设置为1 时,使能可视化跟踪调试;当宏configUSE_TRACE_FACILITY 设置为0 时,则不使能可视化跟踪调试。
  3. configUSE_STATS_FORMATTING_FUNCTIONS
    当此宏与configUSE_TRACE_FACILITY 同时设置为1 时,将编译函数vTaskList()和函数vTaskGetRunTimeStats(),否则将忽略编译函数vTaskList()和函数vTaskGetRunTimeStats()。

协程相关定义

这部分包含的是与协程相关的配置项,如下表所示:

在这里插入图片描述

  1. configUSE_CO_ROUTINES
    此宏用于启用协程,当宏configUSE_CO_ROUTINES 设置为1 时,启用协程;当宏
    configUSE_CO_ROUTINES 设置为0 时,则不启用协程。
  2. configMAX_CO_ROUTINE_PRIORITIES
    此宏用于设置协程的最大任务优先级数量,协程的最大任务优先级数值为
    configMAX_CO_ROUTINE_PRIORITIES-1。

软件定时器相关定义

这部分是与软件定时器相关的配置项,如下表所示:

在这里插入图片描述

  1. configUSE_TIMERS
    此宏用于启用软件定时器功能,当宏configUSE_TIMERS 设置为1 时,启用软件定时器功能;当宏configUSE_TIMERS 设置为0 时,则不启用软件定时器功能。
  2. configTIMER_TASK_PRIORITY
    此宏用于设置软件定时器处理任务的优先级,当启用软件定时器功能时,系统会创建一个用于处理软件定时器的软件定时器处理任务。
  3. configTIMER_QUEUE_LENGTH
    此宏用于定义软件定时器队列的长度,软件定时器的开启、停止与销毁等操作都是通过队列实现的。
  4. configTIMER_TASK_STACK_DEPTH
    此宏用于设置软件定时器处理任务的栈空间大小,当启用软件定时器功能时,系统会创建一个用于处理软件定时器的软件定时器处理任务。

中断嵌套行为配置

这部分包含于中断嵌套相关的配置项,如下表所示:

在这里插入图片描述

  1. configPRIO_BITS
    此宏应定义为MCU 的8 位优先级配置寄存器实际使用的位数。
  2. configLIBRARY_LOWEST_INTERRUPT_PRIORITY
    此宏应定义为MCU 的最低中断优先等级,对于STM32,在使用FreeRTOS 时,建议将中断优先级分组设置为组4 ,此时中断的最低优先级为15 。此宏定义用于辅助配置宏
    configKERNEL_INTERRUPT_PRIORITY。
  3. configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
    此宏定义用于设置FreeRTOS 可管理中断的最高优先级,当中断的优先级数值小于
    configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 时,此中断不受FreeRTOS 管理。
    此宏定义用于辅助配置宏configMAX_SYSCALL_INTERRUPT_PRIORITY。
  4. configKERNEL_INTERRUPT_PRIORITY
    此宏应定义为MCU 的最低中断优先等级在中断优先级配置寄存器中的值,对于STM32,即宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY 偏移4bit 的值。
  5. configMAX_SYSCALL_INTERRUPT_PRIORITY
    此宏应定义为FreeRTOS 可管理中断的最高优先等级在中断优先级配置寄存器中的值,对于STM32,即宏configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 偏移4bit 的值。
  6. configMAX_API_CALL_INTERRUPT_PRIORITY
    此宏为宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的新名称,只被用在FreeRTOS官方一些新的移植当中,此宏与宏configMAX_SYSCALL_INTERRUPT_PRIORITY 是等价的。

断言

这部分包含了FreeRTOS 中断言的相关配置项,如下表所示:
在这里插入图片描述

  1. vAssertCalled(char, int)
    此宏用于辅助配置宏configASSERT( x )以通过串口打印相关信息。
  2. configASSERT( x )
    此宏为FreeRTOS 操作系统中的断言,断言会对表达式x 进行判断,当x 为假时,断言失败,表明程序出错,于是使用宏vAssertCalled(char, int)通过串口打印相关的错误信息。断言常用于检测程序中的错误,使用断言将增加程序的代码大小和执行时间,因此建议在程序调试通过后将宏configASSERT( x )进行注释,以较少额外的开销。

FreeRTOS MPU 特殊定义

这部分为FreeRTOS 中MPU 的相关配置项,如下表所示:

在这里插入图片描述

本文暂不涉及FreeRTOS 中MPU 的相关内容,感兴趣的读者可自行查阅相关资料。

“INCLUDE”配置项(配置可选的API函数)

FreeRTOS 使用“INCLUDE”配置项对部分API 函数进行条件编译,当“INCLUDE”配置
项被定义为1 时,其对应的API 函数则会加入编译。对于用不到的API 函数,用户则可以将其对应的“INCLUDE”配置项设置为0,那么这个API 函数就不会加入编译,以减少不必要的系统开销。“INCLUDE”配置项与其对应API 函数的功能描述如下表所示:

在这里插入图片描述

其他配置项(PendSV宏定义、SVC宏定义)

剩余的几个其他配置项如下表所示:

在这里插入图片描述

  1. ureconfigMAX_SECURE_CONTEXTS
    此宏为ARMv8-M 安全侧端口的相关配置项,本文暂不涉及ARMv8-M 安全侧端口的相关内容,感兴趣的读者可自行查阅相关资料。
  2. endSVHandler 和vPortSVCHandler
    这两个宏为PendSV 和SVC 的中断服务函数,主要用于FreeRTOS 操作系统的任务切换,有关FreeRTOS 操作系统中任务切换的相关内容

FreeRTOS 中断管理

FreeRTOS 中的中断管理是一个很重要的内容,需要根据所使用的MCU 进行具体的配置,
本章会结合ARM Cortex-M 的NVIC 来讲解STM32 平台下FreeRTOS 的中断管理。
本章分为如下几部分:

4.1 ARM Cortex-M 中断
4.2 FreeRTOS 中断配置项
4.3 FreeRTOS 中断管理详解
4.4 FreeRTOS 中断测试实验

ARM Cortex-M 中断

在这里插入图片描述

ARM Cortex-M 中断简介

中断是CPU 的一种常见特性,中断一般由硬件产生,当中断发生后,会中断CPU 当前正
在执行的程序而跳转到中断对应的服务程序种去执行,ARM Cortex-M 内核的MCU 具有一个用于中断管理的嵌套向量中断控制器(NVIC,全称:Nested vectored interrupt controller)。

ARM Cortex-M 的NVIC 最大可支持256 个中断源,其中包括16 个系统中断和240 个外部
中断。然而芯片厂商一般情况下都用不完这些资源,以正点原子的战舰开发板为例,所使用的STM32F103ZET6 芯片就只用到了10 个系统中断和60 个外部中断。

中断优先级管理

ARM Cortex-M 使用NVIC 对不同优先级的中断进行管理,首先看一下NVIC 在CMSIS 中
的结构体定义,如下所示:

typedef struct {
    __IOM uint32_t ISER[8 U]; /* 中断使能寄存器*/
    uint32_t RESERVED0[24 U];
    __IOM uint32_t ICER[8 U]; /* 中断除能寄存器*/
    uint32_t RSERVED1[24 U];
    __IOM uint32_t ISPR[8 U]; /* 中断使能挂起寄存器*/
    uint32_t RESERVED2[24 U];
    __IOM uint32_t ICPR[8 U]; /* 中断除能挂起寄存器*/
    uint32_t RESERVED3[24 U];
    __IOM uint32_t IABR[8 U]; /* 中断有效位寄存器*/
    uint32_t RESERVED4[56 U];
    __IOM uint8_t IP[240 U]; /* 中断优先级寄存器*/
    uint32_t RESERVED5[644 U];
    __OM uint32_t STIR; /* 软件触发中断寄存器*/
}
NVIC_Type;

在NVIC 的相关结构体中,成员变量IP 用于配置外部中断的优先级,成员变量IP 的定义
如下所示:

__IOM uint8_t IP[240U]; /* 中断优先级寄存器*/

可以看到成员变量IP 是一个uint8_t 类型的数组,数组一共有240 个元素,数组中每一个
8bit 的元素就用来配置对应的外部中断的优先级。

综上可知,ARM Cortex-M 使用了8 位宽的寄存器来配置中断的优先等级,这个寄存器就
是中断优先级配置寄存器,因此最大中断的优先级配置范围位2 ^ 8 = 0~255。

但是芯片厂商一般用不完这些资源,对于STM32,只用到了中断优先级配置寄存器的高 4 位[7:4],低四位[3:0]取零处理,因此STM32 提供了最大2^4=16 级的中断优先等级,如下图所示:

在这里插入图片描述

中断优先级配置寄存器的值与对应的优先等级成反比,即中断优先级配置寄存器的值越小,
中断的优先等级越高。

STM32 的中断优先级可以分为抢占优先级和子优先级,抢占优先级和子优先级的区别如下:

  • 抢占优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断,即中断嵌套。
  • 子优先级:抢占优先级相同时,子优先级高的中断不能打断正在执行但子优先级低的中的中断,即子优先级不支持中断嵌套。

STM32 中每个中断的优先级就由抢占优先级和子优先级共同组成,使用中断优先级配置寄
存器的高4 位来配置抢占优先级和子优先级,那么中断优先级配置寄存器的高4 位是如何分配设置抢占优先级和子优先级的呢?一共由5 种分配方式,对应这中断优先级分组的5 个组,优先级分组的5 种分组情况在HAL 中进行了定义,如下所示:

#define NVIC_PRIORITYGROUP_0 0x00000007U /* 优先级分组0 */
#define NVIC_PRIORITYGROUP_1 0x00000006U /* 优先级分组1 */
#define NVIC_PRIORITYGROUP_2 0x00000005U /* 优先级分组2 */
#define NVIC_PRIORITYGROUP_3 0x00000004U /* 优先级分组3 */
#define NVIC_PRIORITYGROUP_4 0x00000003U /* 优先级分组4 */

优先级分组对应的抢占优先级和子优先级分配方式如下表所示:

在这里插入图片描述

STM32 的中断即要设置中断优先级分组又要设置抢占优先级和子优先级,看起来十分复杂。其实对于FreeRTOS,FreeRTOS 的官方强烈建议STM32 在使用FreeRTOS 的时候,使用中断优先级分组4(NVIC_PriorityGroup_4)即优先级配置寄存器的高4 位全部用于抢占优先级,不使用子优先级,这么一来用户就只需要设置抢占优先级即可,本教程配套的例程源码也全部将中断优先级分组设置为中断优先级分组4(NVIC_PriorityGroup_4),如下所示:

/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

三个系统中断优先级配置寄存器

除了外部中断,系统中断有独立的中断优先级配置寄存器,分别为SHPR1、SHPR2、SHPR3,下面就分别来看一下这三个寄存器的作用。

在这里插入图片描述

  1. SHPR1
    SHPR1 寄存器的地址为0xE000ED18,用于配置MemManage、BusFault、UsageFault 的中断优先级,各比特位的功能描述如下表所示:

在这里插入图片描述

  1. SHPR2
    SHPR2 寄存器的地址为0xE000ED1C,用于配置SVCall 的中断优先级,各比特位的功能描述如下表所示:

在这里插入图片描述

  1. SHPR3
    SHPR3 寄存器的地址为0xE000ED20,用于配置PendSV、SysTick 的中断优先级,各比特位的功能描述如下表所示:

在这里插入图片描述
FreeRTOS 在配置PendSV 和SysTick 中断优先级的时,就使用到了SHPR3 寄存器,因此
请读者多留意此寄存器。

三个中断屏蔽寄存器

ARM Cortex-M 有三个用于屏蔽中断的寄存器,分别为PRIMASK、FAULTMASK 和
BASEPRI,下面就分别来看一下这三个寄存器的作用。

在这里插入图片描述

  1. PRIMASK
    作用:PRIMASK 寄存器有32bit,但只有bit0 有效,是可读可写的,将PRIMASK 寄存器设置为1 用于屏蔽除NMI 和HardFault 外的所有异常和中断,将PRIMASK 寄存器清0 用于使能中断。

用法一:

CPSIE I /* 清除PRIMASK(使能中断)*/
CPSID I /* 设置PRIMASK(屏蔽中断)*/

用法二:

MRS R0, PRIMASK /* 读取PRIMASK值*/
MOV R0, #0
MSR PRIMASK, R0 /* 清除PRIMASK(使能中断)*/
MOV R0, #1
MSR PRIMASK, R0 /* 设置PRIMASK(屏蔽中断)*/

用法三:

__get_PRIMASK(); /* 读取PRIMASK值*/
__set_PRIMASK(0U); /* 清除PRIMASK(使能中断)*/
__set_PRIMASK(1U); /* 设置PRIMASK(屏蔽中断)*/
  1. FAULTMASK
    作用:FAULTMASK 寄存器有32bit,但只有bit0 有效,也是可读可写的,将FAULTMASK寄存器设置为1 用于屏蔽除NMI 外的所有异常和中断,将FAULTMASK 寄存器清零用于使能中断。

用法一:

CPSIE F /* 清除FAULTMASK(使能中断)*/
CPSID F /* 设置FAULTMASK(屏蔽中断)*/

用法二:

MRS R0, FAULTMASK /* 读取FAULTMASK值*/
MOV R0, #0
MSR FAULTMASK, R0 /* 清除FAULTMASK(使能中断)*/
MOV R0, #1
MSR FAULTMASK, R0 /* 设置FAULTMASK(屏蔽中断)*/

用法三:

__get_FAULTMASK(); /* 读取FAULTMASK值*/
__set_FAULTMASK(0U); /* 清除FAULTMASK(使能中断)*/
__set_FAULTMASK(1U); /* 设置FAULTMASK(屏蔽中断)*/
  1. BASEPRI
    作用:BASEPRI 有32bit,但只有低8 位[7:0]有效,也是可读可写的。BASEPRI 寄存器比起PRIMASK 和FAULTMASK 寄存器直接屏蔽掉大部分中断的方式,BASEPRI 寄存器的功能显得更加细腻,BASEPRI 用于设置一个中断屏蔽的阈值,设置好BASEPRI 后,中断优先级低于BASEPRI 的中断就都会被屏蔽掉,FreeRTOS 就是使用BASEPRI 寄存器来管理受FreeRTOS管理的中断的,而不受FreeRTOS 管理的中断,则不受FreeRTOS 的影响。

用法一:

MRS R0, BASEPRI /* 读取BASEPRI值*/
MOV R0, #0
MSR BASEPRI, R0 /* 清除BASEMASK(使能中断)*/
MOV R0, #0x60 /* 举例*/
MSR BASEPRI, R0 /* 设置BASEMASK(屏蔽优先级低于0x60的中断)*/

用法二:

__get_BASEPRI(); /* 读取BASEPRI值*/
__set_BASEPRI(0); /* 清除BASEPRI(使能中断)*/
__set_BASEPRI(0x60); /* 设置BASEPRI(屏蔽优先级小于0x60的中断)*/

中断控制状态寄存器

中断状态状态寄存器(ICSR)的地址为0xE000ED04,用于设置和清除异常的挂起状态,
以及获取当前系统正在执行的异常编号,各比特位的功能描述如下表所示:

在这里插入图片描述
这个寄存器主要关注VECTACTIVE 段[8:0],通过读取VECTACTIVE 段就能够判断当前执行
的代码是否在中断中。

FreeRTOS 中断配置项

在这里插入图片描述

FreeRTOSConfig.h 文件中有6 个与中断相关的FreeRTOS 配置项,如表3.2.7.1 所示。这6个FreeRTOS 配置项在3.2.7 小节“中断嵌套行为相关定义”中已经做了相应的描述,本小节主
要根据本章4.1 小节“ARM Cortex-M 中断”,为读者分析,如何配置这6 个中断相关的FreeRTOS配置项。

  1. configPRIO_BITS
    此宏是用于辅助配置的宏,主要用于辅助配置宏configKERNEL_INTERRUPT_PRIORITY
    和宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的,此宏应定义为MCU 的8 位优先级配置寄存器实际使用的位数,因为STM32 只使用到了中断优先级配置寄存器的高4 位,因此,此宏应配置为4。
  2. configLIBRARY_LOWEST_INTERRUPT_PRIORITY
    此宏是用于辅助配置宏configKERNEL_INTERRUPT_PRIORITY 的,此宏应设置为MCU的最低优先等级,因为STM32 只使用了中断优先级配置寄存器的高4 位,因此MCU 的最低优先等级就是2^4-1=15,因此,此宏应配置为15。
  3. configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
    此宏是用于辅助配置宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的,此宏适用于配置FreeRTOS 可管理的最高优先级的中断,此功能就是操作BASEPRI 寄存器来实现的。此宏的值可以根据用户的实际使用场景来决定,本教程的配套例程源码全部将此宏配置为5,即中断优先级高于5 的中断不受FreeRTOS 影响,如下图所示:

在这里插入图片描述
在这里插入图片描述

  1. configKERNEL_INTERRUPT_PRIORITY
    此宏应配置为MCU 的最低优先级在中断优先级配置寄存器中的值,在FreeRTOS 的源码中,使用此宏将SysTick 和PenSV 的中断优先级设置为最低优先级。因为STM32 只使用了中断优先级配置寄存器的高4 位,因此,此宏应配置为最低中断优先级在中断优先级配置寄存器高4 位的表示,即(configLIBRARY_LOWEST_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))。

  2. configMAX_SYSCALL_INTERRUPT_PRIORITY
    此宏用于配置FreeRTOS 可管理的最高优先级的中断,在FreeRTOS 的源码中,使用此宏来打开和关闭中断。因为STM32 只使用了中断优先级配置寄存器的高4 位,因此,此宏应配置为(configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY<<(8-configPRIO_BITS))。

  3. configMAX_API_CALL_INTERRUPT_PRIORITY
    此宏为宏configMAX_SYSCALL_INTERRUPT_PRIORITY 的新名称,只被用在FreeRTOS官方一些新的移植当中,此宏于宏configMAX_SYSCALL_INTERRUPT_PRIORITY 是等价的。

FreeRTOS 中断管理详解

在了解了ARM Cortex-M 中断和FreeRTOS 中断配置项的相关内容后,接下来本小节将通
过分析FreeRTOS 源码的方式来讲解FreeRTOS 是如何管理中断的。

PendSV 和SysTick 中断优先级

在4.1.3 小节中提到,FreeRTOS 使用SHPR3 寄存器配置PendSV 和SysTick 的中断优先级,那么FreeRTOS 是如何配置的呢?在FreeRTOS 的源码中有如下定义:

#define portNVIC_SHPR3_REG \
		( *( ( volatile uint32_t * ) 0xe000ed20 ) )

#define portNVIC_PENDSV_PRI \
		( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )

#define portNVIC_SYSTICK_PRI \
		( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

可以看到宏portNVIC_SHPR3_REG 被定义成了一个指向 0xE000ED20 地址的指针,而
0xE000ED20 就是SHPR3 寄存器地址的指针,因此只需通过宏portNVIC_SHPR3_REG 就能够访问SHPR3 寄存器了。

接着是宏portNVIC_PENDSV_PRI 和宏portNVIC_SYSTICK_PRI 分别定义成了宏
configKERNEL_INTERRUPT_PRIORITY 左移16 位和24 位,其中宏
configKERNEL_INTERRUPT_PRIORITY 在FreeRTOSConfig.h 文件中被定义成了系统的最低优先等级,而左移的16 位和24 位,正好是PendSV 和SysTick 中断优先级配置在SHPR3 寄存器中的位置,因此只需将宏portNVIC_PENDSV_PRI 和宏portNVIC_SYSTICK_PRI 对应地写入SHPR3 寄存器,就能将PendSV 和SysTick 的中断优先级设置为最低优先级

优先级设置最低:保证系统任务切换不会阻塞系统其他中断的响应

接着FreeRTOS 在启动任务调度器的函数中设置了PendSV 和SysTick 的中断优先级,代码
如下所示:

BaseType_t xPortStartScheduler(void) {
    /* 忽略其他代码*/
    /* 设置PendSV和SysTick的中断优先级为最低中断优先级*/
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
    /* 忽略其他代码*/
}

FreeRTOS 开关中断

前面说过,FreeRTOS 使用BASEPRI 寄存器来管理受FreeRTOS 管理的中断,而不受
FreeRTOS 管理的中断不受FreeRTOS 开关中断的影响,那么FreeRTOS 开关中断是如何操作的呢?首先来看一下FreeRTOS 开关中断的宏定义,代码如下所示:

#define portDISABLE_INTERRUPTS() 	vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() 	vPortSetBASEPRI( 0 )
#define taskDISABLE_INTERRUPTS() 	portDISABLE_INTERRUPTS()
#define taskENABLE_INTERRUPTS() 	portENABLE_INTERRUPTS()

根据上面代码,再来看一下函数vPortRaiseBASEPRI()和函数vPortSetBASEPRI(),具体的
代码如下所示:

  1. 函数vPortRaiseBASEPRI()
{							// 0x50 左移四位
    uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm {
        /* 设置BasePRI寄存器*/
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }
}

可以看到,函数vPortRaiseBASEPRI() 就是将BASEPRI 寄存器设置为宏
configMAX_SYSCALL_INTERRUPT_PRIORITY 配置的值。

这里再简单介绍一下DSB 和ISB 指令,DSB 和ISB 指令分别为数据同步隔离和指令同步
隔离,关于DSB 和ISB 指令更详细内容,感兴趣的读者请自行查阅相关资料。

  1. 函数vPortSetBASEPRI()
static portFORCE_INLINE void vPortSetBASEPRI(uint32_t ulBASEPRI) {
    __asm {
        /* 设置BasePRI寄存器*/
        msr basepri, ulBASEPRI
    }
}

可以看到,函数vPortSetBASEPRI()就是将BASEPRI 寄存器设置为指定的值。

下面再来看看FreeRTOS 中开关中断的两个宏定义:

  1. 宏portDISABLE_INTERRUPTS()
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

从上面的宏定义可以看出,FreeRTOS 关闭中断的操作就是将BASEPRI 寄存器设置为宏
configMAX_SYSCALL_INTERRUPT_PRIORITY 的值,以此来达到屏蔽受FreeRTOS 管理的中断,而不影响到哪些不受FreeRTOS 管理的中断。
2. 宏portENABLE_INTERRUPTS()

#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )

从上面的宏定义可以看出,FreeRTOS 开启中断的操作就是将BASEPRI 寄存器的值清零,
以此来取消屏蔽中断。

FreeRTOS 进出临界区

临界区是指那些必须完整运行的区域,在临界区中的代码必须完整运行,不能被打断。例
如一些使用软件模拟的通信协议,通信协议在通信时,必须严格按照通信协议的时序进行,不能被打断。FreeRTOS 在进出临界区的时候,通过关闭和打开受FreeRTOS 管理的中断,以保护临界区中的代码。FreeRTOS 的源码中就包含了许多临界区的代码,这部分代码都是用临界区进行保护,用户在使用FreeRTOS 编写应用程序的时候,也要注意一些不能被打断的操作,并为这部分代码加上临界区进行保护。

对于进出临界区,FreeRTOS 的源码中有四个相关的宏定义,分别为
taskENTER_CRITICAL() 、taskENTER_CRITICAL_FROM_ISR() 、taskEXIT_CRITICAL() 、taskEXIT_CRITICAL_FROM_ISR(x),这四个宏定义分别用于在中断和非中断中进出临界区,定义代码如下所示:

/* 进入临界区*/
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
/* 中断中进入临界区*/
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
/* 退出临界区*/
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
/* 中断中退出临界区*/
#define taskEXIT_CRITICAL_FROM_ISR(x) portCLEAR_INTERRUPT_MASK_FROM_ISR(x)
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)

下面分别来看一下这四个进出临界区的宏定义。

  1. 宏taskENTER_CRITICAL()
    此宏用于在非中断中进入临界区,此宏展开后是函数vPortEnterCritical() ,函数
    vPortEnterCritical()的代码如下所示:
void vPortEnterCritical(void) {
    /* 关闭受FreeRTOS管理的中断*/
    portDISABLE_INTERRUPTS();
    /* 临界区支持嵌套*/
    uxCriticalNesting++;
    if (uxCriticalNesting == 1) {
        /* 这个函数不能在中断中调用*/
        configASSERT((portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK) == 0);
    }
}

从上面的代码中可以看出,函数vPortEnterCritical()进入临界区就是关闭中断,当然了,不
受FreeRTOS 管理的中断是不受影响的。还可以看出,FreeRTOS 的临界区是可以嵌套的,意思就是说,在程序中可以重复地进入临界区,只要后续重复退出相同次数的临界区即可。
在上面的代码中还有一个断言,代码如下所示:

if( uxCriticalNesting == 1 )
{
	/* 这个函数不能在中断中调用*/
	configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}

断言中使用到的两个宏定义在FreeRTOS 的源码中都有定义,定义如下所示:

#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portVECTACTIVE_MASK ( 0xFFUL )

可以看出,宏portNVIC_INT_CTRL_REG 就是指向中断控制状态寄存器(ICSR)的指针,
而宏portVECTACTIVE_MASK 就是ICSR 寄存器中VECTACTIVE 段对应的位置,因此这个断言就是用来判断当第一次进入临界区的时候,是否是从中断服务函数中进入的,因为函数
vportEnterCritical()是用于从非中断中进入临界区,如果用户错误地在中断服务函数中调用函数vportEnterCritical(),那么就会通过断言报错。

  1. 宏taskENTER_CRITICAL_FROM_ISR()
    此宏用于从中断中进入临界区,此宏展开后是函数ulPortRaiseBASEPRI() ,函数
    ulPortRaiseBASEPRI()的代码如下所示:
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI(void) {
    uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
    __asm {
        /* 读取BASEPRI寄存器*/
        mrs ulReturn, basepri
        /* 设置BASEPRI寄存器*/
        msr basepri, ulNewBASEPRI
        dsb
        isb
    }
    return ulReturn;
}

可以看到函数ulPortRaiseBASEPRI() 同样是将BASEPRI 寄存器设置为宏
configMAX_SYSCALL_INTERRUPT_PRIORITY 的值,以达到关闭中断的效果,当然了,不受FreeRTOS 管理的中断是不受影响的。只不过函数ulPortRaiseBASEPRI()在设置BASEPRI 寄存器之前,先读取了BASEPRI 的值,并在函数的最后返回这个值,这是为了在后续从中断中退出临界区时,恢复BASEPRI 寄存器的值。
从上面的代码中也可以看出,从中断中进入临界区时不支持嵌套的。

  1. 宏taskEXIT_CRITICAL()
    此宏用于从非中断中退出临界区,此宏展开后是函数vPortExitCritical() ,函数
    vPortExitCritical()的代码如下所示:
void vPortExitCritical(void) {
    /* 必须是进入过临界区才能退出*/
    configASSERT(uxCriticalNesting);
    uxCriticalNesting--;
    if (uxCriticalNesting == 0) {
        /* 打开中断*/
        portENABLE_INTERRUPTS();
    }
}

这个函数就很好理解了,就是将用于临界区嵌套的计数器减1,当计数器减到0 的时候,
说明临界区已经没有嵌套了,于是调用函数portENABLE_INTERRUPT()打开中断。在函数的一开始还有一个断言,这个断言用于判断用于临界区嵌套的计数器在进入此函数的不为0,这样就保证了用户不会在还未进入临界区时,就错误地调用此函数退出临界区。
4. taskEXIT_CRITICAL_FROM_ISR(x)此宏用于从中断中退出临界区,此宏展开后是调用了函数vPortSetBASEPRI(),并将参数x传入函数vPortSetBASEPRI()。其中参数x 就是宏taskENTER_CRITICAL_FROM_ISR()的返回值,用于在从中断中对出临界区时,恢复BASEPRI 寄存器。
读者在使用FreeRTOS 进行开发的时候,应适当并合理地使用临界区,以让设计的程序更
加可靠。

FreeRTOS 中断测试实验

功能设计

  1. 例程功能
    本实验主要用于测试FreeRTOS 打开和关闭中断对中断的影响,本实验设计了两个任务,这两个任务的功能如下表所示:
    在这里插入图片描述
    在这里插入图片描述

该实验的实验工程,请参考《FreeRTOS 实验例程4 FreeRTOS 中断测试实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数portDISABLE_INTERRUPTS()
    此函数是一个宏定义,此宏的具体解析,请参考4.3.2 小节《FreeRTOS 开关中断》。
    (2) 函数portENABLE_INTERRUPTS()
    此函数是一个宏定义,此宏的具体解析,请参考4.3.2 小节《FreeRTOS 开关中断》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
    start_task 任务的入口函数代码如下所示
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 初始化TIM3、TIM5 */
    btim_tim3_int_init(10000 - 1, 7200 - 1);// 1s中断一次
    btim_tim5_int_init(10000 - 1, 7200 - 1);// 1s中断一次
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

由于正点原子各开发板的资源有所差异,因此上面代码中初始化定时器3 和定时器5 的部
分,针对正点原子的不同开发板也有所差异,上面代码是以正点原子战舰开发板为例,配置定时器3 和定时器5 的更新中断,中断频率设置为1Hz,即1s 中断一次。

(2) 定时器中断配置(优先级分别设置为4和6)

void btim_tim3_int_init(uint16_t arr, uint16_t psc) {
    /* 忽略其他代码*/
    HAL_NVIC_SetPriority(BTIM_TIM3_INT_IRQn, 4, 0); /* 设置抢占优先级4,子优先级 0 */
    HAL_NVIC_EnableIRQ(BTIM_TIM3_INT_IRQn); /* 开启ITM3中断*/
    /* 忽略其他代码*/
}

void btim_tim5_int_init(uint16_t arr, uint16_t psc) {
    /* 忽略其他代码*/
    HAL_NVIC_SetPriority(BTIM_TIM5_INT_IRQn, 6, 0); /* 设置抢占优先级6,子优先级 0 */
    HAL_NVIC_EnableIRQ(BTIM_TIM5_INT_IRQn); /* 开启ITM5中断*/
    /* 忽略其他代码*/
}

从以上代码中可以看到,在初始化定时器3 和定时器5 时,配置并打开了定时器3 和定时
器5 的中断,其中定时器3 的中断优先级设置为抢占优先级4、子优先级0,定时器5 的中断优先级设置为抢占优先级6、子优先级0。

由于在FreeRTOSConfig.h 文件中配置了受FreeRTOS 管理的最高优先级为5,如下所示:

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

因此在下载验证之前,就能够猜测:定时器3 的中断不受FreeRTOS 管理,定时器5 的中
断受FreeRTOS 管理。

下面再来看一下定时器3 和定时器5 的更新中断服务函数,代码如下所示:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef * htim) {
    if (htim == ( & g_tim3_handle)) {
        printf("TIM3输出\r\n");
    } else if (htim == ( & g_tim5_handle)) {
        printf("TIM5输出\r\n");
    }
}

从以上代码可以看出,当定时器3 中断时,会从串口打印“TIM3 输出\r\n”;当定时器5 中
断时,会从串口打印“TIM5 输出\r\n”。

(3) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    uint32_t task1_num = 0;
    while (1) {
        if (++task1_num == 5) {
            printf("FreeRTOS关闭中断\r\n");
            portDISABLE_INTERRUPTS(); /* FreeRTOS关闭中断*/
            delay_ms(5000);// 此处不能使用vTaskDelay()延时,因为函数内部会打开中断
            printf("FreeRTOS打开中断\r\n");
            portENABLE_INTERRUPTS(); /* FreeRTOS打开中断*/
        }
        vTaskDelay(1000);
    }
}

可以看到,task1 任务就是按照流程图4.4.2.1 的当时打开和关闭中断,本实验的目的就是
为了观察FreeRTOS 开关中断对定时器3 和定时器5 中断的影响。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

同时,通过串口调试助手就能看到本次实验的结果,如下图所示:

在这里插入图片描述

从上图可以看到,在FreeRTOS 关闭中断之前,定时器3 和定时器5 都以1s 一次的频率从
串口输出中断信息;当FreeRTOS 关闭中断后,由于定时器5 的中断优先级在FreeRTOS 的中断管理范围内,因此,定时器5 的中断被屏蔽,定时器3 的中断则不受影响;当FreeRTOS 重新打开中断后,定时器5 又可以发生中断了。

FreeRTOS 任务基础知识

任务和任务管理是RTOS 的核心,FreeRTOS 也不例外,并且,绝大多是使用RTOS 的目的
就是为了使用RTOS 的多任务管理能力。对于初学者,特别是没有RTOS 基础的读者,了解
FreeRTOS 的任务管理机制,是非常有必要的。为了帮助读者更好地理解FreeRTOS 的任务管理机制,本章就先介绍FreeRTOS 任务的一些基础知识。

本章分为如下几部分:
5.1 单任务系统和多任务系统
5.2 FreeRTOS 任务状态
5.3 FreeRTOS 任务优先级
5.4 FreeRTOS 任务调度方式
5.5 FreeRTOS 任务控制块
5.6 FreeRTOS 任务栈

单任务和多任务系统

单任务系统

单任务系统的编程方式,即裸机的编程方式,这种编程方式的框架一般都是在main()函数
中使用一个大循环,在循环中顺序地调用相应的函数以处理相应的事务,这个大循环的部分可以视为应用程序的后台,而应用程序的前台,则是各种中断的中断服务函数。因此单任务系统也叫做前后台系统,前后台系统的运行示意图,如下图所示:
在这里插入图片描述
从上图可以看出,前后台系统的实时性很差,因为大循环中函数处理的事务没有优先级之
分,必须是顺序地被执行处理的,不论待处理事务的紧急程度有多高,没轮到只能等着,虽然中断能够处理一些紧急的事务,但是在一些大型的嵌入式应用中,这样的单任务系统就会显得力不从心。

多任务系统

多任务系统在处理事务的实时性上比单任务系统要好得多,从宏观上来看,多任务系统的
多个任务是可以“同时”运行的,因此紧急的事务就可以无需等待CPU 处理完其他事务,在被
处理。
要注意的是多任务系统的多个任务可以“同时”运行,是从宏观的角度而言的,对于单核
的CPU 而言,CPU 在同一时刻只能够处理一个任务,但是多任务系统的任务调度器会根据相关的任务调度算法,将CPU 的使用权分配给任务,在任务获取CPU 使用权之后的极短时间(宏观角度)后,任务调度器又会将CPU 的使用权分配给其他任务,如此往复,在宏观的角度看来,就像是多个任务同时运行了一样。
多任务系统的运行示意图,如下图所示:
在这里插入图片描述
从上图可以看出,相较于单任务系统而言,多任务系统的任务也是具有优先级的,高优先
级的任务可以像中断的抢占一样,抢占低优先级任务的CPU 使用权;优先级相同的任务则各自轮流运行一段极短的时间(宏观角度),从而产生“同时”运行的错觉。以上就是抢占式调度和时间片调度的基本原理。
在任务有了优先级的多任务系统中,用户就可以将紧急的事务放在优先级高的任务中进行
处理,那么整个系统的实时性就会大大地提高。

FreeRTOS 任务状态(运行态、就绪态、阻塞态、挂起态)

FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。FreeRTOS运行时,任务的状态一定是这四种状态中的一种,下面就分别来介绍一下这四种任务状态。

  1. 运行态
    如果一个任务得到CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果
    运行RTOS 的MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。
  2. 就绪态
    如果一个任务已经能够被执行(不处于阻塞态后挂起态),但当前还未被执行(具有相同优
    先级或更高优先级的任务正持有CPU 使用权),那么这个任务就处于就绪态。
  3. 阻塞态
    如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任
    务调用了函数vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。
    要注意的是,处于阻塞态的任务是无法被运行的。
  4. 挂起态
    任务一般通过函数vTaskSuspend()和函数vTaskResums()进入和退出挂起态与阻塞态一样,
    处于挂起态的任务也无法被运行。
    四种任务状态之间的转换图如下图所示:
    在这里插入图片描述

FreeRTOS 任务优先级

任务优先级是决定任务调度器如何分配CPU 使用权的因素之一。每一个任务都被分配一个
0~(configMAX_PRIORITIES-1)的任务优先级,宏configMAX_PRIORITIES 在FreeRTOSConfig.h文件中定义(更详细的内容,请参考3.2.1 小节)。
如果在FreeRTOSConfig.h 文件中,将宏configUSE_PORT_OPTIMISED_TASK_SELECTION
定义为1,那么FreeRTOS 则会使用特殊方法计算下一个要运行的任务,这种特殊方法一般是使用硬件计算前导零指令,对于STM32 而言,硬件计算前导零的指令,最大支持32 位的数,因此宏configMAX_PRIORITIES 的值不能超过32。当然,系统支持的优先级数量越多,系统消耗的资源也就越多,因此读者在实际的工程开发当中,应当合理地将宏configMAX_PRIORITIES定义为满足应用需求的最小值。
FreeRTOS 的任务优先级高低与其对应的优先级数值,是成正比的,也就是说任务优先级数
值为0 的任务优先级是最低的任务优先级,任务优先级数值为(configMAX_PRIORITIES-1)的任务优先级是最高的任务优先级。FreeRTOS 的任务优先级高低与其对应数值的逻辑关系正好与STM32 的中断优先级高低与其对应数值的逻辑关系相反,如下图所示,因此作为刚入门FreeRTOS 的读者,特别注意。

在这里插入图片描述

Free RTOS 任务调度方式

FreeRTOS 一共支持三种任务调度方式,分别为抢占式调度、时间片调度和协程式调度。
在FreeRTOS 官方的在线文档中,FreeRTOS 官方对协程式调度做了特殊说明,说明如下图
所示:
在这里插入图片描述
FreeRTOS 官方对协程式调度的特殊说明,翻译过来就是“协程式调度是用于一些资源非常
少的设备上的,但是现在已经很少用到了。虽然协程式调度的相关代码还没有被删除,但是今后也不打算继续开发协程式调度。”
可以看出,FreeRTOS 官方已经不再开发协程式调度了,因此笔者并不推荐读者在开发中使
用协程式调度。协程式调度是专门为资源十分紧缺的设备开发的,因此使用协程式调度也会有受到很多的限制,但是现在MCU 的资源都已经十分富裕了,因此也就没有必要再使用和学习协程式调度了,本开发文档也就不再提供协程式调度的相关教程。

抢占式调度

抢占式调度主要时针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可
以抢占优先级低的任务,只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。

时间片调度

时间片调度主要针对优先级相同的任务,当多个任务的优先级相同时,任务调度器会在每
一次系统时钟节拍到的时候切换任务,也就是说CPU 轮流运行优先级相同的任务,每个任务运行的时间就是一个系统时钟节拍。有关系统时钟节拍的相关内容,在下文讲解FreeRTOS 系统时钟节拍的时候会具体分析。

FreeRTOS 任务控制块

FreeRTOS 中的每一个已创建任务都包含一个任务控制块,任务控制块是一个结构体变量,
FreeRTOS 用任务控制块结构体存储任务的属性。
任务控制块的定义如以下代码所示:

typedef struct tskTaskControlBlock {
    /* 指向任务栈栈顶的指针*/
    volatile StackType_t * pxTopOfStack;
#if (portUSING_MPU_WRAPPERS == 1)
    /* MPU相关设置*/
        xMPU_SETTINGS xMPUSettings;
#endif
    /* 任务状态列表项*/
    ListItem_t xStateListItem;
    /* 任务等待事件列表项*/
    ListItem_t xEventListItem;
    /* 任务的任务优先级*/
    UBaseType_t uxPriority;
    /* 任务栈的起始地址*/
    StackType_t * pxStack;
    /* 任务的任务名*/
    char pcTaskName[configMAX_TASK_NAME_LEN];
#if ((portSTACK_GROWTH > 0) || (configRECORD_STACK_HIGH_ADDRESS == 1))
    /* 指向任务栈栈底的指针*/
        StackType_t * pxEndOfStack;#
    endif#
    if (portCRITICAL_NESTING_IN_TCB == 1)
    /* 记录任务独自的临界区嵌套次数*/
        UBaseType_t uxCriticalNesting;
#endif
#if (configUSE_TRACE_FACILITY == 1)
    /* 由系统分配(每创建一个任务,值增加一),分配任务的值都不同,用于调试*/
        UBaseType_t uxTCBNumber;
    /* 由函数vTaskSetTaskNumber()设置,用于调试*/
    UBaseType_t uxTaskNumber;
#endif
#if (configUSE_MUTEXES == 1)
    /* 保存任务原始优先级,用于互斥信号量的优先级翻转*/
        UBaseType_t uxBasePriority;
    /* 记录任务获取的互斥信号量数量*/
    UBaseType_t uxMutexesHeld;#
    endif
#if (configUSE_APPLICATION_TASK_TAG == 1)
    /* 用户可自定义任务的钩子函数用于调试*/
        TaskHookFunction_t pxTaskTag;
#endif
#if (configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0)
    /* 保存任务独有的数据*/
        void * pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS];
#endif
#if (configGENERATE_RUN_TIME_STATS == 1)
    /* 记录任务处于运行态的时间*/
        configRUN_TIME_COUNTER_TYPE ulRunTimeCounter;
#endif
#if (configUSE_NEWLIB_REENTRANT == 1)
    /* 用于Newlib */
        struct _reent xNewLib_reent;
#endif
#if (configUSE_TASK_NOTIFICATIONS == 1)
    /* 任务通知值*/
        volatile uint32_t ulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES];
    /* 任务通知状态*/
    volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES];
#endif
#if (tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0)
    /* 任务静态创建标志*/
        uint8_t ucStaticallyAllocated;
#endif
#if (INCLUDE_xTaskAbortDelay == 1)
    /* 任务被中断延时标志*/
        uint8_t ucDelayAborted;
#endif
#if (configUSE_POSIX_ERRNO == 1)
    /* 用于POSIX */
        int iTaskErrno;
#endif
}
tskTCB;

typedef struct tskTaskControlBlock * TaskHandle_t;

从上面的代码可以看出,FreeRTOS 的任务控制块结构体中包含了很多成员变量,但是,大
部分的成员变量都是可以通过FreeRTOSConfig.h 配置文件中的配置项宏定义进行裁剪的。

FreeRTOS 任务栈

不论是裸机编程还是RTOS 编程,栈空间的使用的非常重要。函数中的局部变量、函数调
用时的现场保护和函数的返回地址等都是存放在栈空间中的。
对于FreeRTOS,当使用静态方式创建任务时,需要用户自行分配一块内存,作为任务的栈
空间,静态方式创建任务的函数原型如下所示:

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
								const char * const 		pcName,
								const uint32_t 			ulStackDepth,
								void * const 			pvParameters,
								UBaseType_t 			uxPriority,
								StackType_t * const 	puxStackBuffer,
								StaticTask_t * const 	pxTaskBuffer)

其中函数的参数ulStackDepth,为任务栈的大小;参数puxStackBuffer,为任务的栈的内存
空间。FreeRTOS 会根据这两个参数,为任务设置好任务的栈。
而使用动态方式创建任务时,系统则会自动从系统堆中分配一块内存,作为任务的栈空间,
动态方式创建任务的函数原型如下所示:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
						const char * const 				pcName,
						const configSTACK_DEPTH_TYPE 	usStackDepth,
						void * const 					pvParameters,
						UBaseType_t 					uxPriority,
						TaskHandle_t * const 			pxCreatedTask)

其中函数的参数usStackDepth,即为任务栈的大小。FreeRTOS 会根据栈的大小,从FreeRTOS的系统堆中分配一块内存,作为任务的栈空间。
值得一提的是,参数usStackDepth 表示的任务栈大小,实际上是以字为单位的,并非以字
节为单位。对于静态方式创建任务的函数xTaskCreateStatic(),参数usStackDepth 表示的是作为任务栈且其数据类型为StackType_t 的数组puxStackBuffer 中元素的个数;而对于动态方式创建任务的函数xTaskCreate(),参数usStackDepth 将被用于申请作为任务栈的内存空间,其内存申请相关代码,如下所示:

pxStack = pvPortMallocStack((((size_t)usStackDepth) * sizeof(StackType_t)));

可以看出,静态和动态创建任务时,任务栈的大小都与数据类型StackType_t 有关,对于
STM32 而言,该数据类型的相关定义,如下所示:

#define portSTACK_TYPE uint32_t
typedef portSTACK_TYPE StackType_t;

因此,不论是使用静态方式创建任务还是使用动态方式创建任务,任务的任务栈大小都应
该为ulStackDepth*sizeof(uint32_t)字节,即ulStackDepth 字。

FreeRTOS 任务相关API 函数

在了解了FreeRTOS 中任务的基础知识后,本章就开始学习FreeRTOS 中任务相关的API
函数。本章着重讲解FreeRTOS 中几个任务相关API 函数的用法,而不会涉及太多原理性的知识,相关的原理性知识会在后面的章节进行详细地讲解。由简入难,先知其然,然后再知其所虽然,这也是学习的一种方法。
本章分为如下几部分:
6.1 FreeRTOS 创建和删除任务
6.2 FreeRTOS 任务创建与删除实验(动态方法)
6.3 FreeRTOS 任务创建与删除实验(静态方法)
6.4 FreeRTOS 挂起和恢复任务
6.5 FreeRTOS 任务挂起与恢复实验

FreeRTOS 创建和删除任务相关API 函数

FreeRTOS 中用于创建和删除任务的API 函数如下表所示:
在这里插入图片描述

  1. 函数xTaskCreate()
    此函数用于使用动态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存,
    均由FreeRTOS 从FreeRTOS 管理的堆中分配,若使用此函数,需要在FreeRTOSConfig.h 文件中将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:
BaseType_t xTaskCreate(
	TaskFunction_t 					pxTaskCode,
	const char * const 				pcName,
	const configSTACK_DEPTH_TYPE 	usStackDepth,
	void * const 					pvParameters,
	UBaseType_t 					uxPriority,
	TaskHandle_t * const 			pxCreatedTask);

函数xTaskCreate()的形参描述,如下表所示:
在这里插入图片描述
在这里插入图片描述
函数xTaskCreate()的返回值,如下表所示:
在这里插入图片描述
2. 函数xTaskCreateStatic()
此函数用于使用静态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存,
需要由用户分配提供,若使用此函数,需要在FreeRTOSConfig.h 文件中将宏
configSUPPORT_STATIC_ALLOCATION 配置为1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:

TaskHandle_t xTaskCreateStatic(
		TaskFunction_t 			pxTaskCode,
		const char * const 		pcName,
		const uint32_t 			ulStackDepth,
		void * const 			pvParameters,
		UBaseType_t 			uxPriority,
		StackType_t * const 	puxStackBuffer,
		StaticTask_t * const 	pxTaskBuffer);

函数xTaskCreateStatic()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskCreateStatic()的返回值,如下表所示:
在这里插入图片描述
3. 函数xTaskCreateRestricted()
此函数用于使用动态的方式创建受MPU 保护的任务,任务的任务控制块以及任务的栈空
间所需的内存,均由FreeRTOS 从FreeRTOS 管理的堆中分配,若使用此函数,需要将宏
configSUPPORT_DYNAMIC_ALLOCATION 和宏portUSING_MPU_WRAPPERS 同时配置为1。
此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:

BaseType_t xTaskCreateRestricted(
const TaskParameters_t * const pxTaskDefinition,
TaskHandle_t * pxCreatedTask);

函数xTaskCreateRestricted()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskCreateRestricted()的返回值,如下表所示:
在这里插入图片描述
4. 函数xTaskCreateRestrictedStatic()
此函数用于使用静态的方式创建受MPU 保护的任务,此函数创建的任务的任务控制块以
及任务的栈空间所需的内存,需要由用户自行分配提供,若使用此函数,需要将宏
configSUPPORT_STATIC_ALLOCATION 和宏portUSING_MPU_WRAPPERS 同时配置为1。此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。函数原型如下所示:

BaseType_t xTaskCreateRestrictedStatic(
	const TaskParameters_t * const 	pxTaskDefinition,
	TaskHandle_t * 					pxCreatedTask);

函数xTaskCreateRestrictedStatic()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskCreateRestrictedStatic()的返回值,如下表所示:
在这里插入图片描述
5. 函数vTaskDelete()
此函数用于删除已被创建的任务,被删除的任务将被从就绪态任务列表、阻塞态任务列表、
挂起态任务列表和事件列表中移除,要注意的是,空闲任务会负责释放被删除任务中由系统分配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否则将导致内存泄露。若使用此函数,需要在FreeRTOSConfig.h文件中将宏INCLUDE_vTaskDelete配置为1。函数原型如下所示:

void vTaskDelete(TaskHandle_t xTaskToDelete);

函数vTaskDelete()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskDelete()无返回值。

FreeRTOS 任务创建与删除实验(动态方法)

功能设计

  1. 例程功能
    本实验主要实现FreeRTOS 使用动态方法创建和删除任务,本实验设计了四个任务,这四
    个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程6-1 FreeRTOS 任务创建与删除实验(动态
    方法)》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数xTaskCreate()
    此函数用于使用动态方法创建任务,请参考6.1 小节《FreeRTOS 创建和删除任务》。
    (2) 函数vTaskDelete()
    此函数用于删除任务,请参考6.1 小节《FreeRTOS 创建和删除任务》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    由于本实验需要使用动态方法创建任务,因此需要配置FreeRTOS 以支持动态内存管理,
    并向工程添加动态内存管理算法文件。
    首先,在FreeRTOSConfig.h 文件中开启支持动态内存管理,如下所示:
    #define configSUPPORT_DYNAMIC_ALLOCATION 1
    接着向工程添加动态内存管理算法文件,本教程使用的是heap_4.c 文件。
    (2) start_task 任务
    start_task 任务的入口函数代码如下所示:
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, /* 任务函数*/ (const char * )
        "task1", /* 任务名称*/ (uint16_t) TASK1_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK1_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task1Task_Handler); /* 任务句柄*/
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t) task2, /* 任务函数*/ (const char * )
        "task2", /* 任务名称*/ (uint16_t) TASK2_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK2_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task2Task_Handler); /* 任务句柄*/
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t) task3, /* 任务函数*/ (const char * )
        "task3", /* 任务名称*/ (uint16_t) TASK3_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK3_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task3Task_Handler); /* 任务句柄*/
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

从上面的代码中可以看到,start_task 任务使用了函数xTaskCreate(),动态地创建了task1、
task2 和task3 任务。
(3) task1 和task2 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            lcd_fill(6, 131, 114, 313, lcd_discolor[++task1_num % 11]);
            lcd_show_xnum(71, 111, task1_num, 3, 16, 0x80, BLUE);
            vTaskDelay(500);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        lcd_fill(126, 131, 233, 313, lcd_discolor[11 - (++task2_num % 11)]);
        lcd_show_xnum(191, 111, task2_num, 3, 16, 0x80, BLUE);
        vTaskDelay(500);
    }
}

从以上代码中可以看到,task1 和task2 任务分别每间隔500ticks 就区域刷新一次屏幕,task1和task2 任务主要是用于测试任务的创建与删除,当task1 和task2 任务被创建后,就能够看到屏幕上每间隔500ticks 进行一次区域刷新;而当task1 和task2 任务被删除后,屏幕上显示的内容就不再变化。
(4) task3 任务

/**
 * @brief task3
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task3(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                /* 删除任务1 */ {
                    vTaskDelete(Task1Task_Handler);
                    break;
                }
            case KEY1_PRES:
                /* 删除任务2 */ {
                    vTaskDelete(Task2Task_Handler);
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从上面的代码中可以看到,task3 任务负责扫描按键,当检测到KEY0 按键被按下时候,调
用函数vTaskDelaete()删除task1 任务;当检测到KEY1 按键被按下时,调用函数vTaskDelete()删除task2 任务。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
其中,每间隔500ticks,Task1 和Task2 的屏幕区域就刷新一次,并且Task1 和Task2 后方
数字也随之加一,此时表示task1 和task2 任务均被创建,并且正在运行中。
当按下KEY0 按键后,task1 任务被删除,此时,Task1 的屏幕区域不再刷新,并且Task1
后方的数字也不再改变,表明task1 任务已被删除,不再运行。当按下KEY1 按键后,task2 任务被删除,此时,Task2 的屏幕区域不再刷新,并且Task2 后方的数字也不再改变,表明task2任务已经被删除,不再运行。

FreeRTOS 任务创建与删除实验(静态方法)

功能设计

  1. 例程功能
    本实验主要实现FreeRTOS 使用静态方法创建和删除任务,本实验设计了四个任务,这四
    个任务的功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程6-2 FreeRTOS 任务创建与删除实验(静态
    方法)》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述

  2. FreeRTOS 函数解析
    (1) 函数xTaskCreateStatic()
    此函数用于使用静态方法创建任务,请参考6.1 小节《FreeRTOS 创建和删除任务》。
    (2) 函数vTaskDelete()
    此函数用于删除任务,请参考6.1 小节《FreeRTOS 创建和删除任务》。

  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    由于本实验要使用静态方法创建任务,因此需要在FreeRTOSConfig.h 文件中作相应的配
    置,具体的配置如下所示:

#define configSUPPORT_STATIC_ALLOCATION 1

当在FreeRTOSConfig.h 文件中将宏configSUPPORT_STATIC_ALLOCATION 配置为1 后,
不论宏configSUPPORT_DYNAMIC_ALLOCATION 配置为何值,系统都不再使用动态方式管理内存,因此需要用户提供用于提供空闲任务和软件定时器服务任务(如果启用了软件定时器)内存的两个回调函数,这两个回调函数分别为函数vApplicationGetIdleTaskMemory()和函数vApplicationGetTimerTaskMemory()。本实验在freertos_demo.c 文件中定义了这两个回调函数,具体的代码如下所示:

/* 空闲任务任务堆栈*/
static StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
/* 空闲任务控制块*/
static StaticTask_t IdleTaskTCB;
/* 定时器服务任务堆栈*/
static StackType_t TimerTaskStack[configTIMER_TASK_STACK_DEPTH];
/* 定时器服务任务控制块*/
static StaticTask_t TimerTaskTCB;
/**
* @brief 获取空闲任务地任务堆栈和任务控制块内存,因为本例程使用的
静态内存,因此空闲任务的任务堆栈和任务控制块的内存就应该
有用户来提供,FreeRTOS提供了接口函数vApplicationGetIdleTaskMemory()
实现此函数即可。
* @param ppxIdleTaskTCBBuffer:任务控制块内存
ppxIdleTaskStackBuffer:任务堆栈内存
pulIdleTaskStackSize:任务堆栈大小
* @retval 无
*/
void vApplicationGetIdleTaskMemory(StaticTask_t * * ppxIdleTaskTCBBuffer,
        StackType_t * * ppxIdleTaskStackBuffer,
        uint32_t * pulIdleTaskStackSize) { * ppxIdleTaskTCBBuffer = & IdleTaskTCB; * ppxIdleTaskStackBuffer = IdleTaskStack; * pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
    }
    /**
    * @brie 获取定时器服务任务的任务堆栈和任务控制块内存
    * @param ppxTimerTaskTCBBuffer:任务控制块内存
    ppxTimerTaskStackBuffer:任务堆栈内存
    pulTimerTaskStackSize:任务堆栈大小
    * @retval 无
    */
void vApplicationGetTimerTaskMemory(StaticTask_t * * ppxTimerTaskTCBBuffer,
    StackType_t * * ppxTimerTaskStackBuffer,
    uint32_t * pulTimerTaskStackSize) { * ppxTimerTaskTCBBuffer = & TimerTaskTCB; * ppxTimerTaskStackBuffer = TimerTaskStack; * pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}

(2) start_task 任务

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    Task1Task_Handler = xTaskCreateStatic(
        (TaskFunction_t) task1, /* 任务函数*/ (const char * )
        "task1", /* 任务名称*/ (uint32_t) TASK1_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传递给任务函数的参数*/ (UBaseType_t) TASK1_PRIO, /* 任务优先级*/ (StackType_t * ) Task1TaskStack, /* 任务堆栈*/ (StaticTask_t * ) & Task1TaskTCB); /* 任务控制块*/
    /* 创建任务2 */
    Task2Task_Handler = xTaskCreateStatic(
        (TaskFunction_t) task2, /* 任务函数*/ (const char * )
        "task2", /* 任务名称*/ (uint32_t) TASK2_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传递给任务函数的参数*/ (UBaseType_t) TASK2_PRIO, /* 任务优先级*/ (StackType_t * ) Task2TaskStack, /* 任务堆栈*/ (StaticTask_t * ) & Task2TaskTCB); /* 任务控制块*/
    /* 创建任务3 */
    Task3Task_Handler = xTaskCreateStatic(
        (TaskFunction_t) task3, /* 任务函数*/ (const char * )
        "task3", /* 任务名称*/ (uint32_t) TASK3_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传递给任务函数的参数*/ (UBaseType_t) TASK3_PRIO, /* 任务优先级*/ (StackType_t * ) Task3TaskStack, /* 任务堆栈*/ (StaticTask_t * ) & Task3TaskTCB); /* 任务控制块*/
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

从上面的代码中可以看到,start_task 任务使用了函数xTaskCreateStatic(),静态地创建了
task1、task2 和task3 任务。
(3) task1 和task2 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            lcd_fill(6, 131, 114, 313, lcd_discolor[++task1_num % 11]);
            lcd_show_xnum(71, 111, task1_num, 3, 16, 0x80, BLUE);
            vTaskDelay(500);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        lcd_fill(126, 131, 233, 313, lcd_discolor[11 - (++task2_num % 11)]);
        lcd_show_xnum(191, 111, task2_num, 3, 16, 0x80, BLUE);
        vTaskDelay(500);
    }
}

从以上代码中可以看到,task1 和task2 任务分别每间隔500ticks 就区域刷新一次屏幕,task1和task2 任务主要是用于测试任务的创建与删除,当task1 和task2 任务被创建后,就能够看到屏幕上每间隔500ticks 进行一次区域刷新;而当task1 和task2 任务被删除后,屏幕上显示的内容就不再变化。
(4) task3 任务

/**
 * @brief task3
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task3(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                /* 删除任务1 */ {
                    vTaskDelete(Task1Task_Handler);
                    break;
                }
            case KEY1_PRES:
                /* 删除任务2 */ {
                    vTaskDelete(Task2Task_Handler);
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从上面的代码中可以看到,task3 任务负责扫描按键,当检测到KEY0 按键被按下时候,调
用函数vTaskDelaete()删除task1 任务;当检测到KEY1 按键被按下时,调用函数vTaskDelete()删除task2 任务。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
其中,每间隔500ticks,Task1 和Task2 的屏幕区域就刷新一次,并且Task1 和Task2 后方
数字也随之加一,此时表示task1 和task2 任务均被创建,并且正在运行中。
当按下KEY0 按键后,task1 任务被删除,此时,Task1 的屏幕区域不再刷新,并且Task1
后方的数字也不再改变,表明task1 任务已被删除,不再运行。当按下KEY1 按键后,task2 任务被删除,此时,Task2 的屏幕区域不再刷新,并且Task2 后方的数字也不再改变,表明task2任务已经被删除,不再运行。
在第6.2 小节《FreeRTOS 任务创建与删除实验(动态方法)》和本小节中,分别介绍了在
FreeRTOS 中创建任务的两种方式,分别为动态方法创建任务和静态方法创建任务,可以看出使用动态创建任务的方法相较于使用静态创建任务的方法简单,在实际的应用中,动态方式创建任务是比较常用的,除非有特殊的需求,一般都会使用动态方式创建任务。

FreeRTOS 挂起和恢复任务相关API 函数

FreeRTOS 中用于挂起和恢复任务的API 函数如下表所示:
在这里插入图片描述

  1. 函数vTaskSuspend()
    此函数用于挂起任务,若使用此函数,需要在FreeRTOSConfig.h 文件中将宏
    INCLUDE_vTaskSuspend 配置为1。无论优先级如何,被挂起的任务都将不再被执行,直到任务被恢复。此函数并不支持嵌套,不论使用此函数重复挂起任务多少次,只需调用一次恢复任务的函数,那么任务就不再被挂起。函数原型如下所示:
void vTaskSuspend(TaskHandle_t xTaskToSuspend)

函数vTaskSuspend()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskSuspend()无返回值。
2. 函数vTaskResume()
此函数用于在任务中恢复被挂起的任务,若使用此函数,需要在FreeRTOSConfig.h 文件中
将宏INCLUDE_vTaskSuspend 配置为1。不论一个任务被函数vTaskSuspend()挂起多少次,只需要使用函数vTakResume()恢复一次,就可以继续运行。函数原型如下所示:

void vTaskResume(TaskHandle_t xTaskToResume)

函数vTaskResume()的形参描述,如下表所示:
在这里插入图片描述
函数vTaskResume()无返回值。
2. 函数xTaskResumeFromISR()
此函数用于在中断中恢复被挂起的任务,若使用此函数,需要在FreeRTOSConfig.h 文件中
将宏INCLUDE_xTaskResumeFromISR 配置为1。不论一个任务被函数vTaskSuspend()挂起多少次,只需要使用函数vTakResumeFromISR()恢复一次,就可以继续运行。函数原型如下所示:

BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume)

函数xTaskResumeFromISR()的形参描述,如下表所示:
在这里插入图片描述
函数xTaskResumeFromISR()的返回值,如下表所示:
在这里插入图片描述

FreeRTOS 任务挂起与恢复实验

功能设计

  1. 例程功能
    本实验主要实现FreeRTOS 挂起和恢复任务,本实验设计了四个任务,这四个任务的功能
    如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程6-3 FreeRTOS 任务挂起与恢复实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示:
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数vTaskSuspend()
    此函数用于挂起任务,请参考6.4 小节《FreeRTOS 挂起和恢复任务》。
    (2) 函数vTaskResume()
    此函数用于恢复被挂起的任务,请参考6.4 小节《FreeRTOS 挂起和恢复任务》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) FreeRTOS 配置
    由于本实验需要使用任务挂起和恢复的相关函数,因此需要在FreeRTOSConfig.h 文件中使
    能包含任务挂起和恢复的配置项,如下所示:
#define INCLUDE_vTaskSuspend 1

(2) start_task 任务

/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, /* 任务函数*/ (const char * )
        "task1", /* 任务名称*/ (uint16_t) TASK1_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK1_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task1Task_Handler); /* 任务句柄*/
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t) task2, /* 任务函数*/ (const char * )
        "task2", /* 任务名称*/ (uint16_t) TASK2_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK2_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task2Task_Handler); /* 任务句柄*/
    /* 创建任务3 */
    xTaskCreate((TaskFunction_t) task3, /* 任务函数*/ (const char * )
        "task3", /* 任务名称*/ (uint16_t) TASK3_STK_SIZE, /* 任务堆栈大小*/ (void * ) NULL, /* 传入给任务函数的参数*/ (UBaseType_t) TASK3_PRIO, /* 任务优先级*/ (TaskHandle_t * ) & Task3Task_Handler); /* 任务句柄*/
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

从上面的代码中可以看到,start_task 任务主要是用于创建task1、task2 和task3 任务。
(3) task1 和task2 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
        uint32_t task1_num = 0;
        while (1) {
            lcd_fill(6, 131, 114, 313, lcd_discolor[++task1_num % 11]);
            lcd_show_xnum(71, 111, task1_num, 3, 16, 0x80, BLUE);
            vTaskDelay(500);
        }
    }
    /**
     * @brief task2
     * @param pvParameters : 传入参数(未用到)
     * @retval 无
     */
void task2(void * pvParameters) {
    uint32_t task2_num = 0;
    while (1) {
        lcd_fill(126, 131, 233, 313, lcd_discolor[11 - (++task2_num % 11)]);
        lcd_show_xnum(191, 111, task2_num, 3, 16, 0x80, BLUE);
        vTaskDelay(500);
    }
}

从以上代码中可以看到,task1 和task2 任务分别每间隔500ticks 就区域刷新一次屏幕,task1任务主要是用于测试任务的挂起与恢复,而task2 任务则用于对照,当task1 和task2 任务被创建后,就能够看到屏幕上每间隔500ticks 进行一次区域刷新;而当task1 任务被挂起后,屏幕上task1 任务刷新的区域就不再变化;当task1 任务被恢复后,屏幕上task1 任务刷新的区域又再次发生变化。
(4) task3 任务

/**
 * @brief task3
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task3(void * pvParameters) {
    uint8_t key = 0;
    while (1) {
        key = key_scan(0);
        switch (key) {
            case KEY0_PRES:
                /* 挂起任务1 */ {
                    vTaskSuspend(Task1Task_Handler);
                    break;
                }
            case KEY1_PRES:
                /* 恢复任务1 */ {
                    vTaskResume(Task1Task_Handler);
                    break;
                }
            default:
                {
                    break;
                }
        }
        vTaskDelay(10);
    }
}

从上面的代码中可以看到,task3 任务负责扫描按键,当检测到KEY0 按键被按下时候,调
用函数vTaskSuspend()挂起task1 任务;当检测到KEY1 按键被按下时,调用函数vTaskResume()恢复task1 任务。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:
在这里插入图片描述
其中,每间隔500ticks,Task1 和Task2 的屏幕区域就刷新一次,并且Task1 和Task2 后方
数字也随之加一,此时表示task1 和task2 任务均被创建,并且正在运行中。
当按下KEY0 按键后,task1 任务被挂起,此时,Task1 的屏幕区域不再刷新,并且Task1后方的数字也不再改变,表明task1 任务已被挂起,暂停运行,而task2 任务不受影响。当按下KEY1 按键后,task1 任务被恢复,此时,Task1 的屏幕区域再次刷新,并且Task1 后方的数字也再次改变,表明task1 任务已经被恢复运行。

FreeRTOS 列表和列表项

在FreeRTOS 的源码中大量地使用了列表和列表项,因此想要深入学习FreeRTOS,列表和
列表项是必备的基础知识。这里所说的列表和列表项,是FreeRTOS 源码中List 和List Item 的直译,事实上,FreeRTOS 中的列表和列表项就是数据结构中的链表和节点。这部分的内容并不难,但对于理解FreeRTOS 相当重要,因此笔者建议读者在对本章内容了解透彻后,再继续下面章节的学习。
本章分为如下几部分:
7.1 FreeRTOS 列表和列表项简介
7.2 FreeRTOS 列表和列表项相关API 函数
7.3 FreeRTOS 操作列表和列表项的宏定义
7.4 FreeRTOS 列表项的插入与删除实验

FreeRTOS 列表和列表项简介

列表(List)

列表是FreeRTOS 中最基本的一种数据结构,其在物理存储单元上是非连续、非顺序的。
列表在FreeRTOS 中的应用十分广泛,要注意的是,FreeRTOS 中的列表是一个双向链表,在list.h 文件中,有列表的相关定义,具体代码如下所示:

typedef struct xLIST {
    listFIRST_LIST_INTEGRITY_CHECK_VALUE 					/* 校验值*/
    volatile UBaseType_t 				uxNumberOfItems; 	/* 列表中列表项的数量*/
    ListItem_t * configLIST_VOLATILE 	pxIndex; 			/* 用于遍历列表*/
    MiniListItem_t 						xListEnd; 			/* 最后一个列表项*/
    listSECOND_LIST_INTEGRITY_CHECK_VALUE 					/* 校验值*/
}
List_t;
  1. 在该结构体中,包含了两个宏,分别为listFIRST_LIST_INTEGRITY_CHECK_VALUE 和
    listSECOND_LIST_INTEGRITY_CHECK_VALUE,这两个宏用于存放确定已知常量,FreeRTOS
    通过检查这两个常量的值,来判断列表的数据在程序运行过程中,是否遭到破坏,类似这样的宏定义在列表项和迷你列表项中也有出现。该功能一般用于调试,默认是不开启的,因此本教程暂不讨论这个功能。
  2. 成员变量uxNumberOfItems 用于记录列表中列表项的个数(不包含xListEnd),当往列表中插入列表项时,该值加1;当从列表中移除列表项时,该值减1。
  3. 成员变量pxIndex 用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项。
  4. 成员变量xListEnd 是一个迷你列表项(详见7.1.3 小节),列表中迷你列表项的值一般被设置为最大值,用于将列表中的所有列表项按升序排序时,排在最末尾;同时xListEnd 也用于挂载其他插入到列表中的列表项。
    列表的结构示意图,如下图所示:
    在这里插入图片描述

列表项(List Item)

列表项是列表中用于存放数据的地方,在list.h 文件中,有列表项的相关定义,具体代码如
下所示:

struct xLIST_ITEM {
    listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE 				/* 用于检测列表项的数据完整性*/
    configLIST_VOLATILE TickType_t 				xItemValue; /* 列表项的值*/
    struct xLIST_ITEM * configLIST_VOLATILE 	pxNext; 	/* 下一个列表项*/
    struct xLIST_ITEM * configLIST_VOLATILE 	pxPrevious; /* 上一个列表项*/
    void * pvOwner; /* 列表项的拥有者*/
    struct xLIST * configLIST_VOLATILE 			pxContainer; /* 列表项所在列表*/
    listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE 				/* 用于检测列表项的数据完整性*/
};
typedef struct xLIST_ITEM ListItem_t; 						/* 重定义成ListItem_t */
  1. 如同列表一样,列表项中也包含了两个用于检测列表项数据完整性的宏定义。
  2. 成员变量xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序。
  3. 成员变量pxNext 和pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列
    表项。
  4. 成员变量pxOwner 用于指向包含列表项的对象(通常是任务控制块),因此,列表项和
    包含列表项的对象之间存在双向链接。
  5. 成员变量pxContainer 用于指向列表项所在列表。
    列表项的结构示意图,如下图所示:
    在这里插入图片描述

迷你列表项(Mini List Item)

迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列
表项,用户是用不到迷你列表项的,在list.h 文件中,有迷你列表项的相关定义,具体的代码录下所示:

struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE/* 用于检测列表项的数据完整性*/
configLIST_VOLATILE TickType_t xItemValue; /* 列表项的值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* 下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 上一个列表项*/
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;/* 重定义成MiniListItem_t */
  1. 迷你列表项中也同样包含用于检测列表项数据完整性的宏定义。
  2. 成员变量xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序。
  3. 成员变量pxNext 和pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列
    表项。
  4. 迷你列表项相比于列表项,因为只用于标记列表的末尾和挂载其他插入列表中的列表项,
    因此不需要成员变量pxOwner 和pxContainer,以节省内存开销。
    迷你列表项的结构示意图,如下图所示:
    在这里插入图片描述

FreeRTOS 列表和列表项相关API 函数

FreeRTOS 中列表和列表项相关的API 函数如下表所示:
在这里插入图片描述

函数vListInitialise()

此函数用于初始化列表,在定义列表之后,需要先对其进行初始化,只有初始化后的列表,
才能够正常地被使用。列表初始化的过程,其实就是初始化列表中的成员变量。函数原型如下所示:

void vListInitialise(List_t * const pxList);

函数vListInitialise()的形参描述,如下表所示:
在这里插入图片描述
函数vListInitialise()无返回值。
函数vListInitialise()在list.c 文件中有定义,具体的代码如下所示:

void vListInitialise(
    List_t *
    const pxList) {
    /* 初始化时,列表中只有xListEnd,因此pxIndex指向xListEnd */
    pxList - > pxIndex = (ListItem_t * ) & (pxList - > xListEnd);
    /* xListEnd的值初始化为最大值,用于列表项升序排序时,排在最后*/
    pxList - > xListEnd.xItemValue = portMAX_DELAY;
    /* 初始化时,列表中只有xListEnd,因此上一个和下一个列表项都为xListEnd本身*/
    pxList - > xListEnd.pxNext = (ListItem_t * ) & (pxList - > xListEnd);
    pxList - > xListEnd.pxPrevious = (ListItem_t * ) & (pxList - > xListEnd);
    /*初始化时,列表中的列表项数量为0(不包含xListEnd)*/
    pxList - > uxNumberOfItems = (UBaseType_t) 0 U;
    /* 初始化用于检测列表数据完整性的校验值*/
    listSET_LIST_INTEGRITY_CHECK_1_VALUE(pxList);
    listSET_LIST_INTEGRITY_CHECK_2_VALUE(pxList);
}

函数vListInitialise()初始化后的列表结构示意图,如下图所示:
在这里插入图片描述

函数vListInitialiseItem()

此函数用于初始化列表项,如同列表一样,在定义列表项之后,也需要先对其进行初始化,
只有初始化有的列表项,才能够被正常地使用。列表项初始化的过程,也是初始化列表项中的成员变量。函数原型如下所示:

void vListInitialiseItem(ListItem_t * const pxItem);

函数vListInitialiseItem()的形参描述,如下表所示:
在这里插入图片描述
函数vListInitialiseItem()无返回值。
函数vListInitialiseItem()在list.c 文件中有定义,具体的代码如下所示:

void vListInitialiseItem(
    ListItem_t *
    const pxItem) {
    /* 初始化时,列表项所在列表设为空*/
    pxItem - > pxContainer = NULL;
    /* 初始化用于检测列表项数据完整性的校验值*/
    listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem);
    listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem);
}

这个函数比较简单,只需将列表项所在列表设置为空,以保证列表项不再任何一个列表项
中即可。函数vListInitialiseItem()初始化后的列表项结构示意图,如下图所示:
在这里插入图片描述

函数vListInsertEnd()

此函数用于将待插入列表的列表项插入到列表pxIndex 指针指向列表项的前面,是一种无
序的插入方法。函数原型如下所示:

void vListInsertEnd(
	List_t * const pxList,
	ListItem_t * const pxNewListItem);

函数vListInsertEnd()的形参描述,如下表所示:
在这里插入图片描述
函数vListInsertEnd()无返回值。
函数vListInsertEnd()在list.c 文件中有定义,具体的代码如下所示:

void vListInsertEnd(
    List_t *
    const pxList,
        ListItem_t *
        const pxNewListItem) {
    /* 获取列表pxIndex指向的列表项*/
    ListItem_t *
        const pxIndex = pxList - > pxIndex;
    /* 检查参数是否正确*/
    listTEST_LIST_INTEGRITY(pxList);
    listTEST_LIST_ITEM_INTEGRITY(pxNewListItem);
    /* 更新待插入列表项的指针成员变量*/
    pxNewListItem - > pxNext = pxIndex;
    pxNewListItem - > pxPrevious = pxIndex - > pxPrevious;
    /* 测试使用,不用理会*/
    mtCOVERAGE_TEST_DELAY();
    /* 更新列表中原本列表项的指针成员变量*/
    pxIndex - > pxPrevious - > pxNext = pxNewListItem;
    pxIndex - > pxPrevious = pxNewListItem;
    /* 更新待插入列表项的所在列表成员变量*/
    pxNewListItem - > pxContainer = pxList;
    /* 更新列表中列表项的数量*/
    (pxList - > uxNumberOfItems) ++;
}

从上面的代码可以看出,此函数就是将待插入的列表项插入到列表pxIndex 指向列表项的
前面,要注意的时,pxIndex 不一定指向xListEnd,而是有可能指向列表中任意一个列表项。函数vListInsertEnd()插入列表项后的列表结构示意图,如下图所示:
在这里插入图片描述

函数vListInsert()

此函数用于将待插入列表的列表项按照列表项值升序排序的顺序,有序地插入到列表中。
函数原型如下所示:

void vListInsert(
List_t * const pxList,
ListItem_t * const pxNewListItem);

函数vListInsert()的形参描述,如下表所示:
在这里插入图片描述
函数vListInsert()无返回值。
函数vListInsert()在list.c 文件中有定义,具体的代码如下所示:

void vListInsert(
    List_t *
    const pxList,
        ListItem_t *
        const pxNewListItem) {
    ListItem_t * pxIterator;
    const TickType_t xValueOfInsertion = pxNewListItem - > xItemValue;
    /* 检查参数是否正确*/
    listTEST_LIST_INTEGRITY(pxList);
    listTEST_LIST_ITEM_INTEGRITY(pxNewListItem);
    /* 如果待插入列表项的值为最大值*/
    if (xValueOfInsertion == portMAX_DELAY) {
        /* 插入的位置为列表xListEnd前面*/
        pxIterator = pxList - > xListEnd.pxPrevious;
    } else {
        /* 遍历列表中的列表项,找到插入的位置*/
        for (pxIterator = (ListItem_t * ) & (pxList - > xListEnd); pxIterator - > pxNext - > xItemValue <= xValueOfInsertion; pxIterator = pxIterator - > pxNext) {}
    }
    /* 将待插入的列表项插入指定位置*/
    pxNewListItem - > pxNext = pxIterator - > pxNext;
    pxNewListItem - > pxNext - > pxPrevious = pxNewListItem;
    pxNewListItem - > pxPrevious = pxIterator;
    pxIterator - > pxNext = pxNewListItem;
    /* 更新待插入列表项所在列表*/
    pxNewListItem - > pxContainer = pxList;
    /* 更新列表中列表项的数量*/
    (pxList - > uxNumberOfItems) ++;
}

从上面的代码可以看出,此函数在将待插入列表项插入列表之前,会前遍历列表,找到待
插入列表项需要插入的位置。待插入列表项需要插入的位置是依照列表中列表项的值按照升序排序确定的。函数vListInsert()插入列表项后的列表结构示意图,如下图所示:

在这里插入图片描述

函数uxListRemove()

此函数用于将列表项从列表项所在列表中移除,函数原型如下所示:
UBaseType_t uxListRemove(ListItem_t * const pxItemToRemove);
函数uxListRemove()的形参描述,如下表所示:
在这里插入图片描述
函数uxListRemove()的返回值,如下表所示:
在这里插入图片描述
函数uxListRemove()在list.c 文件中有定义,具体的代码如下所示:

UBaseType_t uxListRemove(
    ListItem_t *
    const pxItemToRemove) {
    List_t *
        const pxList = pxItemToRemove - > pxContainer;
    /* 从列表中移除列表项*/
    pxItemToRemove - > pxNext - > pxPrevious = pxItemToRemove - > pxPrevious;
    pxItemToRemove - > pxPrevious - > pxNext = pxItemToRemove - > pxNext;
    /* 测试使用,不用理会*/
    mtCOVERAGE_TEST_DELAY();
    /* 如果pxIndex正指向待移除的列表项*/
    if (pxList - > pxIndex == pxItemToRemove) {
        /* pxIndex指向上一个列表项*/
        pxList - > pxIndex = pxItemToRemove - > pxPrevious;
    } else {
        mtCOVERAGE_TEST_MARKER();
    }
    /* 将待移除列表项的所在列表指针清空*/
    pxItemToRemove - > pxContainer = NULL;
    /* 更新列表中列表项的数量*/
    (pxList - > uxNumberOfItems) --;
    /* 返回列表项移除后列表中列表项的数量*/
    return pxList - > uxNumberOfItems;
}

要注意的是函数uxListRemove()移除后的列表项,依然于列表有着单向联系,即移除后列
表项中用于指向上一个和下一个列表项的指针,依然指向列表中的列表项。函数uxListRemove()移除列表项后的列表结构示意图,如下图所示:
在这里插入图片描述

FreeRTOS 操作列表和列表项的宏

在list.h 文件中定义了大量的宏,用来操作列表以及列表项,如下表所示:
在这里插入图片描述
在这里插入图片描述
这些宏操作列表及列表项的实现都比较简单,读者可阅读list.h 文件,查看具体的实现方
法;也可在后续阅读FreeRTOS 源码时,遇到这些宏定义时,再进行查阅。

FreeRTOS 列表项的插入与删除实验

功能设计

  1. 例程功能
    本实验主要实现FreeRTOS 列表项的插入与删除,本实验设计了两个任务,这两个任务的
    功能如下表所示:
    在这里插入图片描述
    该实验的实验工程,请参考《FreeRTOS 实验例程7 FreeRTOS 列表项的插入与删除实验》。

软件设计

  1. 程序流程图
    本实验的程序流程图,如下图所示
    在这里插入图片描述
  2. FreeRTOS 函数解析
    (1) 函数vListInitialise()
    此函数用于初始化列表,请参考7.2.1 小节《函数vListInitialise()》。
    (2) 函数vListInitialiseItem()
    此函数用于初始化列表项,请参考7.2.2 小节《函数vListInitialiseItem()》。
    (3) 函数vListInsert()
    此函数用于将列表项插入到列表中,请参考7.2.4 小节《函数vListInsert()》。
    (4) 函数uxListRemove()
    子函数用于将列表项从列表中移除,请参考7.2.5 小节《函数uxListRemove》。
    (5) 函数vListInsertEnd()
    此函数用于将列表项插入到列表末尾,请参考7.2.3 小节《函数vListInsertEnd()》。
  3. 程序解析
    整体的代码结构,请参考2.1.6 小节,本小节着重讲解本实验相关的部分。
    (1) start_task 任务
/**
 * @brief start_task
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void start_task(void * pvParameters) {
    taskENTER_CRITICAL(); /* 进入临界区*/
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t) task1, (const char * )
        "task1", (uint16_t) TASK1_STK_SIZE, (void * ) NULL, (UBaseType_t) TASK1_PRIO, (TaskHandle_t * ) & Task1Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务*/
    taskEXIT_CRITICAL(); /* 退出临界区*/
}

从上面的代码中可以看到,start_task 任务主要是用于创建task1 任务。
(2) task1 任务

/**
 * @brief task1
 * @param pvParameters : 传入参数(未用到)
 * @retval 无
 */
void task1(void * pvParameters) {
    /* 第一步:初始化列表和列表项*/
    vListInitialise( & TestList); /* 初始化列表*/
    vListInitialiseItem( & ListItem1); /* 初始化列表项1 */
    vListInitialiseItem( & ListItem2); /* 初始化列表项2 */
    vListInitialiseItem( & ListItem3); /* 初始化列表项3 */
    /* 第二步:打印列表和其他列表项的地址*/
    printf("/**************第二步:打印列表和列表项的地址**************/\r\n");
    printf("项目\t\t\t地址\r\n");
    printf("TestList\t\t0x%p\t\r\n", & TestList);
    printf("TestList->pxIndex\t0x%p\t\r\n", TestList.pxIndex);
    printf("TestList->xListEnd\t0x%p\t\r\n", ( & TestList.xListEnd));
    printf("ListItem1\t\t0x%p\t\r\n", & ListItem1);
    printf("ListItem2\t\t0x%p\t\r\n", & ListItem2);
    printf("ListItem3\t\t0x%p\t\r\n", & ListItem3);
    printf("/**************************结束***************************/\r\n");
    printf("按下KEY0键继续!\r\n\r\n\r\n");
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第三步:列表项1插入列表*/
    printf("/*****************第三步:列表项1插入列表******************/\r\n");
    vListInsert((List_t * ) & TestList, /* 列表*/ (ListItem_t * ) & ListItem1); /* 列表项*/
    /* 打印信息,代码省略*/
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第四步:列表项2插入列表*/
    printf("/*****************第四步:列表项2插入列表******************/\r\n");
    vListInsert((List_t * ) & TestList, /* 列表*/ (ListItem_t * ) & ListItem2); /* 列表项*/
    /* 打印信息,代码省略*/
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第五步:列表项3插入列表*/
    printf("/*****************第五步:列表项3插入列表******************/\r\n");
    vListInsert((List_t * ) & TestList, /* 列表*/ (ListItem_t * ) & ListItem3); /* 列表项*/
    /* 打印信息,代码省略*/
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第六步:移除列表项2 */
    printf("/*******************第六步:移除列表项2********************/\r\n");
    uxListRemove((ListItem_t * ) & ListItem2); /* 移除列表项*/
    /* 打印信息,代码省略*/
    while (key_scan(0) != KEY0_PRES) {
        vTaskDelay(10);
    }
    /* 第七步:列表末尾添加列表项2 */
    printf("/****************第七步:列表末尾添加列表项2****************/\r\n");
    vListInsertEnd((List_t * ) & TestList, /* 列表*/ (ListItem_t * ) & ListItem2); /* 列表项*/
    /* 打印信息,代码省略*/
    while (1) {
        vTaskDelay(10);
    }
}

从以上代码中可以看到,task1 分别执行了列表和列表项的初始化、列表项插入列表、列表
项移除和列表项插入列表末尾等操作,并将每次操作的结果通过串口输出。

下载验证

编译并下载代码,复位后可以看到LCD 屏幕上显示了本次实验的相关信息,如下图所示:

在这里插入图片描述

同时,通过串口打印了列表及列表项初始化后的地址信息,如下图所示:
在这里插入图片描述
从上图可以得到列表和列表项初始化后信息,如下所示:

  1. 列表的地址为0x200000CC。
  2. 列表xListEnd 的地址为0x200000D4,这符合xListEnd 在列表结构体中的偏移量。
  3. 列表pxIndex 指向0x200000D4,即初始化后,pxIndex 指向xListEnd。
  4. 列表项1 的地址为0x200000E0。
  5. 列表项2 的地址为0x200000F4。
  6. 列表项3 的地址为0x20000108。

接着按下KEY0,将列表项1 插入列表中,如下图所示:
在这里插入图片描述

从上图中可以得到列表项1 插入列表后的信息,如下所示:

  1. 列表xListEnd 的下一个列表项指向0x200000E0,即列表项1 的地址。
  2. 列表项1 的下一个列表项指向0x200000D4,即列表xListEnd 的地址。
  3. 列表xListEnd 的上一个列表项指向0x200000E0,即列表项1 的地址。
  4. 列表项1 的上一个列表项指向0x200000D4,即列表xListEnd 的地址。

接着按下KEY0,将列表项2 插入列表中,如下图所示:

在这里插入图片描述
从上图中可以得到列表项2 插入列表后的信息,如下所示:

  1. 列表xListEnd 的下一个列表项指向0x200000E0,即列表项1 的地址。
  2. 列表项1 的下一个列表项指向0x200000F4,即列表项2 的地址。
  3. 列表项2 的下一个列表项指向0x200000D4,即列表xListEnd 的地址。
  4. 列表xListEnd 的上一个列表项指向0x200000F4,即列表项2 的地址。
  5. 列表项1 的上一个列表项指向0x200000D4,即列表xListEnd 的地址。
  6. 列表项2 的上一个列表项指向0x200000E0,即列表项1 的地址。

接着按下KEY0,将列表项3 插入列表中,如下图所示:
在这里插入图片描述
从上图可以得到列表项2 插入列表后的信息,如下所示:

  1. 列表xListEnd 的下一个列表项指向0x200000E0,即列表项1 的地址。
  2. 列表项1 的下一个列表项指向0x200000F4,即列表项2 的地址。
  3. 列表项2 的下一个列表项指向0x20000108,即列表项3 的地址。
  4. 列表项3 的下一个列表项指向0x200000D4,即列表xListEnd 的地址。
  5. 列表xListEnd 的上一个列表项指向0x20000108,即列表项3 的地址。
  6. 列表项1 的上一个列表项指向0x200000D4,即列表xListEnd 的地址。
  7. 列表项2 的上一个列表项指向0x200000E0,即列表项1 的地址。
  8. 列表项3 的上一个列表项指向0x200000F4,即列表项2 的地址。

接着按下KEY0,移除列表项2,如下图所示:
在这里插入图片描述
从上图可以得到移除列表项2 后的信息,如下所示:

  1. 列表xListEnd 的下一个列表项指向0x200000E0,即列表项1 的地址。
  2. 列表项1 的下一个列表项指向0x20000108,即列表项3 的地址。
  3. 列表项3 的下一个列表项指向0x200000D4,即列表xListEnd 的地址。
  4. 列表xListEnd 的上一个列表项指向0x20000108,即列表项3 的地址。
  5. 列表项1 的上一个列表项指向0x200000D4,即列表xListEnd 的地址。
  6. 列表项3 的上一个列表项指向0x200000E0,即列表项1 的地址。

接着按下KEY0,将列表项2 插入列表末尾,如下图所示:
在这里插入图片描述
从上图可以得到列表项2 插入列表末尾后的信息,如下所示:

  1. 列表pxIndex 指向0x200000D4,即xListEnd 的地址。
  2. 列表xListEnd 的下一个列表项指向0x200000E0,即列表项1 的地址。
  3. 列表项1 的下一个列表项指向0x20000108,即列表项3 的地址。
  4. 列表项2 的下一个列表项指向0x200000D4,即列表xListEnd 的地址。
  5. 列表项3 的下一个列表项指向0x200000F4,即列表项2 的地址。
  6. 列表xListEnd 的上一个列表项指向0x200000F4,即列表项2 的地址。
  7. 列表项1 的上一个列表项指向0x200000D4,即列表xListEnd 的地址。
  8. 列表项2 的上一个列表项指向0x20000108,即列表项3 的地址。
  9. 列表项3 的上一个列表项指向0x200000E0,即列表项1 的地址。

以上得到的实验结果,均与预期的相符。要注意的是,本实验过程中得到的地址信息,在
不同场景下,都有可能发生改变,但是地址信息所对应的列表、列表项等,须与上述实验结果
一致。

FreeRTOS 系统启动流程及任务相关函数解析

在前面的章节中,介绍了FreeRTOS 中任务创建、删除、挂起和恢复等几个基础API 函数
的使用方法,并且讲解了FreeRTOS 中极为重要的列表和列表项。本章将讲解FreeRTOS 系统启动到第一个任务开始运行的一整个流程,也就是FreeRTOS 系统的启动流程。

FreeRTOS 开启任务调度器

函数vTaskStartScheduler()

在前面章节的例程实验中,都是在函数freertos_demo()中使用FreeRTOS 的任务创建API 函
数创建start_task 任务后,再调用函数vTaskStartScheduler(),如下所示(以FreeRTOS 移植实验为例):

/**
 * @brief FreeRTOS例程入口函数
 * @param 无
 * @retval 无
 */
void freertos_demo(void) {
    lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
    lcd_show_string(10, 47, 220, 24, 24, "FreeRTOS Porting", RED);
    lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
    xTaskCreate((TaskFunction_t) start_task, /* 任务函数*/ 
    (const char * )
        "start_task", /* 任务名称*/ 
        (uint16_t) START_STK_SIZE, /* 任务堆栈大小*/ 
        (void * ) NULL, /* 传入给任务函数的参数*/ 
        (UBaseType_t) START_TASK_PRIO, /* 任务优先级*/ 
        (TaskHandle_t * ) & StartTask_Handler); /* 任务句柄*/
    vTaskStartScheduler();
}

函数vTaskStartScheduler()用于启动任务调度器,任务调度器启动后,FreeRTOS 便会开始
进行任务调度,除非调用函数xTaskEndScheduler()停止任务调度器,否则不会再返回。函数vTaskStartScheduler()的代码如下所示:

void vTaskStartScheduler(void) {
    BaseType_t xReturn;
    /* 如果启用静态内存管理,则优先使用静态方式创建空闲任务*/
    #
    if (configSUPPORT_STATIC_ALLOCATION == 1) {
        StaticTask_t * pxIdleTaskTCBBuffer = NULL;
        StackType_t * pxIdleTaskStackBuffer = NULL;
        uint32_t ulIdleTaskStackSize;
        vApplicationGetIdleTaskMemory( & pxIdleTaskTCBBuffer, & pxIdleTaskStackBuffer, & ulIdleTaskStackSize);
        xIdleTaskHandle = xTaskCreateStatic(prvIdleTask,
            configIDLE_TASK_NAME,
            ulIdleTaskStackSize, (void * ) NULL,
            portPRIVILEGE_BIT,
            pxIdleTaskStackBuffer,
            pxIdleTaskTCBBuffer);
        if (xIdleTaskHandle != NULL) {
            xReturn = pdPASS;
        } else {
            xReturn = pdFAIL;
        }
    }#
    else
    /* 未启用静态内存管理,则使用动态方式创建空闲任务*/
    {
        xReturn = xTaskCreate(prvIdleTask,
            configIDLE_TASK_NAME,
            configMINIMAL_STACK_SIZE, (void * ) NULL,
            portPRIVILEGE_BIT, & xIdleTaskHandle);
    }#
    endif
    /* 如果启用软件定时器,则需要创建定时器服务任务*/
    #
    if (configUSE_TIMERS == 1) {
        if (xReturn == pdPASS) {
            xReturn = xTimerCreateTimerTask();
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }#
    endif
    if (xReturn == pdPASS) {#
        ifdef FREERTOS_TASKS_C_ADDITIONS_INIT {
            /* 此函数用于添加一些附加初始化,不用理会*/
            freertos_tasks_c_additions_init();
        }#
        endif
        /* FreeRTOS关闭中断,
         * 以保证在开启任务任务调度器之前或过程中,SysTick不会产生中断,
         * 在第一个任务开始运行时,会重新打开中断。
         */
        portDISABLE_INTERRUPTS();#
        if (configUSE_NEWLIB_REENTRANT == 1) {
            /* Newlib相关*/
            _impure_ptr = & (pxCurrentTCB - > xNewLib_reent);
        }#
        endif
        /* 初始化一些全局变量
         * xNextTaskUnblockTime: 下一个距离取消任务阻塞的时间,初始化为最大值
         * xSchedulerRunning: 任务调度器运行标志,设为已运行
         * xTickCount: 系统使用节拍计数器,宏configINITIAL_TICK_COUNT默认为0
         */
        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = (TickType_t) configINITIAL_TICK_COUNT;
        /* 为任务运行时间统计功能初始化功能时基定时器
         * 是否启用该功能,可在FreeRTOSConfig.h文件中进行配置
         */
        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
        /* 调试使用,不用理会*/
        traceTASK_SWITCHED_IN();
        /* 设置用于系统时钟节拍的硬件定时器(SysTick)
         * 会在这个函数中进入第一个任务,并开始任务调度
         * 任务调度开启后,便不会再返回
         */
        if (xPortStartScheduler() != pdFALSE) {} else {}
    } else {
        /* 动态方式创建空闲任务和定时器服务任务(如果有)时,因分配给FreeRTOS的堆空间
         * 不足,导致任务无法成功创建*/
        configASSERT(xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY);
    }
    /* 防止编译器警告,不用理会*/
    (void) xIdleTaskHandle;
    /* 调试使用,不用理会*/
    (void) uxTopUsedPriority;
}

从上面的代码可以看出,函数vTaskStartScheduler()主要做了六件事情。

  1. 创建空闲任务,根据是否支持静态内存管理,使用静态方式或动态方式创建空闲任务。
  2. 创建定时器服务任务,创建定时器服务任务需要配置启用软件定时器,创建定时器服务
    任务,同样是根据是否配置支持静态内存管理,使用静态或动态方式创建定时器服务任务。
  3. 关闭中断,使用portDISABLE_INTERRUPT()关闭中断,这种方式只关闭受FreeRTOS 管
    理的中断。关闭中断主要是为了防止SysTick 中断在任务调度器开启之前或过程中,产生中断。
    FreeRTOS 会在开始运行第一个任务时,重新打开中断。
  4. 初始化一些全局变量,并将任务调度器的运行标志设置为已运行。
  5. 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时
    器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
  6. 最后就是调用函数xPortStartScheduler()。

函数xPortStartScheduler()

函数xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第
一个任务,具体的代码如下所示:

BaseType_t xPortStartScheduler(void) {#
    if (configASSERT_DEFINED == 1) {
        /* 检测用户在FreeRTOSConfig.h文件中对中断相关部分的配置是否有误,代码省略*/
    }#
    endif
    /* 设置PendSV和SysTick的中断优先级为最低优先级*/
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
    /* 配置SysTick
     * 清空SysTick的计数值
     * 根据configTICK_RATE_HZ配置SysTick的重装载值
     * 开启SysTick计数和中断
     */
    vPortSetupTimerInterrupt();
    /* 初始化临界区嵌套次数计数器为0 */
    uxCriticalNesting = 0;
    /* 使能FPU
     * 仅ARM Cortex-M4/M7内核MCU才有此行代码
     * ARM Cortex-M3内核MCU无FPU
     */
    prvEnableVFP();
    /* 在进出异常时,自动保存和恢复FPU相关寄存器
     * 仅ARM Cortex-M4/M7内核MCU才有此行代码
     * ARM Cortex-M3内核MCU无FPU
     */
    * (portFPCCR) |= portASPEN_AND_LSPEN_BITS;
    /* 启动第一个任务*/
    prvStartFirstTask();
    /* 不会返回这里*/
    return 0;
}

函数xPortStartScheduler()的解析如下所示:

  1. 在启用断言的情况下,函数xPortStartScheduler()会检测用户在FreeRTOSConfig.h 文件
    中对中断的相关配置是否有误,感兴趣的读者请自行查看这部分的相关代码。
  2. 配置PendSV 和SysTick 的中断优先级为最低优先级,请参考4.3.1 小节。
  3. 调用函数vPortSetupTimerInterrupt()配置SysTick,函数vPortSetupTimerInterrupt()首先会
    将SysTick 当前计数值清空,并根据FreeRTOSConfig.h 文件中配置的
    configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和configTICK_RATE_HZ(系统时钟节拍频率)计算并设置SysTick 的重装载值,然后启动SysTick 计数和中断。
  4. 初始化临界区嵌套计数器为0。
  5. 调用函数prvEnableVFP()使能FPU,因为ARM Cortex-M3 内核MCU 无FPU,此函数
    仅在ARM Cortex-M4/M7 内核MCU 平台上被调用,执行改函数后FPU 被开启。
  6. 接下来将FPCCR 寄存器的[31:30]置1,这样在进出异常时,FPU 的相关寄存器就会自
    动地保存和恢复,同样地,因为ARM Cortex-M3 内核MCU 无FPU,此当代码仅在ARM Cortex-M4/M7 内核MCU 平台上被调用。
  7. 调用函数prvStartFirstTask()启动第一个任务。

FreeRTOS 启动第一个任务

函数prvStartFirstTask()

函数prvStartFirstTask()用于初始化启动第一个任务前的环境,主要是重新设置MSP 指针,
并使能全局中断,具体的代码如下所示(这里以正点原子的STM32F1 系列开发板为例,其他类型的开发板类似):

__asm void prvStartFirstTask(void) {
    /* 8字节对齐*/
    PRESERVE8
    ldr r0, = 0xE000ED08 /* 0xE000ED08为VTOR地址*/
    ldr r0, [r0] /* 获取VTOR的值*/
    ldr r0, [r0] /* 获取MSP的初始值*/
    /* 初始化MSP */
    msr msp, r0
    /* 使能全局中断*/
    cpsie i
    cpsie f
    dsb
    isb
    /* 调用SVC启动第一个任务*/
    svc 0
    nop
    nop
}

从上面的代码可以看出,函数prvStartFirstTask()是一段汇编代码,解析如下所示:

  1. 首先是使用了PRESERVE8,进行8 字节对齐,这是因为,栈在任何时候都是需要4 字
    节对齐的,而在调用入口得8 字节对齐,在进行C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
  2. 接下来的三行代码是为了获得MSP 指针的初始值,那么这里就能够引出两个问题:
    (1) 什么是MSP 指针?
    程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。当有信息保存到栈中时,
    MCU 会自动更新SP 指针,使SP 指针指向最后一个入栈的元素,那么程序就可以根据SP 指针来从栈中存取信息。对于正点原子的STM32F1、STM32F4、STM32F7 和STM32H7 开发板上使用的ARM Cortex-M 的MCU 内核来说,ARM Cortex-M 提供了两个栈空间,这两个栈空间的堆栈指针分别是MSP(主堆栈指针)和PSP(进程堆栈指针)。在FreeRTOS 中MSP 是给系统栈空间使用的,而PSP 是给任务栈使用的,也就是说,FreeRTOS 任务的栈空间是通过PSP 指向的,而在进入中断服务函数时,则是使用MSP 指针。当使用不同的堆栈指针时,SP 会等于当前使用的堆栈指针。
    (2) 为什么是0xE000ED08?
    0xE000ED08 是VTOR(向量表偏移寄存器)的地址,VTOR 中保存了向量表的偏移地址。
    一般来说向量表是从其实地址0x00000000 开始的,但是在有情况下,可能需要修改或重定向向量表的首地址,因此ARM Corten-M 提供了VTOR 对向量表进行从定向。而向量表是用来保存中断异常的入口函数地址,即栈顶地址的,并且向量表中的第一个字保存的就是栈底的地址,在start_stm32xxxxxx.s 文件中有如下定义:

在这里插入图片描述
以上就是向量表(只列出前几个)的部分内容,可以看到向量表的第一个元素就是栈指针
的初始值,也就是栈底指针。
在了解了这两个问题之后,接下来再来看看代码。首先是获取VTOR 的地址,接着获取
VTOR 的值,也就是获取向量表的首地址,最后获取向量表中第一个字的数据,也就是栈底指针了。
3. 在获取了栈顶指针后,将MSP 指针重新赋值为栈底指针。这个操作相当于丢弃了程序
之前保存在栈中的数据,因为FreeRTOS 从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
4. 重新赋值MSP 后,接下来就重新使能全局中断,因为之前在函数vTaskStartScheduler()
中关闭了受FreeRTOS 的中断。
5. 最后使用SVC 指令,并传入系统调用号0,触发SVC 中断。

函数vPortSVCHandler()

当使能了全局中断,并且手动触发SVC 中断后,就会进入到SVC 的中断服务函数中。SVC
的中断服务函数为vPortSVCHandler(),该函数在port.c 文件中有定义,具体的代码如下所示(这里以正点原子的STM32F1 系列开发板为例,其他类型的开发板类似):

__asm void vPortSVCHandler(void) {
    /* 8字节对齐*/
    PRESERVE8
    /* 获取任务栈地址*/
    ldr r3, = pxCurrentTCB /* r3指向优先级最高的就绪态任务的任务控制块*/
    ldr r1, [r3] /* r1为任务控制块地址*/
    ldr r0, [r1] /* r0为任务控制块的第一个元素(栈顶)*/
    /* 模拟出栈,并设置PSP */
    ldmia r0!, {
        r4 - r11
    } /* 任务栈弹出到CPU寄存器*/
    msr psp, r0 /* 设置PSP为任务栈指针*/
    isb
    /* 使能所有中断*/
    mov r0, #0
    msr basepri,
    /* 使用PSP指针,并跳转到任务函数*/
    orr r14, #0xd
    bx r14
}

从上面代码中可以看出,函数vPortSVCHandler()就是用来跳转到第一个任务函数中去的,
该函数的具体解析如下:

  1. 首先通过pxCurrentTCB 获取优先级最高的就绪态任务的任务栈地址,优先级最高的就
    绪态任务就是系统将要运行的任务。pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,在前面创建start_task 任务、空闲任务、定时器处理任务时自动根据任务的优先级高低进行赋值的,具体的赋值过程在后续分析任务创建函数时,会具体分析。
    这里举个例子,在《FreeRTOS 移植实验》中,start_task 任务、空闲任务、定时器处理任务的优先级如下表所示:
    在这里插入图片描述
    在这里插入图片描述
    从上表可以看出,在《FreeRTOS 移植实验》中,定时器处理任务的任务优先级为31,是系
    统中优先级最高的任务,因此当进入SVC 中断时,pxCurrentTCB 就是指向了定时器处理任务的任务控制块。
    接着通过获取任务控制块中的第一个元素,得到该任务的栈顶指针,任务控制块的相关内
    容,请查看第5.5 小节《FreeRTOS 任务控制块》。
  2. 接下来通过任务的栈顶指针,将任务栈中的内容出栈到CPU 寄存器中,任务栈中的内
    容在调用任务创建函数的时候,已经初始化了。然后再设置PSP 指针,那么,这么一来,任务的运行环境就准备好了。
  3. 通过往BASEPRI 寄存器中写0,允许中断。
  4. 最后通过两条汇编指令,使CPU 跳转到任务的函数中去执行,代码如下所示:
orr r14, # 0xd
bx r14

要弄清楚这两条汇编代码,首先要清楚r14 寄存器是干什么用的。通常情况下,r14 为链接
寄存器(LR),用于保存函数的返回地址。但是在异常或中断处理函数中,r14 为EXC_RETURN(关于r14 寄存器的相关内容,感兴趣的读者请自行查阅相关资料),EXC_RETURN 各比特位的描述如下表所示:
在这里插入图片描述
EXC_RETURN 只有6 个合法的值,如下表所示:
在这里插入图片描述
因为此时是在SVC 的中断服务函数中,因此此时的r14 应为EXC_RETURN,将r14 与0xd
作或操作,然后将值写入r14,那么就是将r14 的值设置为了0xFFFFFFFD 或0xFFFFFFED(具体看是否使用了浮点单元),即返回后进入线程模式,并使用PSP。这里要注意的是,SVC 中断服务函数的前面,将PSP 指向了任务栈。
说了这么多,FreeRTOS 对于进入中断后r14 为EXC_RETURN 的具体应用就是,通过判断
EXC_RETURN 的bit4 是否为0,来判断任务是否使用了浮点单元。
最后通过bx r14 指令,跳转到任务的任务函数中执行,执行此指令,CPU 会自动从PSP 指
向的栈中出栈R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,并且如果EXC_RETURN 的bit4 为0(使用了浮点单元),那么CPU 还会自动恢复浮点寄存器。

FreeRTOS 任务状态列表

在开始介绍FreeRTOS 中任务相关的API 函数之前,先介绍一下FreeRTOS 中的任务状态
列表,前面章节说了,FreeRTOS 中的任务无非就四种状态,分别为运行态、就绪态、阻塞态和挂起态,这四种状态中,除了运行态,其他三种任务状态的任务都有其对应的任务状态列表,FreeRTOS 使用这些任务状态列表来管理处于不同状态的任务。任务状态列表在task.c 文件中有定义,具体的定义代码如下所示:

/* 就绪态任务列表*/
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/* 阻塞态任务列表*/
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
/* 挂起态任务列表*/
PRIVILEGED_DATA static List_t xPendingReadyList;

下面对上面代码中各个任务状态的列表定义进行解析:

  1. 就绪态任务列表,从定义中可以看出,就绪态任务列表是一个数组,数组中元素的个数
    由宏configMAX_PRIORITY 确定,宏configMAX_PRIORITY 为配置的系统最大任务优先级。
    由此可见,FreeRTOS 为每个优先等级的任务都分配了一个就绪态任务列表,每一个就绪态任务列表中的就绪态任务的任务优先级由于其相同就绪态任务列表的任务相同。
  2. 阻塞态任务列表,阻塞态任务列表一共有两个,分别为是阻塞态任务列表1 和阻塞态任
    务列表2,并且该有两个阻塞态任务列表指针。这么做的么的是为了解决任务阻塞时间溢出的问题,这个会在后续讲解阻塞相关的内容时,具体分析。
  3. 挂起态任务列表,被挂起的任务就会被添加到挂起态任务列表中。

FreeRTOS 创建任务函数解析

函数xTaskCreate()

FreeRTOS 中创建任务的函数描述,请参考第6.1 小节《FreeRTOS 创建和删除任务》。本小节着重分析函数xTaskCreate()。
函数xTaskCreate()在task.c 文件中有定义,具体的代码如下所示:

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
    const char *
    const pcName,
    const configSTACK_DEPTH_TYPE usStackDepth,
    void *
    const pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t *
    const pxCreatedTask) {
    TCB_t * pxNewTCB;
    BaseType_t xReturn;
    /* 宏portSTACK_GROWTH用于定义栈的生长方向
     * STM32的栈是向下生长的,
     * 因此宏portSTACK_GROWTH定义为-1
     */
    #if (portSTACK_GROWTH > 0) 
    {
        /* 为任务控制块申请内存空间*/
        pxNewTCB = (TCB_t * ) pvPortMalloc(sizeof(TCB_t));
        /* 任务控制块内存申请成功*/
        if (pxNewTCB != NULL) {
            /* 为任务栈空间申请内存空间*/
            pxNewTCB - > pxStack =
                (StackType_t * ) pvPortMallocStack((((size_t) usStackDepth) *
                    sizeof(StackType_t)));
            /* 任务栈空间内存申请失败*/
            if (pxNewTCB - > pxStack == NULL) {
                /* 释放申请到的任务控制块内存*/
                vPortFree(pxNewTCB);
                pxNewTCB = NULL;
            }
        }
    }
    #else 
    {
        StackType_t * pxStack;
        /* 为任务栈空间申请内存空间*/
        pxStack = pvPortMallocStack((((size_t) usStackDepth) * sizeof(StackType_t)));
        /* 任务栈空间内存申请成功*/
        if (pxStack != NULL) {
            /* 为任务控制块申请内存空间*/
            pxNewTCB = (TCB_t * ) pvPortMalloc(sizeof(TCB_t));
            /* 任务控制块内存申请成功*/
            if (pxNewTCB != NULL) {
                /* 设置任务控制块中的任务栈指针*/
                pxNewTCB - > pxStack = pxStack;
            } else {
                /* 释放申请到的任务栈空间内存*/
                vPortFreeStack(pxStack);
            }
        } else {
            pxNewTCB = NULL;
        }
    }
    #endif
    /* 任务控制块和任务栈空间的内存均申请成功*/
    if (pxNewTCB != NULL) {
        /* 宏tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE用于
         * 指示系统是否同时支持静态和动态方式创建任务
         * 如果系统同时支持多种任务创建方式,
         * 则需要标记任务具体是静态方式还是动态方式创建的
         */
        #if (tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0) {
            /* 标记任务是动态方式创建的*/
            pxNewTCB - > ucStaticallyAllocated =
                tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
        }
        #endif
        /* 初始化任务控制块中的成员变量*/
        prvInitialiseNewTask(pxTaskCode,
            pcName, (uint32_t) usStackDepth,
            pvParameters,
            uxPriority,
            pxCreatedTask,
            pxNewTCB,
            NULL);
        /* 将任务添加到就绪态任务列表中
         * 这个函数会同时比较就绪态任务列表中的任务优先级
         * 并更新pxCurrentTCB为就绪态任务列表中优先级最高的任务
         */
        prvAddNewTaskToReadyList(pxNewTCB);
        /* 返回pdPASS,说明任务创建成功*/
        xReturn = pdPASS;
    } else {
        /* 内存申请失败,则返回内存申请失败的错误*/
        xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
    }
    return xReturn;
}
  1. 函数xTaskCreate()创建任务,首先为任务的任务控制块以及任务栈空间申请内存,如果
    任务控制块或任务栈空间的内存申请失败,则释放已经申请到的内存,并返回内存申请失败的错误。
  2. 任务创建所需的内存申请成功后,使用函数prvInitialiseNewTask()初始化任务控制块中
    方的成员变量,包括任务函数指针、任务名、任务栈大小、任务函数参数、任务优先级等。
  3. 最后调用函数prvAddNewTaskToReadList()将任务添加到就绪态任务列表中,从这里可
    以看出,任务被创建后,是立马被添加到就绪态任务列表中的。

函数prvInitialiseNewTask()

函数prvInitialiseNewTask()用于创建任务时,初始化任务控制块中的成员变量,函数
prvInitialiseNewTask()在task.c 文件中有定义,具体的代码如下所示:

。。。。

以上就是函数prvInitialiseNewTask()的具体代码,可以看到函数prvInitialiseNewTask()就是
初始化了任务控制块中的成员变量,其中比较重要的操作就是调用函数pxPortInitialiseStack()初始化了任务栈。

函数pxPortInitialiseStack()

函数pxPortInitialiseStack()用于初始化任务栈,就是往任务的栈中写入一些重要的信息,这
些信息会在任务切换的时候被弹出到CPU 寄存器中,以恢复任务的上下文信息,这些信息就包括xPSR 寄存器的初始值、任务的函数地址(PC 寄存器)、任务错误退出函数地址(LR 寄存器)、任务函数的传入参数(R0 寄存器)以及为R1~R12 寄存器预留空间,若使用了浮点单元,那么还会有EXC_RETURN 的值。同时该函数会返回更新后的栈顶指针。
针对ARM Cortex-M3 和针对ARM Cortex-M4 和ARM Cortex-M7 内核的函数
pxPortInitialiseStack()稍有不同,原因在于ARM Cortex-M4 和ARM Cortex-M7 内核具有浮点单元,因此在任务栈中还需保存浮点寄存器的值。
针对ARM Cortex-M3 内核的函数pxPortInitialiseStack(),具体的代码如下所示:

StackType_t * pxPortInitialiseStack(
    StackType_t * pxTopOfStack, /* 任务栈顶指针*/
    TaskFunction_t pxCode, /* 任务函数地址*/
    void * pvParameters) /* 任务函数传入参数*/ {
    /* 模拟栈的格式将信息保存到任务栈中,用于上下文切换*/
    pxTopOfStack--;
    /* xPSR寄存器初始值为0x01000000 */
    * pxTopOfStack = portINITIAL_XPSR;
    pxTopOfStack--;
    /* 任务函数的地址(PC寄存器)*/
    * pxTopOfStack = ((StackType_t) pxCode) & portSTART_ADDRESS_MASK;
    pxTopOfStack--;
    /* 任务错误退出函数地址(LR寄存器)*/
    * pxTopOfStack = (StackType_t) prvTaskExitError;
    /* 为R12、R3、R2、R1寄存器预留空间*/
    pxTopOfStack -= 5;
    /* 任务函数的传入参数(R0寄存器)*/
    * pxTopOfStack = (StackType_t) pvParameters;
    /* 为R11、R10、R9、R8、R7、R6、R5、R4寄存器预留空间*/
    pxTopOfStack -= 8;
    /* 返回更新后的任务栈指针
     * 后续任务运行时需要用到栈的地方,
     * 将从这个地址开始保存信息
     */
    return pxTopOfStack;
}

针对ARM Cortex-M4 和ARM Cortex-M7 内核的函数pxPortInitialiseStack(),具体的代码如
下所示:

StackType_t * pxPortInitialiseStack(
    StackType_t * pxTopOfStack, /* 任务栈顶指针*/
    TaskFunction_t pxCode, /* 任务函数地址*/
    void * pvParameters) /* 任务函数传入参数*/ {
    /* 模拟栈的格式将信息保存到任务栈中,用于上下文切换*/
    pxTopOfStack--;
    /* xPSR寄存器初始值为0x01000000 */
    * pxTopOfStack = portINITIAL_XPSR;
    pxTopOfStack--;
    /* 任务函数的地址(PC寄存器)*/
    * pxTopOfStack = ((StackType_t) pxCode) & portSTART_ADDRESS_MASK;
    pxTopOfStack--;
    /* 任务错误退出函数地址(LR寄存器)*/
    * pxTopOfStack = (StackType_t) prvTaskExitError;
    /* 为R12、R3、R2、R1寄存器预留空间*/
    pxTopOfStack -= 5;
    /* 任务函数的传入参数(R0寄存器)*/
    * pxTopOfStack = (StackType_t) pvParameters;
    pxTopOfStack--;
    /* EXC_RETURN
     * 初始化为0xFFFFFFFD,
     * 即表示不使用浮点单元,且中断返回后进入线程模式,使用PSP
     */
    * pxTopOfStack = portINITIAL_EXC_RETURN;
    /* 为R11、R10、R9、R8、R7、R6、R5、R4寄存器预留空间*/
    pxTopOfStack -= 8;
    /* 返回更新后的任务栈指针
     * 后续任务运行时需要用到栈的地方,
     * 将从这个地址开始保存信息
     */
    return pxTopOfStack;
}

函数pxPortInitialiseStack()初始化后的任务栈如下图所示:
在这里插入图片描述

函数prvAddNewTaskToReadyList()

函数prvAddNewTaskToReadList()用于将新建的任务添加到就绪态任务列表中,在task.c 文
件中有定义,具体的代码如下所示:

static void prvAddNewTaskToReadyList(
    TCB_t * pxNewTCB) /* 任务控制块*/ {
    /* 进入临界区,确保在操作就绪态任务列表时,中断不会访问列表*/
    taskENTER_CRITICAL(); {
        /* 此全局变量用于记录系统中任务数量*/
        uxCurrentNumberOfTasks++;
        /* 此全局变量用于指示当前系统中处于就绪态任务中优先级最高的任务
         * 如果该全局变量为空(NULL),
         * 即表示当前创建的任务为系统中的唯一的就绪任务
         */
        if (pxCurrentTCB == NULL) {
            /* 系统中无其他就绪任务,因此优先级最高的就绪态任务为当前创建的任务*/
            pxCurrentTCB = pxNewTCB;
            /* 如果当前系统中任务数量为1,
             * 即表示当前创建的任务为系统中第一个任务
             */
            if (uxCurrentNumberOfTasks == (UBaseType_t) 1) {
                /* 初始化任务列表(就绪态任务列表,任务阻塞列表)*/
                prvInitialiseTaskLists();
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        } else {
            /* 判断任务调度器是否运行*/
            if (xSchedulerRunning == pdFALSE) {
                /* 当任务调度器为运行时
                 * 将pxCurrentTCB更新为优先级最高的就绪态任务
                 */
                if (pxCurrentTCB - > uxPriority <= pxNewTCB - > uxPriority) {
                    pxCurrentTCB = pxNewTCB;
                } else {
                    mtCOVERAGE_TEST_MARKER();
                }
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        /* 用于调试,不用理会*/
        uxTaskNumber++;
        /* 用于调试,不用理会*/
        #
        if (configUSE_TRACE_FACILITY == 1) {
            pxNewTCB - > uxTCBNumber = uxTaskNumber;
        }#
        endif
        traceTASK_CREATE(pxNewTCB);
        /* 将任务添加到就绪态任务列表中*/
        prvAddTaskToReadyList(pxNewTCB);
        /* 为定义,不用理会*/
        portSETUP_TCB(pxNewTCB);
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
    /* 如果任务调度器正在运行,
     * 那么就需要判断,当前新建的任务优先级是否最高
     * 如果是,则需要切换任务
     */
    if (xSchedulerRunning != pdFALSE) {
        /* 如果当前新建的任务优先级高于pxCurrentTCB的优先级*/
        if (pxCurrentTCB - > uxPriority < pxNewTCB - > uxPriority) {
            /* 进行任务切换*/
            taskYIELD_IF_USING_PREEMPTION();
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    } else {
        mtCOVERAGE_TEST_MARKER();
    }
}
  1. 可以看到函数prvAddNewTaskToReadyList()调用了函数prvAddTaskToReadyList()将新创建的任务添加到就绪态任务队列中。函数prvAddTaskToReadyList()是一个宏,具体的定义如下所示:
#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), \
&( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

从上面的代码可以看出,宏prvAddTaskToReadyList()主要就完成两件事,首先是记录任务
有优先级,FreeRTOS 会以位图的方式记录就绪态任务列表中就绪态任务的优先级,这样能够提高切换任务时的效率;还有就是将任务的任务状态列表项插入到就绪态任务列表的末尾。
2. 此函数还会根据任务调度器的运行状态,已经新创建的任务优先级是否比pxCurrentTCB
的优先级高,决定是否进行任务切换。任务切换时,调用了函数taskYIELD_IF_USING_PREEMPTION()进行任务切换,有关任务切换的相关内容,会在后续的章节中分析。

FreeRTOS 删除任务函数解析

函数vTaskDelete()

FreeRTOS 中删除任务的函数描述,请参考第6.1 小节《FreeRTOS 创建和删除任务》。本小节着重分析函数vTaskDelete()。
函数vTaskDelete()在task.c 文件中有定义,具体的代码如下所示:

void vTaskDelete(
    TaskHandle_t xTaskToDelete) /* 待删除任务的任务句柄*/ {
    TCB_t * pxTCB;
    /* 进入临界区*/
    taskENTER_CRITICAL(); {
        /* 如果传入的任务句柄为空(NULL)
         * 此函数会将待删除的任务设置为调用该函数的任务本身
         * 因此,如果要在任务中删除任务本身,
         * 那么可以调用函数vTaskDelete(),并传入任务句柄,
         * 或传入NULL
         */
        pxTCB = prvGetTCBFromHandle(xTaskToDelete);
        /* 将任务从任务所在任务状态列表(就绪态任务列表或阻塞态任务列表)中移除
         * 如果移除后列表中的列表项数量为0
         * 那么就需要更新任务优先级记录
         * 因为此时系统中可能已经没有和被删除任务相同优先级的任务了
         */
        if (uxListRemove( & (pxTCB - > xStateListItem)) == (UBaseType_t) 0) {
            /* 更新任务优先级记录*/
            taskRESET_READY_PRIORITY(pxTCB - > uxPriority);
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
        /* 判断被删除的任务是否还有等待的事件*/
        if (listLIST_ITEM_CONTAINER( & (pxTCB - > xEventListItem)) != NULL) {
            /* 将被删除任务的事件列表项,从所在事件列表中移除*/
            (void) uxListRemove( & (pxTCB - > xEventListItem));
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
        /* 由于调试,不用理会*/
        uxTaskNumber++;
        /* 判断被删除的任务是否为正在运行的任务(即任务本身)*/
        if (pxTCB == pxCurrentTCB) {
            /* 任务是无法删除任务本身的,于是需要将任务添加到任务待删除列表中
             * 空闲任务会处理任务待删除列表中的待删除任务
             */
            vListInsertEnd( & xTasksWaitingTermination, & (pxTCB - > xStateListItem));
            /* 这个全局变量用来告诉空闲任务有多少个待删除任务需要被删除*/
            ++uxDeletedTasksWaitingCleanUp;
            /* 用于调试,不用理会*/
            traceTASK_DELETE(pxTCB);
            /* 未定义,不用理会*/
            portPRE_TASK_DELETE_HOOK(pxTCB, & xYieldPending);
        } else {
            /* 任务数量计数器减1 */
            --uxCurrentNumberOfTasks;
            /* 用于调试,不用理会*/
            traceTASK_DELETE(pxTCB);
            /* 更新下一个任务的阻塞超时时间,以防被删除的任务就是下一个阻塞超时的任务*/
            prvResetNextTaskUnblockTime();
        }
    }
    /* 退出临界区*/
    taskEXIT_CRITICAL();
    /* 如果待删除任务不是任务本身*/
    if (pxTCB != pxCurrentTCB) {
        /* 此函数用于释放待删除任务占用的内存资源*/
        prvDeleteTCB(pxTCB);
    }
    /* 如果任务调度器正在运行,
     * 那么就需要判断,待删除任务是否为任务本身
     * 如果是,则需要切换任务
     */
    if (xSchedulerRunning != pdFALSE) {
        /* 如果待删除任务就是任务本身*/
        if (pxTCB == pxCurrentTCB) {
            /* 此时任务调度器不能处于挂起状态*/
            configASSERT(uxSchedulerSuspended == 0);
            /* 进行任务切换*/
            portYIELD_WITHIN_API();
        } else {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}
  1. 从上面的代码中可以看出,使用vTaskDelete()删除任务时,需要考虑两种情况,分别为
    待删除任务不是当前正在运行的任务(调用该函数的任务)和待删除任务为当前正在运行的任务(调用该函数的任务)。第一种情况比较简单,当前正在运行的任务可以直接删除待删除任务;
    而第二种情况下,待删除任务时无法删除自己的,因此需要将当前任务添加到任务待删除列表中,空闲任务会处理这个任务待删除列表,将待删除的任务统一删除,有关空闲任务的相关内容,在后续的章节中会进行讲解。
  2. 在待删除任务不是当前正在运行的任务这种情况下,当前正在运行的任务可以删除待删
    除的任务,因此调用了函数prvDeleteTCB(),将待删除的任务删除。

函数prvDeleteTCB()

该函数主要用于释放待删除任务所占的内存空间,在task.c 文件中有定义,具体的代码如
下所示:

static void prvDeleteTCB(
    TCB_t * pxTCB) /* 待删除任务的任务控制块*/ {
    /* 未定义,不用理会*/
    portCLEAN_UP_TCB(pxTCB);
    /* 与Newlib相关*/
    #
    if (configUSE_NEWLIB_REENTRANT == 1) {
        _reclaim_reent( & (pxTCB - > xNewLib_reent));
    }#
    endif /* configUSE_NEWLIB_REENTRANT */
    /* 当系统只支持动态内存管理时,
     * 任务待删除任务所占用的内存空间是通过动态内存管理分配的,
     * 因此只需要将内存空间通过动态内存管理释放掉即可
     * 当系统支持静态内存管理和动态内存管理时,
     * 则需要分情况讨论
     */
    #
    if ((configSUPPORT_DYNAMIC_ALLOCATION == 1) && \
        (configSUPPORT_STATIC_ALLOCATION == 0) && \
        (portUSING_MPU_WRAPPERS == 0)) {
        /* 动态内存管理释放待删除任务的任务控制块和任务的栈空间*/
        vPortFreeStack(pxTCB - > pxStack);
        vPortFree(pxTCB);
    }#
    elif(tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0) {
        /* 待删除任务的任务控制块和任务栈都是由动态内存管理分配的*/
        if (pxTCB - > ucStaticallyAllocated == tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB) {
            /* 动态内存管理释放待删除任务的任务控制块和任务的栈空间*/
            vPortFreeStack(pxTCB - > pxStack);
            vPortFree(pxTCB);
        }
        /* 待删除任务的任务控制块是由动态内存管理分配的*/
        else if (pxTCB - > ucStaticallyAllocated ==
            tskSTATICALLY_ALLOCATED_STACK_ONLY) {
            /* 动态内存管理释放待删除任务的任务控制块*/
            vPortFree(pxTCB);
        }
        /* 待删除任务的任务控制块和任务栈都是由静态内存管理分配的*/
        else {
            /* 不能还存在其他情况*/
            /* 这种情况下,待删除任务的任务控制块和任务栈空间所占用的内存
             * 由用户管理
             */
            configASSERT(pxTCB - > ucStaticallyAllocated ==
                tskSTATICALLY_ALLOCATED_STACK_AND_TCB);
            mtCOVERAGE_TEST_MARKER();
        }
    }#
    endif
}

FreeRTOS 挂起任务函数解析

函数vTaskSuspend()

FreeRTOS 中挂起任务的函数描述,请参考第6.4 小节《FreeRTOS 挂起和恢复任务》。本小节着重分析函数vTaskSuspend()。
函数vTaskSuspend()在task.c 文件中有定义,具体的代码如下所示:

。。。

使用函数vTaskSuspend()挂起任务时,如果任务调度器没有运行,并且待挂起的任务又是
调用函数vTaskSuspend()的任务,那么pxCurrentTCB 需要指向其他优先级最高的就绪态任务,更新pxCurrentTCB 的操作,时通过调用函数vTaskSwitchContext()实现的。

函数vTaskSwitchContext()

该函数用于更新pxCurrentTCB 指向就绪态任务列表中优先级最高的任务,该函数在task.c
文件中有定义,具体代码如下所示:

void vTaskSwitchContext(void) {
    if (uxSchedulerSuspended != (UBaseType_t) pdFALSE) {
        /* 任务调度器没有运行,不允许切换上下文,直接退出函数*/
        xYieldPending = pdTRUE;
    } else {
        xYieldPending = pdFALSE;
        /* 用于调试,不用理会*/
        traceTASK_SWITCHED_OUT();
        /* 此宏用于启用任务运行时间统计功能*/
        #if (configGENERATE_RUN_TIME_STATS == 1) {
        #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
            portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);
            #else
                ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
            #endif
            if (ulTotalRunTime > ulTaskSwitchedInTime) {
                pxCurrentTCB - > ulRunTimeCounter +=
                    (ulTotalRunTime - ulTaskSwitchedInTime);
            } else {
                mtCOVERAGE_TEST_MARKER();
            }
            ulTaskSwitchedInTime = ulTotalRunTime;
        }
        #endif
        /* 未定义,不用理会*/
        taskCHECK_FOR_STACK_OVERFLOW();
        /* 与POSIX相关配置,不用理会*/
        
        #if (configUSE_POSIX_ERRNO == 1) {
            pxCurrentTCB - > iTaskErrno = FreeRTOS_errno;
        }
        #endif
        /* 此函数用于将pxCurrentTCB更新为指向优先级最高的就绪态任务*/
        taskSELECT_HIGHEST_PRIORITY_TASK();
        /* 用于调试,不用理会*/
        traceTASK_SWITCHED_IN();
        /* 与POSIX相关配置,不用理会*/
        #if (configUSE_POSIX_ERRNO == 1) {
            FreeRTOS_errno = pxCurrentTCB - > iTaskErrno;
        }
        #endif
        /* 与Newlib相关配置,不用理会*/
        #if (configUSE_NEWLIB_REENTRANT == 1) {
            _impure_ptr = & (pxCurrentTCB - > xNewLib_reent);
        }
        #endif
    }
}

此函数的重点在于调用了函数taskSELETE_HIGHEST_PRIORITY_TASK() 更新
pxCurrentTCB 指向优先级最高的就绪态任务,函数taskSELETE_HIGHEST_PRIORITY_TASK()
实际上是一个宏定义,task.c 文件中有定义,具体的代码如下所示:

#
define taskSELECT_HIGHEST_PRIORITY_TASK()\ {\
    UBaseType_t uxTopPriority;\\
    /* 查找就绪态任务列表中最高的任务优先级*/
    \
    portGET_HIGHEST_PRIORITY(uxTopPriority, uxTopReadyPriority);\
    /* 此任务优先级不能时最低的任务优先级*/
    \
    configASSERT(\
        listCURRENT_LIST_LENGTH( & (pxReadyTasksLists[uxTopPriority])) > \
        0\
    );\
    /* 让pxCurrentTCB指向该任务优先级就绪态任务列表中的任务*/
    \
    listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, \ & (pxReadyTasksLists[uxTopPriority]));\
}

FreeRTOS 恢复任务函数解析

函数vTaskResume()

FreeRTOS 中恢复任务的函数描述,请参考第6.4 小节《FreeRTOS 挂起和恢复任务》。本小节着重分析函数vTaskResume()。
函数vTaskResume()在task.c 文件中有定义,具体的代码如下所示:
。。

FreeRTOS 空闲任务

函数prvIdleTask()

空闲任务主要用于处理待删除任务列表和低功耗,函数prvIdleTask()在task.c 文件中有定
义,具体的代码如下所示:
。。。。

  • 2
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值