消息队列简介
队列是为了任务与任务、 任务与中断之间的通信而准备的, 可以在任务与任务、 任务与中断之间传递消息, 队列中可以存储有限的、 大小固定的数据项目。任务与任务、 任务与中断之间要交流的数据保存在队列中, 叫做队列项目。 队列所能保存的最大数据项目数量叫做队列的长度, 创建队列的时候会指定数据项目的大小和队列的长度。 由于队列用来传递消息的, 所以也称为消息队列。
消息队列数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制, 也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部, 而从队列提取数据的时候(也叫出队)是从队列的头部提取的。 但是也可以使用 后进先出(LIFO) 的存储缓冲,FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制。
数据发送到队列中会导致数据拷贝, 也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值, 而不是原数据的引用(即只传递数据的指针), 这个也叫做值传递。
采用值传递的话虽然会导致数据拷贝, 会浪费一点时间, 但是一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写, 这样的话这些缓冲区就可以被重复的使用。 FreeRTOS 中使用队列传递消息的话虽然使用的是数据拷贝,但是也可以使用引用来传递消息, 直接往队列中发送指向这个消息的地址指针就可以了
消息队列阻塞机制
出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间, 这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。 出队就是从队列中读取消息, 出队阻塞是针对从队列中读取消息的任务而言的。
任务 A 用于处理串口接收到的数据, 串口接收到数据以后就会放到队列 Q 中, 任务 A 从队列 Q 中读取数据。 但是如果此时队列 Q 是空的, 说明还没有数据, 任务 A 这时候来读取的话肯定是获取不到任何东西, 那该怎么办呢?
三种选择
一: 二话不说扭头就走
阻塞时间为 0 的话就是不阻塞, 没有数据的话就马上返回任务继续执行接下来的代码
二: 要不我在等等吧, 等一会看看, 说不定一会就有数据了
如果阻塞时间为 0~ portMAX_DELAY 之间, 当任务没有从队列中获取到消息的话就进入阻塞态, 阻塞时间指定了任务进入阻塞态的时间, 当阻塞时间到了以后还没有接收到数据的话就退出阻塞态, 返回任务接着运行下面的代码, 如果在阻塞时间内接收到了数据就立即返回, 执行任务中下面的代码
三: 死等, 死也要等到你有数据
当阻塞时间设置为 portMAX_DELAY 的话, 任务就会一直进入阻塞态等待, 直到接收到数据为止
入队阻塞
入队说的是向队列中发送消息, 将消息加入到队列中。 和出队阻塞一样, 当一个任务向队列发送消息的话也可以设置阻塞时间。
消息队列操作示图
(1) 创建队列
图中任务 A 要向任务 B 发送消息, 这个消息是 x 变量的值。 首先创建一个队列, 并且指定队列的长度和每条消息的长度。 这里我们创建了一个长度为 4的队列, 因为要传递的是 x 值, 而 x 是个 int 类型的变量, 所以每条消息的长度就是 int 类型的长度, 在 STM32 中是 4 字节, 即每条消息是 4 个字节的。
(2) 向队列发送第一个消息
图中任务 A 的变量 x 值为 10, 将这个值发送到消息队列中。 此时队列剩余长度就是 3 了。 前面说了向队列中发送消息是采用拷贝的方式, 所以一旦消息发送完成变量 x 就可以再次被使用, 赋其他的值。
(3) 向队列发送第二个消息
图中任务 A 又向队列发送了一个消息, 即新的 x 的值, 这里是 20。 此时队列剩余长度为 2。
(4) 从队列中读取消息
图中任务 B 从队列中读取消息, 并将读取到的消息值赋值给 y, 这样 y 就等于 10 了。 任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。 当选择清除这个消息的话其他任务或中断就不能获取这个消息了, 而且队列剩余大小就会加一, 变成 3。 如果不清除的话其他任务或中断也可以获取这个消息, 而队列剩余大小依旧是 2。
消息队列控制块
FreeRTOS 的消息队列控制块由多个元素组成, 当消息队列被创建时, 系统会为控制块分配对应的内存空间, 用于保存消息队列的一些信息如消息的存储位置, 头指针pcHead、 尾指针 pcTail、 消息大小 uxItemSize 以及队列长度uxLength, 以及当前队列消息个数uxMessagesWaiting 等
typedef struct QueueDefinition
{
int8_t *pcHead; (1)
int8_t *pcTail; (2)
int8_t *pcWriteTo; (3)
union
{
int8_t *pcReadFrom; (4)
UBaseType_t uxRecursiveCallCount; (5)
} u;
List_t xTasksWaitingToSend; (6)
List_t xTasksWaitingToReceive; (7)
volatile UBaseType_t uxMessagesWaiting; (8)
UBaseType_t uxLength; (9)
UBaseType_t uxItemSize; (10)
volatile int8_t cRxLock; (11)
volatile int8_t cTxLock; (12)
#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;
代码(1): pcHead 指向队列消息存储区起始位置, 即第一个消息空间。
代码(2): pcTail 指向队列消息存储区结束位置地址。
代码(3): pcWriteTo 指向队列消息存储区下一个可用消息空间。
代码(4): pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量, 使用联合体用来确保两个互斥的结构体成员不会同时出现。 当结构体用于队列时,pcReadFrom 指向出队消息空间的最后一个, 见文知义, 就是读取消息时候是从pcReadFrom 指向的空间读取消息内容。
代码(5): 当结构体用于互斥量时, uxRecursiveCallCount 用于计数, 记录递归互斥量被“调用” 的次数。
代码(6): xTasksWaitingToSend 是一个发送消息阻塞列表, 用于保存阻塞在此队列的任务, 任务按照优先级进行排序, 由于队列已满, 想要发送消息的任务无法发送消息。
代码(7): xTasksWaitingToReceive 是一个获取消息阻塞列表, 用于保存阻塞在此队列的任务, 任务按照优先级进行排序, 由于队列是空的, 想要获取消息的任务无法获取到消息。
代码(8): uxMessagesWaiting 用于记录当前消息队列的消息个数, 如果消息队列被用于信号量的时候, 这个值就表示有效信号量个数。
代码(9): uxLength 表示队列的长度, 也就是能存放多少消息。
代码(10): uxItemSize 表示单个消息的大小。
代码(11): 队列上锁后, 储存从队列收到的列表项数目, 也就是出队的数量,如果队列没有上锁, 设置为 queueUNLOCKED。
常用消息队列 API 函数
消息队列创建函数
xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。 队列句柄其实就是一个指向队列数据结构类型的指针。
动态创建(常用)
静态创建
复位
队列刚被创建时,里面没有数据;使用过程中可以调用 xQueueReset()把队列恢复为初始状态
删除
删除队列的函数为 vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。
向消息队列发送消息函数
xQueueSend()用于向队列尾部发送一个队列消息。 消息以拷贝的形式入队,而不是以引用的形式。 该函数绝对不能在中断服务程序里面被调用, 中断中必须使用带有中断保护功能的QueueSendFrxomISR()来代替。
xQueueSendFromISR()是一个宏, 宏展开是调用函数xQueueGenericSendFromISR()。 该宏是 xQueueSend()的中断保护版本, 用于在中断服务程序中向队列尾部发送一个队列消息, 等价于
xQueueSendToBackFromISR()。
xQueueSendToFront()用于向队列队首发送一个消息。 消息以拷贝的形式入队,而不是以引用的形式。 该函数绝不能在中断服务程序里面被调用, 而是必须使用带有中断保护功能的 xQueueSendToFrontFromISR ()来代替。
xQueueSendToFrontFromISR()是一个宏, 宏展开是调用函数xQueueGenericSendFromISR()。 该宏是 xQueueSendToFront()的中断保护版本,用于在中断服务程序中向消息队列队首发送一个消息。
从消息队列读取消息函数
xQueueReceive()用于从一个队列中接收消息并把消息从队列中删除。 接收的消息是以拷贝的形式进行的, 所以我们必须提供一个足够大空间的缓冲区。 具体能够拷贝多少数据到缓冲区, 这个在队列创建的时候已经设定。 该函数绝不能在中断服务程序里面被调用, 而是必须使用带有中断保护功能的xQueueReceiveFromISR ()来代替。
如果不想删除消息的话, 就调用 xQueuePeek()函数
xQueueReceiveFromISR()是 xQueueReceive ()的中断版本, 用于在中断服务程序中接收一个队列消息并把消息从队列中删除;
xQueuePeekFromISR()是xQueuePeek()的中断版本, 用于在中断中从一个队列中接收消息, 但并不会把消息从队列中移除。
说白了这两个函数只能用于中断, 是不带有阻塞机制的, 并且是在中断中可以安全调用
消息队列使用注意事项
1.使用 xQueueSend()、 xQueueSendFromISR()、 xQueueReceive()等这些函数之前应先创建需消息队列, 并根据队列句柄进行操作。
2.队列读取采用的是先进先出(FIFO) 模式, 会先读取先存储在队列中的数据。 当然也 FreeRTOS 也支持后进先出(LIFO) 模式, 那么读取的时候就会读取到后进队列的数据。
3.在获取队列中的消息时候, 我们必须要定义一个存储读取数据的地方, 并且该数据区域大小不小于消息大小, 否则, 很可能引发地址非法的错误。
4.无论是发送或者是接收消息都是以拷贝的方式进行, 如果消息过于庞大,可以将消息的地址作为消息进行发送、 接收。
5.队列是具有自己独立权限的内核对象, 并不属于任何任务。 所有任务都可以向同一队列写入和读出。 一个队列由多任务或中断写入是经常的事, 但由多个任务读出倒是用的比较少。