FreeRTOS使用

下面为最基础的用法,其余用法请:看文档学习

RTOS 基础知识 - FreeRTOS™

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()

image-20250719160918093

其中又会调用vPortSetupTimerInterrupt(),配置SysTick为RTOS的系统节拍定时器,生成对应频率的中断信号

image-20250719161111167

这里对SysTick的计数值、重载值、频率等进行寄存器配置

image-20250719161332169

对应的SysTick中断服务程序:

image-20250808173419023

任务介绍

官方全介绍:任务和协程 - FreeRTOS™

任务状态

嵌入式操作系统中的任务一共有四种状态,任务一旦创建后,将在这四种状态中切换:

  • 运行态(Running):当前正在执行的任务处于运行状态,此时该任务拥有CPU的使用权。任何时刻下只能有一个任务处于运行态

  • 就绪态(Ready):指能够运行(没有阻塞或挂起),但是当前没有运行的任务,往往是因为同优先级任务或者高优先级任务正在运行

  • 阻塞态(Blocked):任务在执行过程中需要等待某一个事件的发生(信号量、消息队列、事件标识组)或者调用延时函数,此时任务将处于阻塞态。当任务等待的事件发生或者延时时间到后,才会切换到就绪态

  • 挂起态(Suspended):类似暂停,通过调用函数vTaskSuspend()对指定任务进行挂起,挂起后该任务不会被执行,只有调用函数xTaskResume()才可以将任务从挂起态恢复。

img

只有就绪态可变成运行态,其他任务状态想要运行,必须先转变为就绪态

任务优先级

在创建任务时可以指定该任务的优先级编号,每个任务被分配从0~configMAX_PRIORITIES-1的优先级编号,

其中configMAX_PRIORITIES在FreeRTOSConfig.h中配置

比如这里我们就定义为有56个的优先级

image-20250808181808318

对应优先级编号枚举定义:

image-20250808181930403

FreeRTOS调度器可确保在就绪或运行状态下的任务始终比同样处于就绪状态下的更低优先级任务先获得处理器 (CPU)时间。换句话来说,处于运行状态的任务始终是能够运行的最高优先级任务

处于相同优先级的任务数量不限。如果configUSE_TIME_SLICING(时间片轮转配置)未经定义,或者如果 configUSE_TIME_SLICING设置为1,则具有相同优先级的若干就绪状态任务将通过时间切片轮询调度方案共享可用的处理时间。

任务调度

什么是任务调度器

任务调度器是实时操作系统(RTOS)的一个关键组件,它负责决定在多个可运行任务中哪一个将获得CPU时间得以执行。它基于任务的优先级状态来做出这些决定。

在一个RTOS中,可能会有多个任务同时运行,但是在任意时刻,CPU只能执行一个任务。任务调度器的主要目标是按照系统的需求合理分配CPU时间

调度器就是使用相关的调度算法来决定当前需要执行的那个任务

FreeRTOS中开启任务调度的函数是vTaskStartScheduler()

而CubeMX中进行更进一步的封装为osKernelStart()

任务调度方式

FreeRTOS是一个实时操作系统,其核心任务调度器支持以下调度方式:

1.抢占式调度:抢占式调度器是一种优先级基础的调度器,高优先级抢占低优先级任务,只要有更高优先级的任务变为就绪,系统会立即中断当前低优先级任务,切换到高优先级任务

FreeRTOSConfig.hconfigUSE_PREEMPTION控制,configUSE_PREEMPTION = 1 代表启动抢占式调度,默认开启

image-20250808181415377

2.时间片轮转度:只对相同优先级的就绪任务有效。它将CPU时间划分成小的时间片段,相同优先级的每个任务在一个时间片段内运行轮转切换。

FreeRTOS.hconfigUSE_TIME_SLICING控制,configUSE_TIME_SLICING = 1 代表启动时间片轮转,默认是处于开启状态的

image-20250808181354523

3.协作式调度:协作式调度器依赖于任务自行释放CPU,没有明确的任务优先级。任务必须自愿放弃CPU控制权,以便其他任务能够运行。

FreeRTOSConfig.hconfigUSE_PREEMPTION控制,configUSE_PREEMPTION = 0 代表启动抢占式调度,默认关闭

FreeRTOS默认启动抢占式调度时间片轮转调度,对我们同优先级不同优先级任务的执行

抢占式调度

优先级:Task1<Task2<Task3 图中根据时间依次被高优先级任务抢占执行

image-20250705151655882

高优先级任务优先执行,且高优先级任务不停止(阻塞)的话,低优先级任务永远无法执行

被抢占的任务将进入就绪态

优点 适用于实时要求严格的系统,可以确保高优先级任务及时响应。
缺点 上下文切换的开销较大,可能会影响系统的性能。

时间片轮转调度

首先:时间片轮转调度时对于相同优先级任务的而言,不同优先级任务不存在时间片轮转

CPU的执行时间被划分成固定长度的时间片段。每个时间片段可以是几毫秒或者更短的时间,具体取决于系统的配置(滴答定时器中断等)。

图中同样的优先级任务: Task1 = Task2 = Task3

image-20250705152419807

  1. 同等优先级任务,轮流执行,时间片流转

  2. 一个时间片大小,通常取决于滴答定时器中断周期(如1ms)

注意没有用完的时间片不会再使用,如图中的Task3如果只执行了半个时间片就被阻塞了,那么会直接让出时间片,下次任务Task3得到执行,还是按照一个时间片的时钟节拍运行。

协作式调度

协作式调度器依赖于任务自行释放CPU,没有明确的任务优先级。

任务之间不会主动抢占CPU,任务必须自愿放弃CPU控制权,以便其他任务能够运行。

用的不多,用的时候再查吧

任务创建与删除

什么是任务

任务可以理解为一个线程/进程,每创建一个任务,就会在内存中开辟一个空间

任务通常都含有while(1)死循环

API

官方文档:xTaskCreate - FreeRTOS™

任务创建于删除相关函数有如下三个:

函数名称 函数作用
xTaskCreate() 动态方式创建任务
xTaskCreateStatic() 静态方式创建任务
vTaskDelete 删除任务

任务动态创建与静态创建的区别

动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递

通常使用动态方式创建任务

  • xTaskCreate函数原型

image-20250701215208670

  1. pvTaskCode:指向任务函数的指针,任务必须实现永不返回

  2. pcName:任务的名字,主要用于调试,默认最大长度为16

  3. uxStackDepth:堆栈大小

  4. pvParameters:传递给任务的参数,一般为NULL

  5. uxPriority: 任务的优先级范围 0 ~ configMAX_PRIORITIES-1

  6. pxCreatedTask: 用于返回已创建任务的句柄,可以被引用

返回值 描述
pdPASS 任务创建成功
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 任务创建失败

官方用法示例

xTaskCreate - FreeRTOS™

  • 任务创建

image-20250701220108851

  • vTaskDelete函数原型

image-20250701220336970

1
2
3
4
/*1.删除指定xHandle句柄对应的任务*/
vTaskDelete(xHandle);
/*2.删除当前任务*/
vTaskDelete(NULL);

将删除任务的句柄传入即可删除任务

若传入参数为NULL,代表删除任务自身(当前运行的任务)

任务控制

延时函数分类

相对延时:vTaskDelay(),从每一次调用vTaskDelay()后才开始计时

image-20250706180205314

绝对延时:vTaskDelayUntil(),从上一次调用vTaskDelayUntil()后一瞬间就开始计时了

image-20250706180638051

vTaskDelay和HAL_Delay

vTaskDelay:是FreeRTOS中的延时函数,作用是让任务阻塞,任务阻塞后,RTOS系统调用其他处于就绪态的优先级最高的任务来执行。

image-20250706181440670

HAL_Delay:裸机开发时使用的HAL库的延时函数,使用while循环一直不停的调用获取系统时间的函数,直到满足等待时间才会结束,故其占用了全部的CPU时间,其他任何事情都不能做

image-20250706181350259

vTaskDelay使用

1
2
3
void vTaskDelay( const TickType_t xTicksToDelay );

/*按给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。*/

示例

1
2
3
4
5
6
7
8
9
10
11
static void task1(void *parameters)
{
while (1)
{

vTaskDelay(pdMS_TO_TICKS(1000));
}
vTaskDelete(NULL);
}


因为对于1滴答的时间可能定义不同。所以我们通常会使用pdMS_TO_TICKS()来固定延时时间,将ms转化为对应的滴答数

该函数自动将输入的ms数转化为对应1000ms的滴答数

任务间传递信息

消息队列介绍

队列又称消息队列,是一种用于任务间通信的数据结构,队列可以在任务与任务间中断和任务间传递信息。

为什么不使用全局变量替代队列呢?

如果使用一个全局变量a任务1修改了变量a,等待任务3处理,但任务3处理速度很慢,在处理数据的过程中,任务2可能对变量a进行改变,导致任务3得到的不是任务1想要传递给任务3的数据.

在这种情况下,就可以使用队列,将数据依次放在流水线上,一个一个按顺序处理

队列项目:队列中的每一个数据

队列长度:队列能够存储队列项目的最大数量

创建队列时,需要指定队列长度及队列项目的大小

消息队列特点

1.队列入队出队的方式

采用FIFO(先进先出)的数据存储缓冲机制,即先入队的数据会先从队列中读取。也可以配置为后进先出(LIFO)方式,但是用得比较少。


2.数据传递方式

采用实际值传递,即将数据拷贝到队列中进行传递,也可以传递指针,传递较大数据的时候采用指针传递

在实际应用时,由于消息队列采用数据复制的方式传输数据,而不是传输存放数据的地址。如果任务间传输的数据量较大时,使用消息队列的效率会比较低。

这时,可以考虑使用全局变量来实现任务问的通信,只是要注意全局变量的互斥访问(利用互斥量实现)

所以:通常我们会在队列中传递以下几类消息:简单原始类型(如 uint32_tenum),小型结构体(几个字段的 POD 类型),指针或句柄(指向更大缓冲区或资源)以及带类型字段+联合体的复合消息。

最佳实践是尽量保持每条消息尺寸较小,以减少内存和阻塞时间;如果需要传输大块数据,则在消息中仅传递指针或索引,由接收端再去处理实际数据。


3.多任务访问

队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息


4.出队入队阻塞

当任务向一个队列发送消息时,可以指定一个阻塞事件,假设当时队列已满无法入队。

阻塞时间可以设置为:

  • 0:直接返回不会等待
  • 0 ~ port_MAX_DELAY: 等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待
  • por_MAX_DELAY: 死等,一直等到可以入队为止。出队阻塞与入队阻塞类似

相关API

参考文档:

xQueueCreate - FreeRTOS™

创建消息队列

image-20250418161621374

1
2
3
4
5
6
/*1.队列的句柄*/
xQueueHandle queue;

/*2.创建队列*/
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );

参数:

  • uxQueueLength: 队列一次可存储的最大项目数。
  • uxItemSize: 存储队列中每个项目所需的大小(以字节为单位)。项目通过复制而非引用的方式入队,因此该参数值是每个入队项目将复制的 字节数。队列中的每个项目必须具有相同的大小。

返回值:创建成功,返回队列句柄,创建失败返回NULL。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "freertos/queue.h"

xQueueHandle queue;

int main()
{

queue = xQueueCreate(10, sizeof(key_message));
/*注意队列的创建要在任务创建之前*/
if(queue == NULL)
{
wm_log_err("queue create failed!");
return -1;
}

xTaskCreate(task1,"task1",512,NULL,5,NULL);

return 0;
}

写队列

函数 描述
xQueueSend() 往队列的尾部写入消息
xQueueSendToBack() 同xQueueSend()
xQueueSendToFront() 往队列头部写入消息
xQueueOverwrite() 覆写队列消息(只适用于队列长度为1的情况)
xQueueSendFromISR() 在中断中往队列尾部写入消息
xQueueSendToBackFromISR() 同 xQueueSendFromISR()
xQueueSendToFrontFromISR() 在中断中往队列的头部写入消息
xQueueOverwriteFromISR() 在中断中覆写队列消息(只适用于队列长度为1的情况)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "freertos/queue.h"

/* 1.普通使用 */
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);

/* 2.中断中使用 */
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);

参数:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "freertos/queue.h"

//1.普通使用
BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);

//2.中断使用
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxHigherPriorityTaskWoken
);

参数:

  • xQueue: 任务队列句柄

  • pvitemToQueue:指向要将所接收项目复制到缓冲区的指针。

  • xTicksToWait(普通)阻塞超时时间(取值:0~portMAX_DELAY)。如果队列已空且xTicksToWait设置为0,则调用将立即返回。也可以使用portMAX_DELAY一直阻塞等待,直到队列有数据。

    pxHigherPriorityTaskWoken(中断):如果在调用时队列为空,则任务应阻塞等待项目接收的最长时间。如果队列为空,将 xTicksToWait设置为 0 将导致函数立即返回,。时间是以滴答周期为单位定义的。如果 INCLUDE_vTaskSuspend 设置为 1,则将阻塞时间指定为 portMAX_DELAY会导致任务无限期地阻塞(没有超时限制)。

返回值:如果成功写入,返回 pdTRUE,否则返回 errQUEUE_FULL。

为什么要区分普通使用和在中断使用专用的消息队列发送和接收?

  1. 非中断形式的发送,可能会被阻塞,而这对于中断服务中是万万不能有的,而专用的xQueueSendFromISR绝不会阻塞,消息队列满了会直接返回。
  2. 对于接收专用xQueueReceiveFromISR也是一样的不会阻塞,消息队列空了会直接返回,但是我们一般不使用该函数,因为中断服务函数中主要负责发送(通知任务),很少“去取数据”。但有时中断也可能从某种共享队列取命令执行。

官方用法示例

  • 创建消息队列

image-20250705210910148

示例:一个任务接收另一个任务/中断发送的消息,对应进行处理

在消息传递的过程中我们常常使用枚举类型,enum传递消息比较容易处理,也很方便,我们应避免使用较长较大数据,如传输:字符串等等

以下演示中断发送消息队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
typedef enum{
KEY_IDLE,
KEY_PRESSED,
KEY_RELEASED,
}key_message;

//任务1:
static void task1(void *parameters)
{

//init something ......
key_message key_state;

while (1)
{
//没收到会阻塞,是否无限阻塞与 INCLUDE_vTaskSuspend参数有关,收到返回pdPASS
if(xQueueReceive(queue,&key_state,portMAX_DELAY) == pdPASS)
{
//doing something......
}

vTaskDelay(10);
}

vTaskDelete(NULL);
}

/*gpio 中断回调函数*/
static void wm_drv_gpio_isr_callback(void *arg)
{

key_message message = KEY_PRESSED;

if(xQueueSendFromISR(queue,&message,NULL) == pdTRUE)
{
wm_log_info("gpio interrupt:message send!");
}
}

任务同步

信号量

信号量(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
2
xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );

参数:

  • xSemaphore: 信号量句柄
  • xTicksToWait :超时时间,取值 0 ~ portMAX_DELAY

返回值:成功返回pdPASS,失败返回errQUEUE_FULL

官方用法示例

  • 创建二值信号量

image-20250705205652556

  • 释放二值信号量

image-20250705205717173

  • 获取二值信号量

image-20250705205730090

计数信号量

计数信号量介绍

计数信号量相当于长度大于1的队列,所以计数信号量可以容纳多个资源。

主要用于资源的计数,初始值一般代表可用资源的数量

相关API

函数名 描述
xSemaphoreCreateCounting() 创建一个计数信号量,指定最大计数值和初始值
xSemaphoreCreateCountingStatic() 静态方式创建计数信号量
uxSemphoreGetCount() 获取信号量的计数值

计数信号量和二值信号量的获取和释放的api完全相同

创建计数信号量
1
2
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);

参数:

  • uxMaxCount: 最大计数值

  • uxInitialCount:创建信号量时分配给信号量的计数值,常设置为0

返回值:成功:返回对应计数信号量的句柄 失败:返回NULL

官方用法示例

xSemaphoreCreateCounting - FreeRTOS™

  • 创建计数信号量

image-20250705205623515

互斥量

互斥量介绍

互斥量也是一个长度为1,大小为0的队列,取值只能为0或1,主要用于共享资源的互斥访问

在多数情况下,互斥信号量和二值信号量非常类似,但是从功能上

二值型信号量:用于任务同步

互斥量信号量:用于资源保护,实现共享资源的互斥访问

互斥量和二值信号量的最大区别就是:互斥信号量拥有优先级继承机制,可以有效解决优先级翻转现象

什么是优先级翻转

优先级翻转:低优先级的任务优先执行,高优先级的任务反而慢执行。

优先级翻转

三个任务的优先级的顺序为H>M>L,且我们使用二值信号量控制任务的同步

L先获取信号量还未来得及释放,由于H就绪抢占L并开始运行,信号量被L占有从而导致H进入阻塞,此时L应该继续运行。

但M的优先级高于L,所以M就绪抢占L(M没有相关信号量控制),任务M运行完后,任务L才继续运行并释放信号量,最后H才获取信号量,H才运行。

高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上看,就像是中低优先级的任务比高优先级任务具有更高的优先权(即优先级翻转)

优先级翻转的危害:

  1. 实时性丧失:高优先级任务无法在预期时间内执行完成
  2. 系统行为不可预测:高优先级任务的阻塞时间取决于中低优先级任务的执行时间
  3. 可能导致严重后果:在关键实时系统中(如航空、医疗设备),这可能导致灾难性后果

互斥量处理优先级翻转

互斥量具有优先级继承机制,可以解决优先级翻转问题,FreeRTOS提供更多的解决办法还有:优先级天花板协议临时关闭中断或调度器

优先级继承:当低优先级任务持有一个高优先级任务需要的资源时(互斥量等),如果高优先级的任务尝试获取这个资源,这个高优先级任务会被阻塞,但是这个低优先级任务会临时继承高优先级任务的优先级,直到释放资源。

在FreeRTOS中,互斥量(Mutex)默认启用优先级继承机制

优先级继承机制并不能完全消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响

因为低优先级任务还是比高优先级任务先执行了

相关API

函数名 描述
xSemaphoreCreateMutex() 使用动态方法创建互斥信号量
xSemaphoreCreateMutexStatic() 使用静态方式创建互斥信号量

其余api操作和其他信号量完全相同

创建互斥量

1
SemaphoreHandle_t xSemaphoreCreateMutex( void )

参数:

返回值:成功返回互斥信号量的句柄,失败返回NULL

注意:使用该函数创建的互斥量默认值为1,可以直接Take

注意:互斥量没有不能在中断中使用,因为其没有中断专用的不会阻塞的函数,在终端使用可能会造成阻塞

官方用法示例

xSemaphoreCreateMutex - FreeRTOS™

  • 创建互斥量

image-20250705205531461

互斥量和信号量区别总结

在多数情况下,互斥信号量和二值信号量非常类似,主要是从功能上对比区别:

二值型信号量:用于任务同步

互斥量信号量:用于资源保护,实现共享资源的互斥访问

同时互斥锁具有优先级继承机制,但二进制信号量没有。因此,二进制信号量是实现同步的更好选择(任务之间或任务与中断之间), 也是实施简单互斥方面的更好选择

事件标志组

事件标志位和事件标志组

事件标志位:表明某个事件是否发生,通常按位表示,每一个位表示一个事件(取值为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位无符号类型

EventBits_t数据类型变量位使用情况

虽然使用了32位无符号的数据类型来存储事件标志,但却分成了两部分,其中的高8位用作存储事件标志组的控制信息,低24位用作存储事件标志,所以说一个事件组最多可以存储24个事件标志!

相关API

函数名 描述
xEventGroupCreate() 创建一个事件标志组,返回事件组句柄
xEventGroupCreateStatic() 静态方式创建事件标志组
xEventGroupSetBits() 设置(置位)一个或多个事件位
xEventGroupClearBits() 清除(复位)一个或多个事件位
xEventGroupSetBitsFromISR() 在中断服务程序中设置事件位
xEventGroupClearBitsFromISR() 在中断服务程序中清除事件位
xEventGroupWaitBits() 等待一个或多个事件位被置位,带阻塞和超时选项
xEventGroupGetBits() 读取事件标志组当前的所有事件位
xEventGroupGetBitsFromISR() 在中断服务程序中读取事件标志组的事件位
vEventGroupDelete() 删除事件标志组,释放相关资源

创建事件标志组

1
EventGroupHandle_t xEventGroupCreate( void );

参数:无

返回值:成功返回事件标志组句柄,失败返回NULL

设置事件标志位

1
2
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );

参数:

  • xEventGroup:要设置位的事件标志组句柄
  • uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值,如:0X09代表设置第3位和第0位

返回值:

设置之后的事件标志组的值

清除事件标志位

1
2
3
EventBits_t xEventGroupClearBits(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );

参数:

  • xEventGroup:要清除位的事件标志组的句柄
  • uxBitsToCleart:指定要在事件组中清除的一个或多个位的按位值,如:0X09代表清除第3位和第0位

返回值:

清除之后的事件标志组的值

等待事件标志位

1
2
3
4
5
6
EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

参数:

  • xEventGroup:对应事件标志组的句柄

  • uxBitsToWaitFor:指定要在事件组中等待的一个或多个位的按位值。

    例如:要等待第0位和第2位,将uxBitsToWaitFor设置为0x05(00000101)即可。uxBitsToWaitFor不得设置为0

  • xClearOnExit:设置为pdTRUE–返回前清除对应对应事件位,pdFALSE–返回前不清除对应事件位

  • xWaitForAllBitspdTRUE–等待所有事件位为1才满足(逻辑于),pdFALSE–等待的事件中任意有一个为1就满足(逻辑或)

  • xTicksToWait:等待的最长时间,取值0 ~ portMAX_DELAY

返回值:

等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位

其他值:等待事件标志位失败,返回事件组中的事件标志位

官方用法示例

xEventGroupWaitBits() - FreeRTOS™

  • 创建事件标志组

image-20250705211230099

  • 设置事件标志组

image-20250705211304702

  • 清除事件标志组

image-20250705211325890

  • 等待事件标志位

image-20250705205146118

在信号量的应用中。我们发现信号量只能实现两个任务之间的同步,如果要实现多个任务之间的同步,则需要使用事件标志组。

事件标志组是多个二值信号的组合,其中每一个二值信号就是一个事件标志位(相当于一个二值信号量),用来表明某一个事件是否发生,该标志位由一个相关的任务或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
2
3
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );

参数:

  • xTaskNotify:需要接收通知的任务句柄
  • ulValue:用于更新接收任务的通知值,具体如何更新由形参eAction决定
  • eAction:一个枚举,代表如何使用任务通知的值
枚举值 描述
eNoAction 发送通知,但不更新值(参数ulValue未使用)
eSetBits 将通知值的指定位(ulValue对应的位)按位或,某些场景下代替事件组,效率更高
eIncrement 被通知任务的通知值增加1(参数ulValue未使用),相当于xTaskNotifyGive
eSetValueWithOverwrite 直接用ulValue覆盖通知值(不管原来有没有被读取),某些场景下可以代替xQueueOverwrite,效率更高
eSetValueWithoutOverwrite 如果被通知的任务当前没有通知,则被通知的任务的通知值设为ulValue。
如果被通知任务没有取走上一个通知,又接收到了一个通知,则这次通知值丢弃,在这种情况下视为调用失败并返回pdFALSE
(某些场景下可以代替xQueueSend,效率更高)

返回值:

如果被通知任务还没取走上一个通知,又接收了一个通知,则这次的通知值未能更新并返回pdFALSE,其他情况返回pdPASS


1
2
3
4
BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t *pulPreviousNotifyValue );

参数:

  • xTaskNotify:需要接收通知的任务句柄
  • ulValue:用于更新接收任务的通知值,具体如何更新由形参eAction决定
  • eAction:一个枚举,代表如何使用任务通知的值
  • pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为NULL,则不需要回传,这个时候就等价于函数xTaskNotify()

返回值:


1
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

参数:

  • xTaskToNotify:接收通知的任务句柄,并让自身的任务通知值加1

返回值:总是返回pdPASS

等待通知

等待通知API函数只能用在任务中,不可用在中断中!

函数 描述
ulTaskNotifyTake() 获取任务通知,可以设置在退出此函数的时候将任务通知值清零或者减1。
当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量
xTaskNotifyWait() 获取任务通知,比ulTaskNotifyTake()更为复杂,可获取通知值和清除通知值指定位

1
2
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );

参数:

  • xClearCountOnExit:指定在成功接收通知后,将通知值清零或减1,pdTRUE:把通知值清零(二值信号量);pdFALSE:把通知值减一(计数型信号量)
  • xTicksToWait:阻塞等待任务通知值最大时间

返回值: 0:接收失败,非0:接收成功,返回任务通知的通知值


1
2
3
4
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );

参数:

  • 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的软件定时器使用的,用户不能直接访问!

image-20250706184041435

软件定时器相关配置

软件定时器有一个定时器服务任务和定时器命令队列,这两个东西需要一些宏定义配置,相关的宏定义配置也是放到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
2
3
4
5
6
#include "freertos/timers.h"

/*两种定义都可以*/
xTimerHandle timer;

TimerHandle_t timer;

image-20250418141153068

官方文档xTimerCreate - FreeRTOS™

创建定时器

  • 创建定时器
1
2
3
4
5
6
7
8
#include "freertos/timers.h"

TimerHandle_t xTimerCreate
( const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );

参数:

  • pcTimerName:自定义给定时器的名称
  • xTimerPeriod:定时器的周期,系统时钟节拍(滴答)为单位,建议使用pdMS_TO_TICKS()转为tick
  • uxAutoReload:是否开启自动重装载,取值pdTRUE和pdFALSE,代表周期定时器和单词定时器
  • pvTimerID:软件定时器标识符,用于多个软件定时器共用一个超时回调函数,不共用设置为NULL即可
  • pxCallbackFunction:定时器回调函数,必须使用以下原型:函数名自定

返回值:

成功:定时器句柄

失败:NULL

  • 回调函数
1
2
3
4
void vCallbackFunction( TimerHandle_t xTimer )
{

}

启动和关闭定时器(普通)

1
2
3
4
5
BaseType_t xTimerStart( TimerHandle_t xTimer,
TickType_t xBlockTime );

BaseType_t xTimerStop( TimerHandle_t xTimer,
TickType_t xBlockTime );

参数:

  • xTimer:软件定时器句柄
  • xBlockTime:发送命令软件定时器命令队列的最大等待时间(单位:Tick)。freeRTOS用一个定时器命令队列来处理定时器的命令(比如启动、停止、重载等)。如果在某些系统压力很大的时候调用了 xTimerStart(),但这个队列刚好满了,系统可以等待一会儿再放进去。

返回值:

pdPASS:发送启动/关闭命令到定时器命令队列成功

pdFAIL:发送启动/关闭命令到定时器命令队列失败

启动和关闭定时器(中断)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 BaseType_t xTimerStartFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);

BaseType_t xTimerStopFromISR(
TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken);


/*使用参数2示例*/
void IRAM_ATTR gpio_isr_handler(void *arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;

// 启动定时器
xTimerStartFromISR(myTimer,&xHigherPriorityTaskWoken);

// 如果定时器服务任务被唤醒,并且它的优先级比当前中断任务高
if (xHigherPriorityTaskWoken == pdTRUE)
{
// 触发上下文切换,立即切换到高优先级任务
portYIELD_FROM_ISR();
}
}

参数:

  • 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
2
BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xBlockTime );

参数:

  • xTimer:软件定时器句柄
  • xBlockTime:发送命令软件定时器命令队列的最大等待时间(单位:Tick)。freeRTOS用一个定时器命令队列来处理定时器的命令(比如启动、停止、重载等)。如果在某些系统压力很大的时候调用了 xTimerStart(),但这个队列刚好满了,系统会阻塞等待xBlockTime再放进去。

返回值:

pdPASS:发送复位命令成功

pdFAIL:发送复位命令失败

说明:复位后定时器以复位时刻作为开始时刻重新定时

更改软件定时器时间

1
2
3
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xBlockTime );

参数:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "freertos/timers.h"

xTimerHandle timer;

int main(void)
{
//1.创建定时器
timer = xTimerCreate("timer", pdMS_TO_TICKS(1000), pdTRUE, NULL,(TimerCallbackFunction_t)vTimerCallback);

if(timer == NULL) //创建失败
{
wm_log_err("timer create failed!");
return -1;
}


//2.启动定时器
xTimerStart(timer,NULL);


//3.创建任务
xTaskCreate(task1,"task1",512,NULL,5,NULL);


return 0;
}


void vTimerCallback(TimerHandle_t xTimer)
{
....
}

static void task1(void *parameters)
{

while (1)
{

if( xTimerIsTimerActive( xTimer ) != pdFALSE )
{
/* xTimer is active, do something. */
}
else
{
/* xTimer is not active, do something else. */
}

}

vTaskDelete(NULL);
}

注意在任务创建前创建定时器

内存管理

FreeRTOS 堆内存管理 - FreeRTOS™

钩子函数

FreeRTOS的**钩子函数(Hook Functions)**是一类特殊的回调函数,允许用户在特定事件或状态发生时自动执行自定义代码。

钩子函数在FreeRTOS的内存管理(堆栈溢出钩子,内存分配失败钩子)、低功耗管理(空闲任务钩子)有着重要的作用

官方文档:钩子函数 - FreeRTOS™

Tick钩子函数

Tick钩子介绍

tick中断(一般是SysTick中断)可以选择性地调用应用程序定义的钩子(或回调)函数 — tick 钩子。

tick钩子提供了一个方便的地方来实现定时器功能。

SysTick_Handler定义如下:

image-20250807214719452

xPortSysTickHandler中又调用xTaskIncrementTick

image-20250807214730825

该函数中会调用用户自定义的钩子函数vApplicationTickHook(),可以把SysTick当成1ms的定时器使用

image-20250807214902522

使用Tick钩子

1.在FreeRTOSConfig.h中配置configUSE_IDLE_HOOK1,启用Tick钩子函数

image-20250807222131884

运行周期:由configTICK_RATE_HZ决定, 一般设置为1000,也就是1ms,可以不管

image-20250807222225838

2.程序自己实现vApplicationTickHook,内部写每1ms想要做的事

1
2
3
4
void vApplicationTickHook()
{
//用户想做的事情
}

注意:vApplicationTickHook()在ISR内执行,因此必须非常短,不使用很多堆栈,并且不 调用任何不以 “FromISR” 或 “FROM_ISR” 结尾的 API 函数!!!

空闲钩子函数

malloc失败钩子函数

堆栈溢出钩子函数

每个任务都拥有其独立维护的堆栈,堆栈溢出是应用程序不稳定的一个很常见的原因,堆栈溢出会造成系统卡死等

因此 FreeRTOS 提供堆栈溢出检测的三种方法,用于协助检测并纠正出现的堆栈溢出问题。

如果启用堆栈溢出钩子(configCHECK_FOR_STACK_OVERFLOW未设置为0) ,则应用程序必须提供堆栈溢出钩子函数。钩子函数必须命名为vApplicationStackOverflowHook(), 并且具备以下原型:

1
2
void vApplicationStackOverflowHook( TaskHandle_t xTask,
char *pcTaskName );

参数:

  • xTask:堆栈溢出任务句柄
  • pcTaskName:堆栈溢出任务名字

堆栈溢出检查会增加上下文切换的开销,因此建议只在开发或测试阶段使用此检查!!!

堆栈溢出检测-方法1

configCHECK_FOR_STACK_OVERFLOW设置为1可使用此方法

此方法会在任务退出运行状态后(每次任务切换时),内核检查当前任务的堆栈指针(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

image-20250825120453185

2.程序中实现堆栈溢出钩子

通常是在其中输出日志(串口、RTT等),打印堆栈溢出的任务名称,定位堆栈溢出的任务,以便于对与溢出任务分配的堆栈进行增加等

然后就是执行一些指定的操作

1
2
3
4
5
/* FreeRTOS堆栈溢出钩子,堆栈溢出检查会增加上下文切换的开销,因此建议只在开发或测试阶段使用此检查 */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{

}

比如:

image-20250825120022359

注意:

一般开发测试使用该堆栈检查,帮助开发人员更好的定位错误,开发测试完成后,关闭此检查可以减少开销!

守护进程任务启动钩子

中断管理

中断定义

见STM32基础知识笔记

中断优先级

任何中断的优先级都大于任务!

在我们的FreeRTOS中,中断同样具有优先级,并且我们也可以设置它的优先级,但是他的优先级并不是015,默认情况下他是 515, 0~4这5个中断优先级不是FreeRTOS控制的(5是取决于configMAX_SYSCALL_INTERRUPT_PRIORITY)。

使用中断注意

  1. 在中断中必须使用中断专用函数(定时器、队列、信号量等等),这些函数才一定不会造成中断阻塞,避免造成系统卡死等严重后果

  2. 中断服务函数运行时间越短越好