找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2872

积分

0

好友

396

主题
发表于 昨天 22:15 | 查看: 0| 回复: 0

在深入代码之前,我们先快速回顾一下队列的基本概念,这有助于理解环形缓冲区的设计动机。

队列的概念

队列 (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环形缓冲区实现的详解对你有所帮助。在实际嵌入式开发中,灵活运用数据结构能极大提升系统稳定性和效率。如果你想探讨更多嵌入式或数据结构相关的话题,欢迎来 云栈社区 交流分享。




上一篇:暗月网络安全全栈渗透与实战项目深度解析 从零基础到红队实战,一站式掌握Web安全、内网渗透与漏洞挖掘
下一篇:Vue-Lynx 发布:基于 Vue3 的跨平台原生框架如何挑战 React Native
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-1 19:30 , Processed in 0.291837 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表