同名微信公众号“固件工人”同步发布的文章,欢迎同时关注,及时获取最新文章。

1.1 环形缓冲区的实现思路

单片机程序开发一般都会用到UART串口通信,通过通信来实现上位机和单片机程序的数据交互。通信中为了实现正常的收发,一般都会有对应的发送和接收缓存来暂存通信数据。这里使用环形缓冲区的方式来设计数据收发的缓存,即缓冲区溢出后,从缓冲区数组的起始索引处重新进行数据的存储,这样可以比较高效地使用缓冲区。

环形缓冲区的设计思路参考以下链接。

https://www.cnblogs.com/zengzy/p/5139582.html

核心思路摘抄如下。规定以下所有方案,在缓冲区满时不可再写入数据,缓冲区空时不能读数据。

常规数组环形缓冲区思路:

设缓冲区大小为N,队头out,队尾in,out、in均是下标表示。

  • 初始时,in = out = 0

  • 队头队尾的更新用取模操作,out = (out + 1) % N,in = (in + 1) % N

  • out == in表示缓冲区空,(in + 1) % N == out表示缓冲区满

  • 入队que[in] = value; in = (in + 1) % N;

  • 出队ret = que[out]; out = (out + 1) % N;

  • 数据长度 len = (in - out + N) % N

改进版数组环形缓冲区思路:

同样假设缓冲区大小为N,队头out,队尾in,out、in为数组下标,但数据类型为unsigned int。

  • 初始时,in = out = 0

  • 上调缓冲区大小N为2的幂,假设为M

  • 队头队尾更新不再取模,直接++out,++in

  • out == in表示缓冲区空,(in - out) == M表示缓冲区满

  • 入队que[in & (M - 1)] = value; ++in;

  • 出队ret = que[out & (M - 1)] ; ++out;

  • in - out表示数据长度

1.2 环形缓冲区的代码实现

本文对应的工程代码链接如下。该工程基于eclipse IDE开发,编译器使用arm-none-eabi-gcc,使用的硬件是STM32F429I-DISCO开发板。

https://download.csdn.net/download/goodrenze/85163032

根据以上的环形缓冲区设计思路,先定义缓存对应的结构体类型如下。

typedef struct _UartBuf_t
{
#if UART_RECORD_LOST_NUM
    uint32_t TxLostNum;
#endif
    uint8_t* TxBuf;
#if UART_BUF_SIZE_IS_2POW
    uint16_t TxIn;
    uint16_t TxOut;
    uint16_t TxSize;
#else
    int16_t TxIn;
    int16_t TxOut;
    int16_t TxSize;
#endif
​
#if UART_RECORD_LOST_NUM
    uint32_t RxLostNum;
#endif
    uint8_t* RxBuf;
#if UART_BUF_SIZE_IS_2POW
    uint16_t RxIn;
    uint16_t RxOut;
    uint16_t RxSize;
#else
    int16_t RxIn;
    int16_t RxOut;
    int16_t RxSize;
#endif
}UartBuf_t;

以上结构体中,UART_RECORD_LOST_NUM宏定义用于设置是否记录丢失的数据个数,UART_BUF_SIZE_IS_2POW宏定义用于设置收发缓存的长度是否是2的幂,如果缓存长度是2的幂,则缓存索引和长度使用无符号数,否则使用有符号数。TxBuf和RxBuf指针用于指向对应的发送和接收的缓存数组。

本例程的UART串口发送和接收都是用串口中断来实现的,串口中断处理函数的代码实现如下,只需要在对应的串口中断入口函数中调用该函数进行串口数据的收发处理即可。

// 以下环形缓冲区的设计思路参考以下链接:
// https://www.cnblogs.com/zengzy/p/5139582.html
void UartIrqService(USART_TypeDef* UartX, UartBuf_t* Buf)
{
  uint32_t SR = UartX->SR;
  uint32_t CR1 = UartX->CR1;
  uint32_t CR3 = UartX->CR3;
​
  UNUSED(CR3);
​
  while(SR & USART_SR_RXNE)
  {
#if UART_RECORD_LOST_NUM
    if(SR & USART_SR_ORE)
    {
      Buf->RxLostNum++;
    }
#endif
​
#if UART_BUF_SIZE_IS_2POW
    if ((Buf->RxIn - Buf->RxOut) != Buf->RxSize)
    {
      Buf->RxBuf[Buf->RxIn & (Buf->RxSize - 1)] = (uint8_t)(UartX->DR & (uint8_t)0x00FF);
      Buf->RxIn++;
    }
#else
    if (((Buf->RxIn + 1) % Buf->RxSize) != Buf->RxOut)
    {
      Buf->RxBuf[Buf->RxIn++] = (uint8_t)(UartX->DR & (uint8_t)0x00FF);
      Buf->RxIn %= Buf->RxSize;
    }
#endif
    else
    {
      Buf->RxBuf[Buf->RxIn] = (uint8_t)(UartX->DR & (uint8_t)0x00FF);
#if UART_RECORD_LOST_NUM
      Buf->RxLostNum++;
#endif
    }
​
    SR = UartX->SR;
  }
​
  if((SR & USART_SR_TXE) && (CR1 & USART_CR1_TXEIE))
  {
    if(Buf->TxIn != Buf->TxOut)
    {
#if UART_BUF_SIZE_IS_2POW
      UartX->DR = (uint8_t)(Buf->TxBuf[Buf->TxOut & (Buf->TxSize - 1)] & (uint8_t)0x00FF);
      Buf->TxOut++;
#else
      UartX->DR = (uint8_t)(Buf->TxBuf[Buf->TxOut++] & (uint8_t)0x00FF);
      Buf->TxOut %= Buf->TxSize;
#endif
    }
    else
    {
      CLEAR_BIT(UartX->CR1, USART_CR1_TXEIE);
    }
  }
}

以上代码就是上面提到的环形缓冲区思路的具体实现。当缓冲区的数据长度是2的幂的时候,可以省去求余的运算,可以提高代码的执行速度。所以如果要求代码的执行时间尽量短,可以考虑将缓冲区的长度设置成2的幂。