第7章 消息队列
第七章 消息队列
1. 消息队列的基本概念
队列又称消息队列,是一种常用于任务间通信的数据结构, 队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。 当队列中有新消息时, 被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。 消息队列是一种异步的通信方式。
通过消息队列服务,任务或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个任务可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是也支持后进先出原则(LIFO) 。
2. 消息队列的运作机制
创建消息队列时 FreeRTOS 会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积) ,接着再初始化消息队列,此时消息队列为空。 FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength 等。同时每个消息队列都与消息空间在同一段连续的内存空间中,在创建成功的时候,这些内存就被占用了,只有删除了消息队列的时候,这段内存才会被释放掉,创建成功的时候就已经分配好每个消息空间与消息队列的容量,无法更改,每个消息空间可以存放不大于消息大小 uxItemSize 的任意类型的数据, 所有消息队列中的消息空间总数即是消息队列的长度,这个长度可在消息队列创建时指定。
任务或者中断服务程序都可以给消息队列发送消息, 当发送消息时, 如果队列未满或者允许覆盖入队, FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。
发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时, 发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。
3. 消息队列的阻塞机制
很简单,因为 FreeRTOS 已经为我们做好了,我们直接使用就好了,每个对消息队列读写的函数,都有这种机制,我称之为阻塞机制。假设有一个任务 A 对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务 A 有 3 个选择:第一个选择,任务 A 扭头就走,既然队列没有消息,那我也不等了,干其它事情去,这样子任务 A 不会进入阻塞态;第二个选择,任务 A 还是在这里等等吧,可能过一会队列就有消息,此时任务 A 会进入阻塞状态,在等待着消息的道来,而任务 A 的等待时间就由我们自己定义,比如设置 1000 个系统时钟节拍 tick 的等待,在这 1000 个 tick 到来之前任务 A 都是处于阻塞态,当阻塞的这段时间任务 A 等到了队列的消息,那么任务 A 就会从阻塞态变成就绪态,如果此时任务 A 比当前运行的任务优先级还高,那么,任务 A 就会得到消息并且运行;假如 1000 个 tick 都过去了,队列还没消息,那任务 A 就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务 A 的其他代码;第三个选择,任务 A 死等,不等到消息就不走了,这样子任务 A 就会进入阻塞态,直到完成读取队列的消息。
而在发送消息操作的时候,为了保护数据,当且仅当队列允许入队的时候,发送者才能成功发送消息;队列中无可用消息空间时,说明消息队列已满,此时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码 errQUEUE_FULL,然后解除阻塞状态;当然, 只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的 API 函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。
假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。
4. 消息队列的应用场景
消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换,队列是FreeRTOS 主要的任务间通讯方式,可以在任务与任务间、中断和任务间传送信息,发送到队列的消息是通过拷贝方式实现的, 这意味着队列存储的数据是原数据,而不是原数据的引用。
5. 消息队列控制块
FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength, 以及当前队列消息个数uxMessagesWaiting 等
// 消息队列控制块
typedef struct QueueDefinition {
int8_t *pcHead; // 指向队列消息存储区起始位置,即第一个消息空间
int8_t *pcTail; // 指向队列消息存储区结束位置地址
int8_t *pcWriteTo; // 指向消息存储区下一个可用消息空间
union {
int8_t *pcReadFrom; // 指向消息存储区第一个未读消息位置
UBaseType_t uxRecursiveCallCount; // 等待读取的消息数量
}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; // 发送锁,用于实现多任务间的互斥访问
}
#if((configSUPPORT_STATIC_ALLOCATION == 1))
&&(configSUPPORT_DYNAMIC_ALLOCATION == 1)
uint8_t ucStaticallyAllocated;
#endif
#if(configUSE_QUEUE_SETS == 1)
struct QueueDefinition *pxQueueSetContainer;
#endif
#if(configUSE_TRACE_FACILITY == 1)
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
}xQUEUE;
typedef xQUEUE Queue_t;
6. 消息队列使用注意事项
在使用 FreeRTOS 提供的消息队列函数的时候,需要了解以下几点:
-
使用 xQueueSend()、 xQueueSendFromISR()、 xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。
-
队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也 FreeRTOS 也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。
-
在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。
-
无论是发送或者是接收消息都是以拷贝的方式进行, 如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。
-
队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同一队列写入和读出。一个队列由多任务或中断写入是经常的事,但由多个任务读出倒是用的比较少。
7. 消息队列实验
消息队列实验是在 FreeRTOS 中创建了两个任务,一个是发送消息任务,一个是获取消息任务,两个任务独立运行,发送消息任务是通过检测按键的按下情况来发送消息,假如发送消息不成功,就把返回的错误情代码在串口打印出来,另一个任务是获取消息任务,在消息队列没有消息之前一直等待消息,一旦获取到消息就把消息打印在串口调试助手里
/* FreeRTOS includes. */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
/* 其他头文件 */
#include "uart.h"
#include "led.h"
#include "key.h"
/* 任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL; // 创建任务句柄
static TaskHandle_t Receive_Task_Handle = NULL; // 接收任务句柄
static TaskHandle_t Send_Task_Handle = NULL; // 发送任务句柄
/* 内核对象句柄 */
QueueHandle_t Test_Queue = NULL; // 消息队列句柄
/* 全局变量声明 */
/* 宏定义 */
#define QUEUE_LEN 4 // 消息队列长度,最大可包含多少消息
#define QUEUE_SIZE 4 // 队列中每个消息的大小
/* 任务函数声明 */
static void AppTaskCreate(void); // 创建任务函数
static void Receive_Task(void* pvParameters); // 接收任务函数
static void Send_Task(void* pvParameters); // 发送任务函数
static void BSP_Init(void); // 板级初始化函数
// 主函数启动流程
/*
1.BSP初始化
2.创建APP任务
3.启动FreeRTOS,开启调度
*/
int main(void)
{
BaseType_t xReturn = pdPASS; // 定义一个创建信息返回值,默认为pdPASS
BSP_Init(); // 板级初始化
printf("按下KEY1或者KEY2发送队列消息\r\n");
printf("Receive Task 接收到消息会提供串口回显\r\n");
// 创建AppTaskCreate任务
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,
(const char* )"AppTaskCreate",
(uint16_t )512,
(void* )NULL,
(UBaseType_t )1,
(TaskHandle_t*)&AppTaskCreate_Handle);
if(xReturn == pdPASS)
{
vTaskStartScheduler(); // 启动FreeRTOS
printf("FreeRTOS Start Success\r\n");
}
else
{
printf("AppTaskCreate 创建失败!\r\n");
return -1;
}
while(1);
}
// 为了方便管理,所有任务创建函数都在AppTaskCreate中实现
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;
taskENTER_CRITICAL(); // 进入临界区
Test_Queue = xQueueCreate((UBaseType_t)QUEUE_LEN, (UBaseType_t)QUEUE_SIZE); // 创建消息队列
if(Test_Queue != NULL)
{
printf("消息队列创建成功!\r\n");
}
xReturn = xTaskCreate((TaskFunction_t)Receive_Task, // 任务入口函数
(const char*)"Receive_Task", // 任务名字
(uint16_t)512, // 任务堆栈大小
(void*)NULL, // 任务参数
(UBaseType_t)2, // 任务优先级
(TaskHandle_t*)&Receive_Task_Handle); // 任务控制块
if(xReturn == pdPASS)
{
printf("接收任务创建成功!\r\n");
}
xReturn = xTaskCreate((TaskFunction_t )Send_Task, // 任务入口函数
(const char*)"Send_Task", // 任务名字
(uint16_t)1024, // 任务堆栈大小
(void*)NULL, // 任务参数
(UBaseType_t)3, // 任务优先级
(TaskHandle_t*)&Send_Task_Handle); // 任务控制块
if(xReturn == pdPASS)
{
printf("发送任务创建成功!\r\n");
}
vTaskDelete(AppTaskCreate_Handle);
taskEXIT_CRITICAL(); // 退出临界区
}
// 接收任务函数
static void Receive_Task(void* pvParameters)
{
BaseType_t xReturn = pdTRUE;
uint32_t r_queue; // 接收信息临时变量
while(1)
{
xReturn = xQueueReceive(Test_Queue, &r_queue, portMAX_DELAY); // 接收消息
if(xReturn == pdTRUE)
{
printf("接收到消息:%d\r\n", r_queue); // 打印接收到的消息
}
else
{
printf("接收出错,错误代码:%d\n", xReturn);
}
}
}
// 发送任务函数
static void Send_Task(void* pvParameters)
{
BaseType_t xReturn = pdPASS;
uint32_t send_data1 = 1; // 要发送的信息
uint32_t send_data2 = 2;
while(1)
{
if(Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON)
{
printf("KEY1按下,发送send_data1");
xReturn = xQueueSend(Test_Queue, &send_data1, 0); // 发送消息
if(xReturn == pdPASS)
{
printf("send_data1发送成功!\r\n");
}
}
if(Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY_ON)
{
printf("KEY2按下,发送send_data2");
xReturn = xQueueSend(Test_Queue, &send_data2, 0); // 发送消息
if(xReturn == pdPASS)
{
printf("send_data2发送成功!\r\n");
}
}
vTaskDelay(20);
}
}
// 板级初始化函数
static void BSP_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 设置中断优先级分组4
LED_Init();
USART_Config();
Key_GPIO_Config();
}
本文作者:hazy1k
本文链接:https://www.cnblogs.com/hazy1k/p/18750635
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律