FreeRTOS使用

FreeRTOS使用
THEDI下面为最基础的用法,其余用法请:看文档学习
FreeRTOS介绍
基本概念
实时操作系统 (RTOS) 是一种体积小巧、确定性强的计算机操作系统。RTOS 通常用于需要在严格时间限制内对外部事件做出反应的嵌入式系统。
RTOS 通常比通用操作系统体积更小、重量更轻,因此 RTOS 非常适用于 内存
、计算
和功率
受限的设备
裸机开发与操作系统
裸机开发:一般由一个main函数中的while死循环和各种中断服务程序组成,平时CPU执行while循环中的代码,出现其他事件时,跳转到中断服务程序进行处理,没有多任务、线程的概念。
操作系统(FreeRTOS等):引入操作系统后,程序执行时可以把一个应用程序分割为多个任务,每个任务完成一部分工作,并且每个任务都可以写成死循环。操作系统根据任务的优先级,通过调度器在不同任务见快速切换, 使所有任务看起来像是同时在执行。这就是我们的分时复用
内核
内核是操作系统中最底层、最基础的部分
它负责管理硬件资源(比如CPU、内存、外设)和提供系统服务(比如多任务调度、内存分配、进程通信等)。所有应用程序都需要通过内核才能访问硬件。
内核就像一台电脑的大脑,负责调度和管理一切,只有通过内核,应用程序才能安全、有效地使用硬件资源。
内核主要作用
:
- 管理和调度任务/进程
- 管理内存
- 管理硬件设备
- 提供系统调用接口(让用户程序和硬件之间通信)
常见内核:
- Windows系统的“NT内核”
- Linux系统的“Linux kernel”
- 嵌入式RTOS(如FreeRTOS、uC/OS)的“内核”代码
FreeRTOS时基
FreeRTOS默认使用SysTick
作为其系统节拍定时器,用于任务调度、时间片等。
我们可以在vTaskStartScheduler()
中看到,调用了xPortStartScheduler()
其中又会调用vPortSetupTimerInterrupt()
,配置SysTick为RTOS的系统节拍定时器,生成对应频率的中断信号
这里对SysTick的计数值、重载值、频率等进行寄存器配置
对应的SysTick中断服务程序:
任务介绍
官方全介绍:任务和协程 - FreeRTOS™
任务状态
嵌入式操作系统中的任务一共有四种状态,任务一旦创建后,将在这四种状态中切换:
运行态(Running)
:当前正在执行的任务处于运行状态,此时该任务拥有CPU的使用权。任何时刻下只能有一个任务处于运行态。就绪态(Ready)
:指能够运行(没有阻塞或挂起),但是当前没有运行的任务,往往是因为同优先级任务或者高优先级任务正在运行。阻塞态(Blocked)
:任务在执行过程中需要等待某一个事件的发生(信号量、消息队列、事件标识组)或者调用延时函数,此时任务将处于阻塞态。当任务等待的事件发生或者延时时间到后,才会切换到就绪态。挂起态(Suspended)
:类似暂停,通过调用函数vTaskSuspend()
对指定任务进行挂起,挂起后该任务不会被执行,只有调用函数xTaskResume()
才可以将任务从挂起态恢复。
只有就绪态
可变成运行态
,其他任务状态想要运行,必须先转变为就绪态
任务优先级
在创建任务时可以指定该任务的优先级编号,每个任务被分配从0~configMAX_PRIORITIES-1
的优先级编号,
其中configMAX_PRIORITIES在FreeRTOSConfig.h中配置
比如这里我们就定义为有56个的优先级
对应优先级编号枚举定义:
FreeRTOS调度器可确保在就绪或运行
状态下的任务始终比同样处于就绪状态下的更低优先级任务
先获得处理器 (CPU)时间。换句话来说,处于运行状态的任务始终是能够运行的最高优先级任务。
处于相同优先级的任务数量不限。如果configUSE_TIME_SLICING(时间片轮转配置)未经定义,或者如果 configUSE_TIME_SLICING设置为1,则具有相同优先级的若干就绪状态任务将通过时间切片轮询
调度方案共享可用的处理时间。
任务调度
什么是任务调度器
任务调度器是实时操作系统(RTOS)的一个关键组件,它负责决定在多个可运行任务中哪一个将获得CPU时间得以执行。它基于任务的优先级
和状态
来做出这些决定。
在一个RTOS中,可能会有多个任务同时运行,但是在任意时刻,CPU只能执行一个任务
。任务调度器的主要目标是按照系统的需求合理分配CPU时间
。
调度器就是使用相关的调度算法来决定当前需要执行的那个任务
FreeRTOS中开启任务调度的函数是vTaskStartScheduler()
而CubeMX中进行更进一步的封装为osKernelStart()
任务调度方式
FreeRTOS是一个实时操作系统,其核心任务调度器支持以下调度方式:
1.抢占式调度
:抢占式调度器是一种优先级基础的调度器,高优先级抢占低优先级任务,只要有更高优先级的任务变为就绪,系统会立即中断当前低优先级任务,切换到高优先级任务。
在
FreeRTOSConfig.h
由configUSE_PREEMPTION
控制,configUSE_PREEMPTION = 1
代表启动抢占式调度,默认开启
2.时间片轮转度
:只对相同优先级
的就绪任务有效。它将CPU时间划分成小的时间片段,相同优先级的每个任务在一个时间片段内运行轮转切换。
在
FreeRTOS.h
由configUSE_TIME_SLICING
控制,configUSE_TIME_SLICING = 1
代表启动时间片轮转,默认是处于开启状态的
3.协作式调度
:协作式调度器依赖于任务自行释放CPU,没有明确的任务优先级。任务必须自愿放弃CPU控制权,以便其他任务能够运行。
在
FreeRTOSConfig.h
由configUSE_PREEMPTION
控制,configUSE_PREEMPTION = 0
代表启动抢占式调度,默认关闭
FreeRTOS默认启动抢占式调度
和时间片轮转调度
,对我们同优先级
和不同优先级
任务的执行
抢占式调度
优先级:Task1<Task2<Task3 图中根据时间依次被高优先级任务抢占执行
高优先级任务优先执行,且高优先级任务不停止(阻塞)的话,低优先级任务永远无法执行
被抢占的任务将进入就绪态
优点 | 适用于实时要求严格的系统,可以确保高优先级任务及时响应。 |
---|---|
缺点 | 上下文切换的开销较大,可能会影响系统的性能。 |
时间片轮转调度
首先:时间片轮转调度时对于相同优先级
任务的而言,不同优先级任务不存在时间片轮转
CPU的执行时间被划分成固定长度
的时间片段。每个时间片段可以是几毫秒或者更短的时间,具体取决于系统的配置(滴答定时器中断等)。
图中同样的优先级任务: Task1 = Task2 = Task3
同等优先级任务,轮流执行,时间片流转
一个时间片大小,通常取决于滴答定时器中断周期(如1ms)
注意没有用完的时间片不会再使用,如图中的Task3如果只执行了半个时间片就被阻塞了,那么会直接让出时间片,下次任务Task3得到执行,还是按照一个时间片的时钟节拍运行。
协作式调度
协作式调度器依赖于任务自行释放CPU,没有明确的任务优先级。
任务之间不会主动抢占CPU
,任务必须自愿放弃CPU控制权,以便其他任务能够运行。
用的不多,用的时候再查吧
任务创建与删除
什么是任务
任务可以理解为一个线程/进程,每创建一个任务,就会在内存中开辟一个空间
任务通常都含有while(1)死循环
API
任务创建于删除相关函数有如下三个:
函数名称 | 函数作用 |
---|---|
xTaskCreate() |
动态方式创建任务 |
xTaskCreateStatic() |
静态方式创建任务 |
vTaskDelete |
删除任务 |
任务动态创建与静态创建的区别:
动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递
通常使用动态方式创建任务
- xTaskCreate函数原型
pvTaskCode
:指向任务函数的指针,任务必须实现永不返回pcName
:任务的名字,主要用于调试,默认最大长度为16uxStackDepth
:堆栈大小pvParameters
:传递给任务的参数,一般为NULLuxPriority
: 任务的优先级范围 0 ~ configMAX_PRIORITIES-1pxCreatedTask
: 用于返回已创建任务的句柄,可以被引用
返回值 | 描述 |
---|---|
pdPASS |
任务创建成功 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY |
任务创建失败 |
官方用法示例
- 任务创建
- vTaskDelete函数原型
1 | /*1.删除指定xHandle句柄对应的任务*/ |
将删除任务的句柄传入即可删除任务
若传入参数为NULL
,代表删除任务自身(当前运行的任务)
任务控制
延时函数分类
相对延时
:vTaskDelay(),从每一次调用vTaskDelay()后才开始计时
绝对延时
:vTaskDelayUntil(),从上一次调用vTaskDelayUntil()后一瞬间就开始计时了
vTaskDelay和HAL_Delay
vTaskDelay
:是FreeRTOS中的延时函数,作用是让任务阻塞,任务阻塞后,RTOS系统调用其他处于就绪态的优先级最高的任务来执行。
HAL_Delay
:裸机开发时使用的HAL库的延时函数,使用while循环一直不停的调用获取系统时间的函数,直到满足等待时间才会结束,故其占用了全部的CPU时间,其他任何事情都不能做
vTaskDelay使用
1 | void vTaskDelay( const TickType_t xTicksToDelay ); |
示例
:
1 | static void task1(void *parameters) |
因为对于1滴答的时间可能定义不同。所以我们通常会使用
pdMS_TO_TICKS()
来固定延时时间,将ms转化为对应的滴答数该函数自动将输入的ms数转化为对应1000ms的滴答数
任务间传递信息
消息队列介绍
队列又称消息队列,是一种用于任务间通信的数据结构,队列可以在任务与任务间
、中断和任务间
传递信息。
为什么不使用全局变量替代队列呢?
如果使用一个
全局变量a
,任务1
修改了变量a,等待任务3
处理,但任务3
处理速度很慢,在处理数据的过程中,任务2
可能对变量a进行改变
,导致任务3得到的不是任务1想要传递给任务3的数据.在这种情况下,就可以使用队列,将数据依次放在流水线上,一个一个按顺序处理
队列项目
:队列中的每一个数据
队列长度
:队列能够存储队列项目的最大数量
创建队列时,需要指定队列长度及队列项目的大小
消息队列特点
1.队列入队出队的方式
采用FIFO(先进先出)
的数据存储缓冲机制,即先入队的数据会先从队列中读取。也可以配置为后进先出(LIFO)方式,但是用得比较少。
2.数据传递方式
采用实际值传递
,即将数据拷贝
到队列中进行传递,也可以传递指针,传递较大数据的时候采用指针传递
在实际应用时,由于消息队列采用数据复制的方式传输数据,而不是传输存放数据的地址。如果任务间传输的数据量较大时,使用消息队列的效率会比较低。
这时,可以考虑使用全局变量来实现任务问的通信,只是要注意全局变量的互斥访问(利用互斥量实现)。
所以:通常我们会在队列中传递以下几类消息:简单原始类型(如
uint32_t
、enum
),小型结构体(几个字段的 POD 类型),指针或句柄(指向更大缓冲区或资源)以及带类型字段+联合体的复合消息。最佳实践是尽量保持每条消息尺寸较小,以减少内存和阻塞时间;如果需要传输大块数据,则在消息中仅传递指针或索引,由接收端再去处理实际数据。
3.多任务访问
队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息
4.出队入队阻塞
当任务向一个队列发送消息时,可以指定一个阻塞事件,假设当时队列已满无法入队。
阻塞时间可以设置为:
- 0:直接返回不会等待
- 0 ~ port_MAX_DELAY: 等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待
- por_MAX_DELAY: 死等,一直等到可以入队为止。出队阻塞与入队阻塞类似
相关API
参考文档:
创建消息队列
1 | /*1.队列的句柄*/ |
参数:
- uxQueueLength: 队列一次可存储的最大项目数。
- uxItemSize: 存储队列中每个项目所需的大小(以字节为单位)。项目通过复制而非引用的方式入队,因此该参数值是每个入队项目将复制的 字节数。队列中的每个项目必须具有相同的大小。
返回值:
创建成功,返回队列句柄,创建失败返回NULL。
示例:
1 |
|
写队列
函数 | 描述 |
---|---|
xQueueSend() |
往队列的尾部写入消息 |
xQueueSendToBack() | 同xQueueSend() |
xQueueSendToFront() | 往队列头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只适用于队列长度为1的情况) |
xQueueSendFromISR() |
在中断中往队列尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只适用于队列长度为1的情况) |
1 |
|
参数:
xQueue: 任务队列句柄
pvitemToQueue:带放入队列的数据的指针。队列能够存储的项目的大小在创建队列时即已定义,因此pvItemToQueue中的这些字节将复制到队列存储区中。
xTicksToWait(普通):阻塞超时时间(取值:0~portMAX_DELAY)。如果队列已满且xTicksToWait设置为0,则调用将立即返回,也可以使用
portMAX_DELAY
一直阻塞等待队列空出。pxHigherPriorityTaskWoken(中断):如果发送到队列会导致任务解除阻塞,并且解除阻塞的任务的优先级高于当前正在运行的任务,则 xQueueSendFromISR()会*pxHigherPriorityTaskWoken设置为pdTRUE。如果xQueueSendFromISR()将此值设置为pdTRUE,则应在中断退出前请求上下文切换。从FreeRTOS V7.3.0开始,pxHigherPriorityTaskWoken为可选参数,可设置为 NULL
返回值:
如果成功写入,返回 pdTRUE,否则返回 errQUEUE_FULL。
读队列
函数 | 描述 |
---|---|
xQueueReceive() |
从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息,但是不删除消息 |
xQueueReceiveFromISR() |
在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息, |
1 |
|
参数:
xQueue: 任务队列句柄
pvitemToQueue:指向要将所接收项目复制到缓冲区的指针。
xTicksToWait(普通):阻塞超时时间(取值:0~portMAX_DELAY)。如果队列已空且xTicksToWait设置为0,则调用将立即返回。也可以使用
portMAX_DELAY
一直阻塞等待,直到队列有数据。pxHigherPriorityTaskWoken(中断):如果在调用时队列为空,则任务应阻塞等待项目接收的最长时间。如果队列为空,将 xTicksToWait设置为 0 将导致函数立即返回,。时间是以滴答周期为单位定义的。如果 INCLUDE_vTaskSuspend 设置为 1,则将阻塞时间指定为 portMAX_DELAY会导致任务无限期地阻塞(没有超时限制)。
返回值:
如果成功写入,返回 pdTRUE,否则返回 errQUEUE_FULL。
为什么要区分普通使用和在中断使用专用的消息队列发送和接收?
- 非中断形式的发送,可能会被阻塞,而这对于中断服务中是万万不能有的,而专用的xQueueSendFromISR绝不会阻塞,消息队列满了会直接返回。
- 对于接收专用xQueueReceiveFromISR也是一样的不会阻塞,消息队列空了会直接返回,但是我们一般不使用该函数,因为中断服务函数中主要负责发送(通知任务),很少“去取数据”。但有时中断也可能从某种共享队列取命令执行。
官方用法示例
- 创建消息队列
示例:
一个任务接收另一个任务/中断发送的消息,对应进行处理
在消息传递的过程中我们常常使用
枚举类型
,enum传递消息比较容易处理
,也很方便,我们应避免使用较长较大数据
,如传输:字符串等等
以下演示中断发送消息队列:
1 | typedef enum{ |
任务同步
信号量
信号量(Semaphore)
, 是在多环境任务下使用的一种机制,是可以用来保证两个或多个关键代码段不被并发调用。
信号量一般代表可用资源的数量,可以分为二值信号量
和计数信号量
。
信号量的底层是用队列实现,所以信号量也是队列的一种
二值信号量
二值信号量介绍
二值信号量其实就是一个**长度为1,大小为0的队列,**取值只能为0或1
,与互斥锁(互斥量)类似,主要用于任务同步
和互斥访问
相关API
函数名 | 描述 |
---|---|
xSemaphoreCreateBinary() |
创建一个二值信号量 |
xSemaphoreCreateBinaryStatic() | 静态方式创建二值信号量 |
xSemaphoreGive() |
释放(给予)信号量,使其变为可用 |
xSemaphoreTake() |
获取(消耗)信号量,等待信号量可用 |
xSemaphoreGiveFromISR() | 在中断服务程序中释放信号量 |
xSemaphoreTakeFromISR() | 在中断服务程序中获取信号量 |
vSemaphoreDelete() | 删除信号量,释放相关资源 |
创建二值信号量
1 | SemaphoreHandle_t xSemaphoreCreateBinary( void ); |
参数:
无
返回值:
成功返回二值信号量的句柄,失败返回NULL。
注意:
使用该函数创建的二值信号量默认值为0,需要先Give才可以Take
释放二值信号量
1 | xSemaphoreGive( SemaphoreHandle_t xSemaphore ); |
参数:
信号量的句柄
返回值:
成功返回pdPASS
,失败返回errQUEUE_FULL
。
获取二值信号量
1 | xSemaphoreTake( SemaphoreHandle_t xSemaphore, |
参数:
- xSemaphore: 信号量句柄
- xTicksToWait :超时时间,取值 0 ~ portMAX_DELAY
返回值:
成功返回pdPASS
,失败返回errQUEUE_FULL
。
官方用法示例
- 创建二值信号量
- 释放二值信号量
- 获取二值信号量
计数信号量
计数信号量介绍
计数信号量相当于长度大于1的队列,所以计数信号量可以容纳多个资源。
主要用于资源的计数,初始值一般代表可用资源的数量
相关API
函数名 | 描述 |
---|---|
xSemaphoreCreateCounting() |
创建一个计数信号量,指定最大计数值和初始值 |
xSemaphoreCreateCountingStatic() | 静态方式创建计数信号量 |
uxSemphoreGetCount() | 获取信号量的计数值 |
计数信号量和二值信号量的获取和释放的api完全相同
创建计数信号量
1 | SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, |
参数:
uxMaxCount: 最大计数值
uxInitialCount:创建信号量时分配给信号量的计数值,常设置为0
返回值:
成功:返回对应计数信号量的句柄 失败:返回NULL
官方用法示例
xSemaphoreCreateCounting - FreeRTOS™
- 创建计数信号量
互斥量
互斥量介绍
互斥量也是一个长度为1,大小为0的队列,取值只能为0或1
,主要用于共享资源的互斥访问
在多数情况下,互斥信号量和二值信号量非常类似,但是从功能上
二值型信号量:用于任务同步
互斥量信号量:用于资源保护,实现共享资源的互斥访问
互斥量和二值信号量的最大区别就是:互斥信号量拥有优先级继承
机制,可以有效解决优先级翻转现象
什么是优先级翻转
优先级翻转
:低优先级的任务优先执行,高优先级的任务反而慢执行。
三个任务的优先级的顺序为H>M>L
,且我们使用二值信号量
控制任务的同步
L先获取信号量还未来得及释放,由于H就绪抢占L并开始运行,信号量被L占有从而导致H进入阻塞,此时L应该继续运行。
但M的优先级高于L,所以M就绪抢占L(M没有相关信号量控制),任务M运行完后,任务L才继续运行并释放信号量,最后H才获取信号量,H才运行。
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度
。但其他中等优先级的任务却能抢到CPU资源
。从现象上看,就像是中低优先级的任务比高优先级任务具有更高的优先权(即优先级翻转)
优先级翻转的危害:
- 实时性丧失:高优先级任务无法在预期时间内执行完成
- 系统行为不可预测:高优先级任务的阻塞时间取决于中低优先级任务的执行时间
- 可能导致严重后果:在关键实时系统中(如航空、医疗设备),这可能导致灾难性后果
互斥量处理优先级翻转
互斥量具有优先级继承机制
,可以解决优先级翻转问题,FreeRTOS提供更多的解决办法还有:优先级天花板协议
、临时关闭中断或调度器
优先级继承
:当低优先级任务持有一个高优先级任务需要的资源时(互斥量等),如果高优先级的任务尝试获取这个资源,这个高优先级任务会被阻塞,但是这个低优先级任务会临时继承
高优先级任务的优先级
,直到释放资源。
在FreeRTOS中,互斥量(Mutex)默认启用优先级继承机制
优先级继承机制并不能完全消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响
因为低优先级任务还是比高优先级任务先执行了
相关API
函数名 | 描述 |
---|---|
xSemaphoreCreateMutex() |
使用动态方法创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方式创建互斥信号量 |
其余api操作和其他信号量完全相同
创建互斥量
1 | SemaphoreHandle_t xSemaphoreCreateMutex( void ) |
参数:
无
返回值:
成功返回互斥信号量的句柄,失败返回NULL
注意:
使用该函数创建的互斥量默认值为1,可以直接Take
注意:互斥量没有不能在中断中使用,因为其没有中断专用的不会阻塞的函数,在终端使用可能会造成阻塞
官方用法示例
xSemaphoreCreateMutex - FreeRTOS™
- 创建互斥量
互斥量和信号量区别总结
在多数情况下,互斥信号量和二值信号量非常类似,主要是从功能上对比区别:
二值型信号量:用于任务同步
互斥量信号量:用于资源保护,实现共享资源的互斥访问
同时互斥锁具有优先级继承机制
,但二进制信号量没有。因此,二进制信号量是实现同步的更好选择(任务之间或任务与中断之间), 也是实施简单互斥方面的更好选择
事件标志组
事件标志位和事件标志组
事件标志位
:表明某个事件是否发生,通常按位表示,每一个位表示一个事件(取值为1代表事件发生,0代表事件未发生)
事件标志组
:一组事件标志位的集合,可以理解为事件标志组就是一个整数。
事件标志组本质是一个16位(uint16_t)或32位(uint32_t)无符号的数据类型EventBits_t
。
FreeRTOS将多个事件标志储存在一个变量类型为EventBits_t
的变量中,这个变量就是事件标志组
EventBits_t
类型可以由configUSE_16_BIT_TICKS
决定位数是16还是32。
configUSE_16_BIT_TICKS
配置为0的代表EventBits_t为32位无符号类型configUSE_16_BIT_TICKS
配置为1代表EventBits_t为16位无符号类型
虽然使用了32位无符号
的数据类型来存储事件标志,但却分成了两部分,其中的高8位
用作存储事件标志组的控制信息,低24位
用作存储事件标志,所以说一个事件组最多可以存储24个
事件标志!
相关API
函数名 | 描述 |
---|---|
xEventGroupCreate() |
创建一个事件标志组,返回事件组句柄 |
xEventGroupCreateStatic() | 静态方式创建事件标志组 |
xEventGroupSetBits() |
设置(置位)一个或多个事件位 |
xEventGroupClearBits() |
清除(复位)一个或多个事件位 |
xEventGroupSetBitsFromISR() | 在中断服务程序中设置事件位 |
xEventGroupClearBitsFromISR() | 在中断服务程序中清除事件位 |
xEventGroupWaitBits() |
等待一个或多个事件位被置位,带阻塞和超时选项 |
xEventGroupGetBits() | 读取事件标志组当前的所有事件位 |
xEventGroupGetBitsFromISR() | 在中断服务程序中读取事件标志组的事件位 |
vEventGroupDelete() | 删除事件标志组,释放相关资源 |
创建事件标志组
1 | EventGroupHandle_t xEventGroupCreate( void ); |
参数
:无
返回值
:成功返回事件标志组句柄,失败返回NULL
设置事件标志位
1 | EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, |
参数:
- xEventGroup:要设置位的事件标志组句柄
- uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值,如:0X09代表设置第3位和第0位
返回值:
设置之后的事件标志组的值
清除事件标志位
1 | EventBits_t xEventGroupClearBits( |
参数:
- xEventGroup:要清除位的事件标志组的句柄
- uxBitsToCleart:指定要在事件组中清除的一个或多个位的按位值,如:0X09代表清除第3位和第0位
返回值:
清除之后的事件标志组的值
等待事件标志位
1 | EventBits_t xEventGroupWaitBits( |
参数:
xEventGroup:对应事件标志组的句柄
uxBitsToWaitFor:指定要在事件组中等待的一个或多个位的按位值。
例如:要等待第0位和第2位,将uxBitsToWaitFor设置为0x05(00000101)即可。
uxBitsToWaitFor不得设置为0
xClearOnExit:设置为
pdTRUE
–返回前清除对应对应事件位,pdFALSE
–返回前不清除对应事件位xWaitForAllBits:
pdTRUE
–等待所有事件位为1才满足(逻辑于),pdFALSE
–等待的事件中任意有一个为1就满足(逻辑或)xTicksToWait:等待的最长时间,取值
0 ~ portMAX_DELAY
返回值:
等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位
其他值:等待事件标志位失败,返回事件组中的事件标志位
官方用法示例
xEventGroupWaitBits() - FreeRTOS™
- 创建事件标志组
- 设置事件标志组
- 清除事件标志组
- 等待事件标志位
在信号量的应用中。我们发现信号量只能实现两个任务之间的同步,如果要实现多个任务之间的同步,则需要使用事件标志组。
事件标志组是多个二值信号的组合
,其中每一个二值信号
就是一个事件标志位
(相当于一个二值信号量),用来表明某一个事件是否发生,该标志位由一个相关的任务或ISR 置位。
事件标志组可以实现多个任务(包括ISR)协同控制一个任务,当各个相关任务的对应事件发生时,将设置事件标志组的对应标志位有效,进而触发对应的任务。
使用事件标志组同步的任务可以分为独立性同步(OR)和关联性同步(AND)。独立性同步
表示等待的任何一个事件发生时,就可以触发任务。关联性同步
表示等待的全部事件都发生时,才可以触发任务。
任务通知
任务通知介绍
任务通知,类似于事件标志组,可以用于多个任务的同步。但任务通知不需要用户创建
,每一个任务创建后就自动拥有一个任务通知
。
任务的任务通知只能由任务自身等待,由其他任务设置。而事件标志组则相当于一个公用的资源,任何任务都可以设置或等待。
FreeRTOS从版本V8.2.0开始提供任务通知这个功能,每个任务都有一个32位
的通知值。按照FreeRTOS官方的说法,使用消息通知
比通过二值信号量
方式解除阻塞任务快45%
,并且更加省内存(无需创建队列
)
在大多数情况下,任务通知
可以替代二值信号量、计数信号量、事件标志组,可以替代长度为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快、使用RAM更少!
任务通知值的更新方式
FreeRTOS提供以下几种方式发送通知给任务:
- 发送消息给任务,如果有通知未读,不覆盖通知值
- 发送消息给任务,直接覆盖通知值
- 发送消息给任务,设置通知值得一个或多个位
- 发送消息给任务,递增通知值
通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组
任务通知的优势和劣势
优势
1.使用任务通知向任务发送事件或数据,比使用队列、事件标志组、信号量快得多
2.使用其他方法时要先创建对应的句柄(结构体),使用任务通知时无需额外创建句柄(结构体)
劣势
1.只有任务可以等待通知,而中断服务函数中不可以,因为中断没有TCB(每个任务有块内存)。
2.通知只能一对一,因为通知必须指定任务
3.等待通知的任务可以被阻塞,但是发送消息的任务,任何情况下都不会被阻塞等待
4.任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据
相关API
函数名 | 作用 |
---|---|
xTaskNotify() |
发送通知,带有通知值 |
xTaskNotiyAndQuery() |
发送通知,带有通知值并且保留接收任务的原通知值 |
xTaskNotifyGive() |
给指定任务发送通知(加1),不带通知值 |
xTaskNotifyFromISR() | 在中断服务程序中发送通知 |
xTaskNotiyAndQueryFromISR() | 在中断中发送任务通知 |
xTaskNotifyGiveFromISR() | 在中断中发送任务通知 |
发送通知
1 | BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, |
参数:
- xTaskNotify:需要接收通知的任务句柄
- ulValue:用于更新接收任务的通知值,具体如何更新由形参eAction决定
- eAction:一个枚举,代表如何使用任务通知的值
枚举值 | 描述 |
---|---|
eNoAction | 发送通知,但不更新值(参数ulValue未使用) |
eSetBits | 将通知值的指定位(ulValue对应的位)按位或,某些场景下代替事件组,效率更高 |
eIncrement | 被通知任务的通知值增加1(参数ulValue未使用),相当于xTaskNotifyGive |
eSetValueWithOverwrite | 直接用ulValue覆盖通知值(不管原来有没有被读取),某些场景下可以代替xQueueOverwrite ,效率更高 |
eSetValueWithoutOverwrite | 如果被通知的任务当前没有通知,则被通知的任务的通知值设为ulValue。 如果被通知任务没有取走上一个通知,又接收到了一个通知, 则这次通知值丢弃 ,在这种情况下视为调用失败并返回pdFALSE (某些场景下可以代替xQueueSend,效率更高) |
返回值:
如果被通知任务还没取走上一个通知,又接收了一个通知,则这次的通知值未能更新并返回pdFALSE
,其他情况返回pdPASS
。
1 | BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify, |
参数:
- xTaskNotify:需要接收通知的
任务句柄
- ulValue:用于更新接收任务的通知值,具体如何更新由形参eAction决定
- eAction:一个枚举,代表如何使用任务通知的值
- pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为NULL,则不需要回传,这个时候就等价于函数
xTaskNotify()
返回值:
1 | BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify ); |
参数:
- xTaskToNotify:接收通知的任务句柄,并让自身的任务通知值加1
返回值:
总是返回pdPASS
等待通知
等待通知API函数只能用在任务中,不可用在中断中!
函数 | 描述 |
---|---|
ulTaskNotifyTake() | 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减1。 当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量 |
xTaskNotifyWait() | 获取任务通知,比ulTaskNotifyTake()更为复杂,可获取通知值和清除通知值指定位 |
1 | uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, |
参数:
- xClearCountOnExit:指定在成功接收通知后,将通知值清零或减1,pdTRUE:把通知值清零(二值信号量);pdFALSE:把通知值减一(计数型信号量)
- xTicksToWait:阻塞等待任务通知值最大时间
返回值:
0:接收失败,非0:接收成功,返回任务通知的通知值
1 | BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, |
参数:
- ulBitsToClearOnEntry:函数执行前清零任务通知值那些位
- ulBitsToClearOnExit:在函数退出前,清零任务通知值那些位(通常清空所有位0xFFFFFFFF)。在清0前,接收到的任务通知值会先被保存到形参*pulNotificationValue中
- pulNotificationValue:用于保存接收到的任务通知值。如果不使用,设置为
NULL
- xTicksToWait:阻塞等待任务通知值最大时间
返回值:
pdTRUE:如果收到了通知,或者在调用 xTaskNotifyWait() 时通知已挂起。
pdFALSE:如果调用 xTaskNotifyWait() 超时且在超时前没有收到通知。
示例
模拟二值信号量
使用xTaskNotifyGive()
和ulTaskNotifyTake()
,分别模拟释放和获取二值信号量,不需要创建任务通知
模拟计数信号量
使用xTaskNotifyGive()
和ulTaskNotifyTake()
,分别模拟释放和获取二值信号量,不需要创建任务通知。只是函数参数传入有点小区别
模拟事件标志组
使用xTaskNotifyWait()
和xTaskNotify()
,模拟事件标志组
模拟邮箱
邮箱就是长度为1的队列,直接对其进行覆写
软件定时器
软件定时器介绍
定时器简单理解就是闹钟,到一定时间后就会”响铃”
STM32芯片自带硬件定时器,精度较高,达到定时时间后会触发中断,也可以生成PWM、输入捕获、输出比较等等,容易受硬件资源 的限制,个数有限。
软件定时器也可以实现定时的功能,达到定时时间后可以调用回调函数,可以在回调函数中处理信息
软件定时器优缺点
优点:
1.简单、成本低
2.只要内存够就可以创建多个
缺点:
精度较低,容易受到中断影响。在大多情况下够用,但是对于精度要求比较高的场合不建议使用
软件定时器原理
定时器是一个可选的、不属于FreeRTOS内核的功能,它是由定时器服务任务来提供的。
在调用函数vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务
,主要负责:
1.负责软件定时器超时的判断逻辑
2.调用超时软件定时器的超时回调函数
3.处理软件定时器命令队列
FreeRTOS提供了很多定时器有关的API函数,这些API函数大多都是FreeRTOS的队列发送命令给定时器服务任务的。这个队列叫做定时器命令队列
,定时器命令队列是提供给FreeRTOS的软件定时器使用的,用户不能直接访问!
软件定时器相关配置
软件定时器有一个定时器服务任务和定时器命令队列,这两个东西需要一些宏定义配置,相关的宏定义配置也是放到FreeRTOSConfig.h中的,涉及到的配置如下:
1.configUSE_TIMERS
- 作用:是否启用软件定时器功能。
- 取值:
0
(禁用),1
(启用) - 说明:设为
1
后,定时器服务任务会在启动FreeRTOS调度器时自动创建。如果不需要软件定时器功能可以设为0
节省代码体积。
2.configTIMER_TASK_PRIORITY
- 作用:定时器服务任务(Timer Service/DaemonTask的优先级。
- 取值:0~(configMAX_PRIORITIES-1),一般建议与普通任务优先级体系保持一致。
- 说明:软件定时器的回调函数由定时器服务任务调用,
定时器服务任务
优先级设置越高,其响应定时器事件的实时性越高,但会占用更多CPU资源。通常设为比需要被及时响应的任务高,或与主业务任务优先级相同。
3.configTIMER_QUEUE_LENGTH
- 作用:定时器命令队列的长度。
- 取值:大于0的整数
- 说明:定时器服务任务通过一个队列接收定时器启动、停止、重启等命令。此参数决定队列能同时缓存多少个命令。如果定时器使用频繁或同时有大量定时器操作,建议适当增大该值,防止队列溢出。
4.configTIMER_TASK_STACK_DEPTH
- 作用:定时器服务任务的堆栈大小(以字为单位,通常是uint32_t)。
- 取值:大于0的整数
- 说明:软件定时器回调函数在定时器服务任务上下文中执行,因此要保证该任务堆栈足够大,能容纳所有可能的回调执行。根据实际回调函数栈需求设置,避免栈溢出。
单次定时器和周期定时器
单次定时器
:只超时一次,调用一次回调函数。可手动再开启定时器
周期定时器
:多次超时,多次调用回调函数
相关API
函数名 | 描述 |
---|---|
xTimerCreate() |
创建一个软件定时器,返回定时器句柄 |
xTimerCreateStatic() | 静态方式创建定时器 |
xTimerDelete() | 删除一个已创建的定时器 |
xTimerStart() |
启动定时器,定时器到期后会回调用户函数 |
xTimerStartFromISR() |
中断服务程序中启动定时器 |
xTimerStop() |
停止定时器 |
xTimerStopFromISR() | 中断服务程序中停止定时器 |
xTimerReset() | 重启(复位)定时器的计时 |
xTimerResetFromISR() | 中断服务程序中重启定时器 |
xTimerChangePeriod() | 改变定时器的周期时间 |
xTimerChangePeriodFromISR() | 中断服务程序中改变定时器周期 |
xTimerIsTimerActive() |
查询定时器是否处于活动(正在计时)状态 |
xTimerGetTimerDaemonTaskHandle() | 获取定时器服务任务句柄(Timer Service Task) |
xTimerGetExpiryTime() | 获取定时器下次超时时间戳(Tick数) |
xTimerGetPeriod() | 获取定时器当前的周期 |
定时器句柄
:
1 |
|
创建定时器
- 创建定时器
1 |
|
参数:
- pcTimerName:自定义给定时器的名称
- xTimerPeriod:定时器的周期,系统时钟节拍(滴答)为单位,建议使用
pdMS_TO_TICKS()
转为tick - uxAutoReload:是否开启自动重装载,取值pdTRUE和pdFALSE,代表周期定时器和单词定时器
- pvTimerID:软件定时器标识符,用于多个软件定时器共用一个超时回调函数,不共用设置为
NULL
即可 - pxCallbackFunction:定时器回调函数,必须使用以下原型:函数名自定
返回值:
成功:定时器句柄
失败:NULL
- 回调函数
1 | void vCallbackFunction( TimerHandle_t xTimer ) |
启动和关闭定时器(普通)
1 | BaseType_t xTimerStart( TimerHandle_t xTimer, |
参数:
- xTimer:软件定时器句柄
- xBlockTime:发送命令软件定时器命令队列的最大等待时间(单位:Tick)。freeRTOS用一个
定时器命令队列
来处理定时器的命令(比如启动、停止、重载等)。如果在某些系统压力很大的时候调用了 xTimerStart(),但这个队列刚好满了,系统可以等待一会儿再放进去。
返回值:
pdPASS:发送启动/关闭命令到定时器命令队列成功
pdFAIL:发送启动/关闭命令到定时器命令队列失败
启动和关闭定时器(中断)
1 | BaseType_t xTimerStartFromISR( |
参数:
- xTimer:定时器句柄
- pxHigherPriorityTaskWoken:取值有三种
- pdTRUE:有更高优先级的任务准备好了!
- pdFAIL:没有更高优先级的任务准备好
- NULL:不关心是否有高优先级任务,不关心实时性时可以传NULL
返回值:
pdPASS:发送启动/关闭命令到定时器命令队列成功
pdFAIL:发送启动/关闭命令到定时器命令队列失败
中断服务函数(ISR)在某个条件下调用
xTimerStartFromISR()
来启动定时器。
xTimerStartFromISR()
会向定时器命令队列写入消息,唤醒定时器服务任务。如果定时器服务任务的优先级 等于或高于 当前中断任务的优先级,
pxHigherPriorityTaskWoken
会被设置为pdTRUE
。在ISR中,我们检查
pxHigherPriorityTaskWoken
,如果为pdTRUE
,调用portYIELD_FROM_ISR()
来触发任务切换,确保定时器服务任务被立即执行。我们不关心的时候直接传入
NULL
即可
为什么需要有中断版本的?
中断不能有阻塞:
1.xTimerStartFromISR()
,会直接向队列或缓冲区发送命令,通知调度器执行某些动作,例如启动定时器或激活任务。只会将启动定时器的命令添加到定时器命令队列中,并且不会在ISR中等待,它只会通知 FreeRTOS定时器服务任务去执行启动操作。
2.在普通任务中,如果调用了xTimerStart()
这样的API,它会涉及到操作系统级的队列操作或信号量等资源的等待。这可能导致任务阻塞
,尤其是当任务在等待某些资源时。
如果任务需要等待某个条件满足(例如等待定时器、信号量、消息队列等),它会进入阻塞状态,直到条件满足。
所以:我们在中断中要使用这些专用的不会阻塞的API
复位软件定时器
1 | BaseType_t xTimerReset( TimerHandle_t xTimer, |
参数:
- xTimer:软件定时器句柄
- xBlockTime:发送命令软件定时器命令队列的最大等待时间(单位:Tick)。freeRTOS用一个
定时器命令队列
来处理定时器的命令(比如启动、停止、重载等)。如果在某些系统压力很大的时候调用了 xTimerStart(),但这个队列刚好满了,系统会阻塞等待xBlockTime再放进去。
返回值:
pdPASS:发送复位命令成功
pdFAIL:发送复位命令失败
说明:
复位后定时器以复位时刻作为开始时刻重新定时
更改软件定时器时间
1 | BaseType_t xTimerChangePeriod( TimerHandle_t xTimer, |
参数:
- xTimer:软件定时器句柄
- xNewPeriod:新的定时器的周期,单位:tick
- xBlockTime:发送命令软件定时器命令队列的最大等待时间(单位:tick)。freeRTOS用一个
定时器命令队列
来处理定时器的命令(比如启动、停止、重载等)。如果在某些系统压力很大的时候调用了 xTimerStart(),但这个队列刚好满了,系统会阻塞等待xBlockTime再放进去。
返回值:
pdPASS:发送复位命令成功
pdFAIL:发送复位命令失败
获取定时器状态
1 | BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer ); |
参数:
- xTimer:定时器句柄
返回值:
pdFALSE: 如果定时器处于休眠状态,将返回pdFALSE
pdFALSE以外的值: 如果定时器处于活动状态,将返回pdFALSE以外的值
说明:
如果出现以下情况,定时器将处于休眠状态:1.已创建但尚未启动 2.这是一个尚未重启的过期的单次计时器。
用法示例
创建示例:
1 |
|
注意在任务创建前创建定时器
内存管理
钩子函数
FreeRTOS的**钩子函数(Hook Functions)**是一类特殊的回调函数,允许用户在特定事件或状态发生时自动执行自定义代码。
钩子函数
在FreeRTOS的内存管理
(堆栈溢出钩子,内存分配失败钩子)、低功耗管理
(空闲任务钩子)有着重要的作用
官方文档:钩子函数 - FreeRTOS™
Tick钩子函数
Tick钩子介绍
tick中断(一般是SysTick中断)可以选择性地调用应用程序定义的钩子(或回调)函数 — tick 钩子。
tick钩子提供了一个方便的地方来实现定时器
功能。
在SysTick_Handler
定义如下:
xPortSysTickHandler
中又调用xTaskIncrementTick
该函数中会调用用户自定义的钩子函数vApplicationTickHook()
,可以把SysTick当成1ms的定时器使用
使用Tick钩子
1.在FreeRTOSConfig.h
中配置configUSE_IDLE_HOOK
为 1
,启用Tick钩子函数
运行周期:由configTICK_RATE_HZ
决定, 一般设置为1000,也就是1ms,可以不管
2.程序自己实现vApplicationTickHook
,内部写每1ms想要做的事
1 | void vApplicationTickHook() |
注意:vApplicationTickHook()在ISR内执行,因此必须非常短,不使用很多堆栈,并且不 调用任何不以 “FromISR” 或 “FROM_ISR” 结尾的 API 函数!!!
空闲钩子函数
malloc失败钩子函数
堆栈溢出钩子函数
每个任务都拥有其独立维护的堆栈,堆栈溢出是应用程序不稳定的一个很常见的原因,堆栈溢出会造成系统卡死等
因此 FreeRTOS 提供堆栈溢出检测
的三种方法,用于协助检测并纠正出现的堆栈溢出问题。
如果启用
堆栈溢出钩子(configCHECK_FOR_STACK_OVERFLOW未设置为0) ,则应用程序必须提供堆栈溢出钩子函数。钩子函数必须命名为vApplicationStackOverflowHook()
, 并且具备以下原型:
1 | void vApplicationStackOverflowHook( TaskHandle_t xTask, |
参数:
xTask
:堆栈溢出任务句柄pcTaskName
:堆栈溢出任务名字
堆栈溢出检查会增加上下文切换的开销,因此建议只在开发或测试阶段
使用此检查!!!
堆栈溢出检测-方法1
宏configCHECK_FOR_STACK_OVERFLOW
设置为1
可使用此方法
此方法会在任务退出运行状态后(每次任务切换时),内核检查当前任务的堆栈指针(SP)
是否仍在分配给该任务的堆栈空间范围
内。如果堆栈栈指针包含超出有效堆栈范围
的值,则将调用堆栈溢出钩子函数。
优点:
- 速度极快,几乎不增加系统开销。
- 实现简单。
缺点:
- 只检查SP是否
跑出界
,无法发现栈内数据被覆盖
(即SP虽然还在界内,但实际数据早已被溢出破坏)。 - 不能保证可以捕获所有堆栈溢出
- 只检查SP是否
堆栈溢出检测-方法2(常用)
宏configCHECK_FOR_STACK_OVERFLOW
设置为2
可使用此方法
任务首次创建时,FreeRTOS用一个已知固定值(比如0xA5A5A5A5)填充任务堆栈底部。每次任务切换时(任务退出运行天), RTOS内核检查栈底若干字节(16字节)是否全部保持初始值
,如果任一变化了,则代表被未知任务或中断活动覆盖,就说明栈空间被踩
了,则调用堆栈溢出钩子。
因为栈自顶向下生长(高地址到低地址),最常见的溢出是“穿过栈底”,所以检测“栈底”最能发现真正的溢出!
优点:
- 能检测绝大多数
向下溢出
的堆栈溢出(即函数调用过深/递归等导致数据写穿栈底)。 - 效率略低于方法1,但仍然很快
检测能力
优于方法1,实际项目中极为可靠
- 能检测绝大多数
缺点:
- 仍有极少数场合不能捕获堆栈溢出(如特殊情况下没有覆盖到栈底哨兵区域)。
堆栈溢出检测-方法3
宏configCHECK_FOR_STACK_OVERFLOW
设置为3
可使用此方法
此方法仅适用于选定的端口。如果可用,该方法将启用ISR堆栈检查。 检测到ISR堆栈溢出时,会触发断言。请注意,在这种情况下不会调用堆栈溢出钩子函数
,因为它只针对任务堆栈,而不是针对ISR堆栈。
优点:
- 能检测中断栈溢出(ISR stack overflow),这是方法1/2做不到的。
缺点:
- 只在部分端口支持(如STM32等Cortex-M系列)。
- 溢出时
不会调用堆栈溢出钩子函数
,而是直接assert,可能导致MCU复位/死机。
使用堆栈溢出钩子
1.在FreeRTOS.h文件中找到宏定义configCHECK_FOR_STACK_OVERFLOW
,取值有以下含义
值为0
:不启用堆栈溢出钩子
值为1
:启用堆栈溢出检测方法1
值为2
:启用堆栈溢出检测方法2,常常使用方法2
值为3
:启用堆栈溢出检测方法3
2.程序中实现堆栈溢出钩子
通常是在其中输出日志(串口、RTT等),打印堆栈溢出的任务名称,定位堆栈溢出的任务,以便于对与溢出任务分配的堆栈进行增加等
然后就是执行一些指定的操作
1 | /* FreeRTOS堆栈溢出钩子,堆栈溢出检查会增加上下文切换的开销,因此建议只在开发或测试阶段使用此检查 */ |
比如:
注意
:一般开发测试使用该堆栈检查,帮助开发人员更好的定位错误,开发测试完成后,关闭此检查可以减少开销!
守护进程任务启动钩子
中断管理
中断定义
见STM32基础知识笔记
中断优先级
任何中断的优先级都大于任务!
在我们的FreeRTOS中,中断同样具有优先级,并且我们也可以设置它的优先级,但是他的优先级并不是015,默认情况下他是 515, 0~4这5个中断优先级不是FreeRTOS控制的(5是取决于configMAX_SYSCALL_INTERRUPT_PRIORITY)。
使用中断注意
在中断中必须使用中断专用函数(定时器、队列、信号量等等),这些函数才一定不会造成中断阻塞,避免造成系统卡死等严重后果
中断服务函数运行时间越短越好