一、简介
消息队列是FreeRTOS中用于任务与任务或任务与中断之间数据交换的一种机制,采用FIFO(先进先出)方式管理数据,也可以采用LIFO(后进先出)方式。有点类似全局变量。
1.1 那为什么不直接使用全局变量,原因如下:
1)假设有一个全局变量 a ,初始化为 0,接下来有两个任务对他进行自加操作:


在任务1进行 a++ 时,执行到了第二步也就是加 1,此时任务2 抢占了任务1进行a++,执行完任务2 后又回到了任务1,执行最后的第三步写数据,由于在任务1执行第一步读数据的时候,a的值还没发生改变,中途被任务1抢占后,a的值改变了,但是并不会引起 r0 数据的改变,所以最后写数据,写入 a 的是 1 而不是 2。所以当两个任务各自执行一次后,并不是想象中的 +2 ,而是 +1。这是多个任务同时对这个变量进行操作,导致了数据受损。
2)使用消息队列的情况如下:



对于消息队列进行读写操作时,他会进入临界区也就是关闭中断 ,他会在函数内部关闭FreeRTOS所管理的中断,包括(PendSV任务切换中断),这样就不能进行任务切换了,保证同一时间只有一个任务对消息队列进行访问,也就是同一时间只有一个任务可以对消息队列进行数据的读写操作。
1.2 由于FreeRTOS 的信号量(包括二值信号量、计数信号量、互斥量等)都是基于消息队列实现的功能。所以我们多多少少还是要了解一下消息队列的。
在队列中可以存储数量有限、大小固定的数据。队列中的每一个数据叫做“队列项目”,队列能够存储“队列项目”的最大数量称为队列的长度。在物理结构上有点类似于一个数组:
 
 
1.3 消息队列有下面的特点
-  入队出队:队列通常采用 “先进先出” (FIFO)的数据存储缓冲机制,也可以配置为 “后进先出” (LIFO)方式,不同方式其实就是读数据函数与写数据函数的不同搭配使用 
-  数据传输:FreeRTOS中队列采用实际值传递,即将数据拷贝到队列中进行传递,FreeRTOS采用拷贝数据传递,也可以传递指针,所以在传递较大的数据的时候采用指针传递 
-  多任务访问:队列不属于某个任务,任何任务和中断都可以向队列发送 / 读取消息 
-  出队、入队阻塞:当任务向一个队列发送消息时,可以指定一个阻塞时间,假设当前队列已满无法入队。 -  0:阻塞时间为0,直接返回不会等待 
-  0~port_MAX_DELAY:等待设定的阻塞时间,超时后直接返回不再等待 
-  port_MAX_DELAY:死等,一直等到可以入队为止,出队阻塞与入队阻塞类似、 
 
-  
队列的阻塞情况如下:
1)队列满了的时候,写入数据

由于队列中已经有数据的并且满了,此时写入则会阻塞:
- 将该任务的状态列表项挂载pxDelayedTaskList列表中;
- 将该任务的事件列表项挂载xTasksWaitingToSend列表中;
2)队列为空时,读取数据

可以看到队列是空的,那么这时候读取数据的话也会阻塞:
- 将该任务的状态列表项挂载pxDelayedTaskList列表中;
- 将该任务的事件列表项挂载xTasksWaitingToReceive列表中;
1.4 队列操作流程

创建了一个用于任务 A 与任务 B 之间“沟通交流”的队列,这个队列最大可容纳 5 个队列项目,即队列的长度为 5。刚创建的队列是不包含内容的,因此这个队列为空。

  
任务 A 将一个私有变量写入队列的尾部。由于在写入队列之前,队列是空的,因此新写入的消息,既是是队列的头部,也是队列的尾部

  
任务 A 改变了私有变量的值,并将新值写入队列。现在队列中包含了队列 A写入的两个值,其中第一个写入的值在队列的头部,而新写入的值在队列的尾部。 这个尾部不是物理上的尾部,而是逻辑上的尾部。

1.5 如果在队列满的时候,有多个任务同时对队列进行写入操作会怎么样

- 如果出现了空闲空间,并且优先级不一样,那么高优先级的先写入
- 在优先级相等的情况下,阻塞时间最久的任务先写入
二、队列结构体
2.1 队列结构体:
typedef struct QueueDefinition 
{
    int8_t * pcHead;                             /* 存储区域的起始地址 */
    int8_t * pcWriteTo;                          /* 下一个写入的位置 */
    union                                        /* 共用体,取决于使用功能 */
    {
        QueuePointers_t xQueue;
        SemaphoreData_t xSemaphore;
    } u;
    List_t xTasksWaitingToSend;                   /* 等待发送列表 */
    List_t xTasksWaitingToReceive;                /* 等待接收列表 */
    volatile UBaseType_t uxMessagesWaiting;       /* 非空闲队列项目的数量 */
    UBaseType_t uxLength;                         /* 队列长度 */
    UBaseType_t uxItemSize;                       /* 队列项目的大小 */
    volatile int8_t cRxLock;                      /* 读取上锁计数器 */
    volatile int8_t cTxLock;                      /* 写入上锁计数器 */
    /* 其他的条件编译变量 */
} xQUEUE;-  pcHead:队列使用的内存可以分为两份,该指针变量指向的是队列项的起始地址 
  - 第一份是队列结构体所占用的内存
- 第二份为所有队列项占用的内存
 
- pcWriteTo:用来指向下一个写入的队列项。
- union u:当用于队列时,使用第一个。用于互斥信号量或者递归互斥信号量时,使用第二个。
- xTasksWaitingToSend:当在队列为满,任务继续写入,则会将任务控制块的事件列表项挂入。
- xTasksWaitingToReceive:与上面差不多,只不过是挂载到等待接收列表。
- cRxLock:记录队列锁定期间被移除(接收)的队列项目数量。
- cTxLock:记录队列锁定期间被添加(发送)的队列项目数量。
2.2 共用体的成员结构体:
/* 用于队列时 */
typedef struct QueuePointers
{
    int8_t * pcTail;                   /* 存储区的结束地址 */
    int8_t * pcReadFrom;               /* 最后一个读取队列的地址 */
} QueuePointers_t;
/* 用于互斥信号量和递归互斥信号量时 */
typedef struct SemaphoreData
{
    TaskHandle_t xMutexHolder;         /* 互斥信号量的持有者 */
    UBaseType_t uxRecursiveCallCount;  /* 递归互斥信号量的获取计数值 */
} SemaphoreData_t;2.3 队列结构体示意图:

三、队列相关API函数
3.1 队列创建
/* 动态方式创建队列 */
xQueueCreate( uxQueueLength,                /* 队列长度 */ 
              uxItemSize );                 /* 队列项大小,单位:字节 */
/* 静态方式创建队列 */
xQueueCreateStatic( uxQueueLength,          /* 队列长度 */
                    uxItemSize,             /* 队列项大小 */
                    pucQueueStorage,        /* 指向预分配的队列存储区首地址 */
                    pxQueueBuffer );        /* 指向队列结构体地址 */上面两个函数都是宏函数,下面只是对动态方式创建进行说明,关于动态创建的宏函数定义如下:

动态创建函数原型为:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
                                       const UBaseType_t uxItemSize,
                                       const uint8_t ucQueueType )也就是 xQueueCreate() 函数其实是通过调用 xQueueGenericCreate() 函数实现的,并且对于信号量创建动态创建函数本质上也是调用 xQueueGenericCreate() 函数实现的,下面只讲解参数,具体实现自己去看源码。
相关说明:
- 想要使用宏函数 xQueueCreate(),需要将 configSUPPORT_DYNAMIC_ALLOCATION 置 1
- uxQueueLength:创建队列的长度
- uxItemSize:队列项的大小
- ucQueueType :使用动态方式创建指定类型的队列,前面说 FreeRTOS 基于队列实现
 了多种功能,每一种功能对应一种队列类型,具体代码如下:
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U )                 /* 队列 */
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U )                  /* 队列集 */
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U )                /* 互斥信号量 */
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U )   /* 计数型信号量 */
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U )     /* 二值信号量 */
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U )      /* 递归互斥信号量 */3.2 队列写入消息
/* 往队列的尾部写入消息 */
xQueueSend( xQueue, pvItemToQueue, xTicksToWait )
/* 同 xQueueSend() */
xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait )
/* 往队列的头部写入消息 */
xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )
/* 覆写队列消息(只用于队列长度为 1 的情况)*/
xQueueOverwrite( xQueue, pvItemToQueue )
/* 在中断中往队列的尾部写入消息 */
xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
/* 同 xQueueSendFromISR() */
xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )   
/* 在中断中往队列的头部写入消息 */
xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
/* 在中断中覆写队列消息(只用于队列长度为 1 的情况)*/
xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )写入消息函数有点多了,只说明一下任务级的写入函数,中断级差不多,只是在中断中调用,个别参数不一样
任务级的写入函数在本质上都是对 xQueueGenericSend() 函数的宏定义,只是由于参数不一样导致写入的情况也不同,xQueueGenericSend() 函数原型如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
                              const void * const pvItemToQueue,
                              TickType_t xTicksToWait,
                              const BaseType_t xCopyPosition );参数说明:
- xQueue:需要写入的队列
- pvItemToQueue:待写入消息
- xTicksToWait:写入阻塞等待时间,xQueueOverwrite() 函数覆写阻塞时间为 0。
- xCopyPosition:写入的位置,上面任务级函数之间最大的区别就是写入位置封装不一样。 
  -  queueSEND_TO_BACK 尾部写入。 
-  queueSEND_TO_FRONT 头部写入。 
 
-  
-  返回值:pdTRUE 队列写入成功,errQUEUE_FULL 队列写入失败。 
上面 xQueueGenericSend() 函数的参数列表与宏函数的参数列表是对应的,如果有函数没有相关参数,那就是隐藏了,如:

这个尾部写入并不是在队列的最末尾写入,而是队列的逻辑尾部(即FIFO顺序的末端),头部写入也是类似的。 具体可以看上面的队列写入操作。
3.3 队列读取消息
/* 从队列头部读取消息,并删除消息 */
BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait )
/* 从队列头部读取消息 */
BaseType_t xQueuePeek( QueueHandle_t xQueue,
                       void * const pvBuffer,
                       TickType_t xTicksToWait )
/* 在中断中从队列头部读取消息,并删除消息 */
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
                                 void * const pvBuffer,
                                 BaseType_t * const pxHigherPriorityTaskWoken )
/* 在中断中从队列头部读取消息 */
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,void * const pvBuffer )上面的读取消息函数都是从队列头部开始读取的,下面说明限于任务级的。
参数说明:
- xQueue:待读取的队列
- pvBuffer:信息读取缓冲区
- xTicksToWait :阻塞超时时间
- 返回值:pdTRUE 读取成功,pdFALSE 读取失败
出队与入队有两种方式:先进先出(FIFO),后进先出(LIFO),就是不同读写函数的搭配使用
先进先出(FIFO):尾部写入,头部读出。
后进先出(LIFO):头部写入,头部读取。
四、代码示例
创建 4 个任务,start_task,task1,task2,task3,作用如下
| start_task | 创建task1,task2 ,task3这三个任务,并且创建两个队列,一个用于存储键值,一个用于存储字符串 | 
| task1 | 进行按键扫描,按键1 发送键值,按键2 发送字符串地址 | 
| task2 | 读取队列1,即存储键值的队列并打印 | 
| task3 | 读取队列2,读取字符串地址并打印 | 
代码如下:
main.c 文件:
#include "malloc.h"
#include "freertos_demo.h"
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "usmart.h"
#include "led.h"
#include "lcd.h"
#include "key.h"
int main(void)
{
    
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    my_mem_init(SRAMIN);                /* 初始化内部SRAM内存池 */
    freertos_demo();                    /* 创建起始任务 */
}freertos_demo.c 文件:
#include "freertos_demo.h"
#include "string.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"
#include "key.h"
/*FreeRTOS******************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/***************************************************************************************/
/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK_STACK_SIZE 256
#define TASK_PRIORITY   1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);
/* TASK1 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_STACK_SIZE 256
#define TASK1_PRIORITY   2
TaskHandle_t task1_handle;
void task1(void *pvParameters);
/* TASK2 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK2_STACK_SIZE 256
#define TASK2_PRIORITY   3
TaskHandle_t task2_handle;
void task2(void *pvParameters);
/* TASK3 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK3_STACK_SIZE 256
#define TASK3_PRIORITY   4
TaskHandle_t task3_handle;
void task3(void *pvParameters);
/***************************************************************************************/
QueueHandle_t key_queue;              /* 小数据队列句柄 */
QueueHandle_t big_date_queue;         /* 大数据队列句柄 */
char buffer[64] = "Hello World";      /* 待发送的字符串 */
/* 在main函数中被调用 */
void freertos_demo(void)
{
    xTaskCreate(
        (TaskFunction_t         )start_task,            /*函数指针,函数入口*/
        (char *                )"start_task",           /*任务名字*/
        (configSTACK_DEPTH_TYPE)TASK_STACK_SIZE,        /*任务堆栈的大小*/
        (void *                )NULL,                   /*任务参数*/
        (UBaseType_t           )TASK_PRIORITY,          /*任务优先级*/
        (TaskHandle_t *        )&start_task_handle      /*任务句柄*/
    );
    vTaskStartScheduler();
}
/* 创建其他任务和队列 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();
    key_queue = xQueueCreate( 2, sizeof(uint8_t) );
    if(key_queue != NULL){
        printf("小数据队列创建成功\r\n");
    }
    big_date_queue = xQueueCreate( 1, sizeof(char *) );
    if(big_date_queue != NULL){
        printf("大数据队列创建成功\r\n");
    }
    xTaskCreate(
        (TaskFunction_t         )task1,
        (char *                )"task1",
        (configSTACK_DEPTH_TYPE)TASK1_STACK_SIZE,
        (void *                )NULL,
        (UBaseType_t           )TASK1_PRIORITY,
        (TaskHandle_t *        )&task1_handle
    );
    xTaskCreate(
        (TaskFunction_t         )task2,
        (char *                )"task2",
        (configSTACK_DEPTH_TYPE)TASK2_STACK_SIZE,
        (void *                )NULL,
        (UBaseType_t           )TASK2_PRIORITY,
        (TaskHandle_t *        )&task2_handle
    );
    xTaskCreate(
        (TaskFunction_t         )task3,
        (char *                )"task3",
        (configSTACK_DEPTH_TYPE)TASK3_STACK_SIZE,
        (void *                )NULL,
        (UBaseType_t           )TASK3_PRIORITY,
        (TaskHandle_t *        )&task3_handle
    );
	taskEXIT_CRITICAL();
	vTaskDelete(NULL);		/*删除当前任务*/
}
/* 任务1:读取小数据队列中的数据 */
void task1(void *pvParameters)
{
    uint8_t key_val = 0;
    while(1){
        xQueueReceive(key_queue ,&key_val,portMAX_DELAY);
        printf("接受的键值为:%d\r\n",key_val);
        key_val = 0;
        vTaskDelay(50);
    }
}
/* 任务2:读取大数据队列的数据 */
void task2(void *pvParameters)
{
	char * buf;
    while(1){
        xQueueReceive(big_date_queue,&buf,portMAX_DELAY);
        printf("接受的字符串为: %s\r\n",buf);
        vTaskDelay(50);
    }
}
/* 任务3:按键捕获,往消息队列中发送数据 */
void task3(void *pvParameters)
{
    uint8_t key = 0;
    char*  buf  = buffer;
    while(1){
        key = key_scan(0);
        if(key == KEY0_PRES || key == KEY1_PRES){
            if(pdTRUE != xQueueSend( key_queue, (void *) &key, portMAX_DELAY))
                printf("键值发送失败\r\n");
        }else if(key == WKUP_PRES){
            if(xQueueSend(big_date_queue,&buf,portMAX_DELAY) != pdTRUE)
                printf("字符串发送失败\r\n");
        }
        vTaskDelay(10);
    }
}

















![[Windows] 游戏常用运行库- Game Runtime Libraries Package(6.2.25.0409)](https://i-blog.csdnimg.cn/img_convert/55ca519964b01db8e0d3b2a1e7961c42.webp?x-oss-process=image/format,png)
