FreeRTOS复习笔记

文档名称 版本 作者 时间 备注
FreeRTOS v1.0.0 DuRuofu 2024-12-30 首次建立

备注:

  1. 笔记中的代码使用ESP-IDF 5.3版本,与标准的FreeRTOSAPI可能稍有偏差
  2. 笔记复制了很多他人帖子里的图片和文段(链接位于文末),仅用于个人学习使用。

一、FreeRTOS是什么?

官网文档:https://www.freertos.org/zh-cn-cmn-s/Documentation/01-FreeRTOS-quick-start/01-Beginners-guide/01-RTOS-fundamentals

1.1 RTOS是什么?

实时操作系统 (RTOS) 是一种体积小巧、确定性强的计算机操作系统。 RTOS 通常用于需要在严格时间限制内对外部事件做出反应的嵌入式系统,如医疗设备和汽车电子控制单元 (ECU)。 通常,此类嵌入式系统中只有一两项功能需要确定性时序,即使嵌入式系统不需要严格的实时反应,使用 RTOS 仍能提供诸多优势。RTOS 通常比通用操作系统体积更小、重量更轻,因此 RTOS 非常适用于 内存、计算和功率受限的设备。

1.2 与裸机的对比

1. 裸机:

在裸机系统中,所有的操作都是在一个无限的大循环里面实现,支持中断检测。外部中断紧急事件在中断里面标记或者响应,中断服务称为前台,main 函数里面的while(1)无限循环称为后台,按顺序处理业务功能,以及中断标记的可执行的事件。小型的电子产品用的都是裸机系统,而且也能够满足需求。但当程序复杂之后,这样的裸机程序难以阅读和维护。

2. RTOS:

在实时操作系统中,我们可以把要实现的功能划分为多个任务,每个任务负责实现其中的一部分,每个任务都是一个很简单的程序,通常是一个死循环。 RTOS操作系统的核心内容在于:实时内核。

RTOS的内核负责管理所有的任务,内核决定了运行哪个任务,何时停止当前任务切换到其他任务,这个是内核的多任务管理能力。多任务管理给人的感觉就好像芯片有多个CPU,多任务管理实现了CPU资源的最大化利用,多任务管理有助于实现程序的模块化开发,能够实现复杂的实时应用。可剥夺内核顾名思义就是可以剥夺其他任务的CPU使用权,它总是运行就绪任务中的优先级最高的那个任务。

加入操作系统后,开发人员不需要关注每个功能模块之间的冲突,重心放在子程序的实现。缺点是整个系统随之带来的额外RAM开销,但对目前的单片机的来影响不大。

1.3 RTOS多任务处理与并发的实现

常规单核处理器一次只能执行一个任务,但多任务操作系统可以快速切换任务, 使所有任务看起来像是同时在执行。

切换的过程由任务调度器实现,任务调度器就是使用相关的调度算法来决定当前需要执行的哪个任务。

FreeRTOS一共支持三种任务调度方式:

  • 抢占式调度 :主要是针对优先级不同的任务,每一个任务都有一个任务优先级,优先级高的任务可以抢占低优先级的任务的CPU使用权。
  • 时间片调度 :主要针对相同优先级的任务,当多个任务的优先级相同时,任务调度器会在每个时钟节拍到来的时候切换任务。
  • 协程式调度 :其实就是轮询,当前执行任务将会一直运行,同时高优先级的任务不会抢占低优先级任务。FreeRTOS现在虽然还在支持,但官方已经明确表示不再更新协程式调度。

总的来说:

  1. 高优先级任务,优先执行。
  2. 高优先级任务不停止,低优先级任务无法执行。
  3. 被抢占的任务将会进去就绪态。

1.4 相关概念

时间片:

同等优先级任务轮流享有相同的CPU时间(可设置),叫做时间片,在FreeRTOS中,一个时间片等于SysTick中断周期。

队列:

队列是一种只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。队尾放入数据,对头挤出。先进先出,称为FIFO

任务:

在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。系统中的每一任务都有多种运行状态。系统初始化完成后,创建的任务就可以在系统中竞争一定的资源,由内核进行调度。

任务可以存在于以下状态中:

  • 运行
    当任务实际执行时,它被称为处于运行状态。任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
  • 准备就绪
    准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
  • 阻塞
    如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。 例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束——一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量 事件。处于阻塞状态的任务通常有一个”超时”期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能 被选择进入运行状态。
  • 挂起
    与“阻塞”状态下的任务一样, “挂起”状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务 没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时 才会进入或退出挂起状态。

临界区:

临界区就是一段在执行的时候不能被中断的代码段。在多任务操作系统里面,对全局变量的操作不能被打断,不能执行到一半就被其他任务再次操作。一般被打断,原因就是系统调度或外部中断。对临界区的保护控制,归根到底就是对系统中断的使能控制。
在使用临界区时,关闭中断响应,对部分优先级的中断进行屏蔽,因此临界区不允许运行时间过长。为了对临界区进行控制,就需要使用信号量通信,实现同步或互斥操作。

二、FreeRTOS多任务管理

2.1 创建任务

2.1.1 API说明:

创建任务涉及三个API:

函数名 功能 动态/静态 备注
xTaskCreate 动态创建任务 动态
xTaskCreateStatic 静态创建任务 静态
xTaskCreateRestrictedStatic 静态创建受限任务(权限控制) 静态 很少用
下面是对各个API的详细解释:

xTaskCreate : 动态创建一个任务

当需要在运行时动态分配内存来创建任务时使用,也就是一般的正常情况。

原型:

1
2
3
4
5
6
7
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
const configSTACK_DEPTH_TYPE uxStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask
);

参数:

  • pvTaskCode:任务函数的入口地址。
  • pcName:任务名称(用于调试)。
  • usStackDepth:任务栈大小(以字为单位)。
  • pvParameters:传递给任务函数的参数。
  • uxPriority:任务的优先级(数字越大优先级越大)。
  • pxCreatedTask:存储任务句柄(可选)。

返回值:

  • pdPASS:任务创建成功。
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败(内存不足)

xTaskCreateStatic:静态创建一个任务

手动提供任务栈和任务控制块(TCB),避免动态内存分配。

原型:

1
2
3
4
5
6
7
8
9
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer
);

参数:

  • pvTaskCode:任务函数的入口地址。
  • pcName:任务名称(用于调试)。
  • usStackDepth:任务栈大小(以字为单位)。
  • pvParameters:传递给任务函数的参数。
  • uxPriority:任务的优先级。
  • puxStackBuffer:指向任务栈缓冲区的指针(由用户提供)。
  • pxTaskBuffer:指向任务控制块的缓冲区(由用户提供)。

返回值:

  • pdPASS:任务创建成功。
  • errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败(内存不足)

xTaskCreateRestrictedStatic:为受限任务(restricted task)创建任务

用于创建特定权限的任务,一般用于内存保护单元(MPU)配置中,限制任务的访问权限。

原型:

1
2
3
4
5
TaskHandle_t xTaskCreateRestrictedStatic(
const TaskParameters_t * const pxTaskDefinition,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer
);

参数说明:

  • pxTaskDefinition:任务参数定义结构(包含任务入口、名称、栈大小、优先级等)。
  • puxStackBuffer:栈的静态缓冲区。
  • pxTaskBuffer:任务控制块的静态缓冲区。

返回值:任务句柄。

2.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
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

// 任务函数
void myTask(void *pvParameters)
{
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "myTask");
}
}

void app_main(void)
{
// 创建一个 FreeRTOS 任务
// 参数说明:
// 1. 任务入口函数:myTask
// 2. 任务名称:"myTask",用于调试时标识任务
// 3. 任务堆栈大小:2048 字节(适当分配以避免栈溢出)
// 4. 任务参数:NULL(未传递参数)
// 5. 任务优先级:1(优先级较低,空闲任务优先级为 0)
// 6. 任务句柄:NULL(不需要保存任务句柄)
xTaskCreate(myTask, "myTask", 2048, NULL, 1, NULL);
}

2.2 FreeRTOS删除任务

2.2.1 API说明:

删除任务涉及一个API:

函数名 功能 动态/静态 备注
vTaskDelete 删除任务 动态/静态 可以删除别的任务,也可以自我删除
下面是对各个API的详细解释:

vTaskDelete:删除一个任务

当任务完成其功能后,需要释放资源,或当系统需要动态调整任务时使用
注意:调用后,任务进入删除 状态,但动态分配的内存需要由 FreeRTOS 自动释放。

原型:

1
void vTaskDelete( TaskHandle_t xTask );

参数说明:

  • xTask:要删除的任务句柄。如果传递 NULL,则删除当前任务。

2.2.2 示例代码:

1. 删除别人

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
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

// 任务函数
void myTask(void *pvParameters)
{
for (;;)
{
vTaskDelay(500 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "myTask");
}
}

void app_main(void)
{
// 任务句柄
TaskHandle_t taskHandle = NULL;
// 创建一个 FreeRTOS 任务
xTaskCreate(myTask, "myTask", 2048, NULL, 1, &taskHandle);

vTaskDelay(2000 / portTICK_PERIOD_MS);

// 删除任务
if (taskHandle != NULL)
{
vTaskDelete(taskHandle);
}
}

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
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

// 任务函数
void myTask(void *pvParameters)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "myTask:1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "myTask:2");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "myTask:3");

// 删除任务(如果传递 NULL,则删除当前任务)
vTaskDelete(NULL);
}

void app_main(void)
{
// 任务句柄
TaskHandle_t taskHandle = NULL;
// 创建一个 FreeRTOS 任务
xTaskCreate(myTask, "myTask", 2048, NULL, 1, &taskHandle);
}

2.3 任务创建时传递参数

2.3.1 说明:

在 FreeRTOS 中,任务函数的参数通过创建任务时的 pvParameters 指针传递。pvParameters 是一个 void * 类型的指针,可以传递任意类型的数据(整型、数组、结构体或字符串等)。任务接收到参数后,需要将其强制类型转换为对应的数据类型,以便正确使用。

2.3.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
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

typedef struct
{
int Int;
int Array[3];
} MyStruct;

// 任务函数1:接收整型参数
void Task_1(void *pvParameters)
{
int *pInt = (int *)pvParameters;
ESP_LOGI(TAG, "取得整型参数为:%d", *pInt);
vTaskDelete(NULL);
}

// 任务函数2:接收数组参数
void Task_2(void *pvParameters)
{
int *pArray = (int *)pvParameters;
ESP_LOGI(TAG, "取得数组参数为:%d %d %d", *pArray, *(pArray + 1), *(pArray + 2));
vTaskDelete(NULL);
}

// 任务函数3:接收结构体参数
void Task_3(void *pvParameters)
{
MyStruct *pStruct = (MyStruct *)pvParameters;
ESP_LOGI(TAG, "取得结构体参数为:%d %d %d %d", pStruct->Int, pStruct->Array[0], pStruct->Array[1], pStruct->Array[2]);
vTaskDelete(NULL);
}

// 任务函数4:接收字符串参数
void Task_4(void *pvParameters)
{
char *pChar = (char *)pvParameters;
ESP_LOGI(TAG, "取得字符串参数为:%s", pChar);
vTaskDelete(NULL);
}

int Parameters_1 = 1;
int Parameters_2[3] = {1, 2, 3};
MyStruct Parameters_3 = {1, {1, 2, 3}};
static const char *Parameters_4 = "Hello World!";

void app_main(void)
{
// 传递整形参数
xTaskCreate(Task_1, "Task_1", 2048, (void *)&Parameters_1, 1, NULL);

// 传递数组参数
xTaskCreate(Task_2, "Task_2", 2048, (void *)&Parameters_2, 1, NULL);

// 传递结构体参数
xTaskCreate(Task_3, "Task_3", 3048, (void *)&Parameters_3, 1, NULL);

// 传递字符串参数(注意这里没有取地址符号&)
xTaskCreate(Task_4, "Task_4", 3048, (void *)Parameters_4, 1, NULL);
}

2.4 FreeRTOS任务优先级

FreeRTOS 中每个任务都有一个优先级,优先级决定了任务的执行顺序。优先级数值较大的任务具有更高的优先级,会在低优先级任务之前被调度执行。当多个任务具有相同优先级时,调度器会使用时间片轮转机制在这些任务之间分配 CPU 时间。

2.4.1 API说明:

任务优先级涉及以下两个API:

函数名 功能 备注
uxTaskPriorityGet 获取任务的当前优先级 返回任务的优先级
vTaskPrioritySet 设置任务的优先级 设置指定任务的优先级

uxTaskPriorityGet:获取任务的优先级

该函数用于获取指定任务的当前优先级。如果任务句柄为 NULL,则返回当前任务的优先级。
原型:

1
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );

参数说明:

  • xTask:任务句柄。如果为 NULL,则返回当前任务的优先级。

返回值:任务的优先级。

vTaskPrioritySet:设置任务的优先级

该函数用于设置指定任务的优先级。如果任务句柄为 NULL,则设置当前任务的优先级.
原型:

1
2
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxPriority );

参数说明:

  • xTask:任务句柄。如果为 NULL,则设置当前任务的优先级。
  • uxPriority:要设置的优先级值。

2.4.2 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void app_main(void)
{
UBaseType_t taskPriority_1 = 0;
UBaseType_t taskPriority_2 = 0;
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;

xTaskCreate(Task_1, "Task_1", 2048, NULL, 12, &taskHandle_1);
taskPriority_1 = uxTaskPriorityGet(taskHandle_1);
ESP_LOGI(TAG, "Task_1 优先级:%d", taskPriority_1);

xTaskCreate(Task_2, "Task_1", 2048, NULL, 12, &taskHandle_2);
taskPriority_2 = uxTaskPriorityGet(taskHandle_2);
ESP_LOGI(TAG, "Task_1 优先级:%d", taskPriority_2);
}

2.5 FreeRTOS任务挂起

FreeRTOS 任务挂起是指暂停任务的执行,直到通过显式恢复操作再次启动任务。挂起操作不会影响任务所占用的资源,仅是暂停任务调度。

任务可以存在于以下状态中:

  • 运行
    当任务实际执行时,它被称为处于运行状态。任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
  • 准备就绪
    准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
  • 阻塞
    如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。 例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束——一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量 事件。处于阻塞状态的任务通常有一个”超时”期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能 被选择进入运行状态。
  • 挂起
    与“阻塞”状态下的任务一样, “挂起”状态下的任务不能 被选择进入运行状态,但处于挂起状态的任务 没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时 才会进入或退出挂起状态。

2.5.1 API说明:

任务挂起涉及以下两个API:

函数名 功能 备注
vTaskSuspend 挂起任务 将指定任务挂起
xTaskResume 恢复任务 恢复指定任务的执行

vTaskSuspend:挂起任务

vTaskSuspend() 用于挂起指定任务,任务被挂起后无法再执行,直到通过 xTaskResume() 恢复任务。

原型:

1
void vTaskSuspend(TaskHandle_t xTask);

参数说明:

  • xTask:要挂起的任务句柄。如果传递 NULL,则挂起当前任务。

xTaskResume:恢复任务

用于恢复一个挂起的任务。恢复任务后,任务重新进入准备就绪状态,等待调度器调度。

原型:

1
BaseType_t xTaskResume(TaskHandle_t xTask);

参数说明:

  • xTask:要恢复的任务句柄。如果传递 NULL,则恢复当前任务。

2.5.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
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "main";

TaskHandle_t taskHandle_1 = NULL;

// 任务1:定时打印日志
void Task_1(void *pvParameters)
{
while (1)
{
ESP_LOGI(TAG, "任务1正在运行...");
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒
}
}

// 任务2:控制任务1的挂起与恢复
void Task_2(void *pvParameters)
{
while (1)
{
ESP_LOGI(TAG, "挂起任务1...");
vTaskSuspend(taskHandle_1); // 挂起任务1
vTaskDelay(pdMS_TO_TICKS(5000)); // 延迟5秒

ESP_LOGI(TAG, "恢复任务1...");
vTaskResume(taskHandle_1); // 恢复任务1
vTaskDelay(pdMS_TO_TICKS(5000)); // 再延迟5秒
}
}

void app_main(void)
{
// 创建任务1
xTaskCreate(Task_1, "Task_1", 2048, NULL, 5, &taskHandle_1);
// 创建任务2
xTaskCreate(Task_2, "Task_2", 2048, NULL, 5, NULL);
}

2.6 系统任务信息显示

FreeRTOS 提供了多种方法来显示和分析任务信息,帮助开发者了解系统运行状况、优化性能以及定位问题。

系统任务信息显示主要有以下两个API:

函数名 功能 备注
vTaskList 输出任务列表 需开启配置才能使用
uxTaskGetStackHighWaterMark 获取任务最小剩余栈空间 需开启配置才能使用

2.6.1 API介绍

vTaskList: 输出任务列表

可通过 vTaskList() 来协助分析操作系统当前 task 状态,以帮助优化内存,帮助定位栈溢出问题,帮助理解和学习操作系统原理相关知识。

原型:

1
void vTaskList( char *pcWriteBuffer );

注意:configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS必须在 FreeRTOSConfig.h 中定义为 1,才可使用此函数。
对于ESP32来说。使用 vTaskList() 前需使能:

menuconfig -> Component config -> FreeRTOS -> Kernel->configUSE_TRACE_FACILITY
menuconfig -> Component config -> FreeRTOS -> Kernel->configUSE_STATS_FORMATTING_FUNCTIONS

如下图:

uxTaskGetStackHighWaterMark:获取任务最小剩余栈空间

用于获取任务在运行期间的最小剩余栈空间(即栈的高水位标记)。此函数可帮助检测任务是否存在栈溢出的风险。

原型

1
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );

参数:

  • xTask:任务句柄。如果传递 NULL,则返回当前任务的栈高水位标记。

返回值: 剩余栈空间的字数(单位为字),(但是在ESP-IDF里为字节)

示例:

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
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

void Task_1(void *pvParameters)
{
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "Task_1");
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "Task_2");
}
vTaskDelete(NULL);
}

void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;

xTaskCreate(Task_1, "Task_1", 2048, NULL, 12, &taskHandle_1);
xTaskCreate(Task_2, "Task_1", 2048, NULL, 12, &taskHandle_2);


// 输出任务列表
static char cBuffer[512]={0};
vTaskList(cBuffer);
ESP_LOGI(TAG, "任务列表:\n%s", cBuffer);

while (1)
{
int istack = uxTaskGetStackHighWaterMark(taskHandle_1);
ESP_LOGI(TAG, "Task_1 剩余栈空间:%d", istack);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

效果:

2.7 FreeRTOS 任务看门狗

Task看门狗(Task Watchdog)是一种常用的任务监控机制,用于检测任务运行是否正常以及是否陷入死循环或长时间阻塞。
FreeRTOS 本身并不直接提供“看门狗”功能,需要我们使用软件自行实现, FreeRTOS 提供了 钩子函数,如 vApplicationIdleHook() 和 vApplicationTickHook(),可以在这些钩子函数中检查任务状态或者复位看门狗。

由于看门狗并不是FreeRTOS的原始功能,这里就不展开描述了,写这部分的目的是让大家知道这里应该有这个东西。

三、FreeRTOS软件定时器

FreeRTOS 软件定时器允许在任务上下文之外的时间间隔内执行函数,是一种高效的非阻塞操作方式。

FreeRTOS软件定时器主要有以下几个API:

函数名 功能 备注
xTimerCreate 创建定时器 创建一次性或周期性定时器
xTimerStart 启动定时器 定时器开始计时
xTimerStop 停止定时器 停止定时器计时
xTimerChangePeriod 修改定时器周期 可用于改变定时器运行间隔
xTimerDelete 删除定时器 释放定时器资源
xTimerReset 重置定时器
以下是主要 API 的详细说明:

3.1 API说明:

xTimerCreate

功能: 创建一个软件定时器。

原型:

1
2
3
4
5
6
7
8

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

参数说明:

  • pcTimerName:定时器的名称,仅用于调试,可为 NULL
  • xTimerPeriodInTicks:定时器的时间周期(以系统 tick 为单位)。
  • uxAutoReload:是否为自动重载模式,值为 pdTRUE(周期性定时器)或 pdFALSE(一次性定时器)。
  • pvTimerID:用户定义的定时器标识,用于在回调函数中区分不同定时器。
  • pxCallbackFunction:定时器触发时调用的回调函数。

返回值:

成功返回 TimerHandle_t(定时器句柄),失败返回 NULL。-

xTimerStart

功能: 启动一个软件定时器。

原型:

1
2

BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );

参数说明:

  • xTimer:定时器句柄,由 xTimerCreate 返回。
  • xTicksToWait:阻塞等待时间(以系统 tick 为单位)。通常为 0。
    返回值:
    pdPASS 表示启动成功,pdFAIL 表示失败。

xTimerStop

功能: 停止一个正在运行的定时器。

原型:

1
2

BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );

参数说明:

xTimer:定时器句柄。
xTicksToWait:阻塞等待时间。
返回值:
pdPASS 表示停止成功,pdFAIL 表示失败。

xTimerChangePeriod

功能: 改变定时器的触发周期。

原型:

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

参数说明:

  • xTimer:定时器句柄。
  • xNewPeriod:新的触发周期(以系统 tick 为单位)。
  • xTicksToWait:阻塞等待时间。

返回值: pdPASS 表示成功,pdFAIL 表示失败。

xTimerDelete

功能: 删除一个定时器并释放其资源。

原型:

1
BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

参数说明:

  • xTimer:定时器句柄。
  • xTicksToWait:阻塞等待时间。
    返回值: pdPASS 表示删除成功,pdFAIL 表示失败。

xTimerReset

功能: 重置定时器并重新开始计时。

原型:

1
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );

参数说明:

  • xTimer:定时器句柄。
  • xTicksToWait:阻塞等待时间。
    返回值:pdPASS 表示成功,pdFAIL 表示失败。

3.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
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"

static const char *TAG = "main";

// 定时器回调
void TimerCallback_1(TimerHandle_t xTimer)
{
// 取到定时器的名字
const char *timerName = pcTimerGetName(xTimer);
// 取得定时器的ID
void *timerID = pvTimerGetTimerID(xTimer);
ESP_LOGI(TAG, "定时器名称:%s,定时器id:%d,定时器回调函数执行", timerName, (int)timerID);
}

void app_main(void)
{
// 创建定时器句柄
TimerHandle_t xTimer_1;
TimerHandle_t xTimer_2;
xTimer_1 = xTimerCreate("timer1", pdMS_TO_TICKS(1000), pdTRUE, (void *)0, TimerCallback_1);
xTimer_2 = xTimerCreate("timer2", pdMS_TO_TICKS(2000), pdTRUE, (void *)1, TimerCallback_1);

xTimerStart(xTimer_1, 0); // 启动定时器
xTimerStart(xTimer_2, 0); // 启动定时器

vTaskDelay(pdMS_TO_TICKS(5000));
//停止定时器
xTimerStop(xTimer_1, 0);
xTimerStop(xTimer_2, 0);
}

四、FreeRTOS任务间通信

4.1 队列

4.1.1 队列创建与传参

队列是任务间通信的主要形式。它们可以用于在任务之间 以及中断和任务之间发送消息。在大多数情况下,队列用作线程安全的 FIFO(先进先出)缓冲区, 新数据被发送到队列的后面,但也可以发送到前面。

## 4.1.1.1 API说明:

队列操作主要涉及以下几个API:

函数名 功能 备注
xQueueCreate 创建一个队列 创建指定长度和大小的队列
xQueueSend 向队列中发送数据 如果队列已满,任务可选择阻塞或立即返回
xQueueReceive 从队列中接收数据 如果队列为空,任务可选择阻塞或立即返回
xQueueSendToFront 将数据发送到队列的队头位置 与 xQueueSend 类似,但优先级更高
xQueueSendToBack 将数据发送到队列的队尾位置 默认行为,等效于 xQueueSend
uxQueueMessagesWaiting 查询队列中当前等待的消息数量 返回队列中尚未读取的消息数
xQueueCreate:创建队列

原型:

1
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);

参数说明:

  • uxQueueLength:队列的长度(可以容纳的元素数量)。
  • uxItemSize:队列中每个元素的大小(以字节为单位)。
    返回值: 成功时返回队列句柄;失败时返回 NULL。
    示例:
1
2
3
4
5
QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(int)); // 创建一个可以存储 10 个整数的队列
if (xQueue == NULL) {
// 队列创建失败,处理错误
}
xQueueSend:向队列发送数据

原型:

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

参数说明:

  • xQueue:队列的句柄。
  • pvItemToQueue:指向要发送到队列的数据的指针。
  • xTicksToWait:当队列已满时,任务等待的时间(以 tick 为单位)。设置为 0 表示不等待。

返回值:

  • pdPASS:数据成功发送到队列。
  • errQUEUE_FULL:队列已满,数据发送失败。
    示例:
1
2
3
4
int data = 42;
if (xQueueSend(xQueue, &data, 0) != pdPASS) {
// 数据发送失败,处理错误
}
xQueueReceive:从队列接收数据

原型:

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

参数说明:

  • xQueue:队列的句柄。
  • pvBuffer:指向接收数据的缓冲区的指针。
  • xTicksToWait:当队列为空时,任务等待的时间(以 tick 为单位)。设置为 0 表示不等待。
    返回值:
  • pdPASS:数据成功接收。
  • pdFALSE:队列为空,接收失败。
    示例:
    1
    2
    3
    4
    int receivedData;
    if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
    // 成功接收数据,进行处理
    }

4.1.1.2 队列传参示例:

1.队列传参-常量
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 0;

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0)!= pdPASS) {
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");
i++;
}

if(i == 10)
{
i = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
int receivedData;
if (xQueueReceive(xQueue, &receivedData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%d", receivedData);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);

}
vTaskDelete(NULL);
}

void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;

// 创建队列
xQueue = xQueueCreate(10, sizeof(int));

if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

// 定义结构体
typedef struct
{
int id;
int data[3];
} MyStruct;

void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
MyStruct shendData = {1, {1, 2, 3}};

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &shendData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");
shendData.id++;
}

if (shendData.id == 10)
{
shendData.id = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
MyStruct receivedData;
if (xQueueReceive(xQueue, &receivedData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%d-[%d,%d,%d]", receivedData.id, receivedData.data[0], receivedData.data[1], receivedData.data[2]);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);

}
vTaskDelete(NULL);
}

void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;

// 创建队列
xQueue = xQueueCreate(10, sizeof(MyStruct));

if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}

3.队列传参-指针
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 队列传参_指针: 一般用于传递占用内存较大的数据. 传递指针, 可以避免拷贝数据, 提高效率.
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 0;

for (;;)
{
char *pCharSend = (char *)malloc(50); // 申请内存
snprintf(pCharSend, 50, "Hello World! - %d", i);
i++;
// 发送数据到队列
if (xQueueSend(xQueue, &pCharSend, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");

}

if (i == 10)
{
i = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;

char *pCharReceived = NULL; // 接收数据
for (;;)
{
if (xQueueReceive(xQueue, &pCharReceived, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%s", pCharReceived);
free(pCharReceived); // 释放内存
}
vTaskDelay(1000 / portTICK_PERIOD_MS);

}
vTaskDelete(NULL);
}

void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;

// 创建队列
xQueue = xQueueCreate(10, sizeof(char *));

if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}

4.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// 队列多进单出: 任务1和任务2发送数据到队列,任务3接收数据  任务3的优先级高于任务1和任务2,已达到数据监听的目的
// 参考:https://www.bilibili.com/video/BV1R44y177VS/?spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=ef5a0ab0106372751602034cdd9ab98e

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 111;

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 222;

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务2数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务2数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_3(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
int receivedData;
// 使用portMAX_DELAY阻塞等待数据
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) != pdPASS)
{
ESP_LOGI(TAG, "任务3数据接收失败");
}
else
{
ESP_LOGI(TAG, "任务3数据接收成功,数据为:%d", receivedData);
}
}
vTaskDelete(NULL);
}

void app_main(void)
{

QueueHandle_t xQueue;

// 创建队列
xQueue = xQueueCreate(10, sizeof(int));

if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 1, NULL);
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 1, NULL);
// 接收数据任务
xTaskCreate(Task_3, "Task_1", 1024 * 4, (void *)xQueue, 2, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}

4.1.3 队列集合

FreeRTOS 队列集合(Queue Sets)是一种机制,用于同时监听多个队列和信号量,以实现任务间的高效通信。

4.1.3.1 API说明:

函数名 功能 备注
xQueueCreateSet 创建一个队列集合 队列集合必须与队列或信号量配合使用
xQueueAddToSet 将队列或信号量添加到队列集合 被添加的队列或信号量必须为空
xQueueRemoveFromSet 从队列集合中移除队列或信号量
xQueueSelectFromSet 从队列集合中选择一个有数据可用的队列或信号量 返回非空的队列或信号量

4.1.3.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 队列集合:
// https : // www.bilibili.com/video/BV1zq4y1m7UK?spm_id_from=333.788.videopod.sections&vd_source=ef5a0ab0106372751602034cdd9ab98e

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 111;

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 222;

for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务2数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务2数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_3(void *pvParameters)
{
// 取得队列集合句柄
QueueSetHandle_t xQueueSet = (QueueSetHandle_t)pvParameters;
// 取得有数据队列句柄
QueueSetMemberHandle_t QueueData;
for (;;)
{
QueueData = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if (QueueData != NULL)
{
int i;
if (xQueueReceive(QueueData, &i, portMAX_DELAY) != pdPASS)
{
ESP_LOGI(TAG, "任务3数据接收失败");
}
else
{
ESP_LOGI(TAG, "任务3数据接收成功,数据为:%d", i);
}
}
}
vTaskDelete(NULL);
}

void app_main(void)
{
QueueHandle_t xQueue_1;
QueueHandle_t xQueue_2;
// 创建队列
xQueue_1 = xQueueCreate(10, sizeof(int));
xQueue_2 = xQueueCreate(10, sizeof(int));

// 创建队列集合
QueueSetHandle_t xQueueSet;
xQueueSet = xQueueCreateSet(20);

// 将队列添加到队列集合
xQueueAddToSet(xQueue_1, xQueueSet);
xQueueAddToSet(xQueue_2, xQueueSet);

if ((xQueue_1 != NULL )&& (xQueue_2 != NULL) && (xQueueSet != NULL))
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue_1, 1, NULL);
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue_2, 1, NULL);
// 接收数据任务
xTaskCreate(Task_3, "Task_1", 1024 * 4, (void *)xQueueSet, 2, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}

4.1.4 队列邮箱

FreeRTOS的邮箱概念跟别的RTOS不一样,它是一个队列,队列长度只有1.写邮箱:新数据覆盖旧数据,读邮箱:读数据时,数据不会被移除;这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 队列邮箱: FreeRTOS的邮箱概念跟别的RTOS不一样,它是一个队列,队列长度只有1.
// 写邮箱:新数据覆盖旧数据,读邮箱:读数据时,数据不会被移除;
// 这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。
// https : // www.bilibili.com/video/BV1zq4y1m7UK?spm_id_from=333.788.videopod.sections&vd_source=ef5a0ab0106372751602034cdd9ab98e

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

static const char *TAG = "main";

// 写数据队列
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;

for (;;)
{
// 发送数据到队列
if (xQueueOverwrite(Mailbox, &i) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
i++;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}


// 读数据队列
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;

int i = 0;

for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务2数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务2数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_3(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;

for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务3数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务3数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void Task_4(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;

for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务4数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务4数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}

void app_main(void)
{
QueueHandle_t Mailbox; // 创建邮箱

// 创建队列(注意:队列长度只有1)
Mailbox = xQueueCreate(1, sizeof(int));

if ((Mailbox != NULL) )
{
ESP_LOGI(TAG, "队列创建成功");
// 写数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)Mailbox, 2, NULL);
xTaskCreate(Task_2, "Task_2", 1024 * 4, (void *)Mailbox, 1, NULL);
xTaskCreate(Task_3, "Task_3", 1024 * 4, (void *)Mailbox, 1, NULL);
xTaskCreate(Task_4, "Task_4", 1024 * 4, (void *)Mailbox, 1, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}

4.2 信号量、互斥锁

FreeRTOS 提供了信号量和互斥锁,用于任务间的同步和资源共享管理。信号量更偏向于任务同步,而互斥锁用于保护共享资源。

4.2.1 二进制信号量

二进制信号量是最基本的信号量,仅有两个状态:可用和不可用(或 1 和 0)。
通常用于任务之间或中断与任务之间的同步,当一个事件发生时,由中断或任务释放信号量,等待信号量的任务就会被唤醒。
二进制信号量适用于简单的事件通知场景,比如通知某个任务处理外部输入或完成某项任务。

4.2.1.1 API说明:

4.2.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
53
54
55
56
57
58
59
60
// 二进制信号量
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "main";

// 二进制信号量
SemaphoreHandle_t semaphoreHandle;

// 公共变量
int shareVariable = 0;

void task1(void *pvParameters)
{
for (;;)
{
// 获取信号量,信号量变为0
xSemaphoreTake(semaphoreHandle, portMAX_DELAY);
for (int i = 0; i < 10; i++)
{
shareVariable++;
ESP_LOGI(TAG, "task1 shareVariable:%d", shareVariable);
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 释放信号量,信号量变为1
xSemaphoreGive(semaphoreHandle);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

void task2(void *pvParameters)
{
for (;;)
{
// 获取信号量
xSemaphoreTake(semaphoreHandle, portMAX_DELAY);
for (int i = 0; i < 10; i++)
{
shareVariable++;
ESP_LOGI(TAG, "task2 shareVariable:%d", shareVariable);
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 释放信号量,信号量变为1
xSemaphoreGive(semaphoreHandle);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

void app_main(void)
{
semaphoreHandle = xSemaphoreCreateBinary();
xSemaphoreGive(semaphoreHandle);

// 创建任务
xTaskCreate(task1, "task1", 1024 * 2, NULL, 10, NULL);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 10, NULL);
}

4.2.2 计数信号量

4.2.2.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
53
54
55
56
57
58
59
60
// 计数型信号量(占座)
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "main";

// 信号量
SemaphoreHandle_t semaphoreHandle;


// 占座任务
void task1(void *pvParameters)
{
// 定义空位
int seat = 0;
for (;;)
{
// 获取信号量
seat = uxSemaphoreGetCount(semaphoreHandle);
// 输出空位
ESP_LOGI(TAG, "当前空位:%d", seat);

// 获取信号量(占座)
if (xSemaphoreTake(semaphoreHandle, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "占座成功");
}
else
{
ESP_LOGI(TAG, "占座失败");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

// 离开座位任务
void task2(void *pvParameters)
{
for (;;)
{
vTaskDelay(pdMS_TO_TICKS(6000));
// 释放信号量
xSemaphoreGive(semaphoreHandle);
ESP_LOGI(TAG, "释放座位");
}
}

void app_main(void)
{
semaphoreHandle = xSemaphoreCreateCounting(5, 5);

// 创建占座任务
xTaskCreate(task1, "task1", 1024 * 2, NULL, 10, NULL);
// 创建离开座位任务
xTaskCreate(task2, "task2", 1024 * 2, NULL, 10, NULL);
}

4.2.3 互斥量

互斥量(互斥锁Mutex):互斥锁和二进制信号量极为相似,但 有一些细微差异:互斥锁具有优先级继承机制, 但二进制信号量没有。

4.2.3.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "main";

SemaphoreHandle_t mutexHandle;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "task1启动!");
while (1)
{
if (xSemaphoreTake(mutexHandle, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI(TAG, "task1获取到互斥量!");
for (int i = 0; i < 10; i++)
{
ESP_LOGI(TAG, "task1执行中...%d", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(mutexHandle);
ESP_LOGI(TAG, "task1释放互斥量!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
else
{
ESP_LOGI(TAG, "task1获取互斥量失败!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "task2启动!");
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
}
}

void task3(void *pvParameters)
{
ESP_LOGI(TAG, "task3启动!");
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
if (xSemaphoreTake(mutexHandle, 1000) == pdPASS)
{
ESP_LOGI(TAG, "task3获取到互斥量!");
for (int i = 0; i < 10; i++)
{
ESP_LOGI(TAG, "task3执行中...%d",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}

xSemaphoreGive(mutexHandle);
ESP_LOGI(TAG, "task3释放互斥量!");
vTaskDelay(pdMS_TO_TICKS(5000));
}
else
{
ESP_LOGI(TAG, "task3未获取到互斥量!");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}

void app_main(void)
{
mutexHandle = xSemaphoreCreateMutex();
// mutexHandle = xSemaphoreCreateBinary();
xTaskCreate(task1, "task1", 1024 * 2, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 2, NULL);
xTaskCreate(task3, "task3", 1024 * 2, NULL, 3, NULL);
}

4.2.4 递归互斥量

非递归互斥锁只能被一个任务 获取一次,如果同一个任务想再次获取则会失败, 因为当任务第一次释放互斥锁时,互斥锁就一直处于释放状态。与非递归互斥锁相反,递归互斥锁可以被同一个任务获取很多次, 获取多少次就需要释放多少次, 此时才会返回递归互斥锁。

4.2.4.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "main";

SemaphoreHandle_t mutexHandle;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");
while (1)
{
xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY);
ESP_LOGI(TAG, "task1获取到互斥量-使用资源A");
for (int i = 0; i < 10; i++)
{
ESP_LOGI(TAG, "task1执行中...%d -使用资源A", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}

xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY);
ESP_LOGI(TAG, "task1获取到互斥量-使用资源B");
for (int i = 0; i < 10; i++)
{
ESP_LOGI(TAG, "task1执行中...%d -使用资源B", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}

xSemaphoreGiveRecursive(mutexHandle);
ESP_LOGI(TAG, "task1释放互斥量-使用资源B");
vTaskDelay(pdMS_TO_TICKS(3000));
xSemaphoreGiveRecursive(mutexHandle);
ESP_LOGI(TAG, "task1释放互斥量-使用资源A");
}
vTaskDelete(NULL);
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "task2启动!");
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
// 获取递归互斥锁
if (xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI(TAG, "task2获取到互斥量");
for (int i = 0; i < 10; i++)
{
ESP_LOGI(TAG, "task2执行中...%d", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGiveRecursive(mutexHandle);
ESP_LOGI(TAG, "task2释放互斥量");
}
else
{
ESP_LOGI(TAG, "task2获取互斥量失败");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}

void app_main(void)
{
// 创建递归互斥量
mutexHandle = xSemaphoreCreateRecursiveMutex();

xTaskCreate(task1, "task1", 1024 * 2, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 2, NULL);
}

4.3 事件组

事件组是一种实现任务间通信和同步的机制,主要用于协调多个任务或中断之间的执行。

事件位(事件标志)
事件位用于指示事件 是否发生。事件位通常称为事件标志。例如, 应用程序可以:
定义一个位(或标志), 设置为 1 时表示“已收到消息并准备好处理”, 设置为 0 时表示“没有消息等待处理”。
定义一个位(或标志), 设置为 1 时表示“应用程序已将准备发送到网络的消息排队”, 设置为 0 时表示 “没有消息需要排队准备发送到网络”。
定义一个位(或标志), 设置为 1 时表示“需要向网络发送心跳消息”, 设置为 0 时表示“不需要向网络发送心跳消息”。

事件组
事件组就是一组事件位。事件组中的事件位 通过位编号来引用。同样,以上面列出的三个例子为例:
事件标志组位编号 为 0 表示“已收到消息并准备好处理”。
事件标志组位编号 为 1 表示“应用程序已将准备发送到网络的消息排队”。
事件标志组位编号 为 2 表示“需要向网络发送心跳消息”。

事件组和事件位数据类型

事件组由 EventGroupHandle_t 类型的变量引用。

如果 configUSE_16_BIT_TICKS 设为 1,则事件组中存储的位(或标志)数为 8; 如果 configUSE_16_BIT_TICKS 设为 0,则为 24。 configUSE_16_BIT_TICKS 的值取决于 任务内部实现中用于线程本地存储的数据类型。

(ESP-IDF中默认为24位)

事件组中的所有事件位都 存储在 EventBits_t 类型的单个无符号整数变量中。事件位 0 存储在位 0 中, 事件位 1 存储在位1 中,依此类推。

下图表示一个 24 位事件组, 使用 3 个位来保存前面描述的 3 个示例事件。在图片中,仅设置了 事件位 2。(包含 24 个事件位的事件组,其中只有三个在使用中)

事件组 RTOS API 函数

提供的事件组 API 函数 允许任务在事件组中设置一个或多个事件位, 清除事件组中的一个或多个事件位, 并挂起(进入阻塞状态, 因此任务不会消耗任何处理时间)以等待 事件组中一个或多个事件位固定下来。

事件组也可用于同步任务, 创建通常称为“集合”的任务。任务同步点是 应用程序代码中的一个位置,在该位置任务将在 阻塞状态(不消耗任何 CPU 时间)下等待,直到参与同步的所有其他任务 也到达其同步点。

4.3.1 API说明:

事件组操作主要涉及以下几个 API:

函数名 功能 备注
xEventGroupCreate 创建一个事件组 返回一个事件组句柄,供后续操作使用
xEventGroupSetBits 设置一个或多个事件标志 用于通知其他任务某些事件已发生
xEventGroupClearBits 清除一个或多个事件标志 用于复位事件标志,防止重复触发
xEventGroupWaitBits 等待一个或多个事件标志的设置状态 任务可以选择阻塞,直到指定事件发生
xEventGroupGetBits 查询当前事件组的状态 返回事件组中所有事件标志的当前状态
xEventGroupSync 同步多个任务 用于实现多个任务在同一时刻达到某一同步点后继续执行
xEventGroupCreate:创建事件组

原型:

1
EventGroupHandle_t xEventGroupCreate(void);

返回值:成功时返回事件组句柄;失败时返回 NULL。

示例:

1
2
3
4
5
EventGroupHandle_t xEventGroup;
xEventGroup = xEventGroupCreate();
if (xEventGroup == NULL) {
// 创建事件组失败,处理错误
}
xEventGroupSetBits:设置事件标志

原型:

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

参数说明

  • xEventGroup:事件组句柄。
  • uxBitsToSet:需要设置的事件标志位(按位表示,例如 0x01 设置第 0 位)。
    返回值:返回事件组在调用前的状态。
xEventGroupWaitBits:等待事件标志

读取 RTOS 事件组中的位,选择性地进入“阻塞”状态(已设置 超时值)以等待设置单个位或一组位。无法从中断调用此函数。

原型:

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

参数说明:

  • xEventGroup:事件组句柄。
  • uxBitsToWaitFor:需要等待的事件标志位(按位表示)。
  • xClearOnExit:是否在退出等待时清除指定的事件标志。
  • xWaitForAllBits:是否等待所有指定事件标志都被设置,还是任意一个即可。
  • xTicksToWait:等待的最大时间(以 Tick 为单位,portMAX_DELAY 表示无限等待)。

返回值:返回当前满足条件的事件标志状态。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(
xEventGroup, // 事件组句柄
0x03, // 等待第 0 位和第 1 位
pdTRUE, // 退出等待时清除事件标志
pdFALSE, // 等待任意一个事件
portMAX_DELAY // 无限等待
);

if (uxBits & 0x01) {
// 第 0 位事件发生
}

if (uxBits & 0x02) {
// 第 1 位事件发生
}
xEventGroupSync:同步任务

以原子方式设置 RTOS 事件组中的位(标志),然后等待在同一事件组中设置位的组合。此功能通常用于同步多个任务(通常称为任务集合),其中每个任务必须等待其他任务到达同步点后才能继续。

原型

1
2
3
4
5
6
EventBits_t xEventGroupSync(
EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait
);

参数说明

  • xEventGroup:事件组句柄。
  • uxBitsToSet:当前任务设置的事件标志位。
  • uxBitsToWaitFor:需要等待的其他任务设置的事件标志位。
  • xTicksToWait:最大等待时间。
    返回值
    返回事件组的当前状态。

示例

1
2
3
4
5
6
xEventGroupSync(
xEventGroup, // 事件组句柄
0x01, // 当前任务设置第 0 位
0x03, // 等待第 0 位和第 1 位都被设置
portMAX_DELAY // 无限等待
);

4.3.2 示例程序:

1. 事件组等待

task1等待task2设置事件位,然后执行程序:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 事件组
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

static const char *TAG = "main";

EventGroupHandle_t xCreatedEventGroup;

#define BIT_0 (1 << 0)
#define BIT_4 (1 << 4)

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

while (1)
{
EventBits_t uxBits;
uxBits = xEventGroupWaitBits(
xCreatedEventGroup, /* The event group being tested. */
BIT_0 | BIT_4, /* The bits within the event group to wait for. */
pdTRUE, /* BIT_0 & BIT_4 should be cleared before returning. */
pdFALSE, /* Don't wait for both bits, either bit will do. */
portMAX_DELAY); /* Wait a maximum of 100ms for either bit to be set. */

if ((uxBits & (BIT_0 | BIT_4)) == (BIT_0 | BIT_4))
{
ESP_LOGI(TAG, "BIT_0 和 BIT_4 都被设置了");
}
else
{
ESP_LOGI(TAG, "BIT_0 和 BIT_4 有一个被设置了");
}
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "task2启动!");
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
xEventGroupSetBits(xCreatedEventGroup, BIT_0);
ESP_LOGI(TAG, "BIT_0 被设置");
vTaskDelay(pdMS_TO_TICKS(3000));
xEventGroupSetBits(xCreatedEventGroup, BIT_4);
ESP_LOGI(TAG, "BIT_4 被设置");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}

void app_main(void)
{

// 创建事件组
xCreatedEventGroup = xEventGroupCreate();

if (xCreatedEventGroup == NULL)
{
ESP_LOGE(TAG, "创建事件组失败");
}
else
{
xTaskCreate(task1, "task1", 1024 * 2, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 1, NULL);
}
}
2. 事件组同步

每个任务在启动后,等待一段时间,然后调用xEventGroupSync函数进行事件同步,等待所有任务的事件位都被设置。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 事件组
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

/* Bits used by the three tasks. */
#define TASK_0_BIT (1 << 0)
#define TASK_1_BIT (1 << 1)
#define TASK_2_BIT (1 << 2)

#define ALL_SYNC_BITS (TASK_0_BIT | TASK_1_BIT | TASK_2_BIT)

static const char *TAG = "main";
EventGroupHandle_t xEventBits;


void task0(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task0启动!");

while (1)
{
vTaskDelay(pdMS_TO_TICKS(3000));
ESP_LOGI(TAG, "task0: 任务同步开始");
// 事件同步
xEventGroupSync(
xEventBits, /* The event group being tested. */
TASK_0_BIT, /* The bits within the event group to wait for. */
ALL_SYNC_BITS, /* The bits within the event group to wait for. */
portMAX_DELAY); /* Wait a maximum of 100ms for either bit to be set. */

ESP_LOGI(TAG, "task0: 任务同步完成");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

while (1)
{
vTaskDelay(pdMS_TO_TICKS(4000));
ESP_LOGI(TAG, "task1: 任务同步开始");

// 事件同步
xEventGroupSync(
xEventBits, /* The event group being tested. */
TASK_1_BIT, /* The bits within the event group to wait for. */
ALL_SYNC_BITS, /* The bits within the event group to wait for. */
portMAX_DELAY); /* Wait a maximum of 100ms for either bit to be set. */

ESP_LOGI(TAG, "task1: 任务同步完成");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task2启动!");

while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "task2: 任务同步开始");
// 事件同步
xEventGroupSync(
xEventBits, /* The event group being tested. */
TASK_2_BIT, /* The bits within the event group to wait for. */
ALL_SYNC_BITS, /* The bits within the event group to wait for. */
portMAX_DELAY); /* Wait a maximum of 100ms for either bit to be set. */

ESP_LOGI(TAG, "task2: 任务同步完成");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}


void app_main(void)
{
// 创建事件组
xEventBits = xEventGroupCreate();

if (xEventBits == NULL)
{
ESP_LOGE(TAG, "创建事件组失败");
}
else
{
xTaskCreate(task0, "task0", 1024 * 2, NULL, 1, NULL);
xTaskCreate(task1, "task1", 1024 * 2, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 1, NULL);
}
}

4.4 任务通知

任务通知(Task Notifications)是一种轻量级的任务间通信和同步机制,它比队列或事件组更加高效,因为它不需要动态分配内存。每个任务都内置了一个任务通知值,其他任务或中断服务例程(ISR)可以用它来通知该任务事件的发生。

特点

  • 每个任务都有一个任务通知值(32 位整数),可以用于存储信息。
  • 任务通知值默认初始化为 0。
  • 一个任务可以通过 通知函数 来操作另一个任务的通知值。
  • 可以将任务通知值用作二值信号量、计数信号量或简单的 32 位变量。
  • 当一个任务收到通知时,它可以选择阻塞(等待通知)或处理该通知。

4.4.1 API说明:

任务通知主要涉及以下几个 API:

函数名 功能
xTaskNotify 向指定任务发送通知,通知值可以被覆盖
xTaskNotifyGive 简化版通知函数,用于发送信号量通知
xTaskNotifyWait 等待通知值更新,并选择是否清除通知值
ulTaskNotifyTake 等待任务通知并自动减少通知值(通常用于计数信号量)
xTaskNotifyStateClear 清除任务的通知状态

xTaskNotify

功能: 向指定任务发送通知,并修改该任务的通知值。

原型

1
2
3
4
5
BaseType_t xTaskNotify(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
);

参数

  • xTaskToNotify:接收通知的任务句柄。
  • ulValue:要发送的通知值。
  • eAction:通知值的操作类型,可以是以下值:
  • eSetBits:设置通知值的指定位。
  • eIncrement:通知值递增。
  • eSetValueWithOverwrite:覆盖通知值。
  • eSetValueWithoutOverwrite:如果通知值未处理,则不覆盖当前值。
    返回值
  • pdPASS:操作成功。
  • pdFAIL:操作失败(通常在 eSetValueWithoutOverwrite 时发生)。

示例

1
xTaskNotify(xTaskHandle, 0x01, eSetBits); // 设置任务通知值的第 0 位

xTaskNotifyGive

功能: 向任务发送一个 “信号量风格” 的通知,等效于 xTaskNotify() 的 eIncrement 模式。

原型

1
2

void xTaskNotifyGive(TaskHandle_t xTaskToNotify);

参数

  • xTaskToNotify:接收通知的任务句柄。
    示例:
    1
    xTaskNotifyGive(xTaskHandle); // 发送通知,通知值递增 1

ulTaskNotifyTake

功能: 任务等待通知,并在接收到通知时自动减少通知值(通常用于实现计数信号量)。

原型

1
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);

参数

  • xClearCountOnExit:
    pdTRUE:退出等待时将通知值清零。
    pdFALSE:退出等待时保留剩余通知值。
  • xTicksToWait:等待通知的时间(Tick 数,portMAX_DELAY 表示无限等待)。
    返回值:返回通知值(如果是计数信号量,表示剩余信号量计数)。

示例

1
2
3
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY) > 0) {
// 收到通知,处理任务
}

xTaskNotifyWait

功能: 任务等待通知,可以选择获取通知值的内容,并决定是否清除通知值。
原型

1
2
3
4
5
6
7

BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait
);

参数

  • ulBitsToClearOnEntry:进入等待时清除的通知值位。
  • ulBitsToClearOnExit:退出等待时清除的通知值位。
  • pulNotificationValue:保存通知值的指针。
  • xTicksToWait:等待时间(Tick 数)。
    返回值
  • pdTRUE:收到通知。
  • pdFALSE:超时未收到通知。
    示例
    1
    2
    3
    4
    uint32_t ulNotificationValue;
    if (xTaskNotifyWait(0x00, 0xFFFFFFFF, &ulNotificationValue, portMAX_DELAY) == pdTRUE) {
    // 收到通知,ulNotificationValue 保存通知值
    }

xTaskNotifyStateClear

功能: 清除任务的通知状态(即标记任务为“未通知”)。

原型

1
void vTaskNotifyStateClear(TaskHandle_t xTask);

参数:xTask:需要清除通知状态的任务句柄。
示例

1
vTaskNotifyStateClear(xTaskHandle); // 清除通知状态

4.4.2 示例代码:

1.直接任务通知

由task2控制task1的运行:

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
// 事件组
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

static TaskHandle_t xTask1 = NULL, xTask2 = NULL;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

while (1)
{
ESP_LOGI(TAG, "task1: 等待task通知");
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

ESP_LOGI(TAG, "task1: 收到task通知");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task2启动!");

while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "task2: 发送task通知");
xTaskNotifyGive(xTask1);
}
}

void app_main(void)
{

xTaskCreate(task1, "task1", 1024 * 2, NULL, 1, &xTask1);
xTaskCreate(task2, "task2", 1024 * 2, NULL, 1, &xTask2);
}

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
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "main";

static TaskHandle_t xTask1 = NULL, xTask2 = NULL;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

uint32_t ulNotifiedValue;

while (1)
{
ESP_LOGI(TAG, "task1: 等待task通知");
xTaskNotifyWait(0x00, ULONG_MAX, &ulNotifiedValue, portMAX_DELAY);
// 通过不同的bit位来判断通知的来源
if ((ulNotifiedValue & 0x01) != 0)
{
ESP_LOGI(TAG, "task1: 收到task通知-bit0");
}
if ((ulNotifiedValue & 0x02) != 0)
{
ESP_LOGI(TAG, "task1: 收到task通知-bit1");
}
if ((ulNotifiedValue & 0x04) != 0)
{
ESP_LOGI(TAG, "task1: 收到task通知-bit2");
}
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task2启动!");

while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "task2: 发送task通知-bit0");
xTaskNotify(xTask1, 0x01, eSetValueWithOverwrite);

vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "task2: 发送task通知-bit1");
xTaskNotify(xTask1, 0x02, eSetValueWithOverwrite);

vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "task2: 发送task通知-bit2");
xTaskNotify(xTask1, 0x04, eSetValueWithOverwrite);

}
}

void app_main(void)
{

xTaskCreate(task1, "task1", 1024 * 4, NULL, 1, &xTask1);
xTaskCreate(task2, "task2", 1024 * 4, NULL, 1, &xTask2);
}

4.5 流缓冲区和消息缓冲区

4.5.1 流缓冲区

通过流缓冲区,可以将字节流从中断服务程序传递到任务, 也可以将其从一项任务传递到另一项任务。字节流可以是任意长度,不一定 有开头或结尾。可以一次写入任意数量的字节, 也可以一次读取任意数量的字节。数据通过复制的方式传递:数据由发送方复制到缓冲区中, 然后由接收方从缓冲区中读取。

4.5.1.1 API说明

函数名 功能 备注
xStreamBufferCreate 创建一个流数据缓冲区 返回一个缓冲区句柄,供后续使用
xStreamBufferSend 向流缓冲区写入数据 如果缓冲区已满,可选择阻塞或立即返回
xStreamBufferReceive 从流缓冲区读取数据 如果缓冲区为空,可选择阻塞或立即返回
xStreamBufferReset 重置流缓冲区,清空所有数据 需确保无任务正在操作此缓冲区
xStreamBufferSpacesAvailable 查询缓冲区中剩余可写空间 返回缓冲区剩余字节数
xStreamBufferBytesAvailable 查询缓冲区中可读数据量 返回缓冲区中尚未读取的字节数
vStreamBufferDelete 删除流缓冲区并释放内存 清理操作,需在确保无任务使用的情况下执行

以下是流数据缓冲区相关函数的详细说明,包含函数原型、参数解释和返回值:

xStreamBufferCreate

功能: 创建一个流数据缓冲区,用于存储和传输数据。

原型

1
StreamBufferHandle_t xStreamBufferCreate(size_t xBufferSizeBytes, size_t xTriggerLevelBytes);

参数

  • xBufferSizeBytes:缓冲区的总大小(以字节为单位)。
  • xTriggerLevelBytes:触发读取任务的最小字节数(推荐设为 1)。

返回值

成功:返回创建的流缓冲区句柄。
失败:返回 NULL。

xStreamBufferSend

功能: 向流缓冲区写入数据。

原型

1
size_t xStreamBufferSend(StreamBufferHandle_t xStreamBuffer, const void *pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
  • pvTxData:指向要写入数据的指针。
  • xDataLengthBytes:要写入的数据长度(字节数)。
  • xTicksToWait:写入时的最大阻塞时间(Tick 数,0 表示立即返回)。

返回值

成功:实际写入的字节数。
失败:0 表示写入失败(缓冲区已满)。

xStreamBufferReceive

功能: 从流缓冲区中读取数据。

原型

1
2

size_t xStreamBufferReceive(StreamBufferHandle_t xStreamBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
  • pvRxData:指向接收数据的缓冲区指针。
  • xBufferLengthBytes:接收缓冲区的大小(字节数)。
  • xTicksToWait:读取时的最大阻塞时间(Tick 数,0 表示立即返回)。

返回值

成功:实际读取的字节数。
失败:0 表示读取失败(缓冲区为空)。

xStreamBufferReset

功能: 重置流缓冲区,清空所有数据。
原型

1
BaseType_t xStreamBufferReset(StreamBufferHandle_t xStreamBuffer);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
    返回值
  • pdPASS:重置成功。
  • pdFAIL:重置失败(通常是因为缓冲区正在被任务或中断使用)。
xStreamBufferSpacesAvailable

功能: 查询流缓冲区中剩余的可写空间。

原型

1
2

size_t xStreamBufferSpacesAvailable(StreamBufferHandle_t xStreamBuffer);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
    返回值:剩余可写空间的字节数。
xStreamBufferBytesAvailable

功能: 查询流缓冲区中可读取的数据量。
原型

1
size_t xStreamBufferBytesAvailable(StreamBufferHandle_t xStreamBuffer);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
    返回值:当前缓冲区中可读取的数据字节数。
vStreamBufferDelete

功能: 删除一个流缓冲区并释放其内存。
原型

1
2

void vStreamBufferDelete(StreamBufferHandle_t xStreamBuffer);

参数

  • xStreamBuffer:目标流缓冲区的句柄。
    返回值:无返回值。

4.5.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 任务通知值按位判断-代替队列邮箱或者事件组
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h"

static const char *TAG = "main";

StreamBufferHandle_t xStreamBuffer;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

char tx_buf[50];
int i = 0;
while (1)
{
i++;
sprintf(tx_buf, "Hello World!-%d", i);
// 写入数据到流缓冲区
int send_len = xStreamBufferSend(xStreamBuffer, (void *)tx_buf, sizeof(tx_buf), portMAX_DELAY);
ESP_LOGI(TAG, "task1发送数据长度: %d", send_len);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task2启动!");
char rx_buf[50];

while (1)
{
memset(rx_buf, 0, sizeof(rx_buf));
// 读取数据
int recv_len = xStreamBufferReceive(xStreamBuffer, (void *)rx_buf, sizeof(rx_buf), portMAX_DELAY);
if (recv_len > 0)
{
rx_buf[recv_len] = 0;
ESP_LOGI(TAG, "task2接收数据: %s ", rx_buf);
}
}
}

// 监控缓冲区
void task3(void *pvParameters)
{
while (1)
{
// 读取数据
size_t xBytesAvailable = xStreamBufferBytesAvailable(xStreamBuffer);
if (xBytesAvailable > 0)
{
ESP_LOGI(TAG, "监控缓冲区中有数据: %d", xBytesAvailable);
}
else
{
ESP_LOGI(TAG, "监控缓冲区中无数据");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
xStreamBuffer = xStreamBufferCreate(1024, 300); // 创建一个 1024 字节的流缓冲区,每次可读取 1 字节
if (xStreamBuffer != NULL)
{
ESP_LOGI(TAG, "创建流缓冲区成功");
xTaskCreate(task1, "task1", 1024 * 4, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 4, NULL, 1, NULL);
xTaskCreate(task3, "task3", 1024 * 2, NULL, 1, NULL);
}
else
{
ESP_LOGI(TAG, "创建流缓冲区失败");
}
}

4.5.2 消息缓冲区

消息缓冲区允许长度可变的离散消息从中断服务程序传递至 一个任务,或从一个任务传递至另一个任务。例如,长度为 10、20 和 123 字节的消息 都可以在同一个消息缓冲区写入或读取 。与使用流缓冲区不同的是, 长度为 10 个字节的消息只能作为 10 个字节的消息读取,而不能 以单独的字节读取。消息缓冲区构建在流缓冲区之上(即它们使用 流缓冲区实现)。

4.6.1 API说明

以下是消息缓冲区的核心 API,以及其功能和参数说明。

函数名 功能 备注
xMessageBufferCreate 创建一个消息缓冲区 返回一个缓冲区句柄,供后续使用
xMessageBufferSend 向消息缓冲区写入数据 如果缓冲区空间不足,可选择阻塞或立即返回
xMessageBufferReceive 从消息缓冲区读取数据 如果缓冲区为空,可选择阻塞或立即返回
xMessageBufferSpaceAvailable 查询消息缓冲区中剩余可用空间 返回剩余字节数
xMessageBufferReset 重置消息缓冲区,清空所有数据 需确保没有任务正在操作缓冲区
vMessageBufferDelete 删除消息缓冲区并释放内存 必须在没有任务使用此缓冲区的情况下执行
xMessageBufferCreate

功能: 创建一个消息缓冲区,用于传输带长度前缀的消息。

原型

1
MessageBufferHandle_t xMessageBufferCreate(size_t xBufferSizeBytes);

参数

  • xBufferSizeBytes:缓冲区的总大小(字节)。

返回值

成功:返回创建的消息缓冲区句柄。
失败:返回 NULL。

xMessageBufferSend

功能: 向消息缓冲区中写入数据。

原型

1
size_t xMessageBufferSend(MessageBufferHandle_t xMessageBuffer, const void *pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait);

参数

  • xMessageBuffer:目标消息缓冲区的句柄。
  • pvTxData:指向要写入数据的指针。
  • xDataLengthBytes:要写入的数据长度(字节)。
  • xTicksToWait:写入时的最大阻塞时间(Tick 数,0 表示立即返回)。

返回值

  • 成功:实际写入的字节数。
  • 失败:0 表示写入失败(缓冲区空间不足)。
xMessageBufferReceive

功能: 从消息缓冲区中读取数据。
原型:

1
size_t xMessageBufferReceive(MessageBufferHandle_t xMessageBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait);

参数

  • xMessageBuffer:目标消息缓冲区的句柄。
  • pvRxData:指向接收数据的缓冲区指针。
  • xBufferLengthBytes:接收缓冲区的大小(字节)。
  • xTicksToWait:读取时的最大阻塞时间(Tick 数,0 表示立即返回)。

返回值

  • 成功:实际读取的字节数。
  • 失败:0 表示读取失败(缓冲区为空)。

注意,当接收缓冲区小于消息长度时,无法接收到消息。

xMessageBufferSpaceAvailable

功能: 查询消息缓冲区中剩余的可用空间。
原型

1
size_t xMessageBufferSpaceAvailable(MessageBufferHandle_t xMessageBuffer);

参数

  • xMessageBuffer:目标消息缓冲区的句柄。
    返回值:剩余可用空间的字节数。
xMessageBufferReset

功能: 重置消息缓冲区,清空所有数据。

原型

1
2

BaseType_t xMessageBufferReset(MessageBufferHandle_t xMessageBuffer);

参数:- xMessageBuffer:目标消息缓冲区的句柄。
返回值

  • pdPASS:重置成功。
  • pdFAIL:重置失败(缓冲区正在被使用)。
vMessageBufferDelete

功能: 删除一个消息缓冲区并释放其内存。

原型

1
void vMessageBufferDelete(MessageBufferHandle_t xMessageBuffer);

参数:xMessageBuffer:目标消息缓冲区的句柄。

返回值:无返回值。

4.6.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 任务通知值按位判断-代替队列邮箱或者事件组
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/message_buffer.h"

static const char *TAG = "main";

MessageBufferHandle_t xMessageBuffer;

void task1(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task1启动!");

char tx_buf[50];
for (int i = 0; i < 3; i++)
{
sprintf(tx_buf, "Hello World!-%d", i);
// 写入数据到流缓冲区
int send_len = xMessageBufferSend(xMessageBuffer, (void *)tx_buf, sizeof(tx_buf), portMAX_DELAY);
ESP_LOGI(TAG, "task1发送数据长度: %d", send_len);
}

vTaskDelete(NULL);
}

void task2(void *pvParameters)
{
ESP_LOGI(TAG, "-------------------------------");
ESP_LOGI(TAG, "task2启动!");
char rx_buf[200];
vTaskDelay(pdMS_TO_TICKS(3000));
while (1)
{
memset(rx_buf, 0, sizeof(rx_buf));
// 读取数据
int recv_len = xMessageBufferReceive(xMessageBuffer, (void *)rx_buf, sizeof(rx_buf), portMAX_DELAY);
if (recv_len > 0)
{
rx_buf[recv_len] = 0;
ESP_LOGI(TAG, "task2接收数据: %s ", rx_buf);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}


void app_main(void)
{
xMessageBuffer = xMessageBufferCreate(1024); // 创建消息缓冲区
if (xMessageBuffer != NULL)
{
ESP_LOGI(TAG, "创建消息缓冲区成功");
xTaskCreate(task1, "task1", 1024 * 4, NULL, 1, NULL);
xTaskCreate(task2, "task2", 1024 * 4, NULL, 1, NULL);
}
else
{
ESP_LOGI(TAG, "创建流缓冲区失败");
}
}


五、其他高级功能

暂无

六、代码风格约定

6.1 命名惯例

RTOS 内核和演示应用程序源代码使用以下惯例:

变量

  • uint32_t 类型变量以 ul 为前缀,其中“u”表示“unsigned” ,“l”表示“long”。
  • uint16_t 类型变量以 us 为前缀,其中“u”表示“unsigned” ,“s”表示“short”。
  • uint8_t 类型变量以 uc 为前缀,其中“u”表示“unsigned” ,“c”表示“char ”。
  • 非 stdint 类型的变量以 x 为前缀。例如,BaseType_t 和 TickType_t, 二者分别是可移植层定义的定义类型,主要架构的自然类型或最有效类型, 以及用于保存 RTOS 滴答计数的类型。
  • 非 stdint 类型的未签名变量存在附加前缀 u。例如, UBaseType_t(未签名 BaseType_t)类型变量以 ux 为前缀。
  • size_t 类型变量也带有 ux 前缀。
  • 枚举变量以 e 为前缀
  • 指针以附加 p 为前缀,例如,指向 uint16_t 的指针将以 pus 为前缀。
  • 根据 MISRA 指南,未限定标准 char 类型仅可包含 ASCII 字符, 并以 c 为前缀。
  • 根据 MISRA 指南,*char ** 类型变量仅可包含指向 ASCII 字符串的指针, 并以 pc 为前缀。

函数

  • 文件作用域静态(私有)函数以 prv 为前缀。
  • 根据变量定义的相关规定,API 函数以其返回类型为前缀, 并为 void 添加前缀 v。
  • API 函数名称以定义 API 函数文件的名称开头。例如,在 tasks.c 中定义 vTaskDelete, 并且具有 void 返回类型。

  • 宏以定义宏的文件为前缀。前缀为小写。例如, configUSE_PREEMPTION 在 FreeRTOSConfig.h 中定义。
  • 除前缀外,所有宏均使用大写字母书写,并使用下划线来分隔单词。

6.2 数据类型

仅使用 stdint.h 类型和 RTOS 自带的 typedef,但以下情况除外:

  • char
    根据 MISRA 指南,仅在未限定字符类型包含 ASCII 字符方可使用未限定字符类型。
  • char *:
    根据 MISRA 指南,仅在未限定字符指针指向 ASCII 字符串时方可使用未限定字符指针 。使用需要 char * 参数的标准库函数时, 无需抑制良性编译器警告,此举尤其考虑到将一些编译器默认为未限定 char 类型是签名的, 而其他编译器默认未限定 char 类型是未签名的。

针对每个移植定义四种类型。即:

  • TickType_t
    如果 configUSE_16_BIT_TICKS 设置为非零 (true) ,则将 TickType_t 定义为未签名的 16 位类型。
    如果 configUSE_16_BIT_TICKS 设置为零 (false),则将 TickType_t 定义为未签名的 32 位类型。
    32 位架构应始终将 configUSE_16_BIT_TICKS 设置为 0。
  • BaseType_t
    架构中最有效、最自然的类型。例如,在 32 位架构上, BaseType_t 会被定义为 32 位类型。在 16 位架构上,BaseType_t 会被定义为 16 位类型 。如果将 BaseType_t 定义为 char, 则须特别注意确保将签名字符用于可能为负的函数返回值来指示错误。
  • UBaseType_t
    未签名的 BaseType_t。
  • StackType_t
    意指架构用于存储堆栈项目的类型。通常是 16 位架构上的 16 位类型 和 32 位架构上的 32 位类型,但也有例外情况。供 FreeRTOS 内部使用。

参考链接

  1. https://www.freertos.org/zh-cn-cmn-s
  2. https://www.bilibili.com/video/BV1fs4y1G7eu/?spm_id_from=333.999.top_right_bar_window_history.content.click&vd_source=ef5a0ab0106372751602034cdd9ab98e
  3. https://blog.csdn.net/weixin_43321489/article/details/107939188
  4. https://blog.csdn.net/chengjunchengjun/article/details/109710929

FreeRTOS复习笔记
https://www.duruofu.xyz/posts/58683/
作者
DuRuofu
发布于
2024年12月30日
更新于
2025年1月11日
许可协议