文章目录
- 一、简介
-
- [ 1、IPC(进程间通信)](# 1、IPC(进程间通信))
- [ 2、同步(Synchronization)](# 2、同步(Synchronization))
- [ 3、互斥(Mutual Exclusion)](# 3、互斥(Mutual Exclusion))
- 二、底层同步原语
-
- [ 1、临界区(Critical Section)](# 1、临界区(Critical Section))
-
- [ 1.临界区:](# 1.临界区:)
- [ 2.单核MCU临界区机制](# 2.单核MCU临界区机制)
- [ 3.中断关闭效果](# 3.中断关闭效果)
- [ 4.ESP32S3 SMP双核机制](# 4.ESP32S3 SMP双核机制)
- [ 5.API](# 5.API)
- [ 6.使用注意事项](# 6.使用注意事项)
- [ 2、自旋锁(Spinlock)](# 2、自旋锁(Spinlock))
-
- [ 1.自旋锁(Spinlock):](# 1.自旋锁(Spinlock):)
- [ 2.尝试获取锁:](# 2.尝试获取锁:)
- [ 3.运行原理](# 3.运行原理)
- [ 4.API](# 4.API)
- [ 5.注意事项](# 5.注意事项)
- 三、信号量(Semaphore)
-
- [ 1、信号量(Semaphore)](# 1、信号量(Semaphore))
-
- [ 1.信号量逻辑图:](# 1.信号量逻辑图:)
- [ 2.信号量:是FreeRTOS最核心的IPC组件,本质是:](# 2.信号量:是FreeRTOS最核心的IPC组件,本质是:)
- [ 3.两个基本操作:](# 3.两个基本操作:)
- [ 4.头文件:](# 4.头文件:)
- [ 5.操作函数:](# 5.操作函数:)
- [ 6.四种信号量:](# 6.四种信号量:)
- [ 2、二值信号量(Binary Semaphore)同步](# 2、二值信号量(Binary Semaphore)同步)
-
- [ 1.二值信号量:](# 1.二值信号量:)
- [ 2.基础特性](# 2.基础特性)
- [ 3.API](# 3.API)
- [ 3、计数信号量(Counting Semaphore)同步+计数](# 3、计数信号量(Counting Semaphore)同步+计数)
-
- [ 1.计数信号量:](# 1.计数信号量:)
- [ 2.基础特性](# 2.基础特性)
- [ 3.API](# 3.API)
- [ 4、互斥锁(Mutex)互斥(优选级继承)](# 4、互斥锁(Mutex)互斥(优选级继承))
-
- [ 1.互斥锁](# 1.互斥锁)
- [ 2.基础特性](# 2.基础特性)
- [ 3.API](# 3.API)
- [ 5、递归互斥锁(Recursive Mutex)互斥(可嵌套)](# 5、递归互斥锁(Recursive Mutex)互斥(可嵌套))
-
- [ 1.递归互斥锁](# 1.递归互斥锁)
- [ 2.基础特性](# 2.基础特性)
- [ 3.API](# 3.API)
- 四、队列(Queue)同步+数据通信
-
- [ 1、队列基础](# 1、队列基础)
-
- [ 1.队列:](# 1.队列:)
- [ 2.基础特性](# 2.基础特性)
- [ 3.内部结构](# 3.内部结构)
- [ 4.基本操作模型](# 4.基本操作模型)
- [ 5.相关API](# 5.相关API)
- [ 2、三种数据类型传输](# 2、三种数据类型传输)
-
- [ 1.传输整型](# 1.传输整型)
- [ 2.传输结构体](# 2.传输结构体)
- [ 3.传输指针](# 3.传输指针)
- [ 3、多进单出模型](# 3、多进单出模型)
-
- [ 1.模型框图](# 1.模型框图)
- [ 2.模型简介](# 2.模型简介)
- [ 3.注意事项](# 3.注意事项)
- [ 4、多队列单出模型](# 4、多队列单出模型)
-
- [ 1.模型框图](# 1.模型框图)
- [ 2.队列集](# 2.队列集)
- [ 3.模型简介](# 3.模型简介)
- [ 4.Queue集合API](# 4.Queue集合API)
- [ 5、邮箱MailBox](# 5、邮箱MailBox)
-
- [ 1.模型框图](# 1.模型框图)
- [ 2.模型简介](# 2.模型简介)
- [ 3.MailBox API](# 3.MailBox API)
- [五、任务通知(Task Notification)轻量同步一对一](#五、任务通知(Task Notification)轻量同步一对一)
-
- [ 1、任务通知](# 1、任务通知)
- [ 2、优缺点](# 2、优缺点)
-
- [ 1.优点](# 1.优点)
- [ 2.缺点](# 2.缺点)
- [ 3、API](# 3、API)
-
- [ 1. 任务上下文发送通知](# 1. 任务上下文发送通知)
- [ 2. 任务上下文接收通知(阻塞等待)](# 2. 任务上下文接收通知(阻塞等待))
- [ 3. 中断 ISR 专用接口](# 3. 中断 ISR 专用接口)
- [六、事件组(Event Group)多条件同步](#六、事件组(Event Group)多条件同步)
-
- [ 1、事件](# 1、事件)
- [ 2、事件组](# 2、事件组)
- [ 3、核心定义](# 3、核心定义)
- [ 4、标志组同步](# 4、标志组同步)
- [ 5、相关API](# 5、相关API)
- 七、流缓冲
-
- [ 1、流缓冲区(Stream Buffer)字节流](# 1、流缓冲区(Stream Buffer)字节流)
-
- [ 1.流缓冲区](# 1.流缓冲区)
- [ 2.核心特性](# 2.核心特性)
- [ 3.API](# 3.API)
- [ 2、消息缓冲区(Message Buffer)不定长包](# 2、消息缓冲区(Message Buffer)不定长包)
-
- [ 1.消息缓冲区](# 1.消息缓冲区)
- [ 2.核心特性](# 2.核心特性)
- [ 3.API](# 3.API)
- [ 3、环形缓冲区(Ring Buffer)](# 3、环形缓冲区(Ring Buffer))
-
- [ 1.环形缓冲区](# 1.环形缓冲区)
- [ 2.核性特点](# 2.核性特点)
- [ 3.API](# 3.API)
- [ 4、Stream Buffer和Message Buffer的异同](# 4、Stream Buffer和Message Buffer的异同)
-
- [ 1.Stream Buffer流缓冲区](# 1.Stream Buffer流缓冲区)
- [ 2.MessageBuffer 消息缓冲区](# 2.MessageBuffer 消息缓冲区)
- [ 3.如果接收缓冲区小于要接收的数据](# 3.如果接收缓冲区小于要接收的数据)
- 八、示例代码
-
- [ 1、临界区](# 1、临界区)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 2、自旋锁](# 2、自旋锁)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 3、信号量](# 3、信号量)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 4、二值信号量](# 4、二值信号量)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 5、计数信号量(资源池管理)](# 5、计数信号量(资源池管理))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 6、互斥锁(共享资源保护)](# 6、互斥锁(共享资源保护))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 7、递归互斥锁(多层嵌套加锁)](# 7、递归互斥锁(多层嵌套加锁))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 8、队列(基础收发)](# 8、队列(基础收发))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 9、队列(三种数据类型传输)](# 9、队列(三种数据类型传输))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 10、多进单出模型](# 10、多进单出模型)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 11、多队列单出模型(队列集QueueSet)](# 11、多队列单出模型(队列集QueueSet))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 12、邮箱MailBox(一对多广播最新状态)](# 12、邮箱MailBox(一对多广播最新状态))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 13、任务通知同步(替代二值信号量)](# 13、任务通知同步(替代二值信号量))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 14、任务通知的值(传递32位的数值)](# 14、任务通知的值(传递32位的数值))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 15、事件标志组等待(任意 / 全部事件)](# 15、事件标志组等待(任意 / 全部事件))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 16、事件标志组同步(多任务同步屏障)](# 16、事件标志组同步(多任务同步屏障))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 17、流缓冲区(字节流)](# 17、流缓冲区(字节流))
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 18、消息缓冲区](# 18、消息缓冲区)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
- [ 19、环型缓冲区](# 19、环型缓冲区)
-
- [ 1.示例代码](# 1.示例代码)
- [ 2.执行结果](# 2.执行结果)
一、简介
1、IPC(进程间通信)
IPC = Inter-Process Communication 进程间通信。
在 FreeRTOS 中,"进程" 等价于 "任务 Task"。
IPC = 任务与任务、任务与中断之间同步、互斥、传递数据的一套机制。
2、同步(Synchronization)
同步(Synchronization):
是指多个任务之间协调执行顺序,共享资源访问或事件传递的机制。
协调多个任务的执行时序,任务之间互相等待,按照预定次序运行。
目的:
防止多个任务【同时访问共享资源】
3、互斥(Mutual Exclusion)
互斥(Mutual Exclusion):
是指共享资源在任何时刻只能被一个任务或线程访问的机制。
多个任务不能同时访问同一个共享资源,同一时刻只允许一个任务进入临界区操作资源。
目的:
协调任务【执行时序、先后顺序】,任务互相等待事件。
二、底层同步原语
底层原语 = 没有等待链表,拿不到锁不会阻塞任务,只能忙等或关中断。
1、临界区(Critical Section)
1.临界区:
同一时间只能被一个线程或任务执行的代码段。
一段访问共享资源的代码,同一时刻只允许一个执行流进入。
2.单核MCU临界区机制
portENTER_CRITICAL() → 关闭全局可屏蔽中断。
portEXIT_CRITICAL() → 恢复中断。
3.中断关闭效果
SysTick 滴答中断停止,不会触发任务调度;
外设中断无法响应;
任务切换被冻结;
代码执行期间,不会被任何中断、其他任务打断。
4.ESP32S3 SMP双核机制
单纯关闭本 CPU 中断 ≠ 多核安全!
CPU0 关中断,CPU1 仍然可以同时执行同一段代码,并发修改共享变量,造成数据错乱。
ESP-IDF FreeRTOS SMP 下 portENTER_CRITICAL() 底层真实逻辑:
FreeRTOS 使用 portENTER_CRITICAL() / portEXIT_CRITICAL() 实现软件临界保护。
portENTER_CRITICAL():
1. 获取内部自旋锁(阻止另一颗CPU进入临界区)。
2. 关闭当前CPU可屏蔽中断。
3. 临界嵌套计数器 ++。
portEXIT_CRITICAL():
1. 临界嵌套计数器 --。
2. 计数器为0时,恢复当前CPU中断。
3. 释放自旋锁。
所以:portENTER_CRITICAL() 在 SMP 环境天然具备多核互斥能力。
5.API
c
任务上下文(普通任务中使用)
portENTER_CRITICAL();
{
// 临界代码,不允许被抢占
shared_var++;
}
portEXIT_CRITICAL();
//支持嵌套:
portENTER_CRITICAL();
portENTER_CRITICAL();
shared_var++;
portEXIT_CRITICAL();
portEXIT_CRITICAL(); // 两次退出才真正开中断
中断上下文专用(ISR 内)
中断里禁止直接调用 portENTER_CRITICAL(),必须使用中断掩码版本:
uint32_t ulIntMask = portSET_INTERRUPT_MASK_FROM_ISR();
{
shared_var++;
}
portCLEAR_INTERRUPT_MASK_FROM_ISR(ulIntMask);
原理:保存当前中断屏蔽寄存器,退出时恢复,避免粗暴全局开中断破坏中断原有优先级。
6.使用注意事项
成对使用,进入和退出临界区的函数需要志对使用,注意嵌套。
尽可能简短,等待的任务不会进入阻塞,并且长时候关闭中断分影响系统实时性。
2、自旋锁(Spinlock)
1.自旋锁(Spinlock):
自旋锁是基于共享内存标记、忙等轮询实现的互斥原语。
尝试获取锁失败时,任务不进入阻塞态、不切换上下文、不释放 CPU,原地循环不断查询锁状态,直到锁被释放。
"自旋" = 原地循环轮询。
2.尝试获取锁:
锁空闲 → 立刻获取,进入临界代码;
锁被占用 → 原地循环轮询(自旋忙等),不阻塞、不切换任务、不释放 CPU。
3.运行原理
自旋锁是多核互斥原语,依托 CPU 硬件原子指令 实现;
获取锁失败时,任务不阻塞、不切换上下文、不让出 CPU,原地循环轮询(自旋),直到锁释放。
Test-and-Set (TAS) 原子算法
CPU 提供原子交换指令:一条指令同时完成「读取旧值 + 写入新值」,总线层面不可被打断。
伪代码:
c
volatile uint32_t lock = 0;
void spin_lock(void)
{
// atomic_swap:原子指令
// 把lock写为1,返回lock修改之前的值
while( atomic_swap(&lock, 1) == 1 )
{
// 自旋等待,空循环
}
}
void spin_unlock(void)
{
atomic_store(&lock, 0);
}
运行逻辑拆解:
atomic_swap(&lock,1)
如果返回 0:上锁成功,退出循环;
如果返回 1:锁被占用,继续循环自旋。
解锁直接原子清零。
原子指令依靠硬件总线仲裁,保证同一时刻只有一个核完成抢占。
4.API
c
#include "freertos/portmacro.h"
// 定义自旋锁
portMUX_TYPE my_mux = portMUX_INITIALIZER_UNLOCKED;
// 任务上下文使用
portMUX_LOCK(&my_mux);
{
// 受保护代码(必须极短!)
}
portMUX_UNLOCK(&my_mux);
// 中断上下文使用
uint32_t ulMask = portMUX_LOCK_ISR(&my_mux);
{
// 临界代码
}
portMUX_UNLOCK_ISR(&my_mux, ulMask);
5.注意事项
保护代码必须极短,持有锁时间长 → 其他 CPU 持续自旋空转 → CPU 满载发热
禁止在自旋锁内调用任何阻塞 API。
vTaskDelay / xSemaphoreTake 一旦阻塞:
任务主动让出 CPU,但锁还被持有 → 其他 CPU 永久自旋死锁!
适合:简单寄存器读写、少量变量运算
不适合:耗时处理、队列操作、等待资源
三、信号量(Semaphore)
1、信号量(Semaphore)
1.信号量逻辑图:

2.信号量:是FreeRTOS最核心的IPC组件,本质是:
一个计数器 + 一条等待任务链表。
3.两个基本操作:
Give(释放 / 给出):计数器 +1,如果有任务在等待,唤醒最高优先级的等待任务。
Take(获取 / 占用):计数器 -1,计数器为 0 时任务进入阻塞等待。
4.头文件:
freertos/semphr.h
5.操作函数:
c
// 获取信号量
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
// 释放信号量
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
// 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
// 获取信号量(中断)
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
// 释放信号量(中断)
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
6.四种信号量:

2、二值信号量(Binary Semaphore)同步
1.二值信号量:
基于 FreeRTOS 队列实现,队列长度 = 1,队列存储大小为 0,仅做计数标记。
计数器只有两种状态:0(空,无信号) / 1(满,有信号),因此叫二值信号量。
2.基础特性
创建默认初始值 = 0,无可用信号;
连续多次Give仅保留 1 次信号,高频中断会丢失事件。
3.API
c
// 创建二值信号量,初始计数0
SemaphoreHandle_t xSemaphoreCreateBinary(void);
返回值:成功返回信号量句柄;堆内存不足返回NULL。
// 获取信号量,xTicksToWait:阻塞等待超时
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
// 释放信号量
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
返回值:pdPASS操作成功;pdFALSE失败(Take 超时 / Give 时计数已满)。
// 中断内释放信号量
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
pxHigherPriorityTaskWoken:若唤醒更高优先级任务,置为pdTRUE;
末尾必须调用 portYIELD_FROM_ISR(xHigherPriorityTaskWoken) 触发任务切换。
3、计数信号量(Counting Semaphore)同步+计数
1.计数信号量:
基于 FreeRTOS 队列实现,队列长度 = 最大计数值 uxMaxCount,队列元素大小为 0(不存数据,仅计数)。
计数器范围:0 ~ uxMaxCount,可累积多次事件,不会像二值信号量那样只保留 1 次信号。
2.基础特性
计数器可配置范围,支持累积多次事件,解决二值信号量高频事件丢失问题;
无优先级继承机制,不适合做资源互斥保护;
支持 FromISR 中断接口,可在中断中 Give/Take;
两种经典配置模式,对应完全不同的使用场景。
3.API
c
//创建计数型信号量
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, // 最大计数值(队列长度)
UBaseType_t uxInitialCount // 初始计数值
);
返回值:成功返回信号量句柄;堆内存不足返回 NULL。
// 获取信号量(计数器 -1),超时返回 pdFALSE
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
// 释放信号量(计数器 +1),计数已满返回 pdFALSE
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
// 删除信号量
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
// 中断内释放(计数器 +1)
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
// 中断内获取(计数器 -1,一般不推荐,中断不能阻塞)
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
注意:中断中调用后必须配合 portYIELD_FROM_ISR() 触发任务切换。
4、互斥锁(Mutex)互斥(优选级继承)
1.互斥锁
基于 FreeRTOS 队列实现的特殊信号量,计数器仅 0/1 两种状态。
额外保存持有者任务句柄和原始优先级,自带优先级继承机制,专门用于解决多任务共享资源的互斥访问问题。
2.基础特性
初始计数值 = 1(空闲可用),与二值信号量(初始 0)刚好相反;
主打互斥保护,不是用来做同步的;
自带优先级继承,缓解优先级反转问题(与二值信号量最本质的区别);
严格规则:谁 Take(加锁)谁 Give(解锁),必须成对;
禁止在中断中使用,没有 FromISR 版本;
普通 Mutex 不支持递归加锁,重复加锁会自己把自己死锁。
3.API
c
// 创建互斥锁,初始计数值为1(可用)
SemaphoreHandle_t xSemaphoreCreateMutex(void);
返回值:成功返回互斥锁句柄;堆内存不足返回 NULL。
// 加锁(获取互斥锁,计数器 -1)
BaseType_t xSemaphoreTake(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);
// 解锁(释放互斥锁,计数器 +1)
BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex);
// 删除互斥锁
void vSemaphoreDelete(SemaphoreHandle_t xMutex);
没有 xSemaphoreGiveFromISR / xSemaphoreTakeFromISR 中断版本!绝对不能在 ISR 中使用。
5、递归互斥锁(Recursive Mutex)互斥(可嵌套)
1.递归互斥锁
在普通互斥锁的基础上,额外增加递归计数器,支持同一个任务多次加锁不会死锁,需要同等次数的解锁才会真正释放锁。
同样自带优先级继承机制,属于互斥锁家族的扩展成员。
2.基础特性
基于普通互斥锁扩展,仅允许持有者任务重复加锁,不会自己把自己死锁;
递归计数规则:Take 一次计数 +1,Give 一次计数 -1,计数归 0 才真正释放锁;
同样自带优先级继承机制,缓解优先级反转;
同样严格遵循「谁加锁谁解锁」规则;
禁止在中断中使用,无 FromISR 版本;
开销比普通互斥锁略大,能用普通 Mutex 就不要用递归版。
3.API
c
// 创建递归互斥锁,初始递归计数=0(空闲可用)
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
返回值:成功返回句柄;堆内存不足返回 NULL。
// 递归加锁(同一任务可多次调用)
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);
// 递归解锁(必须和加锁次数匹配)
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);
四、队列(Queue)同步+数据通信
1、队列基础
1.队列:
所有信号量、互斥锁的底层实现基础,FreeRTOS 最核心、功能最强的进程间通信组件。
FreeRTOS 最基础的 IPC 组件,本质是一个带阻塞机制的 FIFO 环形缓冲区,支持多任务 / 中断写入、多任务读取,数据按「先进先出」顺序传递。
2.基础特性
数据拷贝传递:
写入时把数据拷贝进队列存储区,读出时拷贝出队列,不依赖原变量生命周期(传指针除外);
多写多读:
支持任意数量的任务 / 中断写队列,任意数量的任务读队列;
双向阻塞:
读队列时队列为空 → 读任务阻塞(可设超时);
写队列时队列已满 → 写任务阻塞(可设超时);
优先级唤醒:
阻塞的任务按优先级排序,有消息 / 有空位时,最高优先级的等待任务先被唤醒;同优先级则等待最久的先唤醒;
灵活长度:
队列长度、单个元素大小均可配置,支持传字节、整型、结构体、指针等任意类型数据。
3.内部结构


4.基本操作模型

阶段 1:队列初始化
队列 5 个存储单元全部为空,Task A、B 各自拥有本地变量,二者没有数据交互通道,等待队列收发操作。
阶段 2:Task A 发送数值 10
Task A 给本地变量x=10,调用Send(入队),把x的值拷贝写入队列尾部;
此时队列仅有这一条数据,这条数据同时是队首、队尾;队列剩余 4 个空位。
关键点:队列保存数据副本,和原变量x相互独立。
阶段 3:Task A 再次发送数值 20
Task A 修改本地变量x=20,再次执行入队,新数据追加到队列尾部;
FIFO 规则:先写入的10留在队首,后写入的20放在队尾;此时队列占用 2 格,剩余 3 个空位。
重点验证:虽然变量x已经改成 20,但队列里旧的10不会被改动,因为队列存储的是拷贝副本。
阶段 4:Task B 接收数据
Task B 调用Receive(出队),遵循先进先出,取出最早入队的队首数据10,拷贝赋值给本地变量y;
取出后队列内剩余元素20整体前移,腾出原队首的存储空间;Task B 的y变为 10。
5.相关API
创建队列
c
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize);
创建一个新队列并返回一个句柄,通过该句柄可以引用该队列。
参数:
uxQueueLength队列在任何时候都可以容纳的最大项目数。
uxItemSize保存队列中每个项目所需的大小(以字节为单位)。
项目按副本排队,而不是按引用排队,因此这是将为每个排队项目复制的字节数。队列中的每个项目必须大小相同。
返回:
如果成功创建了队列,则返回创建的队列的句柄。如果无法分配创建队列所需的内存,则返回NULL。
删除队列
c
void vQueueDelete(QueueHandle_t xQueue);
删除队列 - 释放分配用于存储队列中项目的所有内存。
参数:
xQueue要删除的队列的句柄。
发送数据
c
BaseType_t xQueueSend(QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);
这是一个调用xQueueGenericSend()的宏。
参数:
xQueue要在其上发布项目的队列的句柄。
pvItemToQueue指向要放在队列中的项的指针。队列将保留的项目大小是在创建队列时定义的,因此这多个字节将从pvItemToQueue复制到队列存储区域。
xTicksToWait任务应该阻止等待空间在队列上可用的最长时间(如果已经满了)。如果队列已满并且xTicksToWait设置为0,则调用将立即返回。时间在滴答周期中定义,因此如果需要,应使用常量portTICK_PERIOD_MS转换为实时。
如果INCLUDE_vTaskSuspend设置为"1",则将块时间指定为portMAX_DELAY将导致任务无限期地阻塞(没有超时)。
返回:
如果项目成功发布,则为pdTRUE,否则为errQUEUE_FULL。
接收数据
c
BaseType_t xQueueReceive(QueueHandle_t xQueue,void * pvBuffer,TickType_t xTicksToWait);
这是一个调用xQueueGenericReceive()函数的宏。
参数:
xQueue要从中接收项目的队列的句柄。
pvBuffer指向要将所接收项目复制到的缓冲区的指针。
xTicksToWait如果在调用时队列为空,则任务应阻止等待项目接收的最长时间。将xTicksToWait设置为0将导致函数在队列为空时立即返回。时间在滴答周期中定义,因此如果需要,应使用常量portTICK_PERIOD_MS转换为实时。
如果INCLUDE_vTaskSuspend设置为"1",则将块时间指定为portMAX_DELAY将导致任务无限期地阻塞(没有超时)。
返回:
如果从队列中成功接收到项目,则返回pdTRUE,否则返回pdFALSE。
等待数据
c
BaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);
返回存储在队列中的消息数。
参数:
xQueue正在查询的队列的句柄。
返回:
队列中可用的消息数。
2、三种数据类型传输
1.传输整型
c
//创建队列
QueueHandle_t hQueueHandle;
hQueueHandle = xQueueCreate(5,sizeof(int));
//发送数据到队列
int i =0;
xStatus = xQueueSend(hQueueHandle,&i,0);
//从队列接收数据
int j =0;
xStatus = xQueueReceive(hQueueHandle,&j,portMAX_DELAY);
2.传输结构体
c
//定义结构体
typedef struct A_STRUCT
{
char id;
char data;
}xStruct;
//创建结构体队列
QueueHandle_t hQueueHandle;
hQueueHandle = xQueueCreate(5,sizeof(xStruct));
//发送数据到结构体队列
BaseType_t xStatus;
xStruct xUSB = {1,55};
xStatus = xQueueSend(hQueueHandle,&xUSB,0);
//从结构体队列接收数据
BaseType_t xStatus;
xStruct xUSB = {0,0};
xStatus = xQueueReceive(hQueueHandle,&xUSB,portMAX_DELAY);
3.传输指针
c
传输指针,等同于传输整数,是将分配好的内存地址传送,注意内存的分配和释放。
//创建队列
QueueHandle_t hQueueHandle;
hQueueHandle = xQueueCreate(5,sizeof(char *));
//发送数据到队列
char * pstrToSend;
int i=0;
pstrToSend = (char *)malloc(50);
snprintf(pstrToSend,50,"Today is a good day_%d!",i);
xStatus = xQueueSend(hQueueHandle,&pstrToSend,0);
//从队列中接收数据
char * pstrToSend;
xStatus = xQueueReceive(hQueueHandle,&pstrToSend,portMAX_DELAY);
3、多进单出模型
1.模型框图

2.模型简介
该模型是工业嵌入式典型的多生产者、单消费者队列通信模型:
生产者(发送端,3 个独立任务):
CAN 总线任务、通用 Another Task、人机界面 HMI 任务;
各自采集 / 生成业务数据,封装为统一结构体,投递到共享队列。
中转载体:
FreeRTOS 队列,存储Data_t类型结构体。
消费者(接收端):
Controller 控制任务,从队列逐条取出消息,依据数据 ID 执行对应的控制逻辑。
3.注意事项
多进单出模型要注意创建任务的优先级,接收队列的任务优先级要高于发送队列任务的优先级。
4、多队列单出模型
1.模型框图

2.队列集
队列集是一个事件容器,可以把多个队列、二值信号量、计数信号量添加进去;
任务调用xQueueSelectFromSet()阻塞等待,只要集合内任意一个成员有可读事件,就立刻唤醒任务,返回触发事件的成员句柄,任务再读取对应队列 / 信号量处理。
3.模型简介
生产者端:
3 个独立任务,CAN 总线任务、通用 Another Task、HMI 人机交互任务;
每个任务独占一条专属队列,向自己对应的队列发送消息;
中转层:
三条独立队列全部加入同一个队列集 Queue Set;
消费者端:
单个 Controller 控制任务,阻塞监听整个队列集;
任意一条队列收到新消息,队列集就唤醒控制任务,控制任务再查询具体是哪条队列有数据,从对应队列读取消息处理。
4.Queue集合API
c
创建集合
QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength);
在FreeRTOSConfig.h中,必须将configUSE_QUEUE_SETS设置为1才能使xQueueCreateSet()API函数可用。
参数:
uxEventQueueLength 队列设置存储在集合中包含的队列和信号量上的事件。 uxEventQueueLength指定一次可以排队的最大事件数。
为了绝对确定事件没有丢失,必须将uxEventQueueLength设置为添加到集合的队列长度的总和,其中二进制信号量和互斥量的长度为1,计数信号量的长度由其最大计数值设置。例如:
如果队列集要保存长度为5的队列,另一个长度为12的队列和一个二进制信号量,则uxEventQueueLength应设置为(5 + 12 + 1)或18。
如果队列集要保存三个二进制信号量,则uxEventQueueLength应设置为(1 + 1 + 1)或3。
如果队列集要保存最大计数为5的计数信号量,以及最大计数为3的计数信号量,则uxEventQueueLength应设置为(5 + 3)或8。
返回:
如果成功创建了队列集,则返回创建的队列集的句柄。否则返回NULL。
将队列加入集合
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore,QueueSetHandle_t xQueueSet);
在FreeRTOSConfig.h中,必须将configUSE_QUEUE_SETS设置为1才能使xQueueAddToSet()API函数可用。
参数:
xQueueOrSemaphore 要添加到队列集的队列或信号量的句柄(强制转换为QueueSetMemberHandle_t类型)。
xQueueSet 要添加队列或信号量的队列集的句柄。
返回:
如果队列或信号量已成功添加到队列集,则返回pdPASS。如果队列无法成功添加到队列集,因为它已经是不同队列集的成员,则返回pdFAIL。
从集合中删除队列
BaseType_t xQueueRemoveFromSet(QueueSetMemberHandle_t xQueueOrSemaphore,QueueSetHandle_t xQueueSet);
在FreeRTOSConfig.h中,必须将configUSE_QUEUE_SETS设置为1才能使xQueueRemoveFromSet()API函数可用。
从队列集中删除RTOS队列或信号量。
如果队列或信号量为空,则只能从队列集中删除RTOS队列或信号量。
参数:
xQueueOrSemaphore 从队列集中删除的队列或信号量的句柄(强制转换为QueueSetMemberHandle_t类型)。
xQueueSet 包含队列或信号量的队列集的句柄。
返回:
如果从队列集中成功删除了队列或信号量,则返回pdPASS。 如果队列不在队列集中,或者队列(或信号量)不为空,则返回pdFAIL。
选择一个队列
QueueSetMemberHandle_t xQueueSelectFromSet(QueueSetHandle_t xQueueSet,const TickType_t xTicksToWait);
在FreeRTOSConfig.h中,必须将configUSE_QUEUE_SETS设置为1才能使xQueueSelectFromSet()API函数可用。
xQueueSelectFromSet()从队列成员中选择一个队列或信号量,该队列或信号量包含数据(在队列的情况下)或可用于(在信号量的情况下)。 xQueueSelectFromSet()有效地允许任务同时阻塞(挂起)队列集中所有队列和信号量的读操作。
笔记:
阻止包含互斥锁的队列集不会导致互斥锁持有者继承被阻止任务的优先级。
除非对xQueueSelectFromSet()的调用首先返回该set成员的句柄,否则不得对队列集的成员执行接收(在队列的情况下)或take(在信号量的情况下)操作。
参数:
xTicksToWait 调用任务将保持在阻塞状态(其他任务正在执行)的最长时间(以时钟周期为单位),以等待队列集的成员为成功的队列读取或信号量执行操作做好准备。
返回:
xQueueSelectFromSet()将返回包含在包含数据的队列集中的队列句柄(强制转换为QueueSetMemberHandle_t类型),或者返回包含在可用队列集中的信号量句柄(强制转换为QueueSetMemberHandle_t类型),或者为NULL如果在指定的块时间到期之前没有这样的队列或信号量。
5、邮箱MailBox
1.模型框图

2.模型简介
邮箱是长度为 1 的特殊队列,属于队列的典型用法:队列最大容量uxLength=1,仅能存放最新 1 份数据;
也常被称作单槽队列、信箱。
架构组成:
写入端:
单个writeTask写任务,更新邮箱内的数据;
载体 Mailbox:
容量为 1 的队列,永远只保存最新的一份数据;旧数据会被新写入的数据直接覆盖;
读取端:
多个读任务readTask1、readTask2、readTask3,都可以读取邮箱内最新数据;
适用于:
单值最新状态分发,不需要历史数据。
3.MailBox API
c
创建MailBoxAPI
xQueueCreate 创建1位队列
写Mailbox
xQueueOverwrite(xQueue, pvItemToQueue)
仅用于长度为1的队列,因此队列为空或已满。
在队列上发布一个项目。如果队列已满,则覆盖队列中保留的值。该项目是通过副本来排队的,而不是通过引用来排队的。
不能从中断服务例程中调用此函数。有关可在ISR中使用的替代方案,请参阅xQueueOverwriteFromISR()。
返回:
xQueueOverwrite()是一个调用xQueueGenericSend()的宏,因此具有与xQueueSendToFront()相同的返回值。但是,pdPASS是唯一可以返回的值,因为即使队列已经满,xQueueOverwrite()也会写入队列。
参数:
xQueue: 要发送数据的队列的句柄。
pvItemToQueue: 指向要放置在队列中的项目的指针。
在创建队列时定义了队列将保留的项的大小,因此这许多字节将从pvItemToQueue复制到队列存储区域中。
读Mailbox
BaseType_t xQueuePeek(QueueHandle_t xQueue, void *const pvBuffer, TickType_t xTicksToWait)
从队列中接收项目,而不从队列中删除该项目。
该项目通过副本接收,因此必须提供足够大小的缓冲区。
在创建队列时定义了复制到缓冲区中的字节数。
成功接收的项目仍在队列中,因此将通过下次调用或调用xQueueReceive()再次返回。
不能在中断服务例程中使用此宏。有关可以从中断服务例程中调用的替代方案,请参见xQueuePeekFromISR()。
返回:
如果已从队列成功接收到项目,则为pdTRUE,否则为pdFALSE。
参数
xQueue: 要接收项目的队列的句柄。
pvBuffer: 指向接收项目复制到的缓冲区的指针。
xTicksToWait: 如果调用时队列为空,任务应阻塞等待项目接收的最长时间量。
时间在滴答周期中定义,因此如果需要,应使用常数portTICK_PERIOD_MS转换为实时。
如果xTicksToWait为0,xQueuePeek()将立即返回。
五、任务通知(Task Notification)轻量同步一对一
1、任务通知
每个 TCB 任务控制块内部内置一组专用通知寄存器,无需队列、信号量这类单独内核对象;

一个任务可以直接给另一个任务发送通知,目标任务可阻塞等待自身的通知,属于轻量化 IPC。
可以理解为:
绑定到单个任务身上的内置信号量 / 短消息通道,不占用堆内存创建 IPC 对象,效率远高于队列、信号量。
2、优缺点
1.优点
高效:直接设置值,没有经过中间变量。
内存占用小:无须额外创建其它变量,可以直接使用内部的值。
2.缺点
数据量小:notifyValue只有一个32位值。
无法广播:只能给一个任务发送数据。
中断中无法获取数据:中断不属于任务,无法在中断中等待数据。
3、API
1. 任务上下文发送通知
c
// 基础发送:通知目标任务,可更新通知值,可选唤醒任务
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
// 功能最全发送:可指定更新模式(覆盖/按位或/累加)、写入通知值
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction);
eNotifyAction枚举(决定通知值处理逻辑):
eNoAction:仅唤醒任务,不修改通知值,等价二值信号量 Give;
eSetBits:通知值和传入值按位或,等价事件标志组;
eIncrement:通知值自增,等价计数信号量 Give;
eSetValueOverwrite:直接覆盖通知值,等价邮箱xQueueOverwrite;
eSetValueWithoutOverwrite:通知为空才写入,否则失败,等价普通队列 Send。
2. 任务上下文接收通知(阻塞等待)
c
// 计数信号量形式接收,通知值减1,portMAX_DELAY永久阻塞
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
// 通用接收:读取通知值,可选择清零、保留位
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait);
ulTaskNotifyTake:适配计数 / 二值信号量场景;xClearCountOnExit=pdTRUE时,接收后通知计数清零,就是二值信号量逻辑;pdFALSE则递减计数,就是计数信号量。
xTaskNotifyWait:适配事件标志、单字邮箱场景。
3. 中断 ISR 专用接口
c
BaseType_t xTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, BaseType_t *pxHigherPriorityTaskWoken);
调用结束后配合portYIELD_FROM_ISR触发任务切换,和信号量 FromISR 用法一致。
六、事件组(Event Group)多条件同步
1、事件
事件 "标志 "是一个布尔值(1或0),用于指示一个事件是否发生。事件 "组 "是一组事件标志。
事件标志只能为1或0,允许事件标志的状态存储在单个位中,事件组中所有事件标志的状态存储在单个变量中;
事件组中每个事件标志的状态由类型为EventBits_t的变量中的单个位表示。
因此,事件标志也被称为事件"位"。
如果EventBits_t变量中的一位被设为1,则该位表示的事件已经发生。
如果在EventBits_t变量中一个位被设置为0,那么由该位表示的事件没有发生。
2、事件组
事件组用一个整数来表示,其中的高8位留给内核使用,只能用其他的位来表示事件。那么这个整数是多少位的?
如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件

3、核心定义
本质:
事件组是32 位无符号整数作为事件标志位集合,每一个二进制位代表一种独立事件;支持多任务等待任意 / 全部事件位,是多任务同步、多事件协同处理的 IPC 组件。
位规则:
EventGroup_t 内部存储 EventBits_t(uint32_t),最高位 bit31 为内核保留位,用户只能使用 bit0~bit30;
某一位置 1 = 对应事件已发生;置 0 = 事件未触发。
特性:
多事件独立标记:
最多同时管理 31 种不同事件(按键、串口、CAN、定时器、故障告警等);
灵活等待逻辑:任务可选择两种等待模式:
pdTRUE(ALL_BIT):所有指定 bit 全部置 1才唤醒任务;
pdFALSE(ANY_BIT):任意一个指定 bit 置 1立刻唤醒任务;
自动清零 / 保留位配置:
唤醒后可自动清除已触发事件位,也可保留标志供其他任务读取;
多任务可监听同一事件组:
支持一对多事件广播,区别于只能发给单一任务的任务通知;
支持中断置位事件标志(xEventGroupSetBitsFromISR)。
4、标志组同步

事件组分配 3 个 bit:bit0、bit1、bit2,分别对应 task1、task2、task3;
单个任务运行到同步位置,就置位自身对应的 bit,然后调用xEventGroupWaitBits,使用等待全部位(xWaitForAllBits=pdTRUE)模式阻塞;
当 3 个 bit 全部被置 1,代表三个任务全部抵达同步点,内核唤醒所有阻塞任务,同时清空事件位,任务向下执行后续逻辑。
5、相关API
c
创建事件标志组
EventGroupHandle_t xEventGroupCreate( void );
创建一个事件组,返回它的句柄。
此函数内部会分配事件组结构体
返回值:
返回句柄,非NULL表示成功
删除事件标志组
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
xEventGroup: 事件组句柄,你要删除哪个事件组
设置标志组标志位事件
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );
设置事件组中的位
参数:
xEventGroup: 哪个事件组
uxBitsToSet: 设置哪些位?
如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
返回值:
返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
等待事件标志组事件
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数:
xEventGroup:
等待哪个事件组?
uxBitsToWaitFor:
等待哪些位?哪些位要被测试?
xClearOnExit:
函数提出前是否要清除事件?pdTRUE: 清除uxBitsToWaitFor指定的位pdFALSE: 不清除
xWaitForAllBits:
怎么测试?是"AND"还是"OR"?pdTRUE: 等待的位,全部为1;pdFALSE: 等待的位,某一个为1即可
xTicksToWait:
如果期待的事件未发生,阻塞多久。可以设置为0:判断后即刻返回;可设置为portMAX_DELAY:一定等到成功才返回;可以设置为期望的Tick Count,一般用pdMS_TO_TICKS()把ms转换为Tick Count
返回值:
返回的是事件值,如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值;如果是超时退出,返回的是超时时刻的事件值。
同步等待
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait);
参数名称:
xEventGroup
事件组的句柄,用来在其中设置事件位,然后对其进行测试。事件组句柄是创建事件组的xEventGroupCreate()的调用的返回值。
uxBitsToSet
一个位掩码,用于指定要在事件组中设置为1的一个或多个事件位。通过将事件组的现有值与uxBitsToSet中传递的值进行按位或运算,可以更新事件组的值。
例如,将uxBitsToSet设置为0x04(二进制0100)将导致事件位3被置位(如果尚未设置),而事件组中的所有其他事件位均保持不变。
uxBitsToWaitFor
位掩码,指定要在事件组中测试的一个或多个事件位。
例如,如果调用任务想要等待事件位0、1和2在事件组中被设置,则将uxBitsToWaitFor设置为0x07(二进制111)。
xTicksToWait
任务应保持在"阻塞"状态以等待其解除阻塞条件所需的最长时间。
如果xTicksToWait为零,或者在调用xEventGroupSync()时满足解锁条件,则xEventGroupSync()将立即返回。
块时间以滴答周期指定,因此它表示的绝对时间取决于滴答频率。宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。
如果在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1,则将xTicksToWait设置为portMAX_DELAY将导致任务无限期等待(无超时)。
返回值:
如果由于满足调用任务的取消阻塞条件而返回xEventGroupSync(),则返回值是满足调用任务的取消阻塞条件时事件组的值(在自动将所有位清零之前)。在这种情况下,返回的值也将满足调用任务的解锁条件。
如果由于xTicksToWait参数指定的阻塞时间到期而返回xEventGroupSync(),则返回的值是阻塞时间到期时事件组的值。在这种情况下,返回的值将不满足调用任务的取消阻塞条件。
七、流缓冲
1、流缓冲区(Stream Buffer)字节流
1.流缓冲区
纯连续字节流,无消息分隔,适合透明数据流;
流缓冲区是面向字节流的单向 FIFO 缓冲区,内核原生 IPC 组件;
以字节为最小操作单位,可单次读写任意长度字节。
2.核心特性
字节粒度操作:
写入可传入 N 字节,读取可请求读取 M 字节,不需要预先定义单元大小;
单向数据流:
典型生产者写、消费者读;支持任务、中断向流缓冲区写入,仅任务侧读取;
内置阻塞机制:
缓冲区数据不足时,读任务阻塞等待数据;缓冲区剩余空间不足时,写任务可阻塞等待空余空间;
内存占用低,拷贝开销小;相比用大量单字节队列存储数据流,性能更高、内存更少;
中断安全:
提供FromISR系列接口,串口接收中断可直接把字节写入流缓冲区,解析任务阻塞读取。
3.API
c
创建函数
StreamBufferHandle_t xStreamBufferCreate( size_t xBufferSizeBytes,size_t xTriggerLevelBytes );
参数说明:
xBufferSizeBytes :buffer的最大容量字节数
xTriggerLevelBytes:最小有效触发字节,意思是,当buffer至少有大于等于这个值的字节数时,消息才能被接收,该值最小为1,如果设置为0,内部会自动将其设置为1
返回值:
NULL:创建失败
发送函数
size_t xStreamBufferSend( StreamBufferHandle_t xStreamBuffer,const void *pvTxData,size_t xDataLengthBytes,TickType_t xTicksToWait );
参数说明:
xStreamBuffer :句柄
pvTxData:待发送消息的指针
xDataLengthBytes:拷贝到stream buffer的字节数量
xTicksToWait:stream buffer 满了后发送的等待时间
返回值:
最终拷贝到stream buffer的字节数量
接收函数
size_t xStreamBufferReceive( StreamBufferHandle_t xStreamBuffer,void *pvRxData,size_t xBufferLengthBytes,TickType_t xTicksToWait );
参数说明:
xStreamBuffer:句柄
pvRxData:接收buffer的指针
xBufferLengthBytes:一次最大的接收长度
xTicksToWait:等待时间
返回值:
实际接收到的长度
查询流缓冲区剩余空间
size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer );
参数:
xStreamBuffer
被查询的流缓冲区的句柄。
返回值:
在流缓冲区满之前可以写入流缓冲区的字节数。
2、消息缓冲区(Message Buffer)不定长包
1.消息缓冲区
每条消息前置固定长度头,读取时自动识别单条完整消息,兼顾字节流传输与独立消息分包。
2.核心特性
自动消息边界:
写入任意长度消息时,内核自动在数据前存入 2 字节长度标识;读取时直接返回一整条完整消息,不会出现消息截断、字节混杂;
单向通信模型:
仅支持多生产者(任务 / 中断写)、单一消费者(仅任务读),不支持多任务同时读取;
内置阻塞等待机制:
缓冲区空间不足时写任务阻塞;无完整消息时读任务阻塞;
支持中断写入:
提供xMessageBufferSendFromISR中断专用接口,适配串口、CAN 中断分包传输;
内存开销极小,相比普通队列:
无需固定单条消息尺寸,可动态发送不同长度数据包,节省 RAM。
3.API
c
创建
MessageBufferHandle_t xMessageBufferCreate( size_t xBufferSizeBytes );
参数说明:
xBufferSizeBytes :buffer的最大容量,buffer占用的空间为xBufferSizeBytes+4.4字节uint32存放实际buff有效数据的长度
返回值:
NULL:创建失败,一般是堆空间不够
发送
size_t xMessageBufferSend( MessageBufferHandle_t xMessageBuffer,const void *pvTxData,size_t xDataLengthBytes,
TickType_t xTicksToWait );
参数说明:
xMessageBuffer:句柄
pvTxData:发送消息的指针
xDataLengthBytes:发送消息的长度
xTicksToWait :buffer满了等待时间
返回值:
实际写入buffer的数量,如果messagebuff没有足够的长度存储pvTxData,则返回值将会是0
接收
size_t xMessageBufferReceive( MessageBufferHandle_t xMessageBuffer,void *pvRxData,size_t xBufferLengthBytes,TickType_t xTicksToWait );
参数说明:
xMessageBuffer:句柄
pvTxData:接收消息的指针
xDataLengthBytes:接收buffer的长度,如果长度小于messge的长度,返回值为0
xTicksToWait :等待接收时间
返回值
读取到的长度,如果message超过接收buff的长度,返回值将会是0,消息仍然存在消息buffer中。如果message buffer 空,接收超过了xTicksToWait时间,则返回的也是0.
3、环形缓冲区(Ring Buffer)
1.环形缓冲区
环形缓冲区(循环缓冲区、FIFO 环形队列)是一块固定长度连续内存,通过读指针(rd)、写指针(wr)实现循环读写,天然实现先进先出 FIFO,是流缓冲区、消息缓冲区的底层基础数据结构。
2.核性特点
固定内存,无动态分配:
创建时一次性分配整块内存,运行时不堆分配,无内存碎片;
循环复用:
读写指针走到缓冲区末尾时自动折返到头部,内存循环重复利用;
单向 FIFO:
先写入的数据一定先被读出;
区分裸机无锁环形缓冲(单生产者单消费者,无互斥)、带锁线程安全环形缓冲(多任务读写,配合互斥锁)。
3.API
c
//创建与销毁
xRingbufferCreate:动态创建一个指定大小和类型的环形缓冲区。
xRingbufferCreateNoSplit:通过指定单个数据项大小和最大数据项数量来创建,内部会自动分配所需的额外头部空间。
vRingbufferDelete:删除并释放动态创建的环形缓冲区。
//数据发送(写入)
xRingbufferSend:向环形缓冲区中插入一个数据项
xRingbufferSendFromISR:在中断服务程序(ISR)中安全地插入数据项。如果空间不足会立即返回失败,不会阻塞。
xRingbufferSendAcquire & xRingbufferSendComplete:先在缓冲区中申请一块内存并获取其直接地址(适合 DMA 硬件直接写入),数据写入完成后再调用 Complete 提交。
//数据接收(读取)
xRingbufferReceive:从 No-Split 缓冲区检索一个数据项。
xRingbufferReceiveSplit:专门用于从 Allow-Split 缓冲区接收数据。如果数据项在缓冲区尾部被拆分,它会返回指向两部分数据的指针和各自的长度。
xRingbufferReceiveUpTo:专门用于 Byte Buffer(字节流),每次检索指定数量的字节。
//内存释放(必须操作)
vRingbufferReturnItem:在任务上下文中归还数据项。
vRingbufferReturnItemFromISR:在中断上下文中归还数据项。
4、Stream Buffer和Message Buffer的异同
1.Stream Buffer流缓冲区
队列长度不固定,类似于环形队列, 以字节为单位存储。
针对数据有长有短,这种类型就比较适合
2.MessageBuffer 消息缓冲区
消息缓冲区是在 2 StreamBuffer流式缓冲区的基础上实现的,其进一步针对"消息"进行设计改进。
MessageBuffer每一条消息的写入增加了一个字节用来表示该条消息的长度。
读取时需要一次性读出至少一条消息,否则会返回 0.
一条消息包含多字节。
3.如果接收缓冲区小于要接收的数据
Stream Buffer会分多次接收,而MessageBuffer则返回0.
八、示例代码
1、临界区
说明:临界区禁止任务调度,保护极短的共享变量操作,执行时间必须极短。
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
uint32_t g_shared_counter = 0; // 共享全局变量
// 1. 定义并静态初始化一个自旋锁(portMUX_TYPE 是 ESP-IDF 提供的自旋锁类型)
static portMUX_TYPE my_spinlock = portMUX_INITIALIZER_UNLOCKED;
void task1(void *arg)
{
while(1)
{
// 2. 进入临界区:传入自旋锁地址
// 此时当前核心会禁用中断/调度,并等待获取自旋锁
taskENTER_CRITICAL(&my_spinlock);
g_shared_counter++;
// 3. 退出临界区:释放自旋锁,恢复中断和调度
taskEXIT_CRITICAL(&my_spinlock);
printf("Task1 计数器:%lu\n", g_shared_counter);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void task2(void *arg)
{
while(1)
{
// 2. 进入临界区:传入自旋锁地址
// 此时当前核心会禁用中断/调度,并等待获取自旋锁
taskENTER_CRITICAL(&my_spinlock);
g_shared_counter += 2;
// 3. 退出临界区:释放自旋锁,恢复中断和调度
taskEXIT_CRITICAL(&my_spinlock);
printf("Task2 计数器:%lu\n", g_shared_counter);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
xTaskCreate(task1, "task1", 2048, NULL, 5, NULL);
xTaskCreate(task2, "task2", 2048, NULL, 5, NULL);
}
2.执行结果

2、自旋锁
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "spinlock.h"
// 1. 定义并静态初始化自旋锁(必须为全局或静态变量)
static spinlock_t my_spinlock = SPINLOCK_INITIALIZER;
//共享资源
uint32_t g_shared_data = 0;
void task_core0(void *arg)
{
while(1)
{
// 2. 获取自旋锁
// 若锁被其他核心占用,当前核心将在此处持续"自旋"等待
spinlock_acquire(&my_spinlock,SPINLOCK_WAIT_FOREVER);
g_shared_data++;
printf("Core0 共享数据:%lu\n", g_shared_data);
// 3. 释放自旋锁
spinlock_release(&my_spinlock);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void task_core1(void *arg)
{
while(1)
{
// 2. 获取自旋锁
// 若锁被其他核心占用,当前核心将在此处持续"自旋"等待
spinlock_acquire(&my_spinlock,SPINLOCK_WAIT_FOREVER);
g_shared_data += 3;
printf("Core1 共享数据:%lu\n", g_shared_data);
// 3. 释放自旋锁
spinlock_release(&my_spinlock);
vTaskDelay(pdMS_TO_TICKS(700));
}
}
void app_main(void)
{
// 分别绑定到两个CPU核心
xTaskCreatePinnedToCore(task_core0, "core0", 2048, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(task_core1, "core1", 2048, NULL, 5, NULL, 1);
}
2.执行结果

3、信号量
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t g_sem;
void task_sender(void *arg)
{
while(1)
{
printf("发送信号...\n");
xSemaphoreGive(g_sem); // 释放信号量
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void task_receiver(void *arg)
{
while(1)
{
if(xSemaphoreTake(g_sem, portMAX_DELAY) == pdPASS)
{
printf("收到信号,执行任务\n");
}
}
}
void app_main(void)
{
g_sem = xSemaphoreCreateBinary(); // 创建二值信号量
xTaskCreate(task_sender, "sender", 2048, NULL, 4, NULL);
xTaskCreate(task_receiver, "receiver", 2048, NULL, 5, NULL);
}
2.执行结果

4、二值信号量
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#define KEY_PIN GPIO_NUM_7
SemaphoreHandle_t g_key_sem;
static void key_isr(void *arg)
{
BaseType_t xWake = pdFALSE;
xSemaphoreGiveFromISR(g_key_sem, &xWake); // 中断释放二值信号量
portYIELD_FROM_ISR(xWake);
}
void key_task(void *arg)
{
uint32_t cnt = 0;
while(1)
{
if(xSemaphoreTake(g_key_sem, portMAX_DELAY) == pdPASS)
{
vTaskDelay(pdMS_TO_TICKS(20)); // 消抖
if(gpio_get_level(KEY_PIN) == 0)
{
printf("按键按下,第 %lu 次\n", ++cnt);
}
}
}
}
void app_main(void)
{
g_key_sem = xSemaphoreCreateBinary();
gpio_config_t io = {
.pin_bit_mask = 1ULL << KEY_PIN,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE
};
gpio_config(&io);
gpio_install_isr_service(0);
gpio_isr_handler_add(KEY_PIN, key_isr, NULL);
xTaskCreate(key_task, "key_task", 2048, NULL, 5, NULL);
}
2.执行结果

5、计数信号量(资源池管理)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#define RESOURCE_TOTAL 3 // 总资源数
#define TASK_NUM 5 // 竞争任务数
SemaphoreHandle_t g_resource_sem;
void worker_task(void *arg)
{
int id = (int)arg;
while(1)
{
if(xSemaphoreTake(g_resource_sem, pdMS_TO_TICKS(200)) == pdPASS)
{
printf("[任务%d] 获取资源,开始使用\n", id);
vTaskDelay(pdMS_TO_TICKS(500)); // 模拟使用资源
printf("[任务%d] 使用完毕,释放资源\n", id);
xSemaphoreGive(g_resource_sem); // 归还资源
}
else
{
printf("[任务%d] 等待资源超时\n", id);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void app_main(void)
{
// 创建计数信号量:最大3,初始3(资源池模式,初始全可用)
g_resource_sem = xSemaphoreCreateCounting(RESOURCE_TOTAL, RESOURCE_TOTAL);
for(int i = 1; i <= TASK_NUM; i++)
{
xTaskCreate(worker_task, "worker", 2048, (void *)i, 4, NULL);
}
}
2.执行结果

6、互斥锁(共享资源保护)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t g_uart_mutex; // 串口互斥锁
void temp_task(void *arg)
{
float temp = 25.0f;
while(1)
{
xSemaphoreTake(g_uart_mutex, portMAX_DELAY); // 加锁
printf("======== 温度 ========\n");
printf("当前温度:%.1f ℃\n", temp);
printf("======================\n");
xSemaphoreGive(g_uart_mutex); // 解锁
temp += 0.2f;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void volt_task(void *arg)
{
float volt = 3.3f;
while(1)
{
xSemaphoreTake(g_uart_mutex, portMAX_DELAY);
printf("-------- 电压 --------\n");
printf("当前电压:%.2f V\n", volt);
printf("----------------------\n");
xSemaphoreGive(g_uart_mutex);
volt -= 0.01f;
vTaskDelay(pdMS_TO_TICKS(800));
}
}
void app_main(void)
{
g_uart_mutex = xSemaphoreCreateMutex(); // 创建互斥锁
xTaskCreate(temp_task, "temp", 2048, NULL, 5, NULL);
xTaskCreate(volt_task, "volt", 2048, NULL, 4, NULL);
}
2.执行结果

7、递归互斥锁(多层嵌套加锁)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t g_rec_mutex;
// 内层函数(第一层加锁)
void inner_func(void)
{
xSemaphoreTakeRecursive(g_rec_mutex, portMAX_DELAY);
printf(" 内层函数执行\n");
xSemaphoreGiveRecursive(g_rec_mutex);
}
// 中层函数(第二层加锁)
void middle_func(void)
{
xSemaphoreTakeRecursive(g_rec_mutex, portMAX_DELAY);
printf("中层函数执行\n");
inner_func(); // 调用内层,内层再加锁
xSemaphoreGiveRecursive(g_rec_mutex);
}
// 外层函数(第三层加锁)
void outer_func(void)
{
xSemaphoreTakeRecursive(g_rec_mutex, portMAX_DELAY);
printf("外层函数执行\n");
middle_func(); // 调用中层,中层再加锁
printf("外层函数结束\n\n");
xSemaphoreGiveRecursive(g_rec_mutex);
}
void app_main(void)
{
g_rec_mutex = xSemaphoreCreateRecursiveMutex(); // 创建递归互斥锁
outer_func(); // 三层嵌套加锁,不会死锁
}
2.执行结果

8、队列(基础收发)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
QueueHandle_t g_queue;
void sender_task(void *arg)
{
int data = 0;
while(1)
{
data++;
xQueueSend(g_queue, &data, portMAX_DELAY); // 发送数据
printf("发送:%d\n", data);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void receiver_task(void *arg)
{
int recv;
while(1)
{
if(xQueueReceive(g_queue, &recv, portMAX_DELAY) == pdPASS)
{
printf("接收:%d\n\n", recv);
}
}
}
void app_main(void)
{
g_queue = xQueueCreate(5, sizeof(int)); // 创建队列:5条,每条int大小
xTaskCreate(sender_task, "sender", 2048, NULL, 4, NULL);
xTaskCreate(receiver_task, "receiver", 2048, NULL, 5, NULL);
}
2.执行结果

9、队列(三种数据类型传输)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 自定义结构体
typedef struct {
float temp;
float humi;
uint32_t time;
} sensor_data_t;
QueueHandle_t g_byte_queue; // 字节队列
QueueHandle_t g_int_queue; // 整型队列
QueueHandle_t g_struct_queue; // 结构体队列
void send_task(void *arg)
{
uint8_t byte_val = 0xAB;
int int_val = 12345;
sensor_data_t struct_val = {25.5f, 60.2f, 100};
while(1)
{
xQueueSend(g_byte_queue, &byte_val, 0);
xQueueSend(g_int_queue, &int_val, 0);
xQueueSend(g_struct_queue, &struct_val, 0);
printf("三种数据已发送\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void recv_task(void *arg)
{
uint8_t byte_recv;
int int_recv;
sensor_data_t struct_recv;
while(1)
{
if(xQueueReceive(g_byte_queue, &byte_recv, portMAX_DELAY))
{
printf("字节:0x%02X\n", byte_recv);
}
if(xQueueReceive(g_int_queue, &int_recv, 0))
{
printf("整型:%d\n", int_recv);
}
if(xQueueReceive(g_struct_queue, &struct_recv, 0))
{
printf("结构体:温度%.1f 湿度%.1f 时间%lu\n\n",
struct_recv.temp, struct_recv.humi, struct_recv.time);
}
}
}
void app_main(void)
{
g_byte_queue = xQueueCreate(3, sizeof(uint8_t));
g_int_queue = xQueueCreate(3, sizeof(int));
g_struct_queue = xQueueCreate(3, sizeof(sensor_data_t));
xTaskCreate(send_task, "send", 2048, NULL, 4, NULL);
xTaskCreate(recv_task, "recv", 2048, NULL, 5, NULL);
}
2.执行结果

10、多进单出模型
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
QueueHandle_t g_queue;
// 生产者1:温度数据
void producer1(void *arg)
{
int data = 100;
while(1)
{
xQueueSend(g_queue, &data, portMAX_DELAY);
printf("生产者1发送:%d\n", data);
data++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 生产者2:电压数据
void producer2(void *arg)
{
int data = 200;
while(1)
{
xQueueSend(g_queue, &data, portMAX_DELAY);
printf("生产者2发送:%d\n", data);
data++;
vTaskDelay(pdMS_TO_TICKS(1500));
}
}
// 消费者:统一接收处理
void consumer(void *arg)
{
int recv;
while(1)
{
if(xQueueReceive(g_queue, &recv, portMAX_DELAY))
{
printf(" 消费者收到:%d\n\n", recv);
}
}
}
void app_main(void)
{
g_queue = xQueueCreate(10, sizeof(int));
xTaskCreate(producer1, "prod1", 2048, NULL, 4, NULL);
xTaskCreate(producer2, "prod2", 2048, NULL, 4, NULL);
xTaskCreate(consumer, "consumer", 2048, NULL, 5, NULL);
}
2.执行结果

11、多队列单出模型(队列集QueueSet)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
QueueHandle_t g_can_queue;
QueueHandle_t g_uart_queue;
QueueSetHandle_t g_queue_set;
void can_task(void *arg)
{
int data = 10;
while(1)
{
xQueueSend(g_can_queue, &data, portMAX_DELAY);
printf("CAN发送:%d\n", data++);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void uart_task(void *arg)
{
int data = 20;
while(1)
{
xQueueSend(g_uart_queue, &data, portMAX_DELAY);
printf("UART发送:%d\n", data++);
vTaskDelay(pdMS_TO_TICKS(1500));
}
}
void ctrl_task(void *arg)
{
QueueSetMemberHandle_t active;
int recv;
while(1)
{
// 等待队列集中任意队列有数据
active = xQueueSelectFromSet(g_queue_set, portMAX_DELAY);
if(active == g_can_queue)
{
xQueueReceive(g_can_queue, &recv, 0);
printf(" 收到CAN数据:%d\n\n", recv);
}
else if(active == g_uart_queue)
{
xQueueReceive(g_uart_queue, &recv, 0);
printf(" 收到UART数据:%d\n\n", recv);
}
}
}
void app_main(void)
{
g_can_queue = xQueueCreate(3, sizeof(int));
g_uart_queue = xQueueCreate(3, sizeof(int));
g_queue_set = xQueueCreateSet(6); // 创建队列集
xQueueAddToSet(g_can_queue, g_queue_set); // 加入队列集
xQueueAddToSet(g_uart_queue, g_queue_set);
xTaskCreate(can_task, "can", 2048, NULL, 4, NULL);
xTaskCreate(uart_task, "uart", 2048, NULL, 4, NULL);
xTaskCreate(ctrl_task, "ctrl", 2048, NULL, 5, NULL);
}
2.执行结果

12、邮箱MailBox(一对多广播最新状态)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
QueueHandle_t g_temp_mailbox; // 温度邮箱
// 写任务:更新温度(覆盖旧值)
void write_task(void *arg)
{
float temp = 25.0f;
while(1)
{
temp += 0.5f;
xQueueOverwrite(g_temp_mailbox, &temp); // 覆盖写入,邮箱专用
printf("更新温度:%.1f ℃\n\n", temp);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 读任务1:显示
void read1_task(void *arg)
{
float val;
while(1)
{
xQueuePeek(g_temp_mailbox, &val, portMAX_DELAY); // 窥探读取,不移除
printf("[显示任务] 温度:%.1f ℃\n", val);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 读任务2:报警判断
void read2_task(void *arg)
{
float val;
while(1)
{
xQueuePeek(g_temp_mailbox, &val, portMAX_DELAY);
if(val > 28) printf("[报警任务] 温度过高!%.1f ℃\n", val);
vTaskDelay(pdMS_TO_TICKS(1500));
}
}
void app_main(void)
{
// 邮箱 = 长度为1的队列
g_temp_mailbox = xQueueCreate(1, sizeof(float));
xTaskCreate(write_task, "write", 2048, NULL, 5, NULL);
xTaskCreate(read1_task, "read1", 2048, NULL, 4, NULL);
xTaskCreate(read2_task, "read2", 2048, NULL, 4, NULL);
}
2.执行结果

13、任务通知同步(替代二值信号量)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define KEY_PIN GPIO_NUM_7
TaskHandle_t g_key_task_handle; // 任务句柄
static void key_isr(void *arg)
{
BaseType_t xWake = pdFALSE;
vTaskNotifyGiveFromISR(g_key_task_handle, &xWake); // 中断发送通知
portYIELD_FROM_ISR(xWake);
}
void key_task(void *arg)
{
uint32_t cnt = 0;
while(1)
{
// 等待通知,pdTRUE表示接收后清零(二值信号量模式)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(20));
if(gpio_get_level(KEY_PIN) == 0)
{
printf("按键按下,第 %lu 次\n", ++cnt);
}
}
}
void app_main(void)
{
// 创建任务时保存句柄
xTaskCreate(key_task, "key_task", 2048, NULL, 5, &g_key_task_handle);
gpio_config_t io = {
.pin_bit_mask = 1ULL << KEY_PIN,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE
};
gpio_config(&io);
gpio_install_isr_service(0);
gpio_isr_handler_add(KEY_PIN, key_isr, NULL);
}
2.执行结果

14、任务通知的值(传递32位的数值)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
TaskHandle_t g_recv_handle;
// 发送任务:用通知传递数值
void send_task(void *arg)
{
uint32_t val = 100;
while(1)
{
// eSetValueWithOverwrite:覆盖写入通知值(邮箱模式)
xTaskNotify(g_recv_handle, val, eSetValueWithOverwrite);
printf("发送通知值:%lu\n", val);
val += 10;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 接收任务
void recv_task(void *arg)
{
uint32_t notif_val;
while(1)
{
// 等待通知,收到后读取通知值
if(xTaskNotifyWait(0, 0xFFFFFFFF, ¬if_val, portMAX_DELAY) == pdPASS)
{
printf("收到通知值:%lu\n\n", notif_val);
}
}
}
void app_main(void)
{
xTaskCreate(recv_task, "recv", 2048, NULL, 5, &g_recv_handle);
xTaskCreate(send_task, "send", 2048, NULL, 4, NULL);
}
2.执行结果

15、事件标志组等待(任意 / 全部事件)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
// 定义事件位
#define KEY_EVENT (1 << 0)
#define UART_EVENT (1 << 1)
#define CAN_EVENT (1 << 2)
EventGroupHandle_t g_event_group;
// 模拟按键事件
void key_sim_task(void *arg)
{
while(1)
{
vTaskDelay(pdMS_TO_TICKS(2000));
xEventGroupSetBits(g_event_group, KEY_EVENT);
printf("按键事件触发\n");
}
}
// 模拟串口事件
void uart_sim_task(void *arg)
{
while(1)
{
vTaskDelay(pdMS_TO_TICKS(3000));
xEventGroupSetBits(g_event_group, UART_EVENT);
printf("串口事件触发\n");
}
}
// 处理任务:等待任意事件
void handle_task(void *arg)
{
EventBits_t bits;
while(1)
{
// 等待任意一个事件,唤醒后自动清零
bits = xEventGroupWaitBits(
g_event_group,
KEY_EVENT | UART_EVENT | CAN_EVENT,
pdTRUE, // 退出时清除匹配位
pdFALSE, // pdFALSE=任意位触发(ANY模式)
portMAX_DELAY
);
printf("\n===== 事件处理 =====\n");
if(bits & KEY_EVENT) printf("处理按键逻辑\n");
if(bits & UART_EVENT) printf("处理串口数据\n");
if(bits & CAN_EVENT) printf("处理CAN报文\n");
printf("====================\n\n");
}
}
void app_main(void)
{
g_event_group = xEventGroupCreate();
xTaskCreate(key_sim_task, "key", 1024, NULL, 3, NULL);
xTaskCreate(uart_sim_task, "uart", 1024, NULL, 3, NULL);
xTaskCreate(handle_task, "handle", 2048, NULL, 4, NULL);
}
2.执行结果

16、事件标志组同步(多任务同步屏障)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
// 三个任务的就绪位
#define TASK1_READY (1 << 0)
#define TASK2_READY (1 << 1)
#define TASK3_READY (1 << 2)
#define ALL_READY (TASK1_READY | TASK2_READY | TASK3_READY)
EventGroupHandle_t g_sync_group;
// 任务1:到达同步点
void task1(void *arg)
{
printf("任务1:执行前置工作...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟工作
xEventGroupSetBits(g_sync_group, TASK1_READY); // 标记就绪
printf("任务1:到达同步点,等待其他任务\n");
// 等待所有任务都就绪
xEventGroupWaitBits(g_sync_group, ALL_READY, pdFALSE, pdTRUE, portMAX_DELAY);
printf("任务1:全部就绪,继续执行\n\n");
}
// 任务2
void task2(void *arg)
{
printf("任务2:执行前置工作...\n");
vTaskDelay(pdMS_TO_TICKS(2000));
xEventGroupSetBits(g_sync_group, TASK2_READY);
printf("任务2:到达同步点,等待其他任务\n");
xEventGroupWaitBits(g_sync_group, ALL_READY, pdFALSE, pdTRUE, portMAX_DELAY);
printf("任务2:全部就绪,继续执行\n\n");
}
// 任务3
void task3(void *arg)
{
printf("任务3:执行前置工作...\n");
vTaskDelay(pdMS_TO_TICKS(3000));
xEventGroupSetBits(g_sync_group, TASK3_READY);
printf("任务3:到达同步点,等待其他任务\n");
xEventGroupWaitBits(g_sync_group, ALL_READY, pdFALSE, pdTRUE, portMAX_DELAY);
printf("任务3:全部就绪,继续执行\n\n");
}
void app_main(void)
{
g_sync_group = xEventGroupCreate();
xTaskCreate(task1, "task1", 2048, NULL, 4, NULL);
xTaskCreate(task2, "task2", 2048, NULL, 4, NULL);
xTaskCreate(task3, "task3", 2048, NULL, 4, NULL);
}
2.执行结果

17、流缓冲区(字节流)
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h"
StreamBufferHandle_t g_stream_buf;
// 写任务:写入字节流
void write_task(void *arg)
{
uint8_t data[] = "Hello StreamBuffer!";
while(1)
{
size_t len = xStreamBufferSend(g_stream_buf, data, sizeof(data), portMAX_DELAY);
printf("写入 %zu 字节\n", len);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 读任务:读取字节流
void read_task(void *arg)
{
uint8_t buf[64];
while(1)
{
// 触发阈值设为10,攒够10字节才唤醒
size_t len = xStreamBufferReceive(g_stream_buf, buf, sizeof(buf), portMAX_DELAY);
printf("读取 %zu 字节:%s\n\n", len, buf);
}
}
void app_main(void)
{
// 总容量256字节,触发阈值10字节
g_stream_buf = xStreamBufferCreate(256, 10);
xTaskCreate(write_task, "write", 2048, NULL, 4, NULL);
xTaskCreate(read_task, "read", 2048, NULL, 5, NULL);
}
2.执行结果

18、消息缓冲区
1.示例代码
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/message_buffer.h"
MessageBufferHandle_t g_msg_buf;
// 写任务:发送多条不同长度消息
void write_task(void *arg)
{
char msg1[] = "短消息";
char msg2[] = "这是一条比较长的消息内容";
while(1)
{
xMessageBufferSend(g_msg_buf, msg1, sizeof(msg1), portMAX_DELAY);
printf("发送消息1(%zu字节)\n", sizeof(msg1));
vTaskDelay(pdMS_TO_TICKS(1000));
xMessageBufferSend(g_msg_buf, msg2, sizeof(msg2), portMAX_DELAY);
printf("发送消息2(%zu字节)\n\n", sizeof(msg2));
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 读任务:每次读取一条完整消息
void read_task(void *arg)
{
char buf[128];
while(1)
{
size_t len = xMessageBufferReceive(g_msg_buf, buf, sizeof(buf), portMAX_DELAY);
printf("收到完整消息,长度 %zu 字节:%s\n\n", len, buf);
}
}
void app_main(void)
{
g_msg_buf = xMessageBufferCreate(512); // 总容量512字节
xTaskCreate(write_task, "write", 2048, NULL, 4, NULL);
xTaskCreate(read_task, "read", 2048, NULL, 5, NULL);
}
2.执行结果

19、环型缓冲区
1.示例代码
c
#include <stdio.h>
#include <string.h>
#include "freertos/ringbuf.h"
#include "freertos/task.h"
// 1. 创建环形缓冲区句柄
RingbufHandle_t rb_handle = NULL;
void app_main(void)
{
// 2. 创建一个 1024 字节的字节流环形缓冲区
// RINGBUF_TYPE_BYTEBUF 不将数据视为独立项,适合处理连续字节流
rb_handle = xRingbufferCreate(1024, RINGBUF_TYPE_BYTEBUF);
if (rb_handle == NULL) {
printf("创建环形缓冲区失败!\n");
return;
}
// 3. 生产者:向缓冲区写入数据
char tx_data[] = "Hello ESP32 RingBuf!";
UBaseType_t res = xRingbufferSend(
rb_handle,
tx_data,
strlen(tx_data),
pdMS_TO_TICKS(1000) // 超时等待时间
);
if (res != pdTRUE) {
printf("数据写入失败(可能缓冲区已满)\n");
}
// 4. 消费者:从缓冲区读取数据
size_t rx_len = 0;
// 注意:字节缓冲区读取时,返回的是指向缓冲区内部内存的指针
char *rx_data = (char *)xRingbufferReceive(rb_handle, &rx_len, pdMS_TO_TICKS(1000));
if (rx_data != NULL) {
printf("读取到 %d 字节数据: %.*s\n", rx_len, rx_len, rx_data);
// 核心步骤:必须将读取到的项归还给缓冲区,否则空间不会被释放
vRingbufferReturnItem(rb_handle, (void *)rx_data);
}
// 5. 删除缓冲区
vRingbufferDelete(rb_handle);
}
2.执行结果
