前几篇文章中,我们学会了任务创建与延时管理,但任务之间仍无法传递数据。本篇将引入 FreeRTOS 最基础、最常用的 IPC(进程间通信)机制------队列。通过队列,任务与任务、中断与任务之间可以安全、高效地传递数据,彻底告别裸机全局变量带来的隐患。我们将以按键中断通知 LED 闪烁模式切换为例,完整演示队列的创建、发送、接收及阻塞等待的全过程。

一、为什么需要队列?
在裸机程序中,任务间的数据共享通常靠全局变量实现,但在 RTOS 下这种做法极易引发问题:
- 竞争条件:多个任务同时读写同一个变量,可能造成数据错乱;
- 非阻塞需求:接收方任务需要等待数据,又不希望浪费 CPU 做轮询;
- 中断中传递数据:中断需要快速退出,不能等待任务处理完数据。
队列提供了一种线程安全的机制:它自带互斥访问和阻塞/唤醒逻辑,可以方便地在不同任务(或中断)之间传递消息,而无需开发者自行实现复杂的锁机制。
二、队列的核心概念与 API
2.1 队列的基本模型
队列可以看作一个先进先出(FIFO)的环形缓冲区,每个队列项的大小在创建时固定。任务可以向队列尾部发送 数据,也可以从队列头部接收 数据。当队列为空时,接收任务可以选择阻塞等待,直到有数据进入队列;当队列满时,发送任务也可以阻塞等待,直到队列有空位。
2.2 关键 API
| 功能 | API 名称 | 说明 |
|---|---|---|
| 创建队列 | xQueueCreate |
返回队列句柄,后续操作均基于此句柄 |
| 发送数据(任务) | xQueueSend |
类似 xQueueSendToBack,将数据放到队尾 |
| 发送数据(中断) | xQueueSendFromISR |
中断服务函数中使用的版本 |
| 接收数据(任务) | xQueueReceive |
从队首取出数据,可设置阻塞超时 |
| 接收数据(中断) | xQueueReceiveFromISR |
中断中使用的版本(但不常用) |
| 查询队列中项数 | uxQueueMessagesWaiting |
返回当前队列中的有效数据项数量 |
| 删除队列 | vQueueDelete |
释放队列占用的内存 |
三、硬件准备与优先级分组设置
3.1 硬件连接
本章实验使用一个按键(PA0)和一个 LED(PC13),要求按一次按键切换一次 LED 的闪烁模式。
3.2 中断优先级分组(必须)
为了让 FreeRTOS 的中断管理策略正确生效,必须在系统初始化早期设置 NVIC 优先级分组为4位抢占优先级 。在 main() 函数开头调用:
c
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
这样,每个中断的 4 位优先级都用于抢占,子优先级不再使用,与我们 FreeRTOSConfig.h 中的配置完全匹配。
四、扩展板级驱动
4.1 按键 BSP
在 BSP 目录下新建 bsp_key.h 和 bsp_key.c。
c
// bsp_key.h
#ifndef BSP_KEY_H
#define BSP_KEY_H
#include "stm32f10x.h"
void KEY_Init(void);
uint8_t KEY_Read(void); // 返回 1 表示按下
#endif
c
// bsp_key.c
#include "bsp_key.h"
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,按下为低电平
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
uint8_t KEY_Read(void)
{
// 返回 1 表示按键按下(PA0 为低电平)
return (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == Bit_RESET) ? 1 : 0;
}
4.2 LED BSP
沿用之前文章的 bsp_led.h 和 bsp_led.c,确保已包含 LED_InitAll() 和 LED3_Toggle() 函数。LED3_Toggle 翻转的是 GPIOC->ODR 的 Pin_13。
五、按键中断配置
5.1 EXTI 初始化(标准库)
在 BSP 目录下新建 bsp_exti.h 和 bsp_exti.c。
c
// bsp_exti.h
#ifndef BSP_EXTI_H
#define BSP_EXTI_H
#include "stm32f10x.h"
void EXTI0_Init(void);
#endif
c
// bsp_exti.c
#include "bsp_exti.h"
#include "bsp_key.h"
void EXTI0_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
KEY_Init(); // 初始化 PA0 引脚
// 将 PA0 连接到 EXTI 线 0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5; // 优先级 5,可安全调用 RTOS API
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
说明 :中断优先级设为 5,与我们配置的
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY一致,表示该中断可以安全调用 FreeRTOS 的 FromISR 系列函数。优先级 0~4 的中断完全不被打扰,但绝对不能在其内部调用任何 RTOS API。
5.2 中断服务函数
在 stm32f10x_it.c 中编写 EXTI0_IRQHandler(如果该文件已有弱定义的空函数,直接覆盖即可)。
c
// stm32f10x_it.c
#include "stm32f10x_it.h"
#include "FreeRTOS.h"
#include "queue.h"
extern QueueHandle_t xKeyQueue; // 队列句柄,在 main.c 中定义
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 发送按键消息到队列,仅发送一个字节(内容无关紧要,只表示事件)
uint8_t key_event = 1;
xQueueSendFromISR(xKeyQueue, &key_event, &xHigherPriorityTaskWoken);
EXTI_ClearITPendingBit(EXTI_Line0);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 如有更高优先级任务被唤醒,则触发切换
}
六、实现队列通信与 LED 模式切换
6.1 任务设计
- KeyProcessTask:阻塞等待队列中的按键事件,收到后切换 LED 模式;
- LedTask:根据当前模式以不同频率闪烁 LED。
6.2 main.c 完整代码
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "bsp_led.h"
#include "bsp_exti.h"
/* 队列句柄,需在中断服务函数中引用 */
QueueHandle_t xKeyQueue = NULL;
/* 闪烁模式(0 = 慢闪,1 = 快闪) */
volatile uint8_t led_mode = 0;
/* 按键处理任务 */
void vKeyProcessTask(void *pvParameters)
{
uint8_t key_val;
while (1)
{
// 阻塞等待队列数据(portMAX_DELAY 表示无限等待)
if (xQueueReceive(xKeyQueue, &key_val, portMAX_DELAY) == pdTRUE)
{
// 收到按键事件,切换模式
led_mode = !led_mode;
}
}
}
/* LED 闪烁任务 */
void vLedTask(void *pvParameters)
{
while (1)
{
if (led_mode == 0)
{
LED3_Toggle();
vTaskDelay(pdMS_TO_TICKS(500)); // 慢闪:周期 1s
}
else
{
LED3_Toggle();
vTaskDelay(pdMS_TO_TICKS(100)); // 快闪:周期 200ms
}
}
}
int main(void)
{
/* 设置中断优先级分组为 4 位抢占优先级(必须) */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_InitAll(); // 初始化 LED
EXTI0_Init(); // 初始化按键中断
/* 创建队列,每个消息为 1 个 uint8_t,队列长度 5(足够缓冲按键事件) */
xKeyQueue = xQueueCreate(5, sizeof(uint8_t));
if (xKeyQueue == NULL)
{
// 创建失败,阻塞程序(实际项目中可重启或报错)
while (1);
}
xTaskCreate(vKeyProcessTask, "KeyProc", 128, NULL, 2, NULL);
xTaskCreate(vLedTask, "Led", 128, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
七、实验现象与验证
- 上电后,LED 以 1Hz 频率闪烁(慢闪模式);
- 每按一次 PA0 按键,LED 在慢闪(周期 1s)和快闪(周期 200ms)之间切换;
- 由于在中断中使用了
xQueueSendFromISR,按键响应实时且不丢事件; KeyProcessTask使用portMAX_DELAY阻塞等待,完全不占用 CPU,仅在按键按下时才被唤醒执行。
如果需要传递更复杂的数据(如按键次数、按键类型),可以扩大队列项大小,或传递结构体。但请注意:队列存储的是数据副本,而非指针,因此不必担心悬挂指针问题。
八、队列使用的注意事项
- 阻塞超时 :
portMAX_DELAY表示无限等待;0表示不等待;其他值为等待的 tick 数。 - 中断中必须使用 FromISR 版本 :
xQueueSendFromISR和xQueueReceiveFromISR,它们的最后一个参数pxHigherPriorityTaskWoken必须传递给portYIELD_FROM_ISR。 - 队列拷贝语义:所有通过队列传递的数据都会进行字节级拷贝,接收方修改拷贝的数据不会影响队列内部状态。
- 大小估算:队列长度应能容纳最坏情况下的突发数据,接收任务要及时处理以免队列溢出。
九、总结
通过本篇的实战,你应该掌握了:
- 队列的创建、发送、接收与阻塞等待;
- 如何在标准库外部中断中使用
xQueueSendFromISR安全地传递事件; - 队列天然解决了中断与任务间的同步问题,实现了硬件事件与业务逻辑的解耦。
队列是 FreeRTOS 中使用最频繁的 IPC 工具,所有稍复杂的 RTOS 应用都会用到它。下一篇文章我们将学习二值信号量与计数信号量,解决任务同步、中断通知以及资源管理的常见场景。
下一篇:FreeRTOS 信号量 ------ 任务同步与中断通知的优雅解决方案。