1.概念
消息队列:
-
什么是消息队列?
-
一种 异步通信机制,允许任务/中断服务程序(ISR)之间传递数据块
-
数据结构:先进先出(FIFO) 的缓冲区(可自定义队列长度和元素大小)
-
典型应用场景:传感器数据处理、事件通知、任务间数据共享
-
-
关键特性
-
多元素存储:可同时存放多个数据项(队列深度由用户定义)
-
阻塞机制:队列空时读取阻塞、队列满时写入阻塞(可设置超时)
-
线程安全:自带互斥保护,无需额外同步
-
支持中断:提供专用
FromISR
API 保证中断安全
-
-
数据传递方式
-
值传递:数据会被完整复制到队列(非指针传递)
-
固定大小元素:队列中每个元素的大小在创建时确定
-
二、关键API函数详解
1. 队列创建
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列长度(最大元素数量)
UBaseType_t uxItemSize // 单个元素大小(字节)
);
返回值:
-
成功:队列句柄(
QueueHandle_t
类型) -
失败:
NULL
(内存不足时)
示例:
// 创建可存储10个int的队列
QueueHandle_t xIntQueue = xQueueCreate(10, sizeof(int));
2. 数据发送
BaseType_t xQueueSend(
QueueHandle_t xQueue, // 队列句柄
const void *pvItemToQueue, // 发送数据指针
TickType_t xTicksToWait // 阻塞时间(portMAX_DELAY表示永久等待)
);
// 变种API:
xQueueSendToBack(); // 等效xQueueSend()
xQueueSendToFront(); // 插队到队列头部
xQueueSendFromISR(); // 中断专用版本
返回值:
-
pdPASS
:发送成功 -
errQUEUE_FULL
:队列满(仅在非阻塞模式下返回)
3. 数据接收
BaseType_t xQueueReceive(
QueueHandle_t xQueue, // 队列句柄
void *pvBuffer, // 接收缓冲区指针
TickType_t xTicksToWait // 阻塞时间
);
// 中断专用版本:
xQueueReceiveFromISR();
返回值:
-
pdPASS
:接收成功 -
errQUEUE_EMPTY
:队列空(非阻塞模式下)
二进制信号量:
-
什么是二进制信号量?
-
一种 任务同步机制,用于协调任务与任务、任务与中断之间的操作时序
-
状态特性:只有0和1两种状态(类似布尔值),不可累加
-
典型应用场景:
-
事件通知(如按键触发、数据就绪)
-
资源互斥访问(轻量级替代互斥锁)
-
中断服务程序(ISR)与任务通信
-
-
-
关键特性
-
原子操作:信号量的获取(Take)和释放(Give)是线程安全的
-
阻塞机制:任务可设置超时等待信号量
-
中断安全:提供专用的
FromISR
API 版本 -
无所有者:与互斥锁不同,任何任务均可释放信号量
-
-
与计数信号量的区别
特性 二进制信号量 计数信号量 最大值 固定为1 用户自定义 典型用途 事件通知/互斥 资源池管理 释放次数限制 满时释放无效 可累加到最大值
二、关键API函数详解
1. 信号量创建
SemaphoreHandle_t xSemaphoreCreateBinary(void);
特性:
-
创建时初始状态为 0(不可用)
-
需手动调用
xSemaphoreGive
激活
示例:
SemaphoreHandle_t xButtonSemaphore = xSemaphoreCreateBinary();
2. 信号量释放
BaseType_t xSemaphoreGive(
SemaphoreHandle_t xSemaphore // 信号量句柄
);
// 中断专用版本:
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
行为:
-
若信号量为0 → 置1并唤醒等待任务
-
若信号量已为1 → 操作无效(返回
errQUEUE_FULL
)
返回值:
-
pdPASS
:释放成功 -
errQUEUE_FULL
:信号量已满(二进制信号量特有)
3. 信号量获取
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore, // 信号量句柄
TickType_t xTicksToWait // 阻塞时间
);
// 中断专用版本(仅用于计数信号量,二进制信号量不推荐):
xSemaphoreTakeFromISR();
行为:
-
若信号量为1 → 置0并继续执行
-
若信号量为0 → 根据超时设置阻塞等待
返回值:
-
pdTRUE
:获取成功 -
pdFALSE
:超时或信号量无效
这里有一个注意点,就是二进制信号量的释放和获取都有个中断版本的,因为一般的二进制信号量释放后可能触发 调度器立即唤醒等待该信号量的线程,而中断处理程序必须满足 无阻塞、无睡眠、快速执行 的要求,所以中断中必须使用中断版本的信号量释放获取,这样在中断释放信号量时,不会直接触发上下文切换,而是通过标记一个待处理的调度请求(例如设置 pendSV
标志),在中断退出后由调度器统一处理,这里需要注意,笔者曾经吃过亏!
2.代码
思路:
创建一个处理串口接收数据消息队列的任务,并获取二进制信号量阻塞该任务,通过配置串口1的中断,当串口接收到数据时,会产生中断接收数据,在中断处理程序中将接收到的数据发送到队列中,并且这里我使用了;作为一个数据帧的结束符,当接收到结束符时,释放二进制信号量,处理队列接收数据任务获取到信号量,弹出消息队列中的数据进行处理。
这里我们用STM32F407的串口1作为例子:
usart.c
#include "usart.h"
/*
串口1
PA9 ---- USART1_TX(发送端)
PA10 ---- USART1_RX(接收端)
串口接受数据到消息队列中,当接收到;结束符时,
释放二进制信号量,串口发送数据任务得到信号量
发送消息队列中的数据到串口中
*/
// 消息队列句柄和信号量句柄
SemaphoreHandle_t xBinarySemaphore_usart;
QueueHandle_t xUsartRxQueue_usart;
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
int _sys_exit(int x)
{
x = x;
}
//重定义fputc函数 printf 是一个宏
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,ch); //通过串口发送数据
//等待数据发送完毕
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
return ch;
}
/************************************
引脚说明:
PA9 ---- USART1_TX(发送端)
PA10 ---- USART1_RX(接收端)
*************************************/
void Usart1_Init(int myBaudRate)
{
//结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
//GPIOA 时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//串口时钟使能,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
//设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
GPIO_InitStructure.GPIO_Pin = USART1_TX_PIN|USART1_RX_PIN; //引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; //速度
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP ; //上拉
//GPIO 初始化设置:要设置模式为复用功能。
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStruct.USART_BaudRate = myBaudRate; //波特率
USART_InitStruct.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //双全工
USART_InitStruct.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1; //1位停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b; //8位数据
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件控制流
//5、串口参数初始化:设置波特率,字长,奇偶校验等参数。
USART_Init(USART1, &USART_InitStruct);
//配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //中断通道,只能在stm32f4xx.h 查阅
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 7; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //通道使能
NVIC_Init(&NVIC_InitStructure);
//配置为接收中断(表示有数据过来,CPU要中断进行接收)
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
xBinarySemaphore_usart = xSemaphoreCreateBinary();
xUsartRxQueue_usart = xQueueCreate(USART_RX_QUEUE_LENGTH, USART_RX_BUFFER_SIZE);
if ((xUsartRxQueue_usart == NULL) || (xBinarySemaphore_usart == NULL))
{
return ;
}
//使能串口。
USART_Cmd(USART1, ENABLE);
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t data = USART_ReceiveData(USART1);
//将数据放入消息队列
xQueueSendFromISR(xUsartRxQueue_usart,&data,NULL);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
if(data == ';')
{
xSemaphoreGiveFromISR(xBinarySemaphore_usart,NULL);
}
}
}
这里需要注意的就是串口中断配置的优先级需要从5开始,一般用户的自定任务是从5开始,0-4的任务优先级是系统任务的
USART1_IRQHandler串口中断函数中,当接收到数据既串口中断标志位USART_IT_RXNE置1时,data存储接收的数据,并且发送到消息队列中,清除中断的标志位,判断接收到的数据是否为结束符,如果是的话释放信号量。
main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h"
#include "sys.h"
#include "semphr.h"
#include "event_groups.h"
#include "queue.h"
#include "task.h"
#include "delay.h"
#include "typedef.h"
#include "usart.h"
//串口任务
static TaskHandle_t app_usart1_task_handle = NULL;
void usart1_task(void* pvParameters);
int main(void)
{
//设置NVIC分组(一个项目只能配置一次)
//抢占优先级取值范围:0~15
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init(168);
Usart1_Init(USART1_BAUDRATE);
/* 创建app_task1任务 */
xTaskCreate((TaskFunction_t )usart1_task, /* 任务入口函数 */
(const char* )"usart1task", /* 任务名字 */
(uint16_t )512, /* 任务堆栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )7, /* 任务的优先级 */
(TaskHandle_t* )&app_usart1_task_handle); /* 任务控制块指针 */
/* 开启任务调度 */
vTaskStartScheduler();
}
//串口1任务
void usart1_task(void* pvParameters)
{
uint8_t rx_buffer[128] = {0};
uint8_t count = 0;
for(;;)
{
if(uxSemaphoreGetCount(xBinarySemaphore_usart) == 1)
{
xSemaphoreTake(xBinarySemaphore_usart, portMAX_DELAY);
while (xQueueReceive(xUsartRxQueue_usart, &rx_buffer[count++], 10) == pdTRUE);
printf("%s\n",rx_buffer);
count = 0;
memset(rx_buffer,'\0',sizeof(rx_buffer));
}
vTaskDelay(1000);
}
}
创建处理串口消息队列的任务后,通过xSemaphoreTake(xBinarySemaphore_usart, portMAX_DELAY);获取信号量无限期阻塞等待中断释放信号量,当获取到信号量后,将消息队列中的数据弹出到rx_buffer中,这里我做的处理就是把接收到的数据再次发送到串口中。