干货|高质量代码是怎么写出来的?串口环形队列

摘要:串口是通信中最常用的通信方式,可能写串口的驱动,能写几十种方法, 查询方式,中断方式,DMA方式,定时器方式。可能也其中几种方式的组合形式,经典的用法是:发送用查询方式,接收用中断方式,或者DMA+空闲中断。本篇不讲串口是啥,现在还在讲串口是啥,估计会被喷。今天来聊一聊串口常用的几种方式,最简单的方法就不说了。

一、经典方法

  1. 查询方式 可靠性很高,要考虑下个数据包覆盖上一个数据包的问题,小数据量,在10个字节以内,可以这样考虑, 很简单,很方便,很可靠。但是在数据量大的时候,程序阻塞的时间特别长,影响其他比较重要的外设的处理。

  2. 中断方式 中断方式 , 不占用系统资源,但是如果数据量大,会频繁中断cpu, 会其他高优先的数据处理造成影响。但是没有DMA不占用资源的好处, 如果没有串口队列的实现,必须通过标志位判断上一个包数据是否发送完成,在把新的数据覆盖到串口的缓冲区。

  3. DMA方式 优点:  不占用系统资源,减少CPU对中断的响应。如何不建立数据包的队列,还是会出现,需要等待阻塞的问题。

二、环形队列

队列这个词在数据局结构中出现的比较多,与之对应的就是堆栈,但是两者的读取方式又完全不同。

FIFO 是First-In First-Out的缩写,它是一个具有先入先出特点的缓冲区。串口设计FIFO的目的是为了提高串口的通讯性能。如果没有FIFO或者说缓冲区的长度只有1字节,那么使用接收中断,就意味着每次收到一个字节的数据就要进一次中断,这样频繁进中断会占用CPU资源。另外如果没有及时读走数据,那么下一个字节数据就会覆盖之前的数据,导致数据丢失,这在通讯速率高的场合很有可能出现

使用FIFO,可以在连续接收若干个数据后才产生一次中断,然后一起进行处理。这样可以提高接收效率,避免频繁进中断,适用于大数据传输。你可能会想到如果FIFO中的数据没有达到指定长度而无法产生中断怎么办,通常MCU会有接收超时中断,即在一定的时间内没有接收到数据会进入中断,可以利用这个中断把不足FIFO长度的数据最后都读取完。

FIFO类似售票排队窗口,先到的人看到能先买到票,然后先走,后来的人只能后买到票。

干货|高质量代码是怎么写出来的?串口环形队列的图1

在计算机中,每个信息都是存储在存储单元中的,当有大量数据的时候,我们不能存储所有的数据,那么计算机处理数据的时候,只能先处理先来的,那么处理完后呢,就会把数据释放掉,再处理下一个。那么,已经处理的数据的内存就会被浪费掉。因为后来的数据只能往后排队,如过要将剩余的数据都往前移动一次,那么效率就会低下了,肯定不现实,所以,环形队列就出现了。

点击下方视频动态演示出队入队干货|高质量代码是怎么写出来的?串口环形队列的图2

1、环形队列的实现

在计算机中,是没有环形的内存的,只不过是我们将顺序的内存处理过,让某一段内存形成环形,使他们首尾相连,简单来说,这其实就是一个数组,只不过有两个指针,一个指向列队头,一个指向列队尾。指向列队头的指针是缓冲区可读的数据,指向列队尾的指针是缓冲区可写的数据,通过移动这两个指针即可对缓冲区的数据进行读写操作了,直到缓冲区已满(头尾相接),将数据处理完,可以释放掉数据,又可以进行存储新的数据了。

实现的原理

视频来自正在一名考研的UP主:秃头少女王某。计算机专业考研这个是必考点,视频讲的很棒,祝她一战成硕,金榜题名!干货|高质量代码是怎么写出来的?串口环形队列的图3干货|高质量代码是怎么写出来的?串口环形队列的图4

串口环形缓冲区收发:在初学单片机的时候我们知道的串口收发都是:接收一个数据,触发中断,然后把数据发回来。这种处理方式是没有缓冲的,当数量太大的时候,亦或者当数据接收太快的时候,我们来不及处理已经收到的数据,那么,当再次收到数据的时候,就会将之前还未处理的数据覆盖掉。那么就会出现丢包的现象了,对我们的程序是一个致命的创伤。

那么如何避免这种情况的发生呢,很显然,上面说的一些队列的特性很容易帮我们实现我们需要的情况。将接受的数据缓存一下,让处理的速度有些许缓冲,使得处理的速度赶得上接收的速度,上面又已经分析了普通队列与环形队列的优劣了,那么我们肯定是用环形队列来进行实现了。下面就是代码的实现:

2、定义一个结构体

typedef struct
{

    uint16_t usWrite;
    uint16_t usRead;
    uint16_t usLenght;
    /* FIFO 结构 */
    uint8_t  ucRing_Buff[RINGBUFF_LEN];

}RingBuff_T;

extern RingBuff_T g_ringBuff;

3、初始化队列

初始化结构体相关信息:使得我们的环形缓冲区是头尾相连的,并且里面没有数据,也就是空的队列,所有元素清0。

void RingBuff_Init(void)
{
    g_ringBuff.usWrite = 0;
    g_ringBuff.usRead = 0;
    g_ringBuff.usLenght = 0;
}

4、数据压入队列

/**
* @brief  Write_RingBuff
* @param  uint8_t _ucWriteData
* @return 0:环形缓冲区已满,写入失败;1:写入成功
* @note   往环形缓冲区写入uint8_t类型的数据
*/

uint8_t Write_RingBuff(uint8_t _ucWriteData)
{
    if(g_ringBuff.usLenght >= RINGBUFF_LEN) /*判断缓冲区是否已满*/
    {
        return 0;
    } 
    g_ringBuff.ucRing_Buff[g_ringBuff.usRead] = _ucWriteData;
    g_ringBuff.usRead = (g_ringBuff.usRead + 1) % RINGBUFF_LEN; /*防止越界非法访问*/
    g_ringBuff.usLenght++;
    return 1;
}

5、从队列中读出数据

/**
* @brief  Read_RingBuff
* @param  u8 *rData,用于保存读取的数据
* @return 0:环形缓冲区没有数据,读取失败; 1:读取成功
* @note   从环形缓冲区读取一个uint8_t类型的数据
*/

uint8_t Read_RingBuff(uint8_t *_usReadData)
{
    if(g_ringBuff.usLenght == 0)/*判断非空*/
    {
        return 0;
    }
    *_usReadData = g_ringBuff.ucRing_Buff[g_ringBuff.usWrite];/*先进先出FIFO,从缓冲区头出*/
    g_ringBuff.usWrite = (g_ringBuff.usWrite + 1) % RINGBUFF_LEN; /*防止越界非法访问*/
    g_ringBuff.usLenght--;
    return 1;
}

对于读写操作需要注意的地方有两个:

1:判断队列是否为空或者满,如果空的话,是不允许读取数据的,返回0。如果是满的话,也是不允许写入数据的,避免将已有数据覆盖掉。那么如果处理的速度赶不上接收的速度,可以适当增大缓冲区的大小,用空间换取时间。2:防止指针越界非法访问,程序有说明,需要使用者对整个缓冲区的大小进行把握。

四、环形缓冲器

环形缓冲器(ringr buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),圆形缓冲区(circula buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。

圆形缓冲区的一个有用特性是:当一个数据元素被用掉后,其余数据元素不需要移动其存储位置。相反,一个非圆形缓冲区(例如一个普通的队列)在用掉一个数据元素后,其余数据元素需要向前搬移。换句话说,圆形缓冲区适合实现先进先出缓冲区,而非圆形缓冲区适合后进先出缓冲区。

那么如何将环形缓冲器ringr buffer应用到串口上面呢?这里我们使用RT-Thread的源码。

1、定义一个结构体

/* ring buffer */
struct rt_ringbuffer
{

    uint8_t *buffer_ptr;
    uint16_t read_mirror : 1;
    uint16_t read_index : 15;
    uint16_t write_mirror : 1;
    uint16_t write_index : 15;
    uint16_t buffer_size;
};

2、初始化ringbuffer

void rt_ringbuffer_init(struct rt_ringbuffer *rb,
                        uint8_t           *pool,
                        uint16_t            size)

{
    RT_ASSERT(rb != NULL);
    RT_ASSERT(size > 0);

    /* initialize read and write index */
    rb->read_mirror = rb->read_index = 0;
    rb->write_mirror = rb->write_index = 0;

    /* set buffer pool and size */
    rb->buffer_ptr = pool;
    rb->buffer_size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE);
}

3、将数据压入ringbuffer

unsigned long rt_ringbuffer_put(struct rt_ringbuffer *rb,
                            const uint8_t     *ptr,
                            uint16_t           length)

{
    uint16_t size;

    RT_ASSERT(rb != NULL);

    /* whether has enough space */
    size = rt_ringbuffer_space_len(rb);

    /* no space */
    if (size == 0)
        return 0;

    /* drop some data */
    if (size < length)
        length = size;

    if (rb->buffer_size - rb->write_index > length)
    {
        /* read_index - write_index = empty space */
        memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
        /* this should not cause overflow because there is enough space for
         * length of data in current mirror */

        rb->write_index += length;
        return length;
    }

    memcpy(&rb->buffer_ptr[rb->write_index],
           &ptr[0],
           rb->buffer_size - rb->write_index);
    memcpy(&rb->buffer_ptr[0],
           &ptr[rb->buffer_size - rb->write_index],
           length - (rb->buffer_size - rb->write_index));

    /* we are going into the other side of the mirror */
    rb->write_mirror = ~rb->write_mirror;
    rb->write_index = length - (rb->buffer_size - rb->write_index);

    return length;
}

4、从ringbuffer中读数据

unsigned long rt_ringbuffer_get(struct rt_ringbuffer *rb,
                            uint8_t           *ptr,
                            uint16_t           length)

{
    unsigned long size;

    RT_ASSERT(rb != NULL);

    /* whether has enough data  */
    size = rt_ringbuffer_data_len(rb);

    /* no data */
    if (size == 0)
        return 0;

    /* less data */
    if (size < length)
        length = size;

    if (rb->buffer_size - rb->read_index > length)
    {
        /* copy all of data */
        memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
        /* this should not cause overflow because there is enough space for
         * length of data in current mirror */

        rb->read_index += length;
        return length;
    }

    memcpy(&ptr[0],
           &rb->buffer_ptr[rb->read_index],
           rb->buffer_size - rb->read_index);
    memcpy(&ptr[rb->buffer_size - rb->read_index],
           &rb->buffer_ptr[0],
           length - (rb->buffer_size - rb->read_index));

    /* we are going into the other side of the mirror */
    rb->read_mirror = ~rb->read_mirror;
    rb->read_index = length - (rb->buffer_size - rb->read_index);

    return length;
}

5、移植ringbuffer

1.首先将RT-Thread的ringbuffer.c和ringbuffer.h文件加入到我们的MDK工程中去。

干货|高质量代码是怎么写出来的?串口环形队列的图5
干货|高质量代码是怎么写出来的?串口环形队列的图6

2.编写串口相关的底层硬件bsp代码,也就是初始化GPIO和串口相关的配置,这个就很简单,大家应该都会。在串口初始化代码中记得要手动将串口的非空中断和空闲中断打开。

UART_HandleTypeDef huart1;//这个要定义为全局的变量
void uart_init(u32 bound)
{
 huart1.Instance=USART1;         //USART1
 huart1.Init.BaudRate=bound;        //波特率
 huart1.Init.WordLength=UART_WORDLENGTH_8B;  //字长为8位数据格式
 huart1.Init.StopBits=UART_STOPBITS_1;     //一个停止位
 huart1.Init.Parity=UART_PARITY_NONE;   //无奇偶校验位
 huart1.Init.HwFlowCtl=UART_HWCONTROL_NONE;   //无硬件流控
 huart1.Init.Mode=UART_MODE_TX_RX;      //收发模式
 HAL_UART_Init(&huart1);//HAL_UART_Init()会使能UART1  
 
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE);/*使能串口接收非空中断 */ 
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE); /*使能串口接收空闲中断 */
 
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    //GPIO端口设置
 GPIO_InitTypeDef GPIO_InitStruct;
 
 if(huart->Instance==USART1)//如果是串口1,进行串口1 MSP初始化
 {
  __HAL_RCC_USART1_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  GPIO_InitStruct.Pin = GPIO_PIN_9;
  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = GPIO_PIN_10;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        //使用中断肯定要配置NVIC
  HAL_NVIC_SetPriority(USART1_IRQn, 10);
  HAL_NVIC_EnableIRQ(USART1_IRQn);

 }
}

3.定义一个结构头rt_ringbuffer 类型的变量ring_buf,变量名随便取,阿猫阿狗都可以,只要你自己认得就行。

struct rt_ringbuffer ring_buf; 

4.定义一个串口接收缓冲区数组,数组名随便取,阿猫阿狗都可以,只要你自己认得就行。

static uint8_t  s_USART1_RxBuf[256];

5.初始化ringbuffer

rt_ringbuffer_init(&ring_buf,s_USART1_RxBuf,256);

位置放在哪里都可以,我这里就放在串口串口初始化之前了。

干货|高质量代码是怎么写出来的?串口环形队列的图7

6.编写中断服务函数

void USART1_IRQHandler(void)                 

 uint8_t receive_char;
 
 if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)) //接收非空中断
    {
  HAL_UART_Receive(&huart1, &receive_char, 11000);//将收到的数据放入receive_char  
  rt_ringbuffer_put(&ring_buf,&receive_char,1);//将receive_char压入到ring_buf中
  USART1_Len++;  
  __HAL_USART_CLEAR_FLAG(&huart1,UART_FLAG_RXNE);//清除接收非空中断
 }
  
 if((__HAL_USART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)) //接收空闲中断
    {
 
  rt_ringbuffer_get(&ring_buf,s_USART1_RxBuf, USART1_Len);//将rring_buf的数据读出到到s_USART1_RxBuf中
  s_USART1_RxBuf[USART1_Len] = '\0';
  printf("%s\r\n",s_USART1_RxBuf);
  USART1_Len = 0 ;//数据帧长度复位
  __HAL_USART_CLEAR_IDLEFLAG(&huart1);
 }
}

这里面的代码我写的应该很简单了,首先我们在初始化中是能了接收中断和空闲中断,那么如果有数据过来,就会触发中断。进入中断服务函数,进来之后首先判断接收中断标志位是否置位为1,如果是1说明收数据来了,通过HAL_UART_Receive函数将数据存入receive_char中,再通过rt_ringbuffer_put将数据压入队列之中。如果数据接收完了就会触发空闲中断,这时通过rt_ringbuffer_get函数将数据读出到我们定义的数组中打印出来。

7.主函数

int main(void)

 HAL_Init(); 
 Stm32_Clock_Init();
 delay_init(72);
 uart_init(115200);  
 while(1)
 { 
 }
}

主函数啥都不要写,完了。

干货|高质量代码是怎么写出来的?串口环形队列的图8

五、队列FIFO

队列 (Queue):是一种先进先出(First In First Out ,简称 FIFO)的线性表,只允许在一端插入,在另一端进行删除。

FIFO一般用于不同时钟域之间的数据传输,比如FIFO的一端是AD数据采集,另一端是计算机的PCI总线,假设其AD采集的速率为16位 100K SPS,那么每秒的数据量为100K×16bit=1.6Mbps,而PCI总线的速度为33MHz,总线宽度32bit,其最大传输速率为1056Mbps,在两个不同的时钟域间就可以采用FIFO来作为数据缓冲。另外对于不同宽度的数据接口也可以用FIFO,例如单片机位8位数据输出,而DSP可能是16位数据输入,在单片机与DSP连接时就可以使用FIFO来达到数据匹配的目的。

1、定义一个结构体

/* 串口设备结构体 */
typedef struct
{

 USART_TypeDef *uart;  /* STM32内部串口设备指针 */
 uint8_t *pTxBuf;   /* 发送缓冲区 */
 uint8_t *pRxBuf;   /* 接收缓冲区 */
 
 uint16_t usTxBufSize;  /* 发送缓冲区大小 */
 uint16_t usRxBufSize;  /* 接收缓冲区大小 */
 
 __IO uint16_t usTxWrite; /* 发送缓冲区写指针 */
 __IO uint16_t usTxRead;  /* 发送缓冲区读指针 */
 __IO uint16_t usTxCount; /* 等待发送的数据个数 */

 __IO uint16_t usRxWrite; /* 接收缓冲区写指针 */
 __IO uint16_t usRxRead;  /* 接收缓冲区读指针 */
 __IO uint16_t usRxCount; /* 还未读取的新数据个数 */

}UART_T;

2、初始化FIFO

static void Uart_FIFO_Init(void)
{
 g_tUart1.uart = USART1;      /* STM32 串口设备 */
 g_tUart1.pTxBuf = g_TxBuf1;     /* 发送缓冲区指针 */
 g_tUart1.pRxBuf = g_RxBuf1;     /* 接收缓冲区指针 */
 g_tUart1.usTxBufSize = 1024;       /* 发送缓冲区大小 */
 g_tUart1.usRxBufSize = 1024;    /* 接收缓冲区大小 */
 g_tUart1.usTxWrite = 0;      /* 发送FIFO写索引 */
 g_tUart1.usTxRead = 0;      /* 发送FIFO读索引 */
 g_tUart1.usRxWrite = 0;      /* 接收FIFO写索引 */
 g_tUart1.usRxRead = 0;      /* 接收FIFO读索引 */
 g_tUart1.usRxCount = 0;      /* 接收到的新数据个数*/
 g_tUart1.usTxCount = 0;      /* 待发送的数据个数 */
}

3、初始化串口

UART_HandleTypeDef UartHandle; 
static void Uart_GPIO_Config(void)
{
 GPIO_InitTypeDef  GPIO_InitStruct; 

 /* 使能 GPIO TX/RX 时钟 */
 USART1_TX_GPIO_CLK_ENABLE();
 USART1_RX_GPIO_CLK_ENABLE();
 
 /* 使能 USARTx 时钟 */
 USART1_CLK_ENABLE(); 

 /* 配置TX引脚 */
 GPIO_InitStruct.Pin       = USART1_TX_PIN;
 GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
 GPIO_InitStruct.Pull      = GPIO_PULLUP;
 GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
 HAL_GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStruct); 
 
 /* 配置RX引脚 */
 GPIO_InitStruct.Pin = USART1_RX_PIN;
 GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
 GPIO_InitStruct.Pull = GPIO_NOPULL;
 HAL_GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStruct);  

 UartHandle.Instance        = USART1;
 UartHandle.Init.BaudRate   = 115200;
 UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
 UartHandle.Init.StopBits   = UART_STOPBITS_1;
 UartHandle.Init.Parity     = UART_PARITY_NONE;
 UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
 UartHandle.Init.Mode       = UART_MODE_TX_RX; 
 HAL_UART_Init(&UartHandle); 
    
 __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_TC);  /* 清除TC发送完成标志 */
 __HAL_USART_CLEAR_FLAG(&UartHandle, USART_FLAG_RXNE);/* 清除RXNE接收标志 */
 __HAL_USART_ENABLE_IT(&UartHandle, USART_IT_RXNE);   /* 使能接收数据寄存器非空中断 */ 
 
 /* 配置NVIC the NVIC for UART */   
 HAL_NVIC_SetPriority(USART1_IRQn, 01);
 HAL_NVIC_EnableIRQ(USART1_IRQn); 
}

4、将数据压入FIFO

什么时候要将数据压入FIFO?当然是在向串口发送数据的时候将数据压入FIFO去啦,这里面的逻辑也很简单就是将要发送的数据压入FIFO缓冲区,之后打开发送中断就可以了。

void UartSendBuf(uint8_t *_ucaBuf, uint16_t _usLen)
{
 uint16_t i;

 for (i = 0; i < _usLen; i++)
 {
  /* 如果发送缓冲区已经满了,则等待缓冲区空 */
  while (1)
  {
   __IO uint16_t usCount;

   usCount = g_tUart1.usTxCount;

   if (usCount < g_tUart1.usTxBufSize)
   {
    break;/*如果发送缓冲区没有满就跳出去,让发送缓冲区填满*/
   }
   else if(usCount == g_tUart1.usTxBufSize)/* 数据已填满缓冲区 */
   {
    if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)== RESET)
    {
     __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE);  /* 使能发送中断(缓冲区空) */
    }  
   }
  }
  /* 将新数据填入发送缓冲区 */
  g_tUart1.pTxBuf[g_tUart1.usTxWrite] = _ucaBuf[i];
  DISABLE_INT();
  if (++g_tUart1.usTxWrite >= g_tUart1.usTxBufSize)
  {
   g_tUart1.usTxWrite = 0;
  }
  g_tUart1.usTxCount++;
  ENABLE_INT();
 }
 __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TXE); /* 使能发送中断(缓冲区空) */
}

如果要发送的数据没有超过发送缓冲区大小,实现起来还比较容易,直接把数据填到 FIFO 里面,并使能发送空中断即可。如果超过了 FIFO 大小,就需要等待有空间可用,针对这种情况有个重要的知识点,就是当缓冲刚刚填满的时候要判断发送空中断是否开启了,如果填满了还没有开启,就会卡死在 while 循环中,所以多了一个刚填满时的判断,填满了还没有开启发送空中断,要开启下。

5、从FIFO中读出数据

什么时候要从FIFO中读出?当然是在从串口获取数据的时候将从FIFO中读数据啦。

uint8_t UartGetChar(uint8_t *_pByte)

 uint16_t usCount;
 /* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */
 DISABLE_INT();
 usCount = g_tUart1.usRxCount;
 ENABLE_INT();

 /* 如果读和写索引相同,则返回0 */
 if (usCount == 0/* 已经没有数据 */
 {
  return 0;
 }
 else
 {
  *_pByte = g_tUart1.pRxBuf[g_tUart1.usRxRead];/* 从串口接收FIFO取1个数据 */

  /* 改写FIFO读索引 */
  DISABLE_INT();
  if (++g_tUart1.usRxRead >=g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxRead = 0;
  }
  g_tUart1.usRxCount--;
  ENABLE_INT();
  return 1;
 }
}

6、中断服务函数

void USART1_IRQHandler(void)

 
 /* 处理接收缓冲区中断  */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_RXNE)!= RESET)
 {
  /* 从串口接收数据寄存器读取数据存放到接收FIFO */
  uint8_t ch;

  ch = READ_REG(g_tUart1.uart->DR);
  g_tUart1.pRxBuf[g_tUart1.usRxWrite] = ch;
  if (++g_tUart1.usRxWrite >= g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxWrite = 0;
  }
  if (g_tUart1.usRxCount < g_tUart1.usRxBufSize)
  {
   g_tUart1.usRxCount++;
  }
 }

 /* 处理发送缓冲区空中断 */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TXE)!= RESET)
 {

  if (g_tUart1.usTxCount == 0)
  {
   /* 发送缓冲区的数据已取完时, 禁止发送缓冲区空中断 (注意:此时最后1个数据还未真正发送完毕)*/
   CLEAR_BIT(g_tUart1.uart->CR1, USART_CR1_TXEIE);

   /* 使能数据发送完毕中断 */
   __HAL_UART_ENABLE_IT(&UartHandle,UART_IT_TC);/* 使能数据发送完毕中断 */
   
  }
  else
  {   
   /* 从发送FIFO取1个字节写入串口发送数据寄存器 */
   g_tUart1.uart->DR = g_tUart1.pTxBuf[g_tUart1.usTxRead];
   if (++g_tUart1.usTxRead >= g_tUart1.usTxBufSize)
   {
    g_tUart1.usTxRead = 0;
   }
   g_tUart1.usTxCount--;
  }

 }
 /* 数据bit位全部发送完毕的中断 */
 if(__HAL_UART_GET_FLAG(&UartHandle,UART_FLAG_TC)!= RESET)
 {
  if (g_tUart1.usTxCount == 0)
  {
   /* 如果发送FIFO的数据全部发送完毕,禁止数据发送完毕中断 */
   __HAL_USART_CLEAR_FLAG(&UartHandle,USART_FLAG_TC);/*发送完毕中断*/
  }
}

7、主函数

const char buf1[] = "接收到串口命令1\r\n";
const char buf2[] = "接收到串口命令2\r\n";
const char buf3[] = "接收到串口命令3\r\n";
const char buf4[] = "接收到串口命令4\r\n";

int main(void)
{
    uint8_t  read[100] = {0} ; 
    bsp_Init();    

    while(1)
    {
        /* 接收到的串口命令处理 */
        if (UartGetChar(read)!=0)
        {
   UartSendBuf((uint8_t *)read, sizeof(read)/sizeof(uint8_t));
            switch (read[0])
            {
                case '1':
                    UartSendBuf((uint8_t *)buf1, strlen(buf1));
                    break;

                case '2':
                    UartSendBuf((uint8_t *)buf2, strlen(buf2));
                    break;

                case '3':
                    UartSendBuf((uint8_t *)buf3, strlen(buf3));
                    break;

                case '4':
                    UartSendBuf((uint8_t *)buf4, strlen(buf4));
                    break;

                default:
                    break;
            }
        }
    }
}
干货|高质量代码是怎么写出来的?串口环形队列的图9

8、关于串口扫盲

串口扫盲就是几个状态标志位不好理解,只要理解好这张图就好办了,其他的请参考相关的教程。

干货|高质量代码是怎么写出来的?串口环形队列的图10
干货|高质量代码是怎么写出来的?串口环形队列的图11
  1. USART_FLAG_TXE当发送数据寄存器里的数据被全部取完时,该寄存器是空的,那么该标志位就会被置1。通过这个标志位的值可以判断发送数据寄存器中的数据有没有完全被取走,当该寄存器是空的时候,可以提醒CPU继续往该寄存器里存入新的数据;

  2. USART_FLAG_TC当发送移位寄存器里的每个字节通过TX脚一位一位发送出去之后,该标志位值就会被置1。通过这个标志位的值可以判断发送移位寄存器里的数据有没有被全部发送出去;

  3. USART_FLAG_TXEUSART_FLAG_TC之间的联系 结合上面流程图来进行说明,实际上发送移位寄存器通过TX脚发送数据这个过程是比较耗时的,所以在此过程进行时,可通过判断当USART_FLAG_TXE = 1,即发送数据寄存器里的数据已被全部转入发送移位寄存器时,就让CPU往发送数据寄存器转入新的数据。当发送移位寄存器把数据帧全部发送出去之后,可通过判断USART_FLAG_TC = 1,证明数据帧的最后一个字节都已经通过TX脚发送完了。

串口通信

干货|高质量代码是怎么写出来的?串口环形队列的评论0条

    暂无评论

    干货|高质量代码是怎么写出来的?串口环形队列的相关案例教程

    摘要:串口是通信中最常用的通信方式,可能写串口的驱动,能写几十种方法, 查询方式,中断方式,DMA方式,定时器方式。可能也其中几种方式的组合形式,经典的用法是:发送用查询方式,接收用中断方式,或者DMA+空闲中断。本篇不讲串口是啥,现在还在讲串口是啥,估计会被喷。今天来聊一聊串口常用的几种方式,最简单的方法就不说了。 一、经典方法 查询方式 可靠性很高,要考虑下个数据包覆盖上一个数据包的问题,小数
    ST 为开发者提供了非常方便的开发库:有标准外设库(SPL库)、HAL 库(Hardware Abstraction Layer,硬件抽象层库)、LL 库(Low-Layer,底层库)三种。前者是ST的老库已经停更了,后两者是ST现在主推的开发库。 相比标准外设库,STM32Cube HAL库表现出更高的抽象整合水平,HAL API集中关注各外设的公共函数功能,这样便于定义一套通用的用户友好的AP
    今年实验室来了三个学妹,其中一个学妹以前是物联网专业的,进了实验室老师二话没说:先把STM32单片机过一遍,有啥问题就找小师弟。还好单片机小师弟会玩一点点,玩的也不好,所以一起学习吧! 上来第一个例程就是使用按键点亮一个LED灯,好家伙。点灯小师弟比较在行,毕竟32、FPGA、Linux的小灯都被小师弟点了一遍。哈哈哈!所以今天还是来说一说按键检测吧! 一、如何进行按键检测 检测按键有中断方式和G
    一、什么是串口通讯? 串行通讯是指仅用一根接收线和一根发送线就能将数据以位进行传输的一种通讯方式。尽管串行通讯的比按字节传输的并行通信慢,但是串口可以在仅仅使用两根线的情况下就能实现数据的传输。 典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进
    嵌入式开发中,UART串口是最常见的一种通信接口,你知道为啥串口这么常见吗?本文就带你深入了解串口最底层的本质内容。 一、什么是串口通讯? 串行通讯是指仅用一根接收线和一根发送线就能将数据以位进行传输的一种通讯方式。尽管串行通讯的比按字节传输的并行通信慢,但是串口可以在仅仅使用两根线的情况下就能实现数据的传输。 典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能
    影响力
    粉丝
    内容
    获赞
    收藏
      项目客服
      培训客服
      0 0