引入
这几天花时间把freertos的队列机制和环形缓冲区内容给学习完了,趁着五一假期记录下来分享一下
简介
FreeRTOS 队列(Queue)是实时操作系统中用于任务间通信(IPC)和同步的核心机制,支持多任务安全的数据传递
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。 任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以也称为消息队列。FreeRTOS中的信号量的也是依据队列实现的!所以有必要深入的了解FreeRTOS的队列。
数据储存形式
通常队列采用先进先出FIFO的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。像是内存栈数据就是LIFO后进先出,但是也可以使用LIFO的存储缓冲,FreeRTOS中的队列也提供了LIFO的存储缓冲机制。数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。UCOS的消息队列采用的是引用传递,传递的是消息指针采用引用传递的话消息内容就必须一直保持可见性,也就是消息内容必须有效,那么局部变量这种可能会随时被删掉的东西就不能用来传递消息,但是采用引用传递会节省时间啊!因为不用进行数据拷贝。采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。(注意:FreeRTOS也可以实现引用传递,只需要传递地址即可)
🫓队列的阻塞机制
队列数据的存储可能因为写队列行为而产生满的情况,而此时读队列未能即使取出数据,导致写队列行为阻塞也就是无法往队列里面写入新的数据;与及读队列行为而导致的队列空的情况,此时读任务队列处于没有数据可读的阻塞状态,所以需要引入阻塞等待机制
-
阻塞(Blocking):当队列满(发送时)或空(接收时),任务会自动进入阻塞状态,暂停执行直到条件满足或超时。
-
阻塞时间(Timeout):任务在阻塞状态下的最长等待时间,单位为系统节拍(Tick)。可通过
pdMS_TO_TICKS(ms)
将毫秒转换为节拍。
出队(接收)阻塞
-
队列空:若队列为空,任务会阻塞,直到队列有数据或超时。
-
队列非空:正常完成接收,不阻塞。
入队(发送)阻塞
-
队列满:队列阻塞,等待读队列消费数据。
-
队列未满:正常写队列,不进行阻塞。
阻塞时间参数
-
xTicksToWait = portMAX_DELAY
:无限等待(死等),直到队列有数据。 -
xTicksToWait = 0
:非阻塞模式,头也不回直接返回失败(pdFALSE
)。 -
xTicksToWait > 0
:指定最大阻塞时间,当在阻塞指定时间后没有接收到数据返回失败。
🥯关键 API 函数
函数 | 功能 |
---|---|
xQueueCreate() |
创建队列(动态内存分配) |
xQueueSend() / xQueueSendToBack() |
发送数据到队列尾(可指定阻塞时间) |
xQueueReceive() |
从队列头接收数据(可指定阻塞时间) |
xQueuePeek() |
读取队列头数据但不删除 |
uxQueueMessagesWaiting() |
查询队列中当前有效数据数量 |
🥪基本使用步骤
(1) 定义队列句柄
QueueHandle_t xQueue;
(2) 创建队列
任务1要向任务2发送消息,这个消息是x变量的值。首先创建一个队列,并且指定队列的长度和每条消息的长度。这里我们创建了一个长度为4的队列,因为要传递的是x值,而x是个int类型的变量,所以每条消息的长度就是int类型的长度,在STM32中就是一个字,即每条消息是4个字节的。
// 参数:队列句柄、队列长度(最大元素数)、单个元素大小(字节)
xQueue = xQueueCreate(4, sizeof(uint32_t));
// 失败返回NULL(需检查内存或配置)
(3) 发送数据
任务1发送信息,队列中发送消息是采用拷贝的方式
uint32_t data = 100;
// 发送到队列尾,阻塞时间 portMAX_DELAY(无限等待)
xQueueSend(xQueue, &data, portMAX_DELAY);
(4) 接收数据
任务2从队列中读取消息,任务2从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一。如果不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小不变。
uint32_t receivedData;
// 从队列头读取,超时100ms
if (xQueueReceive(xQueue, &receivedData, pdMS_TO_TICKS(100)) == pdPASS) {
// 处理数据
}
注意:
-
在中断服务例程(ISR)中,禁止使用阻塞参数,应使用
_FromISR
版本函数(如xQueueSendFromISR
)。 -
多个任务相互等待对方释放资源时,可能导致死锁/
-
队列过小可能导致频繁阻塞,过大则浪费内存,需根据实际数据流量调整。
🍕生产者&消费者模型
本质上就是一个task写队列(生产者),一个task在就绪运行态时候读取数据(消费者),当然这里的消费者和生产者可以是多个,因为创建的队列这个句柄并不属于任何一个任务task,所以任何任务都可以写队列和读队列
// 生产者任务 void vProducer(void *pvParameters) { uint32_t data = 0; while (1) { xQueueSend(xQueue, &data, portMAX_DELAY); data++; vTaskDelay(pdMS_TO_TICKS(100)); } } // 消费者任务 void vConsumer(void *pvParameters) { uint32_t receivedData; while (1) { if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) { printf("Received: %lu ", receivedData); } } }
以上是我对FreeRTOS消息队列的一些理解,如有叙述的不正确的地方请指正