目录
2.2 消息队列静态创建函数 xQueueCreateStatic()
2.4.1 xQueueSend()和xQueueSendToBack()
2.4.2 xQueueSendFromISR()与xQueueSendToBackFromISR()
2.4.4 xQueueSendToFrontFromISR()
2.5.1 xQueueReceive()和xQueuePeek()
2.5.2 xQueueReceiveFromISR()和xQueuePeekFromISR()
1. 消息队列简介
1.1 概念
队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息。
FreeRTOS 中使用队列数据结构实现任务异步通信工作,具有如下特性:
① 消息支持先进先出方式排队,支持异步读写工作方式。
② 读写队列均支持超时机制。
③ 消息支持后进先出方式排队,往队首发送消息(LIFO) 。
④ 可以允许不同长度(不超过队列节点最大值)的任意类型消息。
⑤ 一个任务能够从任意一个消息队列接收和发送消息。
⑥ 多个任务能够从同一个消息队列接收和发送消息。
⑦ 当队列使用结束后,可以通过删除队列函数进行删除。
1.2 数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制,也可以使用LIFO的存储缓冲,也就是后进先出。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。
1.3 阻塞机制
1.3.1 出队阻塞
当任务尝试从一个队列读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列读取消息无效的时候任务阻塞的时间。
1.3.2 入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中,和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。
1.4 操作示意图
1.4.1 创建队列
首先创建两个任务A和B,任务A想向任务B发送消息,这个消息变量为x,我们在创建队列的时候需要创建其对列的长度以及每个消息的长度,消息的长度需要看好其数据类型,此时x的数据类型为int,占据四个字节,因此消息的长度为4:
1.4.2 向队列发送第一个消息
任务 A 的变量 x 值为 10,将这个值发送到消息队列中。此时队列剩余长度就是 3 了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量 x 就可以再次被使用,赋其他的值。
1.4.3 向队列发送第二个消息
任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。紧跟在上一个数据的后面,此时队列剩余长度为 2。
1.4.4 从队列读取消息
任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10 了。任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个 消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成 3。如果 不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。
1.5 消息队列的控制块
在queue.c文件找到如下函数,做一个了解:
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; /*< 存储在队列锁定时从队列中接收(移除)的项数。当队列未锁定时设置为 `queueUNLOCKED`。 */
volatile int8_t cTxLock; /*< 存储在队列锁定时发送到队列(添加)的项数。当队列未锁定时设置为 `queueUNLOCKED`。 */
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为 `pdTRUE`,以确保不尝试释放内存。 */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition *pxQueueSetContainer; /*< 指向包含此队列的队列集合的指针。 */
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber; /*< 队列的唯一编号。 */
uint8_t ucQueueType; /*< 队列的类型。 */
#endif
} xQUEUE;
2. 常用API函数介绍
2.1 消息队列创建函数 xQueueCreate()
函数原型 | QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_tuxItemSize); | |
功能 | 用于创建一个新的队列。 | |
参数 | uxQueueLength | 队列能够存储的最大消息单元数目,即队列长度。 |
uxltemSize | 队列中消息单元的大小,以字节为单位。 | |
返回值 | 如果创建成功则返回一个队列句柄,用于访问创建的队列。如果创建不成功则返回NULL,可能原因是创建队列需要的RAM无法分配成功。 |
xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。 队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态,剩下的作为队列消息的存储区域。
消息队列创建函数默认的是动态创建的方法。通常情况下,在 FreeRTOS 中,凡是创建任务,队列, 信号量和互斥量等内核对象都需要使用动态内存分配,使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存 由编译的时候预先分配好,一般很少使用这种方法。
2.2 消息队列静态创建函数 xQueueCreateStatic()
函数原型 | QueueHandle_t xQueueCreateStatic(UBaseType_tuxQueueLength, UBaseType_t uxltemSize, uint8_t *pucQueueStorageBuffer, StaticQueue_t *pxQueueBuffer); | |
功能 | 用于创建一个新的队列。 | |
参数 | uxQueueLength | 队列能够存储的最大单元数目,即队列深度。 |
uxItemSize | 队列中数据单元的长度,以字节为单位。 | |
pucQueueStorageBuffer | 指针,指向一个uint8_t类型的数组,数组的大小至少有uxQueueLength* uxltemSize个字节。当uxItemSize为0时,pucQueueStorageBuffer可以为NULL. | |
pxQueueBuffer | 指针,指向 StaticQueue_t 类型的变量,该变量用于存储队列的数据结构。 | |
返回值 | 如果创建成功则返回一个队列句柄,用于访问创建的队列。如果创建不成功则返回NULL,可能原因是创建队列需要的RAM无法分配成功。 |
相较于动态方法,静态方法的功能是一样的,只是静态方法需要自己手动分配内存,比较麻烦一些。
2.3 消息队列删除函数 vQueueDelete()
队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的,因为没创建的东西就不存在, 怎么可能被删除。
函数原型 | void vQueueDelete(QueueHandle_t xQueue) |
2.4 消息队列的发送消息函数
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
#define QueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
2.4.1 xQueueSend()和xQueueSendToBack()
函数原型 | BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvltemToQueue, TickType_txTicksToWait); | |
功能 | 用于向队列尾部发送一个队列消息。 | |
参数 | xQueue | 队列句柄。 |
pvItemToQueue | 指针,指向要发送到队列尾部的队列消息。 | |
xTicksToWait | 队列满时,等待队列空闲的最大超时时间。如果队列满并且xTicksToWait被设置成0,函数立刻返回。超时时间的单位为系统节拍周期,常量portTICK-PERIOD-MS用于辅助计算真实的时间,单位为ms。如果INCLUDE_vTaskSuspend设置成1,并且指定延时为 portMAX_DELAY 将导致任务挂起(没有超时)。 | |
返回值 | 消息发送成功成功返回pdTRUE,否则返回errQUEUE-FULL。 |
2.4.2 xQueueSendFromISR()与xQueueSendToBackFromISR()
函数原型 | BaseType_t xQueueSendFromISR(QueueHandle_txQueue, const void *pvItemToQueue, BaseType_t*pxHigherPriority TaskWoken); | |
功能 | 在中断服务程序中用于向队列尾部发送一个消息。 | |
参数 | xQueue | 队列句柄。 |
pvltemToQueue | 指针,指向要发送到队列尾部的消息。 | |
pxHigherPriority TaskWoken | 如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将*pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务。从FreeRTOS V7.3.0 起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为 NULL。 | |
返回值 | 消息发送成功返回pdTRUE,否则返回errQUEUE-FULL。 |
2.4.3 xQueueSendToFront()
函数原型 | BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait); | |
功能 | 于向队列队首发送一个消息。 | |
参数 | xQueue | 队列句柄。 |
pvltemToQueue | 指针,指向要发送到队首的消息。 | |
xTicksToWait | 队列满时,等待队列空闲的最大超时时间。如果队列满并且xTicksToWait被设置成0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)。 | |
返回值 | 发送消息成功返回pdTRUE,否则返回errQUEUE-FULL。 |
2.4.4 xQueueSendToFrontFromISR()
函数原型 | BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t*pxHigherPriorityTaskWoken); | |
功能 | 在中断服务程序中向消息队列队首发送一个消息。 | |
参数 | xQueue | 队列句柄。 |
pvltemToQueue | 指针,指向要发送到队首的消息。 | |
pxHigherPriority TaskWoken | 如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将*pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务。从FreeRTOS V7.3.0 起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为 NULL。 | |
返回值 | 队列项投递成功返回pdTRUE,否则返回errQUEUE_FULL。 |
2.5 消息队列的读取消息函数
2.5.1 xQueueReceive()和xQueuePeek()
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )
#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE )
函数原型 | BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_txTicksToWait); | |
功能 | 用于从一个队列中接收消息,并把接收的消息从队列中删除。 | |
参数 | xQueue | 队列句柄。 |
pvBuffer | 指针,指向接收到要保存的数据。 | |
xTicksToWait | 队列空时,阻塞超时的最大时间。如果该参数设置为0,函数立刻返回。超时时间的单位为系统节拍周期,常量portTICK_PERIOD_MS用于辅助计算真实的时间,单位为ms。如果INCLUDE_vTaskSuspend设置成1,并且指定延时为portMAX_DELAY将导致任务无限阻塞(没有超时)。 | |
返回值 | 队列项接收成功返回pdTRUE,否则返回pdFALSE。 |
xQueuePeek功能上和xQueueReceive是相似的,但是xQueuePeek不会把接收的消息从队列中删除。
2.5.2 xQueueReceiveFromISR()和xQueuePeekFromISR()
函数原型 | BaseType_t xQueueReceiveFromISR(QueueHandle_txQueue, void *pvBuffer, BaseType_t*pxHigherPriorityTaskWoken); | |
功能 | 在中断中从一个队列中接收消息,并从队列中删除该消息。 | |
参数 | xQueue | 队列句柄。 |
pvBuffer | 指针,指向接收到要保存的数据。 | |
pxHigherPriority TaskWoken | 任务在往队列投递信息时,如果队列满,则任务将阻塞在该队列上。如果 xQueueReceiveFromISR()到账了一个任务解锁了则将*pxHigherPriorityTaskWoken设置为pdTRUE, 否则*pxHigherPriorityTaskWoken的值将不变。从FreeRTOS V7.3.0起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为NULL。 | |
返回值 | 队列项接收成功返回pdTRUE,否则返回pdFALSE。 |
函数原型 | BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer); | |
功能 | 在中断中从一个队列中接收消息,但并不会把消息从该队列中移除。 | |
参数 | xQueue | 队列句柄。 |
pvBuffer | 指针,指向接收到要保存的数据。 | |
返回值 | 队列项接收(peek)成功返回pdTRUE,否则返回pdFALSE。 |
3. 注意事项
① 使用 xQueueSend()、xQueueSendFromISR()、xQueueReceive()等这些函数之前应先创建需消息队列,并根据队列句柄进行操作。
② 队列读取采用的是先进先出(FIFO)模式,会先读取先存储在队列中的数据。当然也FreeRTOS也支持后进先出(LIFO)模式,那么读取的时候就会读取到后进队列的数据。
③ 在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。
④ 无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。
⑤ 队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同一队列写入和读出。一个队列由多任务或中断写入是经常的事,但由多个任务读出倒是用的比较少。
4. 代码编写
找到一个动态方法的代码:
4.1 应用任务创建
4.1.1 任务1函数
实现LED灯的闪烁:
//任务1函数
void led1_task(void *pvParameters)
{
while(1)
{
led1_on();
vTaskDelay(200);
led1_off();
vTaskDelay(800);
}
}
4.1.2 接收任务函数
用于接收消息变量:
//接收任务函数
void receive_task(void *pvParameters)
{
BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */
uint32_t r_queue; /* 定义一个接收消息的变量 */
while(1)
{
xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
if(pdTRUE == xReturn)
printf("本次接收到的数据是%d\r\n",r_queue);
else
printf("数据接收出错,错误代码0x%lx\r\n",xReturn);
}
}
这里的任务不需要再调用vTaskDelay(20);进行延时阻塞让出CPU的资源,因为在:
xReturn = xQueueReceive( Test_Queue, /* 消息队列的句柄 */
&r_queue, /* 发送的消息内容 */
portMAX_DELAY); /* 等待时间 一直等 */
portMAX_DELAY表示无限等待,直到有消息可接收,已经进行了阻塞。
4.1.3 发送任务函数
用于发送消息,当按键PB1按下发送&send_data1的内容,当按键PB2按下发送&send_data2的内容:
//发送任务函数
void send_task(void *pvParameters)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
uint32_t send_data1 = 1;
uint32_t send_data2 = 2;
uint8_t KeyNum;
while(1)
{
KeyNum=Key_GetNum();
if(KeyNum==1)
{
printf("发送消息send_data1!\r\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_data1,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("消息send_data1发送成功!\r\n");
}
else if(KeyNum==2)
{
printf("发送消息send_data2!\r\n");
xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */
&send_data2,/* 发送的消息内容 */
0 ); /* 等待时间 0 */
if(pdPASS == xReturn)
printf("消息send_data2发送成功!\r\n");
}
vTaskDelay(20);
}
}
4.1.4 相关宏定义
//任务优先级
#define LED1_TASK_PRIO 2
//任务堆栈大小
#define LED1_STK_SIZE 50
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);
//任务优先级
#define RECEIVE_TASK_PRIO 3
//任务堆栈大小
#define RECEIVE_STK_SIZE 50
//任务句柄
TaskHandle_t ReceiveTask_Handler;
//任务函数
void receive_task(void *pvParameters);
//任务优先级
#define SEND_TASK_PRIO 4
//任务堆栈大小
#define SEND_STK_SIZE 50
//任务句柄
TaskHandle_t SendTask_Handler;
//任务函数
void send_task(void *pvParameters);
4.2 开始任务的创建
之前讲过,开始任务的作用就是将,应用任务的三个任务完整的建立起来,通过调用临界区的API,规避中断带来的风险,创建完后删除自身,只进行应用任务的调度:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
/* 创建Test_Queue */
Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */
(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */
//创建LED1任务
xTaskCreate((TaskFunction_t )led1_task,
(const char* )"led1_task",
(uint16_t )LED1_STK_SIZE,
(void* )NULL,
(UBaseType_t )LED1_TASK_PRIO,
(TaskHandle_t* )&LED1Task_Handler);
//创建接收任务
xTaskCreate((TaskFunction_t )receive_task,
(const char* )"receive_task",
(uint16_t )RECEIVE_STK_SIZE,
(void* )NULL,
(UBaseType_t )RECEIVE_TASK_PRIO,
(TaskHandle_t* )&ReceiveTask_Handler);
//创建发送任务
xTaskCreate((TaskFunction_t )send_task,
(const char* )"send_task",
(uint16_t )SEND_STK_SIZE,
(void* )NULL,
(UBaseType_t )SEND_TASK_PRIO,
(TaskHandle_t* )&SendTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
消息队列的创建以及开始任务相关宏定义:
//任务优先级
#define START_TASK_PRIO 1
//任务堆栈大小
#define START_STK_SIZE 128
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);
QueueHandle_t Test_Queue =NULL;
#define QUEUE_LEN 4 /* 队列的长度,最大可包含多少个消息 */
#define QUEUE_SIZE 4 /* 队列中每个消息大小(字节) */
4.3 主函数
创建开始任务开始调用:
int main()
{
SysTick_Init(72);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
LED_Init();
KEY_Init();
USART1_Init(115200);
printf("FreeRTOS消息队列实验\r\n");
printf("按下KEY_UP或者KEY1发送队列消息\r\n");
printf("Receive任务接收到消息在串口回显\r\n");
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}