目录
- 一、前言
- 二、队列的两种创建方式:动态与静态
- 三、实验平台说明与实战准备
- [四、实战三步:队列在 IR 中断中的应用](#四、实战三步:队列在 IR 中断中的应用)
- [五、写队列 API:任务 / ISR 场景专用函数](#五、写队列 API:任务 / ISR 场景专用函数)
- [六、读队列 API:数据接收核心函数](#六、读队列 API:数据接收核心函数)
- 七、中断场景写队列实现
- 八、总结
- 九、下一篇预告
- 十、结尾
一、前言
大家好,我是 Hello_Embed。上一篇我们解析了队列的本质的核心流程,知道它是 "带互斥 + 阻塞机制" 的环形缓冲区,是多任务数据传输的可靠方案。本次笔记将聚焦队列的核心操作:从创建(动态 / 静态两种方式),到写队列、读队列的 API 详解,再结合 IR 中断场景实战队列应用,为后续编码器控制挡球板的实战打下基础。所有 API 讲解均基于百问网资料,确保知识点准确可靠。
二、队列的两种创建方式:动态与静态
队列创建与任务创建逻辑一致,支持动态分配内存 和静态分配内存两种方式,分别适配不同内存管理需求,核心是通过函数创建并获取队列句柄(操作队列的唯一标识)。
补充:以下创建函数相关资料来源百问网,API 参数与返回值解析均基于 FreeRTOS 标准实现。
2.1 动态创建:xQueueCreate(推荐入门使用)
动态创建无需手动分配内存,队列的结构体和数据缓冲区由 FreeRTOS 内核自动分配,操作简单,适合快速开发。
函数原型
c
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数与返回值说明
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列长度:最多可存放的数据个数(item) |
| uxItemSize | 单个数据大小:以字节为单位(如sizeof(uint32_t)) |
| 返回值 | 非 NULL:创建成功,返回队列句柄;NULL:内存不足,创建失败 |
2.2 静态创建:xQueueCreateStatic(内存可控场景)
静态创建需手动分配 "队列结构体缓冲区" 和 "数据存储缓冲区",内存分配更可控,适合对内存使用有严格要求的场景(如裸机移植、内存紧张的设备)。
函数原型
c
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
参数与返回值说明
| 参数 | 说明 |
|---|---|
| uxQueueLength | 队列长度:最多可存放的数据个数(item) |
| uxItemSize | 单个数据大小:以字节为单位 |
| pucQueueStorageBuffer | 数据存储缓冲区:指向一个uint8_t数组,大小需≥uxQueueLength * uxItemSize |
| pxQueueBuffer | 队列结构体缓冲区:指向StaticQueue_t类型变量,用于保存队列控制信息 |
| 返回值 | 非 NULL:创建成功,返回队列句柄;NULL:pxQueueBuffer为 NULL,创建失败 |
静态创建示例代码
c
// 示例代码:静态创建队列
#define QUEUE_LENGTH 10 // 队列长度:最多存10个数据
#define ITEM_SIZE sizeof( uint32_t ) // 单个数据大小:4字节(uint32_t)
// 1. 队列结构体缓冲区:保存队列控制信息
StaticQueue_t xQueueBuffer;
// 2. 数据存储缓冲区:保存队列实际数据(长度×单个数据大小)
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1; // 队列句柄
// 3. 静态创建队列
xQueue1 = xQueueCreateStatic(
QUEUE_LENGTH, // 队列长度
ITEM_SIZE, // 单个数据大小
ucQueueStorage, // 数据存储缓冲区
&xQueueBuffer // 队列结构体缓冲区
);
}
2.3 核心区别总结
| 创建方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 动态创建 | 操作简单,无需手动分配内存 | 内存分配由内核管理,不可控 | 入门开发、快速验证功能 |
| 静态创建 | 内存分配可控,无内存泄漏风险 | 需手动计算并分配缓冲区大小 | 内存紧张设备、工业项目 |
三、实验平台说明与实战准备
由于此前尝试将图库移植到 TFT-LCD 屏幕未成功,为不耽误 FreeRTOS 学习进度,实验平台调整为:STM32F103C8T6 核心板 + 面包板 + 0.96 寸 OLED 屏 。经 OLED 屏幕测试,烧录程序后显示正常,确保实验环境可靠。

另外,因缺少红外遥控器,本次暂不展示实际运行结果,重点讲解 "IR 中断 + 队列" 的实现原理;后续将替换为编码器控制,核心逻辑一致。
核心目标:将原 "环形缓冲区读取 IR 中断数据" 的方案,改为 "队列读写",解决 CPU 轮询浪费资源的问题。
四、实战三步:队列在 IR 中断中的应用
本次实战核心是 "IR 中断触发写队列,任务读取队列数据",分三步实现,完美契合多任务数据传输的典型场景:
步骤 1:定义数据结构体(适配 IIC 通信需求)
OLED 通过 IIC 通信,需传输 "设备标识" 和 "数据值",因此定义输入数据结构体,统一数据传输格式:
c
// 输入数据结构体:适配IIC通信的设备+数据传输需求
struct input_data
{
uint32_t dev; // 设备标识(区分不同外设)
uint32_t val; // 传输的数据值
};
步骤 2:创建队列(动态创建方式)
与任务句柄类似,队列句柄是操作队列的核心,本质是 "指向队列控制块的指针别名"。通过动态创建函数创建队列:
c
QueueHandle_t g_xQueuePlatform; // 全局队列句柄,供中断和任务访问
// 在初始化函数中创建队列
void Queue_Init(void)
{
// 创建队列:长度10(最多存10组数据),单个数据大小为input_data结构体大小
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));
}
步骤 3:绑定 IR 中断与任务(核心流程)
- IR ISR(中断服务函数):IR 中断触发时,将数据写入队列(写队列操作);
- platform_task(任务):持续读取队列数据,根据数据执行 OLED 控制逻辑(读队列操作);
核心逻辑:中断仅负责 "采集数据并写入队列",任务负责 "读取数据并处理",解耦中断与业务逻辑,提升系统稳定性。
五、写队列 API:任务 / ISR 场景专用函数
写队列支持 "写尾部""写头部" 两种方式,且区分 "任务中使用" 和 "ISR(中断)中使用" 的版本 ------ 中断中函数不可阻塞,需特别注意。
补充:以下写队列 API 资料来源百问网,函数功能与参数解析均符合 FreeRTOS 标准。
5.1 任务中使用的写队列函数(支持阻塞)
| 函数名 | 核心功能 | 适用场景 |
|---|---|---|
| xQueueSend | 等同于 xQueueSendToBack,写队列尾部 | 任务中,需默认写尾部场景 |
| xQueueSendToBack | 写队列尾部(FIFO,先进先出) | 任务中,常规数据传输(推荐) |
| xQueueSendToFront | 写队列头部(LIFO,后进先出) | 任务中,紧急数据优先处理 |
函数原型(以 xQueueSendToBack 为例)
c
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
5.2 中断中使用的写队列函数(不可阻塞)
| 函数名 | 核心功能 | 适用场景 |
|---|---|---|
| xQueueSendToBackFromISR | 写队列尾部,中断专用 | IR、编码器等中断场景 |
| xQueueSendToFrontFromISR | 写队列头部,中断专用 | 中断中紧急数据传输 |
函数原型(以 xQueueSendToBackFromISR 为例)
c
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
5.3 通用参数与返回值说明
所有写队列函数参数逻辑一致,统一说明:
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄:指定要写入的队列 |
| pvItemToQueue | 数据指针:待写入的数据地址,内核会自动复制 "创建时指定大小" 的数据到队列 |
| xTicksToWait | 阻塞时间(仅任务函数支持):队列满时,任务阻塞的最大 Tick 数;0 = 不阻塞;portMAX_DELAY = 永久阻塞 |
| pxHigherPriorityTaskWoken | 高优先级任务唤醒标志(仅 ISR 函数支持):NULL = 无需关注 |
| 返回值 | pdPASS = 写入成功;errQUEUE_FULL = 队列满,写入失败 |
六、读队列 API:数据接收核心函数
读队列的核心是 "从队列中取出数据并移除",同样区分 "任务中使用" 和 "ISR 中使用" 的版本,任务版本支持阻塞等待数据。
6.1 任务中使用的读队列函数(支持阻塞)
函数原型
c
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
6.2 中断中使用的读队列函数(不可阻塞)
函数原型
c
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
6.3 参数与返回值说明
| 参数 | 说明 |
|---|---|
| xQueue | 队列句柄:指定要读取的队列 |
| pvBuffer | 缓冲区指针:接收数据的缓冲区地址,内核会自动复制数据到该缓冲区 |
| xTicksToWait | 阻塞时间(仅任务函数支持):队列空时,任务阻塞的最大 Tick 数;0 = 不阻塞;portMAX_DELAY = 永久阻塞 |
| pxTaskWoken | 高优先级任务唤醒标志(仅 ISR 函数支持):NULL = 无需关注 |
| 返回值 | pdPASS = 读取成功;errQUEUE_EMPTY = 队列空,读取失败 |
6.4 读队列示例代码(任务中使用)
c
struct input_data idata; // 定义接收数据的结构体变量
// 在platform_task任务中持续读队列
void vPlatformTask(void *pvParameters)
{
while(1)
{
// 永久阻塞等待队列数据(portMAX_DELAY),读取成功则执行后续逻辑
if(pdPASS == xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY))
{
// 读取成功:根据idata.dev和idata.val控制OLED
// (此处省略OLED控制逻辑,后续实战补充)
}
}
}
七、中断场景写队列实现
在 IR 中断服务函数中,使用 "ISR 专用写队列函数" 将数据写入队列,确保中断中操作的安全性(不可阻塞)。
实现代码
c
// IR中断服务函数
void IR_IRQHandler(void)
{
struct input_data idata; // 定义要写入队列的数据结构体
// 1. 中断处理:读取IR按键值(此处省略硬件读取逻辑)
idata.dev = 0; // 设备标识:0代表IR遥控器
idata.val = 0; // 按键值:根据实际读取结果赋值(如左移=1,右移=2)
// 2. 中断中写队列:写尾部,无需关注高优先级任务唤醒(传NULL)
xQueueSendToBackFromISR(g_xQueuePlatform, &idata, NULL);
// 3. 清除中断标志位(根据硬件手册补充,此处省略)
}
关键注意:中断中必须使用
xQueueSendToBackFromISR(或 FromISR 结尾的函数),不可使用任务中的xQueueSend,否则会导致系统异常。
八、总结
本次笔记聚焦队列的核心操作,关键要点如下:
- 创建方式:动态创建(xQueueCreate)简单高效,适合入门;静态创建(xQueueCreateStatic)内存可控,适合复杂场景;
- API 分类:写队列分 "任务 / ISR""写头部 / 尾部",读队列分 "任务 / ISR",中断函数必须用 FromISR 结尾的版本;
- 实战逻辑:IR 中断写队列→任务读队列,解耦中断与业务,避免 CPU 轮询浪费;
- 核心原则:队列句柄是唯一操作标识,数据传输通过结构体统一格式,中断中操作不可阻塞。
掌握这些基础操作,就能应对大部分多任务数据传输场景,为后续编码器控制挡球板的实战做好了准备。
九、下一篇预告
本次我们完成了队列创建与读写 API 的学习,下一篇将进入实战环节:用编码器替代红外遥控器,实现挡球板的控制。核心逻辑是:旋转编码器中断写队列 B→创建解析任务处理队列 B 数据→将处理后的数据写入队列 A→任务读队列 A 控制挡球板。这样设计的原因是编码器数据需额外运算,单独创建任务处理可避免中断阻塞,提升系统稳定性。
十、结尾
从队列本质到 API 实战,我们逐步掌握了 FreeRTOS 数据传输的核心工具。队列的价值在于 "封装复杂逻辑,简化多任务协作"------ 无需关注底层互斥与阻塞,仅通过 API 即可实现安全高效的数据传输。
下一篇的编码器实战,将是对队列 API 的综合应用,同时融入中断、任务创建等此前所学知识,形成完整的技术闭环。我是 Hello_Embed,感谢大家的持续关注,让我们在实战中巩固技能,逐步推进 FreeRTOS 的学习之旅!