RTOS之环形缓冲区,队列,队列集
环形缓冲区的理解以及FREERTOS的队列机制理解和操作
一、环形缓冲区(Circular Buffer)
类似一个环形跑道,运动员(数据)在跑道上循环奔跑。跑道首尾相连,运动员跑到终点后又会回到起点继续跑。
实际上环形缓冲区是一个固定大小的连续内存空间,用两个指针管理数据:
- 写指针:指向下一个可以写入数据的位置。
- 读指针:指向下一个可以读取的数据位置。
当数据写到缓冲区末尾时,会自动回到开头继续写(类似“循环”),覆盖旧数据或阻止写入(取决于设计)。
1.运行机制
关键设计:牺牲一个存储单元
环形缓冲区为了明确区分“满”和“空”两种状态,通常会牺牲一个存储单元(即实际可用大小为 N-1,假设总容量为 N)。
状态判断逻辑:
可写入状态:写指针以及写指针的下一个位置不等于读指针。
空状态:读指针和写指针指向同一位置(read_ptr == write_ptr)。
满状态:写指针的下一个位置(考虑回绕)等于读指针((write_ptr + 1) % N == read_ptr)。
示例说明
假设缓冲区总容量 N=5,实际可用大小为 4。
- 可写入状态:read_ptr = 2,write_ptr = 4,那么读指针可以回到位置1进行写入操作。
- 空状态:read_ptr = 0,write_ptr = 0 → 无数据。
- 满状态:read_ptr = 0, write_ptr = 4 → 下一个写入位置是(4+1)%5=0,与读指针重合。
2.优点
1.高效读写
- 无需频繁分配/释放内存,读写直接通过指针移动完成,时间复杂度为 O(1)。
- 适合实时系统。
2.内存利用率高
- 固定大小,内存预分配,避免内存碎片。
- 数据覆盖机制可重复利用空间
3.避免数据搬迁
- 普通队列在出队后需移动数据,而环形缓冲区只需移动指针。
3.缺点
1.固定容量
- 缓冲区大小需预先确定,扩容需重建整个缓冲区(复杂且耗时)。
2.数据覆盖风险
- 写入速度过快时,未读取的旧数据可能被覆盖(需设计是否允许覆盖)。
3.无法随机访问历史数据
- 只能按顺序读取,若需访问旧数据需额外设计(如日志缓存不适用)。
二、队列(Queue)
FreeRTOS 中的队列基于环形缓冲区的设计,增加了任务调度、阻塞机制和同步控制。
1.队列的结构
(1)环形缓冲区(核心数据存储)
作用:实际存储队列中的数据,通过指针回绕实现循环读写。
(2)任务阻塞链表(同步机制的核心)
FreeRTOS 队列通过两个链表管理因队列状态(满或空)而阻塞的任务:
-
xTasksWaitingToSend:
作用:当队列已满时,尝试写入数据的任务会被挂起并加入此链表。
唤醒条件:队列中有空闲位置(消费者读取数据后)。 -
xTasksWaitingToReceive:
作用:当队列为空时,尝试读取数据的任务会被挂起并加入此链表。
唤醒条件:队列中有新数据写入(生产者写入数据后)。
(3)其他
- uxMessagesWaiting(消息计数器):
记录当前队列中有效数据的数量,用于快速判断队列的空/满状态。
替代指针比较:无需通过 pcReadFrom 和 pcWriteTo 的相对位置判断队列状态,直接通过计数器即可(简化逻辑)。 - 互斥锁(uxRxLock 和 uxTxLock):
作用:在中断服务程序(ISR)中操作队列时,通过锁机制保证原子性。
2.FREE中队列的实现逻辑
(1)队列的结构体定义(简化版)
typedef struct QueueDefinition {
int8_t *pcHead; // 队列缓冲区起始地址
int8_t *pcTail; // 缓冲区结束地址的下一个字节
int8_t *pcWriteTo; // 下一个可写入位置(写指针)
int8_t *pcReadFrom; // 下一个可读取位置(读指针)
UBaseType_t uxMessagesWaiting; // 当前队列中数据项的数量
UBaseType_t uxLength; // 队列容量(最大数据项数)
UBaseType_t uxItemSize; // 每个数据项的大小(字节)
List_t xTasksWaitingToSend; // 等待发送的任务列表(队列满时阻塞)
List_t xTasksWaitingToReceive; // 等待接收的任务列表(队列空时阻塞)
volatile UBaseType_t uxRxLock; // 接收锁(用于中断安全操作)
volatile UBaseType_t uxTxLock; // 发送锁(用于中断安全操作)
} Queue_t;
(2)环形缓冲区的实现(简化版)
队列初始化
队列初始化时分配一块连续内存作为缓冲区,并设置指针和容量:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize) {
// 计算总缓冲区大小:容量 * 数据项大小 + 头尾标记
size_t xBufferSize = uxQueueLength * uxItemSize;
Queue_t *pxQueue = pvPortMalloc(sizeof(Queue_t) + xBufferSize);
// 初始化环形缓冲区指针
pxQueue->pcHead = (int8_t *)(pxQueue + 1); // 缓冲区起始位置
pxQueue->pcTail = pxQueue->pcHead + xBufferSize;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->pcReadFrom = pxQueue->pcHead;
pxQueue->uxLength = uxQueueLength;
pxQueue->uxItemSize = uxItemSize;
pxQueue->uxMessagesWaiting = 0;
// 初始化任务阻塞列表
vListInitialise(&pxQueue->xTasksWaitingToSend);
vListInitialise(&pxQueue->xTasksWaitingToReceive);
return pxQueue;
}

写队列
BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToSend, TickType_t xTicksToWait) {
Queue_t *pxQueue = (Queue_t *)xQueue;
// 检查队列是否已满
//pxQueue->uxMessagesWaiting:当前队列中已存储的数据项数量
if (pxQueue->uxMessagesWaiting >= pxQueue->uxLength) {
if (xTicksToWait == 0) {
return errQUEUE_FULL; // 非阻塞模式,直接返回错误
}
// 阻塞当前任务,加入等待发送列表
vTaskPlaceOnEventList(&pxQueue->xTasksWaitingToSend, xTicksToWait);
taskYIELD(); // 触发任务调度
}
// 写入数据到 pcWriteTo 指向的位置
memcpy(pxQueue->pcWriteTo, pvItemToSend, pxQueue->uxItemSize);
// 更新写指针(环形回绕)
pxQueue->pcWriteTo += pxQueue->uxItemSize;
//pcTail指向索引5的位置,即缓冲区外的下一个字节
//检查写指针是否已经到达或超过了缓冲区的物理末尾
if (pxQueue->pcWriteTo >= pxQueue->pcTail) {
pxQueue->pcWriteTo = pxQueue->pcHead;
}
pxQueue->uxMessagesWaiting++;
// 如果有任务在等待接收,唤醒最高优先级任务
if (listLIST_IS_EMPTY(&pxQueue->xTasksWaitingToReceive) == pdFALSE) {
xTaskNotifyFromISR(xTaskGetCurrentTaskHandle(), ...);
}
return pdPASS;
}
读队列
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait) {
Queue_t *pxQueue = (Queue_t *)xQueue;
// 检查队列是否为空
if (pxQueue->uxMessagesWaiting == 0) {
if (xTicksToWait == 0) {
return errQUEUE_EMPTY; // 非阻塞模式,直接返回错误
}
// 阻塞当前任务,加入等待接收列表
vTaskPlaceOnEventList(&pxQueue->xTasksWaitingToReceive, xTicksToWait);
taskYIELD(); // 触发任务调度
}
// 从 pcReadFrom 指向的位置读取数据
memcpy(pvBuffer, pxQueue->pcReadFrom, pxQueue->uxItemSize);
// 更新读指针(环形回绕)
pxQueue->pcReadFrom += pxQueue->uxItemSize;
if (pxQueue->pcReadFrom >= pxQueue->pcTail) {
pxQueue->pcReadFrom = pxQueue->pcHead;
}
pxQueue->uxMessagesWaiting--;
// 如果有任务在等待发送,唤醒最高优先级任务
if (listLIST_IS_EMPTY(&pxQueue->xTasksWaitingToSend) == pdFALSE) {
xTaskNotifyFromISR(xTaskGetCurrentTaskHandle(), ...);
}
return pdPASS;
}
3.应用演示
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
// 定义队列参数
#define QUEUE_LENGTH 5 // 队列容量(最多存放 5 个数据项)
#define ITEM_SIZE sizeof(int) // 每个数据项的大小(此处以 int 为例)
// 全局队列句柄
QueueHandle_t xQueue;
// 生产者任务(写队列)
void vProducerTask(void *pvParameters) {
int value = 0;
while (1) {
// 向队列发送数据(阻塞时间为 0,队列满时立即返回错误)
if (xQueueSend(xQueue, &value, 0) == pdPASS) {
printf("Produced: %d\n", value);
value++;
} else {
printf("Queue full! Retrying...\n");
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟 1 秒
}
}
// 消费者任务(读队列)
void vConsumerTask(void *pvParameters) {
int received_value;
while (1) {
// 从队列接收数据(阻塞时间 portMAX_DELAY 表示无限等待直到有数据)
if (xQueueReceive(xQueue, &received_value, portMAX_DELAY) == pdPASS) {
printf("Consumed: %d\n", received_value);
}
}
}
int main() {
// 初始化队列
xQueue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
if (xQueue == NULL) {
printf("Queue creation failed!\n");
return -1;
}
// 创建生产者和消费者任务
xTaskCreate(vProducerTask, "Producer", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
xTaskCreate(vConsumerTask, "Consumer", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 若调度器启动失败,执行到此
while (1);
return 0;
}
三、队列集(Queue Set)
队列集用于一个任务同时监听多个数据(来源于队列或信号量),当任务读队列集时,通常将阻塞时间设为最大,任务读不到数据时会进入阻塞状态,当队列集中的成员(队列或者信号量)有数据时,任务被唤醒,并且根据队列集里的句柄获取数据。 特别适用于需要从多个通信通道接收数据的场景。
1.核心作用
- 多路复用监听:任务可以阻塞在队列集上,直到集合中的任意一个队列或信号量有数据可用。
- 简化代码逻辑:避免任务需要轮询多个队列或使用多个独立的阻塞操作,减少复杂性。
2.运行机制
(1)队列集的结构
- 队列集本身是一个队列:队列集的底层实现是一个特殊的队列,用于存储成员队列/信号量的句柄,记录哪些成员队列/信号量有数据到达。
- 成员绑定:队列或信号量需显式加入队列集(通过 xQueueAddToSet)。
(2)工作流程
- 创建队列集:使用 xQueueCreateSet 初始化队列集。
- 绑定队列/信号量:将需要监听的队列或信号量添加到队列集中。
- 任务阻塞等待:任务调用 xQueueSelectFromSet 在队列集上阻塞,直到某个成员有数据。
- 处理事件:任务被唤醒后,根据返回的队列句柄确定具体哪个队列有数据,并进行读取。
(3)通知机制
- 当成员队列被写入数据时,内核会向队列集中自动插入该队列的句柄,表示“事件已发生”。
- 任务调用 xQueueSelectFromSet 时,实际是从队列集中读取队列句柄。
常见问题:
1.如果队列已满,中断中写入失败,队列集会记录该事件吗?
不会。只有成功写入队列的操作(即队列未满时)才会触发队列集的记录。
2.写入队列后,任务从队列集获取句柄,但队列数据已被其他任务读取怎么办?
需确保原子性:任务应在唤醒后立即读取队列数据,避免其他任务或中断抢先消费数据。
3.应用演示
任务需要同时监听两个队列(一个接收传感器数据,一个接收用户命令)。
#include "FreeRTOS.h"
#include "queue.h"
// 定义两个队列和一个队列集
QueueHandle_t xSensorQueue, xCommandQueue;
QueueSetHandle_t xQueueSet;
// 创建队列和队列集
void vInitQueues(void) {
xSensorQueue = xQueueCreate(5, sizeof(int)); // 创建传感器数据队列
xCommandQueue = xQueueCreate(5, sizeof(char)); // 创建用户命令队列
xQueueSet = xQueueCreateSet(5 + 5); // 队列集容量=两个队列容量之和
// 将队列加入队列集
xQueueAddToSet(xSensorQueue, xQueueSet);
xQueueAddToSet(xCommandQueue, xQueueSet);
}
// 任务函数:监听队列集
void vTaskListen(void *pvParameters) {
QueueSetMemberHandle_t xActivatedMember;
int sensorValue;
char cmd;
while(1) {
// 阻塞直到队列集中有数据到达
xActivatedMember = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
// 判断是哪个队列触发了事件
if (xActivatedMember == xSensorQueue) {
xQueueReceive(xSensorQueue, &sensorValue, 0);
// 处理传感器数据...
} else if (xActivatedMember == xCommandQueue) {
xQueueReceive(xCommandQueue, &cmd, 0);
// 处理用户命令...
}
}
}
更多推荐



所有评论(0)