前几篇文章中,我们学会了任务创建与延时管理,但任务之间仍无法传递数据。本篇将引入 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()函数开头调用:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);这样,每个中断的 4 位优先级都用于抢占,子优先级不再使用,与我们FreeRTOSConfig.h中的配置完全匹配。
四、扩展板级驱动
4.1 按键 BSP
在BSP目录下新建bsp_key.h和bsp_key.c。
// bsp_key.h#ifndefBSP_KEY_H#defineBSP_KEY_H#include"stm32f10x.h"voidKEY_Init(void);uint8_tKEY_Read(void);// 返回 1 表示按下#endif// bsp_key.c#include"bsp_key.h"voidKEY_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_tKEY_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。
// bsp_exti.h#ifndefBSP_EXTI_H#defineBSP_EXTI_H#include"stm32f10x.h"voidEXTI0_Init(void);#endif// bsp_exti.c#include"bsp_exti.h"#include"bsp_key.h"voidEXTI0_Init(void){EXTI_InitTypeDef EXTI_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;KEY_Init();// 初始化 PA0 引脚// 将 PA0 连接到 EXTI 线 0GPIO_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 APINVIC_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(如果该文件已有弱定义的空函数,直接覆盖即可)。
// stm32f10x_it.c#include"stm32f10x_it.h"#include"FreeRTOS.h"#include"queue.h"externQueueHandle_t xKeyQueue;// 队列句柄,在 main.c 中定义voidEXTI0_IRQHandler(void){BaseType_t xHigherPriorityTaskWoken=pdFALSE;if(EXTI_GetITStatus(EXTI_Line0)!=RESET){// 发送按键消息到队列,仅发送一个字节(内容无关紧要,只表示事件)uint8_tkey_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 完整代码
#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 = 快闪) */volatileuint8_tled_mode=0;/* 按键处理任务 */voidvKeyProcessTask(void*pvParameters){uint8_tkey_val;while(1){// 阻塞等待队列数据(portMAX_DELAY 表示无限等待)if(xQueueReceive(xKeyQueue,&key_val,portMAX_DELAY)==pdTRUE){// 收到按键事件,切换模式led_mode=!led_mode;}}}/* LED 闪烁任务 */voidvLedTask(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}}}intmain(void){/* 设置中断优先级分组为 4 位抢占优先级(必须) */NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);LED_InitAll();// 初始化 LEDEXTI0_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 信号量 —— 任务同步与中断通知的优雅解决方案。