在深入代码之前,我们先快速回顾一下队列的基本概念,这有助于理解环形缓冲区的设计动机。
队列的概念
队列 (Queue) 是一种先进先出 (First In First Out, 简称 FIFO) 的线性表,只允许在一端(队尾)插入(入队),在另一端(队首)删除(出队)。
这很像现实生活中的排队场景,比如在售票窗口,先来的人先买到票离开,后来的人只能排在后面。在计算机程序中,队列常用于缓存数据、解耦生产者与消费者等场景。
队列有两种常见的数据结构形式:普通队列和环形队列。
普通队列

在计算机中,数据存储在一个个连续的存储单元中,你可以将上图的小方格理解为数组,用于存放信息。
普通队列存在一个明显的缺点:当处理完队首的数据后,这部分内存就被释放了。如果后续数据不断进入,队尾指针会一直后移。为了避免“假溢出”(实际队列前端有空闲位置但队尾已到数组末尾),通常需要将整个队列的数据前移,这会带来巨大的性能开销,显然不现实。因此,环形队列应运而生。
环形队列

环形队列的逻辑结构是一个环,它巧妙避免了普通队列数据搬移的问题。本质上,它仍然是一个队列,遵守先进先出 (FIFO) 规则,拥有队首 (Head) 和队尾 (Tail)。我们通常以顺时针方向来理解数据的流动。
- 队首 (Head) : 允许进行删除操作的一端。
- 队尾 (Tail) : 允许进行插入操作的一端。
环形队列的原理与实现
那么,在物理内存是线性的情况下,如何实现“环形”呢?其实,我们就是用一个数组模拟环,并用两个指针(或索引)来标记队首和队尾。队首指针指向下一个待读取的数据,队尾指针指向下一个可写入数据的位置。通过对这两个指针进行取模运算,让它们在到达数组末尾后能“绕回”数组开头,从而在逻辑上形成环形。
原理简述:初始化时,队首和队尾索引都指向数组起始位置0。当有数据写入时,存入队尾索引指向的位置,然后队尾索引加一并取模,指向下一个可写位置。当需要读取数据时,从队首索引指向的位置取出数据,然后队首索引加一并取模,指向下一个待读位置。这样,读写指针循环往复,高效利用了一块固定的内存空间。

如上图所示,队首指针指向待处理的数据(位置1),队尾指针指向下一个可写入的位置(位置5后的下一个)。当位置1的数据被处理(释放)后,队首指针将移动到位置2。同时,如果此时写入一个新数据到位置5,队尾指针也会向前移动。

这个例子更清晰地展示了读写指针在环形队列中的移动轨迹。
环形队列的代码实现
理解了原理,我们来看看如何用 C 语言实现一个环形缓冲区。数据结构是算法与数据结构的核心基础,而环形队列是其中非常经典且实用的一个。
typedef struct ringBuff{
unsigned int in; // 写入位置索引
unsigned int out; // 读出位置索引
unsigned char buffer[RING_BUFF_SIZE]; // 数据存储数组
}stRingBuff;
写入数据前必须判断队列是否已满。
/**
* @brief: 写一字节的数据到环形队列
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
char WriteOneByteToRingBuffer(stRingBuff *ringBuf, char data)
{
if (ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
if(IsRingBufferFull(ringBuf)) // 写之前先判断队列是否写满
{
return FALSE;
}
ringBuf->buffer[ringBuf->in] = data;
ringBuf->in = (++ringBuf->in) % RING_BUFF_SIZE; // 防止越界,实现环形
return TRUE;
}
这是环形队列实现的一个关键点。当 (in + 1) % SIZE == out 时,我们认为队列已满。这意味着我们故意浪费了一个存储单元来区分“队列满”和“队列空”(两者在 in == out 时都成立)的状态。
/**
* @brief: 判断环形队列是否满
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
bool IsRingBufferFull(stRingBuff *ringBuf)
{
if(ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
if(((ringBuf->in+1) % RING_BUFF_SIZE) == ringBuf->out)
{
// printf("Ring buffer is Full\r\n");
return TRUE;
}
return FALSE;
}
解决“队满”和“队空”判断冲突的另一种常见方法是增加一个记录元素个数的变量,但上述“空一格”的方法更简洁高效,是嵌入式CC++开发中的常用技巧。

上图直观展示了“空出一个字节不写”以判断队列已满的策略。
读取数据前需要判断队列是否为空。
/**
* @brief: 从环形队列读一字节数据
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
char ReadOneByteFromRingBuffer(stRingBuff *ringBuf, char *data)
{
if (ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
if(IsRingBufferEmpty(ringBuf)) // 读之前判断队列是否为空
{
return FALSE;
}
*data = ringBuf->buffer[ringBuf->out];
ringBuf->out = (++ringBuf->out) % RING_BUFF_SIZE; // 防止越界,实现环形
return TRUE;
}
当写入位置索引 in 和读出位置索引 out 相等时,队列为空。
/**
* @brief: 判断环形队列是否空
* @param[in]: None
* @retval[out]: None
* @author: AresXu
* @version: v1.0.0
*/
bool IsRingBufferEmpty(stRingBuff *ringBuf)
{
if(ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
if(ringBuf->in == ringBuf->out) // 写入位置和读出位置相等时为空
{
// printf("Ring buffer is Empty\r\n");
return TRUE;
}
return FALSE;
}
基于单字节写入函数,实现多字节写入。
/**
* @brief: 写len个字节数据到环形队列
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
void WriteRingBuffer(stRingBuff *ringBuf, char *writeBuf, unsigned int len)
{
unsigned int i;
if (ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
for(i = 0; i < len; i++)
{
WriteOneByteToRingBuffer(ringBuf, writeBuf[i]);
}
}
基于单字节读取函数,实现多字节读取。
/**
* @brief: 从环形队列读出len个字节的数据
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
void ReadRingBuffer(stRingBuff *ringBuf, char *readBuf, unsigned int len)
{
unsigned int i;
if (ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
for(i = 0; i < len; i++)
{
ReadOneByteFromRingBuffer(ringBuf, &readBuf[i]);
}
}
这个函数非常有用,它能告诉你当前缓冲区里有多少未读数据,便于决定读取多少。
/**
* @brief: 获取已经写入的长度
* @param[in]: None
* @retval[out]: None
* @note:
* @author: AresXu
* @version: v1.0.0
*/
int GetRingBufferLength(stRingBuff *ringBuf)
{
if(ringBuf == NULL)
{
printf("pointer is null\r\n");
return;
}
return (ringBuf->in - ringBuf->out + RING_BUFF_SIZE) % RING_BUFF_SIZE;
}
这个长度计算公式 (in - out + SIZE) % SIZE 是环形队列的经典计算方式,通过画图很容易理解其原理。
到STM32上测试
理论结合实践,我们将这个环形缓冲区应用到STM32的串口接收中,解决数据接收和处理速度不匹配的问题。
static stRingBuff g_stRingBuffer = {0,0,0};
static u8 g_recvFinshFlag = 0;
stRingBuff *GetRingBufferStruct(void)
{
return &g_stRingBuffer;
}
u8 *IsUsart1RecvFinsh(void)
{
return &g_recvFinshFlag;
}
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
res = USART_ReceiveData(USART1); //读取接收到的数据
WriteOneByteToRingBuffer(GetRingBufferStruct(), res); //写入环形缓冲区
}
if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) //空闲中断(一帧数据接收完成)
{
USART_ReceiveData(USART1); //清除空闲中断标志(读取DR寄存器)
g_recvFinshFlag = 1; //设置接收完成标志
}
}
int main(void)
{
char readBuffer[100];
u16 t;
u16 len;
u16 times = 0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
while(1)
{
times++;
if(*IsUsart1RecvFinsh()) // 检查是否接收完一帧数据
{
// 计算长度并读取所有数据
ReadRingBuffer(GetRingBufferStruct(), readBuffer, GetRingBufferLength(GetRingBufferStruct()));
printf("%s", readBuffer); // 通过串口打印出来
memset(readBuffer, 0, 100); // 清空临时缓冲区
*IsUsart1RecvFinsh() = 0; // 清除接收完成标志
}
if(times%500==0)
LED0=!LED0; // LED闪烁,指示系统运行
delay_ms(1);
}
}

使用串口调试助手发送数据,可以看到STM32成功接收并原样返回,证明了环形缓冲区在异步串口通信中的有效性。这种设计确保了即使主程序暂时忙于其他任务,也不会丢失任何串口数据。
原文: https://blog.csdn.net/qq_36413982/article/details/103836946
希望这篇关于STM32环形缓冲区实现的详解对你有所帮助。在实际嵌入式开发中,灵活运用数据结构能极大提升系统稳定性和效率。如果你想探讨更多嵌入式或数据结构相关的话题,欢迎来 云栈社区 交流分享。