目录
[1 认识Queue](#1 认识Queue)
[1.1 Queue定义](#1.1 Queue定义)
[1.2 FreeRTOS中的Queue](#1.2 FreeRTOS中的Queue)
[1.3 Queue状态](#1.3 Queue状态)
[1.4 Queue内容](#1.4 Queue内容)
[1.5 发送和接收Message](#1.5 发送和接收Message)
[1.5.1 发送message](#1.5.1 发送message)
[1.5.2 接收Message](#1.5.2 接收Message)
[2 Queue的特性](#2 Queue的特性)
[2.1 数据存储](#2.1 数据存储)
[2.2 可被多任务存取](#2.2 可被多任务存取)
[2.3 读Queue时阻塞](#2.3 读Queue时阻塞)
[2.4 写Queue时阻塞](#2.4 写Queue时阻塞)
[3 使用Queue](#3 使用Queue)
[3.1 xQueueCreate() 函数](#3.1 xQueueCreate() 函数)
[3.2 xQueueSendToBack() 与 xQueueSendToFront()函数](#3.2 xQueueSendToBack() 与 xQueueSendToFront()函数)
[3.3 xQueueReceive()与 xQueuePeek() 函数](#3.3 xQueueReceive()与 xQueuePeek() 函数)
[3.4 uxQueueMessagesWaiting() API 函数](#3.4 uxQueueMessagesWaiting() API 函数)
[4 一个案例](#4 一个案例)
[4.1 功能描述](#4.1 功能描述)
[4.2 定义Queue的变量](#4.2 定义Queue的变量)
[4.3 创建Queue](#4.3 创建Queue)
[4.4 应用Queue发送或者接收message](#4.4 应用Queue发送或者接收message)
[4.4.1 发送Message](#4.4.1 发送Message)
[4.4.2 接收Message](#4.4.2 接收Message)
[4.5 测试](#4.5 测试)
[5 结论](#5 结论)
源代码下载地址:
概述
本文主要介绍Queue的相关知识,包括Queue的定义,发送和接收消息的方式等内容。重点使用Free RTOS中Queue的接口,实现数据在不同task之间的发送和接收的案例,并在板卡上验证该功能。
1 认识Queue
1.1 Queue定义
消息队列是一个类似于缓冲区的对象。通过它,任务和ISR发送和接收消息,实现到数据的同学和同步。消息队列小一个管道。它暂时保存来自发送者的消息,直到有意的接受者准备读这些消息。这个临时缓冲区把发送任务和接收任务隔开,即它必须同时释放发送和接收消息的任务。
创建一个队列,其应该具备这些要素:
1)分配一个相关的队列控制块(QCB)
- 一个消息队列名
3)一个唯一的ID
- 存储器缓冲区
5)队列长度
6)最大消息长度
7)一个或者多个任务等待列表
1.2 FreeRTOS中的Queue
FreeRTOS 的应用程序由一组独立的任务构成------每个任务都是具有独立权限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
1.3 Queue状态
发送消息状态:
step -1: 当一个任务发送消息给一个消息队列,消息会直接发送给阻塞的任务
step -2: 阻塞任务进入就绪态或者运行态,此时消息队列为空,发送消息成功
step -3: 如果另外的消息送到相同的队列,而且没有任务在消息队列的任务等待列表中等候,此时消息队列的状态为非空
step -4: 当消息数据达到队列总数是,队列为满,此时队列无法接收任何消息。
1.4 Queue内容
消息队列可以用来接收和发送多种数据,有些消息的数据可能相当长,这种情况下,可使用发送数据指针的方式。
1.5 发送和接收Message
1.5.1 发送message
方式一: 先进先出FIFO次序Queue
方式一: 先进后出LIFO次序Queue
1.5.2 接收Message
方式一: 任务等待类表-先进先出FIFO次序Queue
方式二: 任务等待类表-先进后出LIFO次序Queue
2 Queue的特性
2.1 数据存储
队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的"深度"。在队列创建时需要设定其深度和每个单元的大小。通常情况下,队列被作为 FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出。当然,由队列首写入也是可能的。往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
2.2 可被多任务存取
队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以向同一队列写入和读出。一个队列由多方写入是经常的事,但由多方读出倒是很少遇到。
2.3 读Queue时阻塞
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
2.4 写Queue时阻塞
同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
3 使用Queue
3.1 xQueueCreate() 函数
队列在使用前必须先被创建。队列由声明为 xQueueHandle 的变量进行引用。 xQueueCreate()用于创建一个队列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用。当创建队列时, FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数据结构本身以及队列中包含的数据单元。如果内存堆中没有足够的空间来创建队列,xQueueCreate()将返回 NULL。第五章会有关于内存堆管理的更多信息。
cpp
xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength,
unsigned portBASE_TYPE uxItemSize );
参数名称 | 描述 |
---|---|
uxQueueLength | 队列能够存储的最大单元数目,即队列深度 |
uxItemSize | 队列中数据单元的长度,以字节为单位 |
返回值 | 描述 |
---|---|
NULL | 表示没有足够的堆空间分配给队列而导致创建失败 |
非 NULL | 表示队列创建成功。此返回值应当保存下来,以作为 操作此队列的句柄 |
3.2 xQueueSendToBack() 与 xQueueSendToFront()函数
1) xQueueSendToBack()用于将数据发送到队列尾;
2) xQueueSendToFront()用于将数据发送到队列首。
3) xQueueSend()完全等同于 xQueueSendToBack()。
但 切 记 不 要 在 中 断 服 务 例 程 中 调 用 xQueueSendToFront() 或xQueueSendToBack()。
中断模式使用的发送消息函数:
1)xQueueSendToFrontFromISR()
2)xQueueSendToBackFromISR()
cpp
portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue,
const void * pvItemToQueue,
portTickType xTicksToWait );
cpp
portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue,
const void * pvItemToQueue,
portTickType xTicksToWait );
参数值 | 描述 |
---|---|
xQueue | 目标队列的句柄。这个句柄即是调用 xQueueCreate()创建该队 列时的返回值。 |
pvItemToQueue | 发送数据的指针。其指向将要复制到目标队列中的数据单元。 由于在创建队列时设置了队列中数据单元的长度,所以会从该指 针指向的空间复制对应长度的数据到队列的存储区域。 |
xTicksToWait | 阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于 阻塞态等待队列空间有效的最长等待时间。如 果 xTicksToWait 设 为 0 , 并 且 队 列 已 满 , 则xQueueSendToFront()与 xQueueSendToBack()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。如 果 把 xTicksToWait 设 置 为 portMAX_DELAY , 并 且 在FreeRTOSConig.h 中设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。 |
返回值介绍
返回值 | 描述 |
---|---|
pdPASS | 返回 pdPASS 只会有一种情况,那就是数据被成功发送到队列<中。如果设定了阻塞超时时间(xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效---在超时到来前能够将数据成功写入到队列,函数则会返回 pdPASS。 |
errQUEUE_FULL | 如 果 由 于 队 列 已 满 而 无 法 将 数 据 写 入 , 则 将 返 errQUEUE_FULL。如果设定了阻塞超时时间( xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效。但直到超时也没有其它任务或是中断服务例程读取队列而腾出空间,函数则会返回 errQUEUE_FULL。 |
3.3 xQueueReceive()与 xQueuePeek() 函数
xQueueReceive(): 用于从队列中接收(读取)数据单元。接收到的单元同时会从队列中删除xQueuePeek():也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。 其从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。
注意:
切记不要在中断服务例程中调用 xQueueRceive()和 xQueuePeek()。
**中断模式下使用的接收函数:**xQueueReceiveFromISR()
参数介绍
参数值 | 描述 |
---|---|
xQueue | 目标队列的句柄。这个句柄即是调用 xQueueCreate()创建该队 列时的返回值。 |
pvBuffer | 接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来 的数据。数据单元的长度在创建队列时就已经被设定,所以该指针指向的 内存区域大小应当足够保存一个数据单元。 |
xTicksToWait | 阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于 阻塞态等待队列空间有效的最长等待时间。如 果 xTicksToWait 设 为 0 , 并 且 队 列 已 满 , 则xQueueSendToFront()与 xQueueSendToBack()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。如 果 把 xTicksToWait 设 置 为 portMAX_DELAY , 并 且 在FreeRTOSConig.h 中设定 INCLUDE_vTaskSuspend 为 1,那么阻塞等待将没有超时限制。 |
返回值
返回值 | 描述 |
---|---|
pdPASS | 返回 pdPASS 只会有一种情况,那就是数据被成功发送到队列<中。如果设定了阻塞超时时间(xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效---在超时到来前能够将数据成功写入到队列,函数则会返回 pdPASS。 |
errQUEUE_FULL | 如 果 由 于 队 列 已 满 而 无 法 将 数 据 写 入 , 则 将 返 errQUEUE_FULL。如果设定了阻塞超时时间( xTicksToWait 非 0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效。但直到超时也没有其它任务或是中断服务例程读取队列而腾出空间,函数则会返回 errQUEUE_FULL。 |
3.4 uxQueueMessagesWaiting() API 函数
uxQueueMessagesWaiting(): 用于查询队列中当前有效数据单元个数。
注意: 不要在中断服务例程中调用 uxQueueMessagesWaiting()。
中断模式下使用的函数: uxQueueMessagesWaitingFromISR()。
cpp
unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue );
参数值 | 描述 |
---|---|
xQueue | 被查询队列的句柄。这个句柄即是调用 xQueueCreate()创建该队列时 的返回值。 |
返回值:
返回值 | 描述 |
---|---|
非0 | 当前队列中保存的数据单元个数 |
0 | 表明队列为空 |
4 一个案例
4.1 功能描述
使用STM32H7平台,基于Free RTOS平台,创建3个Task, 2个Task发送Message, 一个Task接收Message。
4.2 定义Queue的变量
代码第6行: 定义Queue的属性
代码第7行: 创建Queue ID 变量
代码第9~12行: 定义消息体数据结构
代码第14~17行: 消息体内容
详细代码:
cpp
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
const osMessageQueueAttr_t QueueInputSignalAttribute = {.name = "QueueTest"};
osMessageQueueId_t QueueTest;
typedef struct {
int id;
int32_t value;
}Qdata_stru;
Qdata_stru queList[2] = {
{1, 100},
{2, 50},
};
4.3 创建Queue
使用osMessageQueueNew创建一个Queue, osMessageQueueNew函数在cmsis_os2.c中定义。其函数原型为:
cpp
osMessageQueueId_t osMessageQueueNew (uint32_t msg_count, uint32_t msg_size, const osMessageQueueAttr_t *attr)
参数介绍:
参数名称 | 描述 |
---|---|
msg_count | 整个消息队列的长度 |
msg_size | 消息的字节大小 |
attr | 属性 |
创建方法如下:
详细代码:
cpp
void initTask( void )
{
int ucQuequeLength= sizeof(Qdata_stru);
QueueTest = osMessageQueueNew (10, ucQuequeLength, &QueueInputSignalAttribute);
}
注意:
创建Queue才能实现发送或者接收message
4.4 应用Queue发送或者接收message
4.4.1 发送Message
定义两个Task,在该Task中实现消息发送功能
代码第32行:发送message
代码第33行: 判断message 是否发送成功
代码第47行:发送message
代码第48行: 判断message 是否发送成功
详细代码:
cpp
void mainTask(void *argument)
{
osStatus_t status;
for(;;)
{
status = osMessageQueuePut(QueueTest, &queList[0], NULL,100);
if( status != osOK){
printf("mainTask osStatus_t = %d \r\n",status);
}
osDelay(300);
}
}
void monitorTask(void *argument)
{
osStatus_t status;
for(;;)
{
status = osMessageQueuePut(QueueTest, &queList[1], NULL,100);
if( status != osOK){
printf(" monitorTask osStatus_t = %d \r\n",status);
}
osDelay(200);
}
}
4.4.2 接收Message
定义一个Task,在该Task中仅仅实现接收其他Task发送的消息。
代码第62行: 接收queue消息
代码第64~69行: 根据 message ID解析消息
详细代码:
cpp
void stateTask(void *argument)
{
Qdata_stru recv_que;
static int cnt = 0;
for(;;)
{
if( osOK == osMessageQueueGet(QueueTest, &recv_que, NULL, 100))
{
if( recv_que.id == 1){
printf(" que 1: %d \r\n",recv_que.value);
}
else if( recv_que.id == 2) {
printf(" que 2: %d \r\n",recv_que.value);
}
}
cnt++;
if( (cnt %10) == 0)
{
HAL_GPIO_TogglePin(R_STATUS_GPIO_Port, R_STATUS_Pin);
}
osDelay(1);
}
}
4.5 测试
编译代码,下载到板卡中,通过终端查看发送和接收消息的情况。打开串口终端,代码运行后,log信息如下:
5 结论
消息队列可以不同Task之间的通信,一个消息队列可接收多个Task发送的消息,对于数据长度大于消息队列数据长度的情况,可采用传送指针的方式实现。