文章目录
- 任务间为什么需要通信?
- 如何实现任务间通信
- FreeRTOS的消息队列
-
- 消息队列的机制原理
-
- 数组循环队列
- 四个操作指针
- FreeRTOS消息队列的结构示意图
- 阻塞队列的核心(重点)
- 消息队列的线程安全(重点)
- 消息队列核心操作函数
-
- xQueueCreate:创建消息队列
- xQueueSend:向队列发送数据
- xQueueReceive:从队列接收数据
- 消息队列的使用举例(任务与任务通信)
-
- 场景一:一个任务发送不接收
- 场景二:优先级一致的两个任务通信
- 扩展了解:利用结构体校验数据边界
- 消息队列的使用举例(任务与中断通信)
-
- xQueueSendFromISR:在中断中向消息队列发送数据
- 为什么不学习任务向中断发送数据的函数?
- 使用消息队列的参考实现代码
- 使用消息队列的好处
- FreeRTOS消息队列特点总结
- 消息队列的常用函数
任务间为什么需要通信?
在 FreeRTOS 多任务系统中,本质上就是很多个并行干活的工人。
比如:
但工人多了就一定会遇到一个现实问题:
大多数时候你干你的,我干我的,但总有需要协同工作的时候。
比如:
它们之间必然要传数据、发通知、同步工作节奏、互相“喊一声”。
在基于 FreeRTOS 实时操作系统的多任务环境中,各任务运行在相互独立的执行上下文中。
为了完成协同工作,例如:
就必须通过某种机制完成:
这些机制统称为:任务间通信(Inter-Task Communication, ITC)
所以,我们可以看到,任务间具有通信的需求,就像“多个工人”完成工作需要沟通协作一样。
除此之外,任务与中断之间也有通信的需求,而且多是中断向中断传输数据、或者发送通知。
虽然标题叫做任务间通信,但任务与中断间的通信,也一并也在这里讲解。
如何实现任务间通信
现在我们已经知道,任务间确实存在通信的需求。
任务之间不仅要各自运行,还必须互相交换数据、协调工作节奏。
那问题来了:
既然任务之间需要通信,那么如何实现任务间的通信呢?
一个最简单,最容易想到的方式是:
使用全局变量。
比如定义一个全局变量:
int16_t Temperature;
A 任务负责采集传感器数据:
Temperature = ReadSensor();
B 任务负责在 OLED 上显示数据:
OLED_ShowNum(Temperature);
乍一看,好像也没什么毛病。
但实际上问题还是挺多的,在多个任务并行的系统中:
你会发现:
事情开始变得复杂了。
为了弥补这些问题,往往就需要:
慢慢地,你的程序里就会出现一堆xxxFlag:
IsDataReady
DataUpdatedFlag
BusyFlag
UpdateFinishedFlag
你的大量时间,不再用来做功能开发,而是花在维护这些全局变量和状态逻辑上。
这显然是非常不可取的。
因为:
搞的太复杂,既容易出错,又难以维护,还严重影响系统的可扩展性。
那怎么办呢?
FreeRTOS 官方早就替我们思考过这个问题。
在 FreeRTOS 中,已经为我们提供了一套标准、安全、可控的任务间通信机制。
一共有哪些手段实现任务间通信呢?
FreeRTOS 为我们提供了以下常见的任务间通信手段:
这些通信机制看起来很多,但并不是每一种都同等重要、同等常用。
从工程实践角度来说,可以抓住三条主线:
当然,总的来说:
消息队列是基础,是核心。
不管怎么样,消息队列都是这些手段中的基础。
其余通信手段,其实大部分也都涉及消息队列的使用。
所以接下来,接下来,我们就从最核心的通信手段开始——
看看消息队列是如何实现任务间通信的。
FreeRTOS的消息队列
在前面的课程内容中,我们已经学习过诸如:
等概念。
有了这些概念理论做支撑,我们直接看消息队列的源码,直接搞懂FreeRTOS消息队列的机制原理,也并不困难。
所以,我们学习FreeRTOS的消息队列,第一件事情就是直接看源码。
这既是对前面学习知识点的巩固和实战,也是你了解FreeRTOS消息队列最本质的第一步。
消息队列的机制原理
首先回答一个核心问题:
FreeRTOS 的消息队列到底是什么结构?
在源码queue.c文件中,所有消息队列相关实现都在这里。
当然,我们想要使用这个消息队列,也需要包含头文件queue.h。
消息队列的核心数据结构,如下图所示:

消息队列本质上是一个结构体类型,其中最核心的成员如下(简化版):
typedef struct QueueDefinition{
// 第一部分四个成员
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
int8_t *pcReadFrom;
// 第二部分两个成员
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
// 第三部分三个成员
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
} xQUEUE;
这些最核心的成员,我们分为三个部分来进行讲解。
我们首先讲解一下第三部分,因为它是消息队列的核心。
Tips:
在上面贴出的源码注释中,可以看到频繁出现**“Items”**单词,它是FreeRTOS消息队列的基本组成单元。
在下面的文档中,统一将FreeRTOS消息队列的基本组成单元,翻译成**“队列元素”**。
使用元素这个翻译,更符合数据结构中的术语使用习惯。
数组循环队列
首先我们先来看一下下面三个成员:
// 第三部分三个成员
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
一个一个来介绍一下。
先看第一个:
volatile UBaseType_t uxMessagesWaiting;
这个变量表示:
当前消息队列里已经存放了多少个元素。
注意是“队列元素的个数”,不是字节的个数。
这个成员的作用非常简单直接:就是用于判断队列是否空或满。
可以理解为:这是队列的“元素库存计数器”。
再看第二个成员:
UBaseType_t uxLength;
这个成员表示:
此消息队列中,最多可以存放多少个元素。
强调一点:
它表示的是“元素数量”,不是字节数。
如果uxLength = 5,说明这个队列最多放 5 个元素。
那么一个元素多大呢?
这就要看第三个成员:
UBaseType_t uxItemSize;
这个成员表示:
每个元素占多少字节。
当你创建队列时:会调用函数xQueueCreate。
实际上就是给此结构体的这两个成员传参,比如调用方式如下:
// 参数1:uxLength, 参数2:uxItemSize
xQueueCreate( 5, 4 );
那么:
uxLength = 5
uxItemSize = 4
最终,你所创建的消息队列实际上是一个总长度20个字节,存储了5个大小为4个字节元素的数组,而且它是一个数组循环队列。
上面描述的内容,可以参考以下源码:


通常这些源码,我们能够得到以下结论:
四个操作指针
以下四个成员:
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
int8_t *pcReadFrom;
本质上是:
用来管理一块连续数组内存的“边界指针 + 读写位置指针”。
我们已经知道,消息队列的数据区域本质上是一个数组。
那么这四个指针的作用,就是对这块数组内存区域进行“循环队列式”的管理。
其中:
pcHead指针:指向数组的起始地址,它是整个循环队列的“起点”。
pcTail指针,指向数组末尾的越界地址。
注意:
它并不是指向“最后一个元素”,而是指向“最后一个元素的下一个元素“。
也就是说:
pcTail 本质上是循环队列数组的“终止边界标记”。
以上两个指针是固定不动的。
它们只是用来标识这块内存区域的边界,在队列运行过程中不会发生移动。
关于这两点,可以通过以下源码获悉:


在队列进行读写操作时,真正会移动的是下面两个指针:
pcWriteTo 指针:指向“下一次写入元素”的位置。
FreeRTOS 的设计是:
这就是循环队列的由来。
pcReadFrom 指针:指向“上一次读取元素的位置”。
注意这里非常关键:
它并不是指向“当前可读元素”。
FreeRTOS 的设计是:
把读指针设计为“指向上一次读的位置”,可以让读操作也采用“先移动(含回绕处理)→ 再读取”的统一流程,减少边界的特殊判定。
关于这两个读写位置指针,其源码如下图所示:

所以,这四个指针本质就是:
两个边界指针(Head / Tail)
两个移动指针(Write / Read)
它们配合在一起,让一个普通数组具备了:循环队列的能力。
最后还是需要强调一点的就是:
队列满不满,不靠指针判断,而是靠成员uxMessagesWaiting来进行判断。
指针只负责移动,这个计数器用来记录队列状态。
FreeRTOS消息队列的结构示意图
依照上面的内容,我们可以画出以下 FreeRTOS 消息队列的结构示意图:

这张图主要展示了:
在队列 初始创建完成时,四个核心控制指针的位置如下图所示:

现在向队列 新增一个元素(入队列):
如下图所示

接下来 从队列读取一个元素(出队列):
如下图所示:

在这种情况下,再 向队列连续插入三个元素:
如下图所示:

可以看到:
正是基于两个固定的边界指针(pcHead、pcTail),以及两个可以回绕移动的读写位置指针(pcWriteTo、pcReadFrom)
FreeRTOS 的消息队列最终实现了一个 循环数组队列(Circular Queue) 的数据结构。
阻塞队列的核心(重点)
FreeRTOS的消息队列是一个阻塞队列,那么它为什么可以阻塞呢?
本质原因就是下面两个成员:
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
这个List_t类型,我们并不陌生。
因为我们前面讲过的:就绪列表、阻塞列表等列表,它们底层用的,都是基于List_t类型的一条链表结构。
所以现在你应该意识到一件事:
FreeRTOS的消息队列之所以能“阻塞”,是一个阻塞队列
必然不是因为其数组的结构,也跟四个管理指针没有关系
而是队列控制块内部维护了两个“事件等待列表”,用于管理因等待队列条件而进入阻塞态的任务。
一旦某个消息队列满了,那么负责往队列发送元素的任务,如果还想继续发送元素,就会:
那等待什么条件被唤醒呢?
当有其他任务从队列接收数据,从队列中取走一个元素,腾出一个空位之后。
调度器会唤醒此任务,任务从阻塞态回到就绪态,随后根据优先级进行任务调度。
一旦某个消息队列是空的,那么负责从队列接收元素的任务,如果还想继续接收元素,就会:
等什么时候被唤醒呢?
当有其他任务往队列发送1个元素,消息队列不再为空之后。
调度器会唤醒此任务,任务从阻塞态回到就绪态,随后根据优先级进行任务调度。
所以,我们可以得出一个非常关键的结论:
FreeRTOS的消息队列是一个阻塞队列
但这个阻塞指的是“执行收发队列元素的任务”被阻塞,而不是队列本身是阻塞式的。
被阻塞的是任务,而不是队列本身
消息队列本身,就只是一个普通的数组循环队列。
消息队列的线程安全(重点)
如果你没深入的看上述文档,但看到这里,你也应该记住:
到这里,FreeRTOS消息队列的大部分原理,你就搞懂了。
但还有一个非常核心的问题:
两个任务,乃至于多个任务,都需要操作同一个消息队列。
在FreeRTOS的多任务环境下,这很显然属于,访问了共享资源。
这势必产生竞态条件,导致线程安全问题。
那消息队列是如何解决这个问题,保持线程安全,保证多个任务同时访问它,而不会乱呢?
原因也很简单:
FreeRTOS消息队列的读写操作,都是在临界区内完成的,某个任务在执行读写队列操作时,执行的都是一个原子操作。
比如下列源码:

需要注意的是,消息队列使用的临界区是:
taskENTER_CRITICAL();
...
taskEXIT_CRITICAL();
这意味着:
所以,通过加临界区的方式,使得队列的数据修改阶段,其操作是一个原子操作。
从而保障了消息队列的线程安全。
最后做一个总结:
消息队列的阻塞机制解决了“任务调度问题”
而临界区机制解决了“并发线程安全问题”
两套机制配合,构成了完整的、安全的、消息队列实现。
消息队列核心操作函数
了解了 FreeRTOS 消息队列的核心机制和工作原理之后,下面我们正式进入 消息队列的核心操作函数 部分。
在 FreeRTOS 中,消息队列的核心 API 实际上只有三个,也是我们在实际开发中最常用、最重要的三个函数:
可以说,只要掌握了这三个函数,消息队列的基本使用就已经完全没有问题了。
下面我们逐一介绍这三个函数。
首先,这三个函数在FreeRTOS都是默认开启的,前提是启用了 动态内存分配机制,即开启下面的宏:
#define configSUPPORT_DYNAMIC_ALLOCATION 1
也就是依靠动态分配内存宏,它是默认开启的。
这是因为 xQueueCreate() 本质上是通过动态分配内存来创建结构体和队列数据存储区的。
xQueueCreate:创建消息队列
其函数原型如下:
QueueHandle_t xQueueCreate(
// UBaseType_t也就是BaseType_t的无符号版本,其实就是unsigned long,通常就是无符号4字节
const UBaseType_t uxQueueLength, // 数组循环队列的元素数量,也就是循环队列最多装几个元素
const UBaseType_t uxItemSize, // 数组循环队列中每个元素的大小,以字节为单位
)
两个参数非常关键:
这两个参数共同决定了消息队列的数据区域——数组循环队列的长度大小,以及可以存储几个元素。
举例:
QueueHandle_t Queue = xQueueCreate(5, 4);
这表示:
返回值,得到一个QueueHandle_t类型的返回值。

这种情况我们太熟悉的。
此函数的返回值,就是FreeRTOS消息队列的操作句柄,也就是一个操作消息队列的凭证。
当然,这个消息队列句柄本质上是一个消息队列结构体对象的指针。
由于结构体并没有定义在头文件中,你并不能直接利用这个指针操作结构体。
函数的返回值如果是 NULL, 则说明内存不足,消息队列创建失败。
xQueueSend:向队列发送数据
其函数原型如下:
BaseType_t xQueueSend(
QueueHandle_t xQueue, // 消息队列句柄,也就是“队列对象的指针/引用”
const void * pvItemToQueue, // 要发送数据的指针
TickType_t xTicksToWait // 消息队列满时,最多愿意等多久(单位:tick)
);
该函数用于,在任务中向消息队列发送数据,也就是向队列尾部写入一个元素。
xQueue参数:
表示要操作的队列句柄。或者说,表示往哪个队列发送数据。
它是 xQueueCreate() 返回的值,本质可以理解为队列控制结构的引用。
pvItemToQueue参数:
表示要发送数据的地址。
类型为 const void *,说明可以发送任意类型的数据。
需要特别强调:
队列发送的是元素内容的拷贝,而不是指针本身。
执行时会从 pvItemToQueue 指向的地址开始,拷贝 uxItemSize 个字节,存入数组循环队列的一个元素位置。
整个过程实际上就是执行了一个memcpy函数。如下源码:

例如:
uint32_t Value = 100;
xQueueSend(QueueHandle, &Value, 0);
进入队列的是数值 100 的副本,即使 Value是局部变量也不会出问题。
注意:发送数据的格式要和创建队列时指定元素的长度保持一致。
比如队列创建时一个元素是4个字节,pvItemToQueue指针指向的数据也应该是4字节的。
xTicksToWait参数:
表示当消息队列已满时,当前任务最多等待多少个 tick(1个tick默认设置是1ms)。
它的取值规则如下:
关于宏portMAX_DELAY ,这里要多说两句:
第一,从宏定义上来说,它其实就是4字节无符号整型的最大值:

这么看,使用这个宏填在xTicksToWait位置,表示最大阻塞等待大约49天多。
这个时间对于嵌入式系统而言,确实已经非常“大”了。几乎可以说是无限期阻塞等待。
但事实真的是这样吗?
当然不是。
实际上:
在使用宏portMAX_DELAY作为参数,配合默认允许无限期阻塞的宏配置:

熟悉不,这不就是之前学挂起状态的相关宏。
相关的源码如下:

所以内核实际是这么做的:
使用 portMAX_DELAY 且队列操作的条件不满足时,任务实际上被内核移入挂起列表,进入挂起态。
但这个挂起态比较特殊:
关于函数的返回值也要着重说一下:
此函数的行为逻辑可以这样描述:
理解了往队列发送数据,那么从队列接收数据也是差不多的,只不过数据方向改变了。
xQueueReceive:从队列接收数据
函数原型如下:
BaseType_t xQueueReceive(
QueueHandle_t xQueue, // 表示要操作的队列句柄。
void * pvBuffer, // 表示用于接收数据的缓冲区地址。
TickType_t xTicksToWait // 消息队列空时,最多愿意等多久(单位:tick)
);
该函数用于,在任务中从消息队列接收数据,也就是从队列头部移除一个元素,并返回元素的取值。
xQueueReceive 的本质是:
从队列中取出一个元素,并拷贝到用户提供的缓冲区,若队列为空,则根据等待时间决定是否阻塞。
**xQueue参数:**表示要操作的队列句柄。
pvBuffer参数:
表示用于接收数据的缓冲区地址。
当接收成功时,队列会将内部存储的一个元素,按 uxItemSize 字节拷贝到 pvBuffer 指向的内存空间。
队列创建时的元素大小、发送时的数据大小,接收时的数据大小应当保持一致。
最简单的设定就是,直接让它们的类型都保持一致。
需要注意的是:
xQueueReceive接收队列一个元素后,该元素会被从队列中移除,也就是所谓的“出队”。
xTicksToWait参数:
使用方式和原理与xQueueSend函数的xTicksToWait参数没有任何区别。
这里不再赘述。
返回值有两种:
其余原理和xQueueSend函数一致,这里不再赘述。
消息队列的使用举例(任务与任务通信)
现在我们已经搞懂了,FreeRTOS 消息队列的核心机制以及工作原理。
那么接下来我们就来“实战演练”一下。
下面举一些实际的场景,实际的代码,分析一下最终的行为。
场景一:一个任务发送不接收
示例代码如下:
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "DebugUSART1.h"
static QueueHandle_t QueueHandle = NULL;
/* ——————– 任务1:发送任务 ——————– */
void TaskSend(void *Argument) {
uint32_t Count = 0;
while (1) {
Count++;
// 如果队列满了,最多等待3s
BaseType_t Ret = xQueueSend(QueueHandle, &Count, 3000);
if (Ret == pdPASS) {
/* 发送成功:说明队列里还有空位 */
printf1("[Send OK ] Count = %lu \\n", Count);
} else {
/* 发送失败:最典型原因就是队列满了(errQUEUE_FULL) */
printf1("[Send FAIL] queue full \\n", Count);
}
vTaskDelay(1000);
}
}
int main(void) {
DebugUSART1_Init();
// 创建消息队列,一个元素是4个字节,一共可以存放5个数据
QueueHandle = xQueueCreate(5, sizeof(uint32_t));
xTaskCreate(TaskSend,
"TaskSend",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1) {}
}
程序的行为是:
试想一下,如果把Send函数等待时长改为portMAX_DELAY,会出现什么场景?
任务在发送5个数据后,会始终等待队列空位置,会持续阻塞。
场景二:优先级一致的两个任务通信
示例代码如下:
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "DebugUSART1.h"
static QueueHandle_t QueueHandle = NULL;
/* ——————– 任务1:发送任务 ——————– */
void TaskSend(void *Argument) {
uint32_t Count = 0;
BaseType_t xRet = pdFAIL;
while (1) {
Count++;
/* 向队列发送数据
参数说明:
1. 队列句柄
2. 数据地址
3. 等待时间(这里为0,表示不等待)
*/
xRet = xQueueSend(QueueHandle, &Count, 0);
if (xRet == pdPASS) {
/* 发送成功 */
printf1("[Send] %lu\\r\\n", Count);
} else {
/* 队列已满,发送失败 */
printf1("[Send] Queue Full!\\r\\n");
}
/* 延时 1s */
vTaskDelay(1000);
}
}
/* ——————– 任务2:接收任务 ——————– */
void TaskRecv(void *Argument) {
uint32_t Value = 0;
BaseType_t xRet = pdFAIL;
while (1) {
/* 从队列接收数据
portMAX_DELAY 表示无限等待
在默认配置下会进入“无限期阻塞模式”
*/
xRet = xQueueReceive(QueueHandle, &Value, portMAX_DELAY);
if (xRet == pdPASS) {
/* 接收成功 */
printf1("[Recv] %lu\\r\\n", Value);
} else {
/* 正常情况下不会走到这里 */
printf1("[Recv] Error!\\r\\n");
}
}
}
int main(void) {
DebugUSART1_Init();
/* 创建消息队列
1. 最多存放 5 个元素
2. 每个元素大小为 4 字节(uint32_t)
*/
QueueHandle = xQueueCreate(5, sizeof(uint32_t));
if (QueueHandle == NULL) {
/* 创建失败通常是堆空间不足 */
printf1("Queue Create Failed!\\r\\n");
while (1);
}
/* 创建发送任务 */
xTaskCreate(TaskSend,
"TaskSend",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 创建接收任务 */
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果进入这里,说明堆空间不足,
Idle任务无法创建 */
while (1);
}
最终程序的行为:
扩展了解:利用结构体校验数据边界
在之前学习任务列表时,我们知道列表的基本类型是一个 List_t 类型。如下图所示:

可以看到:这个结构体的首尾会放置 边界校验成员宏,用于检测内存是否存在非法覆盖。
这种机制本质上就是一种 结构体边界保护Guard 保护思想
这是怎么实现的呢?
恰好在学习消息队列时,有同学提出了这样的疑问:
如果我们在使用消息队列时,
队列的元素大小 和 实际收发元素的大小 不一致,会发生什么?
比如:
我们创建消息队列时是这样写的:
pxQueue = xQueueCreate(5, 20);
这意味着:
注意这里的关键点队列拷贝是“按创建时的 uxItemSize 固定长度拷贝”它不会管你实际数据有多大
与此同时,我们发送的数据却是:
uint32_t Value = 1;
xQueueSend(pxQueue, &Value, portMAX_DELAY);
一个 uint32_t 只有 4 字节。
队列却按 20 字节进行拷贝,这 20 字节从哪里来?
从 &Value 开始往后连续读取 20 字节。
也就是说:
Value 后面的栈空间也会被一起复制进队列。这显然是不正确的。
但问题更严重的是接收端。
如果接收端仍然用 uint32_t 去接收数据,那么 FreeRTOS 仍然会从目标指针开始,连续写入 20 个字节的数据。
这就会导致:任务栈空间被非法覆盖,并产生更加严重的问题与后果。
为了把这种“越界覆盖”的现象演示得更直观,我们可以构建这样一个结构体类型:
/* 结构体成员布局是连续的,确保RecvValue前后都被校验成员包围 */
typedef struct {
volatile uint32_t CheckValue1;
uint32_t RecvValue;
volatile uint32_t CheckValue2;
} QueueItemOverflow_t;
其中:
为什么这个方法有效?
因为结构体成员在内存中是连续排列的。
当我们从 &RecvValue 开始写入 20 字节时:
因此:
CheckValue2 是否被改写就是最直观的“越界检测信号”
这也正是 FreeRTOS 在一些数据结构中使用边界校验成员的核心思想。
整体演示代码如下(故意写错,用于实验):
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "DebugUSART1.h"
static QueueHandle_t pxQueue = NULL;
/* ——————– 任务1:发送任务 ——————– */
void TaskSend(void *Argument) {
(void)Argument;
uint32_t Value = 1;
while (1) {
printf1("[Send] Value = %d\\r\\n", Value);
/* 故意错误:队列元素是 20 字节,但这里传的指针指向的内存块实际只有4字节可用 */
xQueueSend(pxQueue, &Value, portMAX_DELAY);
Value++;
vTaskDelay(3000);
}
}
/* ——————– 任务2:接收任务 ——————– */
void TaskRecv(void *Argument) {
(void)Argument;
/* 结构体成员布局是连续的,确保RecvValue前后都被校验成员包围 */
typedef struct {
volatile uint32_t CheckValue1;
uint32_t RecvValue;
volatile uint32_t CheckValue2;
} QueueItemOverflow_t;
QueueItemOverflow_t Struct;
Struct.CheckValue1 = 0xAAAAAAAA;
Struct.RecvValue = 0;
Struct.CheckValue2 = 0xBBBBBBBB;
while (1) {
/*
* 故意错误:
* 队列元素大小 = 20 字节
* 但是RecvValue实际只有4个字节,强行接收20个字节的数据,势必导致越界
* FreeRTOS 会 memcpy 20 字节,从而覆盖 RecvValue 后面的 CheckValue2
*/
xQueueReceive(pxQueue, (void *)&(Struct.RecvValue), portMAX_DELAY);
printf1("[Recv] CheckValue1 = 0x%08X\\r\\n", (unsigned int)Struct.CheckValue1);
printf1("[Recv] RecvValue = %d\\r\\n", Struct.RecvValue);
printf1("[Recv] CheckValue2 = 0x%08X\\r\\n", (unsigned int)Struct.CheckValue2);
printf1("——————————\\r\\n");
vTaskDelay(1000);
}
}
int main(void) {
DebugUSART1_Init();
printf1("system start. \\n");
/* 1) 创建消息队列:一共可存放5个元素,每个元素 20 最大字节 */
pxQueue = xQueueCreate(5, 20);
if (pxQueue == NULL) {
printf1("queue create failed! \\n");
while (1) {}
}
/* 2) 创建任务:发送 */
xTaskCreate(TaskSend,
"TaskSend",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 3) 创建任务:接收 */
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
printf1("tasks created. \\n");
vTaskStartScheduler();
while (1) {}
}
你可以运行这段代码观察现象:
这就说明:队列元素大小与实际接收缓冲区大小不匹配,会导致越界写入。
也就能理解:
为什么 FreeRTOS 会在某些结构体首尾设计边界校验成员,用于快速发现“内存被非法覆盖”的问题。
消息队列的使用举例(任务与中断通信)
现在我们来完成这样的一道编程题目,实现以下功能。
题目描述如下:
在基于SPL库 + FreeRTOS的开发环境下实现一个简单的串口命令解析程序。
PC 通过串口工具向STM32发送字符串命令:
要求如下:
求解这道题目,建议采用下面的思路:
很不使用消息队列的解决方式然,中断和任务之间要共享同一个存储字符串指令的字符数组,于是就涉及到任务间通信。
在本章节的开头,我们已经提到过:如果不使用消息队列,那么中断和任务之间的“任务间通信”,就只能靠共享同一块内存Buffer来完成。
说白了就是:
直接给出参考实现代码:
LED.h头文件:
#ifndef __LED_H__
#define __LED_H__
#include "stm32f10x.h"
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
#endif
LED.c头文件:
#include "LED.h"
void LED_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef initType;
initType.GPIO_Pin = GPIO_Pin_13;
initType.GPIO_Mode = GPIO_Mode_Out_PP;
initType.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &initType);
// LED默认熄灭
LED_Off();
}
// PC13 低电平点亮
void LED_On(void) {
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
}
void LED_Off(void) {
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
USART.h头文件:
#ifndef __DEBUG_USART1_H__
#define __DEBUG_USART1_H__
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#define BUFFER_SIZE 10
// 全局变量仅声明,共享main.c中的全局变量
extern char Buffer[BUFFER_SIZE];
extern uint8_t BufferIndex;
/**
* @brief 初始化串口 USART1
*/
void USART1_Init(void);
/**
* @brief 调试打印函数(串口版 printf)
*/
void printf1(const char *format, ...);
#endif
USART.c源文件:
#include "USART1.h"
/* ================= 内部函数(不对外暴露) ================= */
/**
* @brief USART1 发送单字节(阻塞式)
* @note 仅供本文件内部使用
*/
static void USART1_SendByte(uint8_t Byte) {
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, Byte);
}
/**
* @brief 初始化 USART1 串口
*/
void USART1_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
/* PA9 -> USART1_TX:复用推挽输出 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA10 -> USART1_RX:上拉输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART 参数配置:115200, 8N1 */
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
// 以下为USART1全局中断配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
// 使用FreeRTOS时
// 中断优先级一律建议采用分组4,即4位优先级寄存器全部配置给抢占优先级
// 抢占优先级设置为12,不建议高于11(可以设置为11)
// 原因请看文档或者询问老师
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 调试打印函数(串口版 printf)
* @param format: 格式化字符串,和printf函数的第一个参数完全一致
* @note
* 1. 用法与 printf 类似
* 2. 实际输出通过 USART1 完成
* 3. 用于 FreeRTOS 任务调试与运行观测
*/
void printf1(const char *format, ...) {
char buffer[100];
va_list list;
va_start(list, format);
vsprintf(buffer, format, list);
va_end(list);
for (uint16_t i = 0; buffer[i] != '\\0'; i++) {
USART1_SendByte((uint8_t)buffer[i]);
}
}
/**
* @brief USART1全局中断处理函数,用于接收PC端数据
*/
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
char ReceiveChar = (char)USART_ReceiveData(USART1);
if (ReceiveChar == '\\r' || ReceiveChar == '\\n') {
return; // 不接收处理换行符
}
if (BufferIndex < BUFFER_SIZE – 1) {
Buffer[BufferIndex++] = ReceiveChar;
Buffer[BufferIndex] = '\\0'; // 永远保证当前字符的下一个位置是空字符
} else {
// 如果接收过长的数据导致Buffer溢出
// 那么多余数据直接用空字符拼在Buffer末尾
Buffer[BUFFER_SIZE – 1] = 0;
}
}
}
main.c源代码文件
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "LED.h"
#include "USART1.h"
char Buffer[BUFFER_SIZE];
uint8_t BufferIndex;
/* ——————– 任务1: 判断串口接收数据并处理——————– */
void TaskRecv(void *Argument) {
while (1) {
if (strcmp(Buffer, "OK") == 0) {
printf1("Light On \\n");
LED_On();
// 清零Buffer并归零Idx
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
} else if (strcmp(Buffer, "ERROR") == 0) {
printf1("Light Off \\n");
LED_Off();
// 清零Buffer并归零Idx
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
// 输入非法指令,就清零Buffer
if (BufferIndex > 0 && Buffer[0] != 'O' && Buffer[0] != 'E') {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
// 长度超过最大命令长度,就清零Buffer
if (BufferIndex > 5) {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
// 避免死循环空转,导致空闲任务无法上CPU
vTaskDelay(10);
}
}
int main(void) {
// 使用优先级分组4
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_Init();
USART1_Init();
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE + 128, // 适当增加一些任务栈空间
NULL,
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1) {}
}
使用全局变量来实现,优点是比较简单,缺点是功能需求越多时,需要用的全局变量就会越多,代码就会变得越发难以维护。
下面我们来看一下,使用消息队列的方式来实现这个功能。
当然,在此之前,我们还需要学习一个专门用于,在中断中向消息队列发送数据的函数——xQueueSendFromISR。
xQueueSendFromISR:在中断中向消息队列发送数据
函数原型如下:
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue, // 表示要操作的队列句柄
const void * pvItemToQueue, // 表示要发送数据的地址
BaseType_t * pxHigherPriorityTaskWoken // 是否需要在ISR结束后,触发高优先级任务切换
);
xQueueSendFromISR,在主要作用上其实和xQueueSend函数并无不同:
都是向消息队列 发送一个元素,也就是将用户提供的数据 拷贝到消息队列的内部存储空间。
但仍然主要有两点不同:
前面两个参数和xQueueSend函数一致,这里不再赘述。
这里讲一下第三个参数:pxHigherPriorityTaskWoken。
这个参数通常,只有两种传参的方式,两种不同的调用方式。
第一种,可以直接传参NULL,表示无事发生,什么都不会做,相当于函数没有这个参数。
第二种,调用和传参的方式,如下:
BaseType_t Flag = pdFALSE;
xQueueSendFromISR(xx, &xx, &Flag);
portYIELD_FROM_ISR(Flag);
解释一下这种调用方式。
简单来说,这三行代码实际上完成了 三件事情:
下面用流程的形式来描述一下这三条代码:
任务A正在运行
↓
USART1中断发生,串口接收数据 ↓ 进入 USART1 ISR ↓ xQueueSendFromISR() ↓ 向消息队列发数据 ↓ 如果系统中有任务B,阻塞等待接收队列数据 ↓ 任务B从 阻塞态 → 就绪态 ↓ 如果任务B优先级高于当前运行任务A ↓ Flag = pdTRUE ↓ portYIELD_FROM_ISR(Flag) ↓ 触发 PendSV(只有Flag设置为pdTRUE,才会触发PendSV) ↓ USART1 ISR 退出 ↓ PendSV 中断执行 ↓ FreeRTOS 调度器运行 ↓ 切换到更高优先级任务B执行
所以一旦采用了这种调用方式,可以达成这样的效果:
在 USART1 的 ISR 中向队列发送数据时:
如果系统中存在比当前任务,更高优先级任务正在等待该队列数据,那么在 ISR 结束后,系统会立刻切换到该高优先级任务执行。
接下来可以思考一个问题:
既然任务B优先级更高,那么即使不在 ISR 结束后立即触发任务调度
等到下一个 Tick 到来时,系统依然会触发调度,并最终切换到任务B。
既然如此,又为什么那么“着急”、在ISR结束后就手动触发任务切换呢?
原因很简单:
为了尽可能降低任务响应延迟。
如果不调用 portYIELD_FROM_ISR,系统会按照正常的调度节奏运行:
USART1 ISR结束 ↓ 继续执行任务A ↓ 等待下一次 SysTick ↓ FreeRTOS 调度器运行 ↓ 切换到更高优先级任务B执行
在这种情况下,任务B 最多可能要等待一个 Tick 才能运行。
也就是说,任务B 最坏情况下可能会被延迟约 1ms 才开始执行。
在很多普通应用中,这个延迟可能并不明显。
但在一些 实时性要求较高的系统 中,1ms 的延迟可能已经是比较大的时间开销。
因此,通过这种方式可以让系统在 ISR 结束后立刻进行任务调度,从而让高优先级任务更快地处理数据。
当然,如果系统对这点延迟并不敏感,也可以采用更简单的写法:
xQueueSendFromISR(xx, &xx, NULL);
这种写法表示:
不关心是否唤醒了更高优先级任务,也不主动请求立即调度。
系统会在下一次正常调度时机(例如下一次 Tick 中断)再进行任务切换。
为什么不学习任务向中断发送数据的函数?
在 FreeRTOS 中,我们更多关注的是“中断 → 任务”的通信,而基本不会讨论“任务 → 中断”。
其根本原因在于:中断不具备等待能力。
任务可以通过消息队列,或其他任务间通信机制进入阻塞态,从而等待数据,因此需要中断或其他任务去“唤醒它”。
而中断必须快速执行并退出,既不能阻塞,也不会长期处于等待状态,因此不存在“等待任务发送数据”的需求。
因此,从设计上来说:
通信机制的核心,是让“能等待的一方去等待”。
而中断无法等待,只能作为“事件的产生者”或“数据的发送者”,这就是我们主要学习“中断 → 任务”通信的原因。
使用消息队列的参考实现代码
讲完了上面的函数,那么使用消息队列来实现上述功能,就很简单了。
主要做两点核心变化:
参考代码如下:
USART.h头文件:
#ifndef __USART1_H__
#define __USART1_H__
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "FreeRTOS.h"
#include "queue.h"
/* main.c 里创建的队列句柄 */
extern QueueHandle_t UartRxQueue;
/**
* @brief 初始化串口 USART1
*/
void USART1_Init(void);
/**
* @brief 调试打印函数(串口版 printf)
*/
void printf1(const char *format, ...);
#endif
USART.c源文件:
#include "USART1.h"
/* ================= 内部函数(不对外暴露) ================= */
/**
* @brief USART1 发送单字节(阻塞式)
* @note 仅供本文件内部使用
*/
static void USART1_SendByte(uint8_t Byte) {
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, Byte);
}
/**
* @brief 初始化 USART1 串口
*/
void USART1_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
/* PA9 -> USART1_TX:复用推挽输出 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA10 -> USART1_RX:上拉输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART 参数配置:115200, 8N1 */
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
// 以下为USART1全局中断配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
// 使用FreeRTOS时
// 中断优先级一律建议采用分组4,即4位优先级寄存器全部配置给抢占优先级
// 抢占优先级设置为12,不建议高于11(可以设置为11)
// 原因请看文档或者询问老师
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 调试打印函数(串口版 printf)
* @param format: 格式化字符串,和printf函数的第一个参数完全一致
* @note
* 1. 用法与 printf 类似
* 2. 实际输出通过 USART1 完成
* 3. 用于 FreeRTOS 任务调试与运行观测
*/
void printf1(const char *format, ...) {
char buffer[100];
va_list list;
va_start(list, format);
vsprintf(buffer, format, list);
va_end(list);
for (uint16_t i = 0; buffer[i] != '\\0'; i++) {
USART1_SendByte((uint8_t)buffer[i]);
}
}
/**
* @brief USART1中断:收到 1 字节就塞进 FreeRTOS 队列
*/
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
char ch = (char)USART_ReceiveData(USART1);
if (UartRxQueue != NULL) {
BaseType_t xFlag = pdFALSE;
xQueueSendFromISR(UartRxQueue, &ch, &xFlag);
// 如果xFlag在上述函数中被设置为pdTRUE
// 则下面的宏函数会触发PendSV,使得ISR结束后触发任务切换
portYIELD_FROM_ISR(xFlag);
}
}
}
main.c源文件:
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "LED.h"
#include "USART1.h"
#define BUFFER_SIZE 20
QueueHandle_t UartRxQueue;
/* ——————– 任务:接收并解析串口命令 ——————– */
void TaskRecv(void *Argument) {
char Buffer[BUFFER_SIZE] = { 0 }; // 局部变量必须手动初始化0值
uint8_t BufferIndex = 0;
while (1) {
char ch;
/* 无限阻塞等待接收队列数据,也就是等待串口发数据 */
if (xQueueReceive(UartRxQueue, &ch, portMAX_DELAY) == pdPASS) {
/* 如果接收到的是换行符相关的,直接丢弃 */
if (ch == '\\r' || ch == '\\n') {
continue;
}
/* 拼接字符串,永远保证 '\\0' 结尾 */
if (BufferIndex < BUFFER_SIZE – 1) {
Buffer[BufferIndex++] = ch;
Buffer[BufferIndex] = '\\0';
} else {
// 如果接收过长的数据导致Buffer溢出
// 那么多余数据直接用空字符拼在Buffer末尾
Buffer[BUFFER_SIZE – 1] = 0;
}
if (strcmp(Buffer, "OK") == 0) {
printf1("Light On \\r\\n");
LED_On();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
} else if (strcmp(Buffer, "ERROR") == 0) {
printf1("Light Off \\r\\n");
LED_Off();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 非法指令输入,清空Buffer */
if (BufferIndex > 0 && Buffer[0] != 'O' && Buffer[0] != 'E') {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 长度超过 ERROR(5) 就清空Buffer */
if (BufferIndex > 5) {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
}
}
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_Init();
/* 创建消息队列:长度 20,每个元素 1 字节 */
UartRxQueue = xQueueCreate(20, sizeof(char));
if (UartRxQueue == NULL) {
printf1("Error: UartRxQueue create failed. \\n");
while (1) {}
}
USART1_Init();
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE + 128,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1) {}
}
以上。
使用消息队列的好处
相比较于使用全局变量实现任务间通信,使用 FreeRTOS 消息队列(Queue) 具有更好的系统结构和更高的可靠性。
下面从几个角度简单说明两者的区别。
首先,从 数据传递方式 来看。
使用全局变量时,本质上是:
这种方式依赖任务不断轮询数据是否发生变化。
而使用消息队列时:
队列在内核中负责缓存数据并协调任务与中断之间的通信。
因此,消息队列本质上是一种内核提供的安全通信机制。
其次,从 CPU利用率 来看。
使用全局变量方案时,任务通常需要写成:
// 伪代码
while(1){
判断全局变量状态
vTaskDelay(...)
}
这种方式实际上属于 轮询机制。
如果不加入 vTaskDelay(),任务会一直空转,占用大量 CPU 时间。
而加入延时,任务在延时期间会进入阻塞态,在此期间即使数据已经到达,任务也无法立刻处理。
从而 引入额外的响应延迟,降低系统的实时响应能力。
而使用消息队列时,接收数据的任务可以这样写:
xQueueReceive(queue, &data, portMAX_DELAY);
当队列中没有数据时:
因此 消息队列属于事件驱动机制,而不是轮询机制,CPU利用率更高。
因此 消息队列属于事件驱动机制,而不是轮询机制,CPU利用率更高。
第三,从 系统可靠性 来看。
使用全局变量时,需要程序员自己处理很多细节,例如:
如果处理不当,很容易导致程序逻辑错误。
而消息队列由 FreeRTOS 内核管理缓冲区,具有以下特点:
因此,使用FreeRTOS后,系统稳定性会更高。
第四,从 程序结构 来看。
全局变量方案中:
这种设计在系统复杂后容易变得混乱。
而使用消息队列时:
两者通过队列解耦。
这种设计更加符合现代软件工程的设计理念,即**“高内聚低耦合”**。
最后,从 系统扩展性 来看。
如果系统中只有一个任务处理串口数据,两种方法差别不大。
但如果系统变复杂,例如:
此时全局变量方案就会变得非常难维护。
而消息队列天然支持:
因此在实际工程中,FreeRTOS 更推荐使用消息队列或其他RTOS通信机制,而不是直接使用全局变量。
总结来说:
使用全局变量实现任务间通信的优点是 实现简单、代码少,简单实验。
而使用消息队列则具有以下优势:
因此,在实际 FreeRTOS 项目开发中,推荐优先使用 消息队列 来实现任务间通信。
FreeRTOS消息队列特点总结
经过上面文档的阅读与学习,相信大家已经对FreeRTOS的消息队列有了一定的理解。
下面总结一下,FreeRTOS消息队列的核心特点:
因此,在FreeRTOS系统中,消息队列是最常用、也是最基础的任务间通信机制,是我们学习FreeRTOS的重点。
消息队列的常用函数
从实际工程使用的角度来看,FreeRTOS 的消息队列,其实只需要掌握下面 四个核心函数,基本就已经能够满足绝大多数开发需求。
| xQueueCreate() | 创建一个消息队列 | 系统初始化阶段 | 在内核中创建队列对象,并分配存储空间,返回 QueueHandle_t 队列句柄 |
| xQueueSend() | 向队列发送数据 | 任务中发送数据 | 如果队列满,可以选择立即返回或进入阻塞等待 |
| xQueueReceive() | 从队列接收数据 | 任务中接收数据 | 如果队列为空,可以选择立即返回或进入阻塞等待 |
| xQueueSendFromISR() | 在中断中发送数据 | 中断 → 任务通信 | ISR 专用版本,不能阻塞,可触发高优先级任务立即调度 |
在实际项目中,绝大多数情况下 只需要使用这几个函数就足够了。



