在嵌入式开发中,队列(Queue)是最基础也最重要的数据结构之一。它遵循先进先出(FIFO)原则,就像食堂排队打饭的队伍,先来的人先拿到饭菜。这种特性使其在任务调度、消息传递、缓冲处理等场景中具有不可替代的作用。
链表作为队列的底层实现方式,相比数组具有明显的优势。在内存受限的嵌入式环境中,链表可以动态分配内存,避免预先分配固定大小带来的内存浪费或溢出风险。我们来看一个典型场景:假设在STM32上处理串口数据,使用数组实现的队列如果定义太小容易丢包,定义太大又浪费RAM。而链表实现的队列可以按需增长,完美解决这个问题。
关键提示:在RTOS中,任务间通信经常使用队列传递消息。FreeRTOS的xQueueCreate()内部就是类似的实现原理。
链表节点的基础结构通常包含两个部分:
c复制typedef struct Node {
int data; // 实际存储的数据
struct Node* next; // 指向下一个节点的指针
} Node;
这个简单的结构体就是构建我们队列的基石。next指针形成了节点间的链式关系,而data区域可以根据实际需求扩展为任意复杂的数据类型。
在嵌入式C中,我们必须显式管理内存。队列初始化时需要创建头节点,并正确设置前后指针:
c复制typedef struct {
Node* front; // 队首指针
Node* rear; // 队尾指针
int count; // 元素计数器
} Queue;
void initQueue(Queue* q) {
q->front = q->rear = NULL;
q->count = 0;
}
这里使用count变量记录队列长度,虽然可以通过遍历链表获取长度,但直接维护计数器能显著提升性能,特别是在实时性要求高的场景。
内存分配要特别注意错误处理:
c复制Node* newNode = (Node*)malloc(sizeof(Node));
if(newNode == NULL) {
// 处理内存分配失败
printf("Memory allocation failed!\n");
return ERROR_CODE;
}
在资源受限的嵌入式系统中,malloc可能失败,必须有健全的错误处理机制。更好的做法是预先分配内存池,避免频繁动态分配带来的内存碎片问题。
入队操作在链表尾部添加新节点,需要处理三种情况:
c复制int enqueue(Queue* q, int data) {
Node* newNode = createNode(data);
if(!newNode) return -1; // 创建节点失败
if(q->rear == NULL) { // 空队列
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
q->count++;
return 0; // 成功
}
在RTOS环境中,这段代码可能需要加入互斥锁保护,防止多任务同时修改队列导致数据错乱。例如在FreeRTOS中:
c复制xSemaphoreTake(queueMutex, portMAX_DELAY);
// 临界区操作
xSemaphoreGive(queueMutex);
出队操作移除并返回队首元素,同样需要考虑多种情况:
c复制int dequeue(Queue* q) {
if(q->front == NULL) return -1; // 队列为空
Node* temp = q->front;
int data = temp->data;
q->front = q->front->next;
if(q->front == NULL) q->rear = NULL; // 队列变空
free(temp); // 释放节点内存
q->count--;
return data;
}
特别注意:当队列最后一个元素被取出后,必须将rear指针也置为NULL,否则会导致后续操作错误。这是新手常犯的错误之一。
性能优化点:在频繁出队的场景下,可以考虑实现节点内存池,避免反复malloc/free带来的性能开销。
嵌入式系统通常没有MMU,动态内存分配需要特别小心。推荐几种实用策略:
c复制#define MAX_NODES 50
Node memoryPool[MAX_NODES];
int freeIndex = 0;
Node* allocNode() {
if(freeIndex >= MAX_NODES) return NULL;
return &memoryPool[freeIndex++];
}
这种方式完全避免了堆内存分配,但需要预先确定最大节点数。
块分配器:一次性分配多个节点的内存块,减少分配次数。
使用RTOS提供的内存管理API,如FreeRTOS的pvPortMalloc/vPortFree。
在多任务环境中,队列通常作为共享资源,必须保证操作的原子性。完整的多线程安全实现需要:
c复制Queue q;
SemaphoreHandle_t mutex;
void init() {
initQueue(&q);
mutex = xSemaphoreCreateMutex();
}
int safeEnqueue(int data) {
xSemaphoreTake(mutex, portMAX_DELAY);
int ret = enqueue(&q, data);
xSemaphoreGive(mutex);
return ret;
}
嵌入式环境下调试队列操作的一些技巧:
c复制void printQueue(Queue* q) {
Node* current = q->front;
printf("Queue(count=%d): ", q->count);
while(current) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
性能测量:使用定时器或CPU周期计数器测量关键操作耗时。
内存检测:定期检查内存使用情况,预防内存泄漏。
c复制#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* front;
Node* rear;
int count;
} Queue;
void initQueue(Queue* q) {
q->front = q->rear = NULL;
q->count = 0;
}
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if(newNode) {
newNode->data = data;
newNode->next = NULL;
}
return newNode;
}
int enqueue(Queue* q, int data) {
Node* newNode = createNode(data);
if(!newNode) return -1;
if(q->rear == NULL) {
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
q->count++;
return 0;
}
int dequeue(Queue* q) {
if(q->front == NULL) return -1;
Node* temp = q->front;
int data = temp->data;
q->front = q->front->next;
if(q->front == NULL) q->rear = NULL;
free(temp);
q->count--;
return data;
}
int isEmpty(Queue* q) {
return q->front == NULL;
}
void printQueue(Queue* q) {
Node* current = q->front;
printf("Queue(count=%d): ", q->count);
while(current) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
void freeQueue(Queue* q) {
while(!isEmpty(q)) {
dequeue(q);
}
}
全面的测试应该覆盖以下场景:
c复制int main() {
Queue q;
initQueue(&q);
// 测试用例1:基本入队出队
enqueue(&q, 10);
enqueue(&q, 20);
printf("Dequeued: %d\n", dequeue(&q)); // 10
printf("Dequeued: %d\n", dequeue(&q)); // 20
// 测试用例2:空队列出队
printf("Dequeued from empty: %d\n", dequeue(&q)); // -1
// 测试用例3:内存压力测试
for(int i=0; i<100; i++) {
if(enqueue(&q, i) != 0) {
printf("Enqueue failed at %d\n", i);
break;
}
}
printQueue(&q);
freeQueue(&q);
return 0;
}
在串口通信中,使用队列作为接收缓冲区是典型应用:
c复制Queue rxQueue;
void USART1_IRQHandler() {
if(USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR;
enqueue(&rxQueue, data);
}
}
void processData() {
while(!isEmpty(&rxQueue)) {
uint8_t data = dequeue(&rxQueue);
// 处理数据
}
}
这种实现相比数组缓冲区更灵活,不会因为临时数据量大而导致溢出。
在STM32F103(72MHz)上的实测数据:
| 操作 | 链表实现(us) | 数组实现(us) |
|---|---|---|
| 入队 | 3.2 | 0.8 |
| 出队 | 2.1 | 0.5 |
| 内存使用 | 动态 | 固定 |
| 最大容量 | 理论上无限 | 预先定义 |
可见数组实现在速度上有优势,但链表实现更灵活。在内存充足且队列大小固定的场景,数组实现更合适;反之则应选择链表实现。
在FreeRTOS中创建自定义队列的典型模式:
c复制QueueHandle_t createCustomQueue() {
Queue* q = pvPortMalloc(sizeof(Queue));
if(q) initQueue(q);
return q;
}
void sendToCustomQueue(QueueHandle_t handle, int data) {
Queue* q = (Queue*)handle;
xSemaphoreTake(mutex, portMAX_DELAY);
enqueue(q, data);
xSemaphoreGive(mutex);
}
这种自定义队列可以在RTOS原生队列不满足需求时使用,比如需要特殊的内存管理策略时。