- 消息队列简介
基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权限的程序。这些独立的任务之间的通讯与同步一般都是基于操作系统提供的IPC通讯机制,而FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
消息队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传送信息,实现了任务接收来自其他任务或中断的不固定长度的消息。任务能够从队列里面读取消息,当队列中的消息是空时,挂起读取任务,用户还可以指定挂起的任务时间;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息,消息队列是一种异步的通信方式。
队列特性
1.数据存储
队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。
通常情况下,队列被作为 FIFO(先进先出)缓冲区使用,即数据由队列尾写入,从队列首读出。当然,由队列首写入也是可能的。
往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
2.读阻塞
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
3.写阻塞
与读阻塞想反,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。
消息队列的工作流程
主要调用的API
1.发送消息(入队)
任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队, FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。
发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。
下面是消息队列的发送API接口,函数中有FromISR则表明在中断中使用的。
2.消息队列读取(出队)
任务调用接收函数收取队列消息, 函数首先判断当前队列是否有未读消息, 如果没有, 则会判断参数 xTicksToWait, 决定直接返回函数还是阻塞等待。
如果队列中有消息未读, 首先会把待读的消息复制到传进来的指针所指内, 然后判断函数参数 xJustPeeking == pdFALSE的时候, 符合的话, 说明这个函数读取了数据, 需要把被读取的数据做出队处理, 如果不是, 则只是查看一下(peek),只是返回数据,但是不会把数据清除。
对于正常读取数据的操作, 清除数据后队列会空出空位, 所以查看队列中的等待列表中是否有任务等发送数据而被挂起, 有的话恢复一个任务就绪, 并根据优先级判断是否需要出进行任务切换。
对于只是查看数据的, 由于没有清除数据, 所以没有空间新空出,不需要检查发送等待链表, 但是会检查接收等待链表, 如果有任务挂起会切换其到就绪并判断是否需要切换。
- 工程创建
1.RCC和时钟树的配置
2.时基设置
3.FreeTROS的设置
开启FreeRTOS,v1与v2版本不同,一般选用v1即可
在 Config parameters 进行具体参数配置。
这里一般默认就好了
然后就是创建任务:
在 Tasks and Queues 进行配置。
创建队列Queue
在 Tasks and Queues 进行配置。
创建一个消息队列TestQueue,
Queue Name: 队列名称
Queue Size: 队列能够存储的最大单元数目,即队列深度
Queue Size: 队列中数据单元的长度,以字节为单位
Allocation: 分配方式:Dynamic 动态内存创建
Buffer Name: 缓冲区名称
Buffer Size: 缓冲区大小
Conrol Block Name: 控制块名称
- 相关API讲解
- 库函数
队列创建:xQueueCreate()
参数:
uxQueueLength 队列可同时容纳的最大项目数 。
uxItemSize 存储队列中的每个数据项所需的大小(以字节为单位)。
数据项按副本排队,而不是按引用排队, 因此该值为每个排队项目将被复制的字节数。队列中每个数据项 必须大小相同。
返回值
如果队列创建成功,则返回所创建队列的句柄 。 如果创建队列所需的内存无法 分配 ,则返回 NULL。
用法示例:
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1, xQueue2;
/* Create a queue capable of containing 10 unsigned long values. */
xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );
if( xQueue1 == NULL )
{
/* Queue was not created and must not be used. */
}
/* Create a queue capable of containing 10 pointers to AMessage
structures. These are to be queued by pointers as they are
relatively large structures. */
xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );
if( xQueue2 == NULL )
{
/* Queue was not created and must not be used. */
}
/* ... Rest of task code. */
}
队列删除:vQueueDelete()
void vQueueDelete( QueueHandle_t xQueue );
函数功能:删除队列 — 释放分配用于存储放置在队列中的项目的所有内存。
参数:xQueue 要删除的队列的句柄。
向队列写消息:xQueueSend()
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
函数功能:向队列中写入消息
参数:
xQueue | 队列的句柄,数据项将发布到此队列。 |
pvItemToQueue | 指向待入队数据项的指针。创建队列时定义了队列将保留的项的大小,因此固定数量的字节将从 pvItemToQueue 复制到队列存储区域。 |
xTicksToWait | 如果队列已满,则任务应进入阻塞态等待队列上出现可用空间的最大时间。如果队列已满,并且 xTicksToWait 设置为0 ,调用将立即返回。时间在 tick 周期中定义,因此如果需要,应使用常量 portTICK_PERIOD_MS 转换为实时。 如果 INCLUDE_vTaskSuspend 设置为 “1” ,则将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)。 |
返回值:如果成功发布项目,返回 pdTRUE,否则返回 pdFALSE。
用法示例:
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
} xMessage;
unsigned long ulVar = 10UL;
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1, xQueue2;
struct AMessage *pxMessage;
/* Create a queue capable of containing 10 unsigned long values. */
xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );
/* Create a queue capable of containing 10 pointers to AMessage structures.
These should be passed by pointer as they contain a lot of data. */
xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );
/* ... */
if( xQueue1 != 0 )
{
/* Send an unsigned long. Wait for 10 ticks for space to become
available if necessary. */
if( xQueueSend( xQueue1,
( void * ) &ulVar,
( TickType_t ) 10 ) != pdPASS )
{
/* Failed to post the message, even after 10 ticks. */
}
}
if( xQueue2 != 0 )
{
/* Send a pointer to a struct AMessage object. Don't block if the
queue is already full. */
pxMessage = & xMessage;
xQueueSend( xQueue2, ( void * ) &pxMessage, ( TickType_t ) 0 );
}
/* ... Rest of task code. */
}
备注:如果要从尾部添加消息,使用xQueueSendFromISR();用法和xQueueSend()一样.
向队列读消息:xQueueReceive()
函数功能:用于从一个队列中接收消息,并把接收的消息从队列中删除。
参数:
返回值:如果接收消息成功,返回 pdTRUE,否则返回 pdFALSE。
查询队列:
* 1.返回队列中可用数据的个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
* 2.返回队列中可用空间的个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
- HAL函数
队列ID:
typedef QueueHandle_t osMessageQId;
备注:队列ID。例如,对osMessageCreate的调用返回。可用作参数到osMessageDelete以删除队列。
队列创建:
osMessageQId osMessageCreate (const osMessageQDef_t *queue_def, osThreadId thread_id)
函数功能:使用动态内存的方式创建一个新的队列。
参数: 1.queue_def: 引用由osMessageQDef定义的队列
2.thread_id: 线程ID或NULL
返回值:成功返回任务ID,失败返回0
队列删除:
osStatus osMessageDelete (osMessageQId queue_id)
函数功能:队列删除函数是根据消息队列ID直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了。
参数: queue_id: 消息队列ID,表示的是要删除哪个想队列
返回值:错误码
示例如下:
osMessageQId TestQueueHandle;
/* 创建 Test_Queue */
/* Create the queue(s) */
/* definition and creation of TestQueue */
osMessageQDef(TestQueue, 16, uint32_t);/* 第2参数:消息队列的长度,第3参数:消息的大小 */
TestQueueHandle = osMessageCreate(osMessageQ(TestQueue), NULL);
向队列发送消息:
osStatus osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec)
函数功能:用于向队列尾部发送一个队列消息。消息以拷贝的形式入队,而不是以引用的形式。可用在中断服务程序中。
参数:
queue_id: 目标队列ID。这个句柄即是调用 osMessageCreate() 创建该队列时的返回值
info: 发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。
millisec: 队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。
超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)。
返回值:错误码
向队列接收消息:(拷贝形式,会删除队列的消息)
osEvent osMessageGet (osMessageQId queue_id, uint32_t millisec)
函数功能:
用于从一个队列中接收消息并把消息从队列中删除。接收的消息是以拷贝的形式进行的,所以我们必须提供一个足够大空间的缓冲区。具体能够拷贝多少数据到缓冲区,这个在队列创建的时候已经设定。可用在中断服务程序中。
参数:
queue_id: 被读队列ID。这个句柄即是调用 osMessageCreate() 创建该队列时的返回值
millisec: 队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)。
返回值:错误码
向队列接收消息:(不从队列中删出接收到的单元)
osEvent osMessagePeek (osMessageQId queue_id, uint32_t millisec)
函数功能:
从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。osMessagePeek() 从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。可用在中断服务程序中。
参数:
queue_id: 被读队列ID。这个句柄即是调用 osMessageCreate() 创建该队列时的返回值
millisec: 队列空时,阻塞超时的最大时间。如果该参数设置为 0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)。
返回值:错误码
查询队列有效单元个数:
uint32_t osMessageWaiting(osMessageQId queue_id)
函数功能:
从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。osMessagePeek() 从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储序顺。可用在中断服务程序中。
参数: queue_id: 目标队列ID。这个句柄即是调用 osMessageCreate() 创建该队列时的返回值
返回值:当前队列中保存的数据单元个数。返回 0 表明队列为空
- 实验例程
创建两个任务,一个消息队列:
发送消息队列任务:
void SendTask(void const * argument)
{
/* USER CODE BEGIN SendTask */
osEvent xReturn;
uint32_t send_data=2;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == 0)
{
osDelay(10);//消抖
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == 0)
{
printf("发送数据\r\n");
xReturn.status=osMessagePut(TestQueueHandle,send_data,0);
if(osOK != xReturn.status)
{
printf("发送错误\r\n");
}
}
}
while(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == 0)//等待按键释放,防止连续发送
{
osDelay(10);
}
osDelay(100);
}
/* USER CODE END SendTask */
}
接收消息队列任务:
void ReceiveTask(void const * argument)
{
/* USER CODE BEGIN ReceiveTask */
osEvent event;
/* Infinite loop */
for(;;)
{
event = osMessageGet(TestQueueHandle, /* 消息队列的句柄 */
osWaitForever); /* 等待时间 一直等 */
if(osEventMessage == event.status)
{
printf("接收数据:%d\r\n", event.value.v);
}
else
{
printf("error: 0x%d\n", event.status);
}
}
/* USER CODE END ReceiveTask */
}
创建消息队列:
实验效果:
按下按键一就向消息队列发送消息,同时拷贝接收,利用串口打印出来。