总图

简介

RTOS是操作系统的一种类型【Real-Time Operating System】------实时操作系统

实时操作系统强调实时性        使用抢占式调度

TSOS是另外一种类型的操作系统【Time-sharing Operating System】------分时操作系统

分时操作系统强调共享和公平        一般使用时间片轮转调度

FreeRTOS能够做到让单片机在同一时间处理多件事

内核移植

移植资料

百度网盘: 百度网盘 请输入提取码

移植过程

内核的文件结构

为了适配市面上绝大多数单片机,FreeRTOS在portable文件夹内部几乎为每一种【工具链】和【单片机内核】的组合提供了一份移植层代码

一般情况下FreeRTOS里的内核对象都是动态创建的,需要动态地分配内存来存储它们,而且一旦销毁内核对象,就要动态地释放掉之前分配的内存。在标准C语言程序里,这些功能通过malloc()和free()来实现,它们操作的是堆内存。FreeRTOS不能直接使用标准C语言的malloc()和free()函数,因此它需要自己创建一套堆内存管理方法。

头文件引用问题

1. 使用scanf()、printf()、putchar()、getchar()等函数的时候,需要引用“标准输入输出头文件” - stdio.h

2. 使用malloc()和free()管理堆内存的时候,需要引用“标准库头文件” - stdlib.h

3. 使用数学计算相关的函数的时候,需要引用“数学”头文件 - math.h

4. 使用字符串处理相关的函数的时候,需要引用“字符串”头文件 - string.h

配置文件FreeRTOSConfig.h

FreeRTOS官方提供的FreeRTOSConfig.h说明文档: Customization - FreeRTOS™

配置项1:configCPU_CLOCK_HZ - 内核的运行频率

用来告诉FreeRTOS  你的单片机的内核频率

配置项2:configTICK_TYPE_WIDTH_IN_BITS - 设置TickType_t类型的位宽

设置用多少比特位来表示当前的时间

配置项3:configKERNEL_INTERRUPT_PRIORITY - PendSV和SYSTICK的中断优先级

用来设置PendSV中断和SYSTICK中断的优先级,(15<<4)表示最低优先级,(0 << 4)表示最高优先级

配置项4:configMAX_SYSCALL_INTERRUPT_PRIORITY - 能够调用FreeRTOS API的最高优先级

设置能够调用FreeRTOS API的最高中断等级

配置项5:configUSE_TIME_SLICING - 是否使用时间片

设置是否使用时间片

安装中断服务程序

将port.c文件提供的3个中断服务程序安装到单片机的中断向量表里,以确保中断发生时这些函数能够被调用

FreeRTOS和HAL库可以共享Systick,但一般基于效率考虑我们会让HAL库让出Systick定时器,因为共用Systick会降低FreeRTOS的运行效率。让出Systick之后,HAL库可以改用其它的定时器。

FreeRTOS代码规范

变量

作用域和生命周期

变量的作用域,是指变量在多大范围内有效;变量的生命周期,是指变量能存在多久

变量在SRAM里的分布

SRAM里的内容

两种基本数据类型

TickType_t和BaseType_t

TickType_t用来记录时间,经常出现在FreeRTOS的API里,或者作为返回值,或者作为参数; TickType_t的定义并不固定,可以通过FreeRTOSConfig.h里的 configTICK_TYPE_WIDTH_IN_BITS 进行设置

#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_16_BITS // TickType_t = uint16_t

#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS // TickType_t = uint32_t

#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_64_BITS // TickType_t = uint64_t

BaseType_t可以用作函数的返回值,表示函数调用是否成功;还可以用作无符号的数字,还有一个叫做UBaseType_t的类型。UBaseType_t是BaseType_t的无符号版本,在32位单片机里对应uint32_t。

命名规则

变量的命名规则

例:pv = pointer to void = void *;pc = pointer to char = char *;us = unsigned short = uint16_t;ux = unsigned base type = UBaseType_t;px = pointer to struct = 结构体指针

函数的命名规则

对于xTaskCreate()这个函数来说,x代表返回值的类型为BaseType_t;Task代表该函数被定义在task.c文件当中

函数前边加static关键字,表示这是一个静态函数,只在文件内部可见。此时函数名应当遵循如下格式。可以看到,静态函数不需要考虑返回值的类型,其前缀一律是prv

任务管理

创建任务

/ *

@简介:该方法用于创建任务

@返回:pdPASS - 成功;

errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY - 内存不足以创建任务

*/

BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, /* 任务代码 */

                                         const char *const pcName, /* 字符串名称,调试时供人类阅读 */

                                         unsigned short usStackDepth, /* 任务栈的深度 */

                                         void *pvParameters, /* 任务参数 */

                                        UBaseType_t uxPriority, /* 优先级,0 ~ configMAX_PRIORITY-1 */

                                        TaskHandle_t *pxCreatedTask /* 输出参数,用来返回任务句柄 */

);

参数1 - pvTaskCode

TaskFunction_t是函数指针类型,它指向的函数必须符合void fn(void *)的格式(返回值为空,参数为void *类型)

遵循两点规则即可:

1. 任务代码的原型要符合TaskFunction_t规定的格式,即空返回值和一个void *类型的参数

2. 把任务代码的函数名作为参数填入到xTaskCreate()当中

我们一般需要将任务代码写成一个“无限循环”的形式,并尽量避免任务代码退出,一旦退出要用vTaskDelete()删除自身,这是为了防止任务“跑飞”

参数2 - pcName

可通过配置文件FreeRTOSConfig.h里的configMAX_TASK_NAME_LEN设置最大长度的值

FreeRTOS会为每个任务创建一个长度为configMAX_TASK_NAME_LEN的数组来存储任务的名字,这个数组位于SRAM当中,当设置得过大,会浪费掉一些SRAM

参数3 - usStackDepth

用来设置任务的栈深度;任务栈用来存储该任务的局部变量以及函数调用的返回地址;

“栈”溢出是个可怕的事,一些时候会导致程序直接崩溃或者出现莫名其妙的问题,难以排查。所以在实际编程的过程中我们需要为任务分配充足的栈深度。但usStackDepth的值又不能过大,因为那将消耗过多的SRAM资源。

根据经验,简单的任务所需的栈空间非常少。对于这种任务,我们只需要分配非常少的“任务栈深度”即可,比如128word(512byte)。如果程序不能正常运行,我们再把usStackDepth参数改大一点,比如翻一倍256。

参数4 - pvParameters

用来传递任务运行的参数;

例: xTaskCreate(vTask1, "Task1", 128, (void *)500, 1, NULL);

//由于pvParameters要求void *类型,所以这里要强制类型转换

uint32_t halfPeriod = (uint32_t)pvParameters;

//再把void *强制转换为uint32_t

在 STM32 32 位平台上,指针地址和 uint32_t 通常都是 32 位数据;地址本质上由一个整数数值表示;强制转换只是改变编译器理解这个数值的方式

参数5 - uxPriority

用来设置任务的优先级

在FreeRTOS当中,任务优先级的取值范围是0~configMAX_PRIORITY-1,数值越大优先级越高;STM32F1单片机里有个Cortex-M3内核,它相当于CPU。内核会在在时钟信号的驱动下一刻不停地执行代码,代码都执行完了,需要让CPU执行“空闲任务”里的代码来实现空转的效果。

注:能够调用FreeRTOS API的最高中断等级,数字越小优先级越高;

STM32硬件中断优先级(NVIC)由ARM Cortex-M 硬件规定的,FreeRTOS不能改变,而任务优先级编号则由 FreeRTOS 软件规定,两者只是编号方式不同;

int main(void)

{ ...

xTaskCreate(vTaskA, "Task A", 128, NULL, 1, NULL);

xTaskCreate(vTaskB, "Task B", 128, NULL, 2, NULL);

...

vTaskStartScheduler();

/* 除非调度器启动失败,否则代码不会执行到此处 */

for(;;);

}

除非发生错误,否则vTaskStartScheduler()不会返回,CPU只会在各个任务之间切来切去,main()方法里的代码不会执行

参数6 - pxCreatedTask

用来返回任务句柄;

句柄-----FreeRTOS给某个任务分配的“身份编号/引用”,程序通过它找到并操作对应任务

输出参数就是:函数通过参数,把处理结果写回给调用者

xTaskCreate()执行流程:

  1. 分配内存
    FreeRTOS为任务分配任务控制块(TCB)和任务栈。

  2. 保存任务信息
    将任务函数、名称、参数、栈大小和优先级保存到TCB中。

  3. 初始化任务栈
    设置任务首次运行时从 pvTaskCode(pvParameters) 开始执行。

  4. 加入就绪列表
    新任务进入就绪状态,等待调度器运行。

  5. 返回任务句柄
    如果 pxCreatedTask 不为 NULL,将新任务的句柄写入该变量。

  6. 返回创建结果 pdPASS // 创建成功 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY // 内存不足

运行(Running)状态,是指某个任务正在被CPU执行

就绪(Ready)状态,是指某个任务正在等待被CPU执行

阻塞(Blocked)状态,是指某个任务因为自身没有满足“先决条件”,“自愿放弃”被执行的机会

暂停(Suspended)状态,就是指某个任务被禁止使用CPU

调度算法

抢占式调度

核心规则是:只要有高优先级的任务就绪就会立即发生抢占

vTaskDelay()和HAL_Delay()的延迟方式实现原理不同;vTaskDelay()的核心逻辑是“让”,也就是主动进入阻塞把CPU让给别的任务;而HAL_Delay()的核心逻辑是“等”,也就是一直执行while循环,让CPU空转

时间片

相同优先级的任务轮番执行,以保证每个任务享有均等的执行时间

FreeRTOS的运行离不开3个中断。分别是PendSV中断、Systick中断和SVC中断;SVC中断只负责启动第一个任务,时间片关系不大。剩下两个中断与时间片有很大的关系。Systick中断负责时间片轮询调度和递增系统节拍;PendSV负责实际的任务切换,是驱动FreeRTOS运行的主力军

原理

1. 每当Systick递减到0会发生溢出,并触发Systick中断

2. 在Systick中断的中断服务程序里手动触发PendSV中断

3. 实际的任务切换由PendSV中断来执行

“时间片”切换任务这个动作是由Systick发起的,最终的执行者是PendSV

设置

/* configTICK_RATE_HZ用来设置Tick中断的频率,单位是Hz。 一般情况下,它通过

configCPU_CLOCK_HZ的值计算得来。 */

#define configTICK_RATE_HZ 100

//systick每秒钟溢出100次,那么时间片的长度就是10ms

设置给configTICK_RATE_HZ的值会在调用vTaskStartScheduler()之后生效,vTaskStartScheduler()会根据你提供的信息来初始化systick并开启Systick中断

协作式调度

遵循优先级规则但同时不可抢占

算法的选取

configUSE_PREEMPTION - 是否启用抢占。0 - 禁用抢占;1 - 启用抢占

configUSE_TIME_SLICING - 是否使用时间片。0 - 禁用时间片; 1- 启用时间片

configUSE_TIME_SLICING = 0

configUSE_TIME_SLICING = 1

configUSE_PREEMPTION = 0

协作式调度

协作式调度

configUSE_PREEMPTION = 1

不带时间片的抢占式调度

带时间片的抢占式调度

与时间相关的三种API

pdMS_TO_TICKS()pdTICKS_TO_MS()是FreeRTOS里跟时间相关的两个宏。因为位于projdefs.h文件中,所以它们都有字母pd作为前缀。“MS”是毫秒的意思;“TICKS”表示系统节拍,它是FreeRTOS里的时间基准。每次发生Systick中断,节拍的值加1

xTaskGetTickCount()

是FreeRTOS的系统计时计数器读取函数,获取 FreeRTOS系统当前节拍数

vTaskDelay()

void vTaskDelay( const TickType_t xTicksToDelay );

xTicksToDelay = 100ms / 1000 * configTICK_RATE_HZ

延迟的时间是从调用vTaskDelay()的那一刻开始算的。它没有将执行某个函数和切换任务时所消耗的时间计算在内,因此实际执行的时间要稍晚。这就是vTaskDelay()不准确的根本原因。

vTaskDelayUntil()

void vTaskDelayUntil( TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement );

xTimeIncrement表示任务的固定执行周期;是任务两次唤醒之间的固定时间间隔;

使用公式:下次醒来的时间 = xPreviousWakeTime(上次苏醒的时间) + pdMS_TO_TICKS(100)(需要延迟的时间)计算延迟到期的时间,并更新xPreviousWakeTime的值

vTaskDelayUntil()和vTaskDelay()一样都是FreeRTOS里用来做延迟的函数。它们的关键区别在于vTaskDelayUntil()延迟的时间更加准确

堆内存管理

为了存储内核对象,FreeRTOS在标准C语言提供的“堆”之外又自行创建了一个堆。在之前,标准C语言库为我们提供了malloc()和free()来管理堆。但对于FreeRTOS的“新堆”来说,malloc()和free()就不管用了。因此FreeRTOS必须提供一套它自己的内存管理方案

标准C库的malloc()和free()在执行时间上具有不确定性,会破坏RTOS(实时操作系统)的实时性;且不可重入

/* 设置FreeRTOS堆的大小, 以字节为单位, 当向工程中添加了 heap_1.c, heap_2.c 或者heap_4.c之后有效。默认值是4096个字节,但是必须根据项目的具体情况来修改。注意,FreeRTOS 的堆存在于.bss段. */

#define configTOTAL_HEAP_SIZE 4096

软实时和硬实时

“软实时” - 不要求代码在规定时间内执行完成;“硬实时” - 要求代码在规定时间内执行完成

“可重入”和“不可重入”

可重入指同一个函数被多个不同的任务同时调用时不会发生错误

新的管理方案

FreeRTOS提供的新管理方案是pvPortMalloc()和vPortFree();函数原型如下:

// @作用:分配一定数量的堆内存

// @参数:xWantedSize - 要分配的堆内存的数量,单位字节

// @返回:新分配的内存块的指针,分配失败时返回NULL

void * pvPortMalloc( size_t xWantedSize )

// @作用:释放已分配的堆内存

// @参数:pv - 指向要释放的内存块的指针

void vPortFree( void * pv )

每个文件都包含了pvPortMalloc()和vPortFree()。它们的作用都是一样的,都是负责动态分配和释放FreeRTOS的堆内存,只是具体的实现细节不同;在实际使用的时候我们只需要把这5个文件中的一个添加到项目里即可

五种方案

heap_1 - 只分配不释放

优点:简单、高效、速度快,适合用在对实时性要求高的场合;不会产生碎片化问题

缺点:每次调用xTaskCreate()创建任务会分配内存,但调用vTaskDelete()时又不会把这些内存释放,因此堆内存会被一点点耗尽

heap_2 - 最佳匹配算法(Best Fit Algorithm)

优点:支持删除内核对象;优先选择满足要求的最小空闲块;

缺点:不能合并相邻的空闲块,会造成严重的内存碎片化问题(这是heap_2被淘汰的主要原因)

heap_3 - 标准C库的改进版本

优点:对malloc()和free()进行改进,解决了不可重入的问题

缺点:malloc()和free()仍然非常耗时,且执行时间不确定

heap_4 - 能够合并相邻空闲块【最常用】

优点:能够合并相邻空闲块;减少了heap_2带来的内存碎片化问题

heap_5 - 能够使用不连续的内存

信号量

二进制信号量

用法

用于任务和任务之间或者任务和中断之间的同步,我们只需要掌握以下几个编程接口即可

xSempahoreCreateBinary() - 创建二进制信号量

#include "FreeRTOS.h" // FreeRTOS的头文件

#include "semphr.h" // 信号量的头文件

// 作用:创建二进制信号量

// 返回:新创建的二进制信号量的句柄,创建失败时返回NULL

SemaphoreHandle_t xSemaphoreCreateBinary(void);

前缀是xSemaphore,其中x代表返回值的类型 - SemaphoreHandle_t。Semaphore代表这个函数属于“信号量模块”(函数声明位于semphr.h,具体实现位于queue.c,因为信号量的本质是队列)

xSemaphoreCreateBinary()的返回值是SemaphoreHandle_t类型。SemaphoreHandle_t代表信号量的句柄。在FreeRTOS当中有很多种信号量,比如:二进制信号量、计数信号量、互斥锁、递归互斥锁,它们的句柄都是SemaphoreHandle_t。如果成功创建了一个二进制信号量,其句柄就会通过返回值给到编程者。但是如果创建失败(一般是因为内存分配失败)就会返回一个NULL

vSemaphoreDelete() - 删除信号量

// 作用:删除信号量,比如二进制信号量、计数信号量、互斥锁、递归互斥锁

// 参数:xSemaphore - 要被删除的信号量的句柄

void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

前缀是vSemaphore,字母v表示返回值是void;

这个API有一个参数 - xSemaphore,需要填写要删除的信号量的句柄

xSemaphoreGive() - 给信号

// 作用:给信号。适用于所有的信号量类型。

// 参数:xSemaphore - 信号量的句柄

// 返回:pdPASS - 成功,pdFAIL - 失败

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore);

xSemaphoreGive()有一个BaseType_t类型的返回值,用来返回“给信号”这个动作是否成功

对于二进制信号量来说,如果它本身处于有信号的状态,继续调用xSemaphoreGive()的话就会返回pdFAIL

xSemaphoreTake() - 取信号

// 作用:取信号。适用于所有信号量。

// 参数:xSemaphore - 信号量的句柄

// 参数:xTicksToWait - 最长等待时间,portMAX_DELAY表示无限等待

// 返回:pdPASS - 成功取到信号;pdFAIL - 因超时而失败

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

参数xTicksToWait为最长等待时间。超过了这个时间取信号就会失败,返回pdFAIL。如果在时间超过之前取到信号,返回pdPASS

中断

FreeRTOS提供的两种版本API

运行在单片机里的程序分成两类:运行在“中断上下文”里的,和运行在“非中断上下文”里的

写在中断服务程序(ISR)里的代码,比如串口中断的中断服务程序、定时器中断的中断服务程序等,属于“中断上下文”。其它写在main()方法里的或者在FreeRTOS的任务函数里的代码,它们属于“非中断上下文”

“中断上下文”里调用FromISR版本的API;“非中断上下文”里调用普通版本的API,二者不可混用

直接查阅官方提供的手册 -“The FreeRTOS Reference Manual”来确定是否有ISR版的API

xSemaphoreGiveFromISR()- 给信号

#include "FreeRTOS.h"

#include "semphr.h"

// 作用:给信号,xSemaphoreGive()的FromISR版本

// 参数:xSemaphore - 信号量的句柄

// 参数:pxHigherPriorityTaskWoken - 输出参数,如果给信号后有更高优先级的任务退出阻塞状态,该值会被设置为pdTRUE,否则它将保持不变。

// 返回:pdPASS - 给信号成功,其它 - 已经有信号,给信号失败

BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

区别在于多了一个pxHigherPriorityTaskWoken输出参数;【是否有更高优先级的任务苏醒】

当我们调用xSemaphoreGiveFromISR()给信号后,可能会有任务因为得到信号而退出阻塞。如果退出阻塞的任务比(进入中断前)正在运行的任务优先级高,那么xSemaphoreGiveFromISR()在退出前就会向这个参数写入pdTRUE,否则它什么也不做

FromISR版本的API的使用方法:

void xxx_IRQHandler(void)

{

    // 1. 在开头声明xHigherPriorityTaskWoken变量

   // 2. 一定要将初始值设置为pdFALSE

    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

     // 3. 可以多次调用FromISR版本的API,以&xHigherPriorityTaskWoken作为参数

    xxxFromISR(, &xHigherPriorityTaskWoken);

    xxxFromISR(, &xHigherPriorityTaskWoken);

    ...

    xxxFromISR(, &xHigherPriorityTaskWoken);

    // 4. 调用portYIELD_FROM_ISR()进行可能的任务切换

    // 把xHigherPriorityTaskWoken作为参数

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

}

二进制信号量+DMA

基本原理:1. CPU将搬运数据的工作交给DMA 2. 任务在等待DMA搬运完成期间使用二进制信号量进入阻塞

// 二进制信号量的句柄,静态全局变量

static SemaphoreHandle_t xSemaphoreForDMA;

 void v...Task(void *pvParameters)

{

        xSemaphoreForDMA = xSemaphoreCreateBinary();

        ... // 后续代码

}

HAL_SPI_Transmit_DMA(&hspi1, pData, Size);           //启动DMA

xSemaphoreTake(xSemaphoreForDMA, portMAX_DELAY);         //取信号进入阻塞

xSemaphoreGiveFromISR(xSemaphoreForDMA, &xHigherPriorityTaskWoken);

计数信号量

用法

xSemaphoreCreateCounting() - 创建计数信号量

#include "FreeRTOS.h"

#include "semphr.h"

// @作用:创建计数信号量

// @参数:xMaxCount - 计数信号量所能够达到的最大计数值,超过这个值之后将无法继续刚给信号

// @参数:uxInitialCount - 计数信号量被创建时所持有的计数值

// @返回:创建成功后将返回计数信号量的句柄,如果因为堆内存不足而创建失败则返回NULL

SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

uxSemaphoreGetCount() - 获取计数值

// 作用:获取信号量的当前计数值

// 参数:xSemaphore - 信号量的句柄

// 返回:信号量的计数值,对于二进制信号量只会返回0或1

UBaseType_t uxSemaphoreGetCount(SemaphoreHandle_t xSemaphore);

vSemaphoreDelete() - 删除信号量;xSemaphoreGive() - 给信号;xSemaphoreTake() - 取信号;

xSemaphoreGiveFromISR() - FromISR版本的给信号;[与二进制信号量用法相同]

应用场景

事件计数

每发生一次事件,就“给”一次信号量;处理任务时每处理一个事件,就“取”一次;

SemaphoreHandle_t xEventSemaphore;

xEventSemaphore = xSemaphoreCreateCounting(
    10,  // 最大可累计10个事件
    0    // 初始没有事件
);

中断中记录事件

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    BaseType_t taskWoken = pdFALSE;

    xSemaphoreGiveFromISR(xEventSemaphore, &taskWoken);
    portYIELD_FROM_ISR(taskWoken);
}

任务中逐个处理

for (;;)
{
    xSemaphoreTake(xEventSemaphore, portMAX_DELAY);

    // 每取到一次,处理一个事件
    ProcessEvent();
}
守护资源

假设系统有3个相同资源,例如3个缓冲区

xResourceSemaphore = xSemaphoreCreateCounting(
    3,  // 一共有3个资源
    3   // 开始时3个资源都可用
);

使用资源前先取

xSemaphoreTake(xResourceSemaphore, portMAX_DELAY);
UseResource();
xSemaphoreGive(xResourceSemaphore);

池化

事先在资源池当中创建一些资源备用,需要的时候就从中取出,用完了再还回去,从而省掉因频繁创建和销毁资源产生的开销

任务使用流程:从池中申请一个缓冲区--->使用缓冲区--->使用完成后归还到池中

常见的池包括:

  • 内存池:重复使用固定内存块。
  • 缓冲区池:存放串口、网络或ADC数据。
  • 线程池:提前创建若干线程处理工作。
  • 连接池:重复使用数据库或网络连接。
  • 对象池:重复使用结构体对象。

队列

“队列”不仅可以用于同步,还能同时传输一些数据

基本操作

xQueueCreate() - 创建队列

#include "FreeRTOS.h"

#include "queue.h"

// @作用:创建队列

// @参数:uxQueueLength - 队列的长度【能够容纳项目的最大数量】

// @参数:uxItemSize - 队列当中每一个元素的大小,单位是字节

// @返回:队列创建成功时返回新创建的队列的句柄,失败时返回NULL

QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);

队列的长度和项目的尺寸在创建之初就已经确定好;创建队列需要在堆上分配一些内存。这些内存分成两个部分:一部分用来存储Queue_t结构体,Queue_t结构体存储了队列运行过程中的相关信息,约76个字节;另一部分用来存储实际的数据

uxItemSize - 每个项目多少个字节

vQueueDelete() - 删除队列

// @作用:删除队列

// @参数:pxQueueToDelete - 要删除的队列的句柄

void vQueueDelete(QueueHandle_t pxQueueToDelete);

xQueueSend() - 发送数据

// @作用:向队列发送数据

// @参数:xQueue - 要操作的队列的句柄

// @参数:pvItemToQueue - 指向要发送的数据的指针

// @参数:xTicksToWait - 超时时间,即队列满时等待的最长时间,portMAX_DELAY表示无限长。如果超时后队列仍无空位则发送失败。

// @返回:pdPASS - 发送成功;pdFAIL - 发送失败(超时)

BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);

FreeRTOS里的队列采用“按值拷贝”的方式。就是在调用xQueueSend()发送数据的时候,pvItemToQueue指向的变量会被原封不动地拷贝到队列当中

xQueueSendFromISR() - FromISR版本

// @作用:从中断上下文里向队列发送数据,是xQueueSend()的FromISR版本

// @参数:pvItemToQueue - 指向要发送的数据的指针

// @参数:pxHigherPriorityTaskWoken - 输出参数,如果调用该API能够使更高优先级的任务则该参数会输出pdTRUE

// @返回:如果发送数据成功则返回pdPASS,如果队列满则返回pdFAIL

BaseType_t xQueueSendFromISR(        QueueHandle_t xQueue,

                                                                 const void *pvItemToQueue,

                                                                 BaseType_t *pxHigherPriorityTaskWoken);

FromISR版本的API在中断上下文被调用,它不属于任何任务,也就无法阻塞,因此设置超时时间没有意义。调用xQueueSendFromISR()后它会立即返回,如果队列有空位则发送成功,返回pdPASS;如果没有空位也不会等待,立即返回pdFAIL

xQueueReceive() - 接收数据

// @作用:从队列里接收数据

// @参数:xQueue - 队列的句柄

// @参数:pvBuffer - 接收缓冲区,用于存放从队列里读出的数据

// @参数:xTicksToWait - 超时时间,当队列为空的时候调用xQueueReceive()会让当前任务进入阻塞,xTicksToWait用来设置最长的阻塞时间

// @返回:读取成功时返回pdPASS,如果队列空且读取超时则返回pdFAIL

BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);

xQueueReceiveFromISR() - xQueueReceive()的FromISR版本

// @作用:xQueueReceive()的FromISR版本

// @参数:xQueue - 队列的句柄

// @参数:pvBuffer - 接收缓冲区,用于存放从队列里边取出的数据

// @参数:pxHigherPriorityTaskWoken - 输出参数,如果调用该API能够使更高优先级的任务退出阻塞,则该参数会输出pdTRUE

// @返回:如果接收数据成功则返回pdPASS,如果队列空则返回pdFAIL

BaseType_t xQueueReceiveFromISR(     QueueHandle_t xQueue,

                                                                       void *pvBuffer,

                                                                       BaseType_t *pxHigherPriorityTaskWoken);

因为是从中断上下文调用的,不能进入阻塞,所以xQueueReceiveFromISR()也没有xTicksToWait参数。队列为空的情况下调用xQueueReceiveFromISR()接收数据时,该方法会立即返回pdFAIL

邮箱

是队列的一种特殊用法;创建队列的时候将uxQueueLength设置为1就相当于创建了一个邮箱;

xQueueOverwrite() - 覆写

// @作用:向队列发送数据,如果已有数据则会覆写

// @参数:xQueue - 队列的句柄

// @参数:pvItemToQueue - 指向要发送的数据的指针

// @返回:覆盖操作不会失败,因此总是返回pdPASS

// @注意!这个API仅能用在长度等于1的队列里

BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);

 xQueueOverwrite()跟xQueueSend()一样,用来向队列发送数据,区别在于如果队列里边已经有数据了,那么它会对原有的数据进行覆盖,这个xQueueOverwrite()仅能用在长度等于1的队列里,换句话说它仅能用于邮箱

这个API有一个返回值,是BaseType_t类型的,xQueueOverwrite()中总是能成功,因此它的返回值总是pdPASS

xQueuePeek() - 窥视

// @作用:从队列当中读一个项目,但不把它取出

// @参数:xQueue - 队列的句柄

// @参数:pvBuffer - 缓冲区,用来接收读到的数据

// @参数:xTicksToWait - 超时时间,如果队列为空则会等待,该参数用于设置最长等待时间

// @返回:读取成功时返回 pdPASS,超时返回pdFAIL

BaseType_t xQueuePeek(  QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);

 xQueuePeek()用来窥探队列里的第一个项目。它与xQueueReceive()相似,但读取时并不会把队列里的项目取出。因此,当你连续执行xQueuePeek()时(中间没有其它操作,比如xQueueSend()或者xQueueReceive()等)实际上读到的是同一个

xQueuePeek()也有超时时间。因为某些情况下队列可能是空的,比如刚创建队列的时候,或者调用了xQueueReceive()把数据取出之后。当队列为空的情况下调用xQueuePeek()会进入阻塞等待,阻塞的最长时间由超时时间xTicksToWait来指定

使用场景

邮箱最典型的应用场景是“更新传感器的数据”

显示器(邮箱)

显示器的左边是传感器任务,传感器任务每隔一段时间会读取传感器的数值,然后把结果通过xQueueOverwrite()发送给显示器。显示器就能实时显示最新的传感器数据了。如果其它任务想要取得传感器的当前数据,它只要“瞥一眼”显示器就好了,也就是调用xQueuePeek()窥探邮箱的数据

软件定时器

原理

软件定时器有两个主要功能:

定时执行代码

周期性地执行代码

执行过程

如果想“启动”一个定时器,只需调用xTimerStart()函数。但xTimerStart()实际上并不会直接执行启动操作,而是通过发消息的方式来实现的,实际的动作交由“定时器任务”来执行 中间的“定时器命令队列”负责缓存命令。发送方通过xQueueSend()向队列发送消息,接收方通过xQueueReceive()接收消息并进行处理。

定时器任务,由FreeRTOS自动创建的任务,我们对定时器执行的基本操作、以及定时器到期后要执行的代码,都是在这个“定时器任务”的内部被执行的,这一点非常重要。

配置定时器

FreeRTOSConfig.h

/*

将configUSE_TIMERS设置为1来使能软件定时器功能,设置为0则会删除软件软件定时器功能

如果使能了软件定时器功能,你需要将timers.c文件添加到你的工程当中

默认值是0

  */

#define configUSE_TIMERS                1         // 1 - 使能软件定时器功能 0 - 关闭软件定时器功能

/*

configTIMER_QUEUE_LENGTH 用来设置定时器命令队列的长度

*/

#define configTIMER_QUEUE_LENGTH        10

/*

configTIMER_TASK_PRIORITY用来设置定时器任务的优先级.  定时器任务是标准的FreeRTOS任务. 所以它的优先级和其它任务的优先级含义是一样的。

  */

#define configTIMER_TASK_PRIORITY       ( configMAX_PRIORITIES - 1 )  // 默认是最高优先级

/*

configTIMER_TASK_STACK_DEPTH 用来设置分配给定时器任务的栈的大小(单位是word,不是byte,1 word = 4 bytes).

 */

#define configTIMER_TASK_STACK_DEPTH    configMINIMAL_STACK_SIZE      // 默认使用最小栈深度(128

创建定时器

xTimerCreate() - 创建软件定时器

#include "FreeRTOS.h"

#include "timers.h"

// @作用:创建一个软件定时器

// @返回:成功,则返回新创建的软件定时器定时器的句柄;失败,则返回NULL

TimerHandle_t xTimerCreate(

const char *pcTimerName, // 定时器的绰号,供调试工具使用,只给人看、对代码运行无影响

const TickType_t xTimerPeriod, // 周期,间隔多久才会执行代码,单位:Ticks

const UBaseType_t uxAutoReload, // 是否自动重装,即:只做一次还是重复执行。

 // pdTRUE - 重复,pdFALSE - 不重复

void * const pvTimerID, // 定时器的ID,类似任务参数,当多个定时器共用回调函数时使用。不用时填NULL

TimerCallbackFunction_t pxCallbackFunction); // 回调函数,定时器到期后要执行的代码

 pxCallbackFunction是软件定时器到期之后需要执行的代码,软件定时器一旦到期,就会由定时器任务调用这段代码。这个回调函数的类型是TimerCallbackFunction_t,其定义如下:

typedef void (*TimerCallbackFunction_t)( TimerHandle_t xTimer);

创建软件定时器的内存开销

创建一个软件定时器消耗40个字节;

内存开销总共分3部分,创建定时器任务的开销、创建定时器命令队列的开销和创建定时器本身的开销

例:

五种基本操作

xTimerStart() - 启动

#include "FreeRTOS.h"

#include "timer.h"

// @作用:启动软件定时器

// @参数:xTimer - 软件定时器的句柄

// @参数:xTicksToWait - 定时器命令队列满的时候最长等待的时间

// @返回:pdPASS - 成功;pdFAIL - 因超时失败

BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait);

如果队列满了就会阻塞,xTicksToWait表示最长等多久

xTimerStop() - 停止

// @作用:停止定时器

// @参数:xTimer - 定时器的句柄

// @参数:xTicksToWait - 当定时器命令队列满之后最长的阻塞时间

// @返回:pdPASS - 成功,pdFAIL - 因超时而失败

BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xTicksToWait);

xTimerChangePeriod() - 改变周期

// @作用:改变软件定时器的周期(即到期时间)

// @参数:xTimer - 定时器的句柄

// @参数:xNewPeriod - 新的周期值

// @参数:xTicksToWait - 定时器命令队列满之后,最长的阻塞时间

// @返回:成功 - pdPASS;因超时而失败 - pdFAIL

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, TickType_t xNewPeriod, TickType_t xTicksToWait);

xTimerReset() - 复位

// @作用:对定时器进行复位操作,让定时器重新计算到期时间

// @参数:xTimer - 定时器的句柄

// @参数:xTicksToWait - 定时器命令队列满之后,最长的阻塞时间

// @返回:成功 - pdPASS;因超时而失败 - pdFAIL

BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait);

xTimerDelete() - 删除定时器

// @作用:删除定时器,释放资源

// @参数:xTimer - 定时器的句柄

// @参数:xTicksToWait - 队列满后的最长阻塞时间

// @返回:成功 - pdPASS,因超时而失败 - pdFAIL

BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait);

事件组

概念

事件组,相当于一组二进制信号量的集合,不仅能够同步,还能根据信号的来源做不同的动作

底层原理

EventBits_t数据类型

高8位是“控制字节”,用来存储事件组运行过程中的关键信息。而剩余的比特位全部都是事件比特

EventBits_t

在32位单片机里一般是uint32_t

在FreeRTOS当中,EventBits_t就是一个无符号整数。比如uint8_t - 无符号8位整数,uint16_t - 无符号16位整数,uint32_t - 无符号32位整数,uint64_t - 无符号64位整数

当EventBits_t是无符号8位整数(uint8_t)时,它的全部8个比特位均被作为事件组的“控制字节”,没有多余的比特位作为事件比特,这是一种不可用的状态。

对于uint16_t、uint32_t和uint64_t来说,事件比特的个数依次是8、24和56。EventBits_t的位宽越宽,所含的事件比特越多

配置

event_groups.h

 typedef TickType_t EventBits_t;

// 该配置项用来设置TickType_t的位宽

// TICK_TYPE_WIDTH_16_BITS - TickType_t 等于 uint16_t

// TICK_TYPE_WIDTH_32_BITS - TickType_t 等于 uint32_t

// TICK_TYPE_WIDTH_64_BITS - TickType_t 等于 uint64_t

#define configTICK_TYPE_WIDTH_IN_BITS  TICK_TYPE_WIDTH_32_BITS

七种基本操作

创建和删除

/*

configUSE_EVENT_GROUPS = 1,在内核当中使能事件组功能,

configUSE_EVENT_GROUPS = 0,将事件组功能从内核当中删除掉。

当使能了事件组功能之后,必须把 event_groups.c 包含到你的工程当中。

如果不定义此配置项,它的默认值是1。

  */

#define configUSE_EVENT_GROUPS    0

#include "FreeRTOS.h"

#include "event_groups.h"

// @作用:创建一个事件组

// @返回:事件组的句柄

EventGroupHandle_t xEventGroupCreate(void);

// @作用:删除一个事件组

// @参数:xEventGroup - 事件组的句柄

void vEventGroupDelete(EventGroupHandle_t xEventGroup);

一般情况下(STM32单片机、配置文件取默认值)一个事件组需要消耗28个字节的内存,这28个字节全部用来存储EventGroup_t结构体,如下图:

读取、设置和清除

读取,就是把上边的EventBits_t里的数字读出来;设置就是向EventBits_t里的某些事件比特写1;清除就是向EventBits_t某些比特位写0

#include "FreeRTOS.h"

#include "event_groups.h"

// @作用:读取事件组的值

// @参数:xEventGroup - 事件组的句柄

// @返回:事件组的当前值

EventBits_t xEventGroupGetBits( EventGroupHandle_t xEventGroup );

// @作用:设置事件比特

// @参数:xEventGroup - 事件组的句柄

// @参数:uxBitsToSet - 掩码。要设置哪个事件比特就向对应的位置写1。

// @返回:执行设置操作之后事件组的值

EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet);

// @作用:清除事件比特

// @参数:xEventGroup - 事件组的句柄

// @参数:uxBitsToClear - 掩码。要清除哪个事件比特就向对应的位置写1。

// @返回:执行清除操作之前事件组的值

EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear);

等待

#include "FreeRTOS.h"

#include "event_groups.h"

// @作用:等待事件组的比特位置位

// @返回:执行清除操作之前的值(如果xClearOnExit == pdTRUE的话会执行清除操作)

EventBits_t xEventGroupWaitBits ( const EventGroupHandle_t xEventGroup,  // 事件组句柄

                                                     const EventBits_t uxBitsToWaitFor,             // 掩码

                                            const BaseType_t xClearOnExit,  // 等待操作成功后是否执行清除操作

                                                        // pdFALSE - 不执行清除操作  pdTRUE - 等待成功后执行清除

                                                 const BaseType_t xWaitForAllBits,   // 是否等到所有比特位

                                                 // pdFALSE - 只需等待任一比特位  pdTRUE - 需要等待所有比特位

                                                               TickType_t xTicksToWait );                             // 超时时间

用法

事件组可以把一个整数的每一位当成一个独立事件标志

等待函数的参数顺序是:

xEventGroupWaitBits(事件组, 掩码, 成功后是否清零,是否等待全部位, 超时时间);

例:

#define ADC_DONE_BIT   (1U << 0)
#define KEY_DOWN_BIT   (1U << 1)
#define UART_RX_BIT    (1U << 2)

用法一:用作二进制信号量

只使用一个比特位:

  • 置位表示“事件已经发生”。
  • 等待成功后清零表示“事件被取走”。

#define ADC_DONE_BIT (1U << 0)

// ADC完成后设置事件
xEventGroupSetBits(xEventGroup, ADC_DONE_BIT);

// 任务等待ADC完成
xEventGroupWaitBits(
    xEventGroup,
    ADC_DONE_BIT,
    pdTRUE,          // 等到后清零
    pdFALSE,         // 只有一个位,TRUE/FALSE都一样
    portMAX_DELAY
);

// 处理ADC数据
ProcessADCData();

不完全等同于二进制信号量:

  • 同一个位重复设置不会累计
  • 多个任务等待同一个位时,一次设置可能唤醒多个任务
  • 事件组没有互斥锁的优先级继承功能

用法二:等待任意事件并自动清除

任意事件发生就处理并清除;用途是一个任务同时处理多个事件

配置:

xWaitForAllBits = pdFALSE           xClearOnExit = pdTRUE

EventBits_t bits;

bits = xEventGroupWaitBits(
    xEventGroup,
    KEY_DOWN_BIT | UART_RX_BIT | ADC_DONE_BIT,
    pdTRUE,          // 处理完后清除已发生的事件
    pdFALSE,         // 任意一个事件发生即可
    portMAX_DELAY
);

if (bits & KEY_DOWN_BIT)
{
    HandleKey();
}

if (bits & UART_RX_BIT)
{
    HandleUART();
}

if (bits & ADC_DONE_BIT)
{
    HandleADC();
}

用法三:等待全部事件且不自动清除

全部条件满足才继续,并保留状态

配置:xWaitForAllBits = pdTRUE              xClearOnExit = pdFALSE

#define SYSTEM_READY_BITS \
    (SENSOR_READY_BIT | LCD_READY_BIT | CONFIG_READY_BIT)

xEventGroupSetBits(xEventGroup, SENSOR_READY_BIT);
xEventGroupSetBits(xEventGroup, LCD_READY_BIT);
xEventGroupSetBits(xEventGroup, CONFIG_READY_BIT);

xEventGroupWaitBits(
    xEventGroup,
    SYSTEM_READY_BITS,
    pdFALSE,         // 不清零,保持“系统已准备好”状态
    pdTRUE,          // 必须全部置位
    portMAX_DELAY
);

StartSystem();

访问共享资源

竞态条件

什么是竞态条件:多个任务同时访问共享资源,执行结果随任务执行顺序变化

“竞态条件”的源头来自共享资源,当我们使用单片机编程的时候,这种共享资源主要有三类:共享的变量、不可重入的函数以及共享的外设

消除竞态条件

暂停调度器

基本原理

把FreeRTOS的调度器关掉,从而禁止任何形式的任务切换

vTaskSuspendAll()和xTaskResumeAll()

vTaskSuspendAll()用来暂停调度器,xTaskResumeAll()用来恢复调度器

#include "FreeRTOS.h"

#include "task.h"

// @作用:暂停调度器,期间禁止任何任务切换

void vTaskSuspendAll(void);

.....................[任务代码]

// @作用:恢复调度器

// @返回:恢复调度器期间是否执行了被挂起的切换,pdTRUE - 是,pdFALSE - 否。

BaseType_t xTaskResumeAll(void);

暂停调度器的副作用

暂停调度器适合保护极短的代码,长时间暂停调度器会严重破坏系统的实时性

暂停调度器只能保护共享资源不被别的任务打断,一旦涉及中断就无能为力,这是它的最大局限性

临界区

基本原理

FreeRTOSConfig.h

#define configMAX_SYSCALL_INTERRUPT_PRIORITY       3<<4  // 能够调用FreeRTOS API的最高优先级

#define configKERNEL_INTERRUPT_PRIORITY          15<<4 // SYSTICK中断所在的优先级

互斥锁

互斥锁的使用分3步:

第一步,获取锁,你必须先获取到锁才能去访问被保护的资源。

第二步,访问被保护的资源。

第三步,归还锁。如果一个任务“攥着”互斥锁不放,其它任务就没办法访问那个被保护的资源了。

互斥锁的API

#include "FreeRTOS.h"

#include "semaphore.h"

// @作用:创建互斥锁

// @返回:创建成功后返回新建的互斥锁的句柄,如果失败则返回NULL

SemaphoreHandle_t xSemaphoreCreateMutex(void);

// @作用:删除互斥锁

// @参数:xSemaphore - 被删除的互斥锁的句柄

void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

// @作用:获取锁(其实就是取信号)

// @参数:xSemaphore - 互斥锁的句柄

// @参数:xTicksToWait - 未获取到锁时的最长等待时间

// @返回:pdPASS - 成功获取到锁;pdFAIL - 未获取到锁

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

// @作用:释放锁(其实就是给信号)

// @参数:xSemaphore - 互斥锁的句柄

// @返回:pdPASS - 给信号成功,pdFAIL - 当前任务并未持有锁,因此释放锁失败

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore);

互斥锁的适用场景

保护长代码的话推荐使用互斥锁,因为它只会让竞争使用资源的任务进入阻塞,而不会做出类似关闭调度器或者屏蔽中断这种极端的动作。在持有锁期间,高优先级的任务和中断能够正常抢占,实时性不受影响;

互斥锁也有其自身的短板:它是一种内核对象,创建它要消耗相当的系统资源,并且执行速度稍慢于暂停调度器,如果要保护的代码非常短的话,还是推荐使用暂停调度器或者临界区

守门员任务

之前学习的软件定时器,其底层也是通过守门员任务的方式来实现的

守门员任务的优缺点

守门员任务的优点是“灵活”。很难穷尽守门员任务的使用场景

缺点是非常吃资源,它会消耗大量的堆内存;为了使用守门员任务,我们除了创建守门员任务本身之外还要创建一个队列,它的开销比互斥锁还要大得多

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐