新闻  |   论坛  |   博客  |   在线研讨会
附源码-终极串口接收(二)
鱼鹰谈单片机 | 2021-03-19 19:54:04    阅读:2629   发布文章

前段时间需要写个串口接收程序,一时没找到源码,就想着自己写过一篇文章《终极串口接收方式,极致效率》,看看能不能复制个代码,谁知道原理写的还算清楚,但真要直接复制粘贴使用还是有点麻烦,作为 CV 工程师,这怎么可以,所以才有了今天的后续。

在那篇文章之前,鱼鹰还写过一篇串口相关的万字长文《如何写一个健壮且高效的串口接收程序?》,这篇文章也是介绍了很多设计细节问题,值得一读,但经过又一年的底层开发,鱼鹰又有了新的思考。

所以后续,鱼鹰除了会再写一篇串口DMA发送、接收的程序框架,还会再写一篇可重入 printf DMA打印函数设计方法与源码分享、串口数据解析源码等相关的文章。

因为公众号近期改变了推送规则,如果想及时接收本公众号的文章,可以在阅读后,点个赞或在看,也可直接星标本公众号,这样每次推送的文章可以第一时间出现在您的订阅列表里面。

这份代码主要内容:USART1  +  DMA  +  IDLE 中断 +无锁队列。

开发环境:C99、KEIL、STM32F103

主函数:

int main(void)
{         
    USART1_Init(115200);  // 开始 DMA 接收数据
    fifo_init(&fifo_usart_rx_1, usart_buff_rx, sizeof(usart_buff_rx));
    while(1)
    {
        uint8_t length = fifo_read_buff(&fifo_usart_rx_1, buff_read, sizeof(buff_read));// 每次最大取 20 字节数据
        if(length) 
        {
//            printf("lengtt = %d", length); // 实际接收的数据长度
        }
        else
        {
//            printf("no data rx");// 没有数据
        }
        if(fifo_usart_rx_1.error)
        {
//            printf("fifo error %d", fifo_usart_rx_1.error);// 接收错误
            fifo_usart_rx_1.error = 0;            
        }
    }
}
中断处理函数:
void USART1_IRQHandler(void)     //串口1中断服务程序
{
  pfifo_rx_def             pfifo  = &fifo_usart_rx_1;
  USART_TypeDef           *uartx  = USART1;
  DMA_Channel_TypeDef     *dma_ch = DMA1_Channel5;
  if((uartx->SR & USART_FLAG_IDLE) != RESET) 
  {
    (void)uartx->DR; // 清除空闲中断
    if(pfifo != 0)
    {
      uint16_t curr_counter; 
      curr_counter     = dma_ch->CNDTR; // 获取当前接收索引
      pfifo->in       += ((pfifo->last_cnt - curr_counter) & (pfifo->size - 1)); 
      pfifo->last_cnt  = curr_counter; 
      if((pfifo->in - pfifo->out) > pfifo->size)
      {
        pfifo->out = pfifo->in;  // 清空缓存,注意赋值顺序,pfifo->in = pfifo->out 是错误的 
        pfifo->error |= FIFO_DMA_ERROR_RX_FULL;  
      }
    }
    else
    {
      pfifo->error |= FIFO_DMA_ERROR_RX_POINT_NULL;
    }
  }
  else
  {
    pfifo->error |= FIFO_DMA_ERROR_RX_NOT_IDLE;
  }
}

无锁队列内容:

#include "string.h"
typedef struct
{
    uint8_t *buffer;         
    uint32_t in;          
    uint32_t out;        
    uint16_t size;  
    uint16_t error; // 接收错误  
    uint16_t last_cnt;  
}fifo_rx_def;
typedef fifo_rx_def *pfifo_rx_def;
#define IS_POWER_OF_2(x) ((x) != 0 && (((x) & ((x) - 1)) == 0))
#define FIFO_DMA_ERROR_RX_NOT_IDLE              (0x1 << 0)    // 非空闲中断
#define FIFO_DMA_ERROR_RX_POINT_NULL            (0x1 << 1)    // 指针为空
#define FIFO_DMA_ERROR_RX_FULL                  (0x1 << 2)    // 非空闲中断
// 不建议使用宏,除非确定没有使用隐患
uint32_t min(uint32_t X, uint32_t Y)
{
    return ((X) > (Y) ? (Y) : (X));
} 
fifo_rx_def fifo_usart_rx_1;
int32_t fifo_init(pfifo_rx_def pfifo, uint8_t *buff, uint32_t size)
{
    assert_param(pfifo != NULL || buff != NULL);
    if(!IS_POWER_OF_2(size))   // 必须 2 的幂次方
    {
        return -1;
    }
    pfifo->in       = 0;
    pfifo->out      = 0;
    pfifo->buffer   = buff;
    pfifo->size     = size; // 必须最后设置大小
    pfifo->last_cnt = size;
    return 0;
}
uint32_t fifo_read_buff(pfifo_rx_def pfifo, uint8_t* buffer, uint32_t len)
{
    uint32_t length;     
    assert_param(pfifo != NULL || pfifo->buffer != NULL || buffer != NULL);
    len = min(len, pfifo->in - pfifo->out);     
    /* first get the data from pfifo->out until the end of the buffer */     
    length = min(len, pfifo->size - (pfifo->out & (pfifo->size - 1)));     
    memcpy(buffer, pfifo->buffer + (pfifo->out & (pfifo->size - 1)), length);     
    /* then get the rest (if any) from the beginning of the buffer */     
    memcpy(buffer + length, pfifo->buffer, len - length);     
    pfifo->out += len;  
    return len;   
}
串口、DMA、中断初始化:
#define USART_BUFF_SIZE_1   128
static uint8_t usart_buff_rx[USART_BUFF_SIZE_1];
// 输入参数:波特率 比如 115200
void USART1_Init(u32 bound)
{
  //GPIO端口设置
  GPIO_InitTypeDef  GPIO_InitStructure;
  USART_InitTypeDef USART_InitStructure;
  NVIC_InitTypeDef  NVIC_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
  //USART1_TX   PA.9
  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  //USART1_RX    PA.10
  GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_10;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOA, &GPIO_InitStructure);  
  //Usart1 NVIC 配置
  NVIC_InitStructure.NVIC_IRQChannel                      = USART1_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority    = 3 ;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority           = 3;    
  NVIC_InitStructure.NVIC_IRQChannelCmd                   = ENABLE;      //IRQ通道使能
  NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器USART1
  //USART 初始化设置  
  USART_InitStructure.USART_BaudRate              = bound; 
  USART_InitStructure.USART_WordLength            = USART_WordLength_8b;
  USART_InitStructure.USART_StopBits              = USART_StopBits_1;
  USART_InitStructure.USART_Parity                = USART_Parity_No;
  USART_InitStructure.USART_HardwareFlowControl   = USART_HardwareFlowControl_None;
  USART_InitStructure.USART_Mode                  = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USART1, &USART_InitStructure);  
  USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启空闲中断
  DMA_InitTypeDef DMA_InitStructure;
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // 使能DMA传输
  DMA_DeInit(DMA1_Channel5);                     // 将DMA的通道1寄存器重设为缺省值
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;          // DMA 外设C基地址
  DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)usart_buff_rx;        // DMA 内存基地址
  DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;          // 外设作为数据传输的目的地
  DMA_InitStructure.DMA_BufferSize         = sizeof(usart_buff_rx);          // DMA通道的DMA缓存的大小
  DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;      // 外设地址寄存器不变
  DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;           // 内存地址寄存器递增
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    // 数据宽度为8位
  DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;        // 数据宽度为8位
  DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;              // 工作在循环缓存模式
  DMA_InitStructure.DMA_Priority           = DMA_Priority_Medium;            // DMA通道 x拥有中优先级 
  DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;                // DMA通道x没有设置为内存到内存传输
  DMA_Init(DMA1_Channel5, &DMA_InitStructure);  //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
  DMA_Cmd(DMA1_Channel5, ENABLE); //使能DMA
  USART_DMACmd(USART1, USART_DMAReq_Rx,ENABLE); //使能 USART1 接收请求
  USART_Cmd(USART1, ENABLE);                    //使能串口 
}

以上就是终极串口接收方式的具体实现,如果对无所队列不是很熟悉,建议看鱼鹰的《【深度长文】还是没忍住,聊聊神奇的无锁队列吧!》,目前因为只涉及到接收,所以没有其他源码提供,免得分心。

在这里再唠嗑几句:

1、串口初始化函数一旦执行完成,串口就开始使用 DMA接收数据,空闲中断产生时,用户才能在后续得到 DMA缓存接收的数据。

2、因为 DMA数据的更新由串口空闲中断决定,所以一旦一帧数据很长(在这里为大于 128,或者一帧数据大于剩余缓存空间),那么程序会发现这个错误,并设置标志位(有些错误可能无法发现),所以这里的缓存大小设置比较关键,必须大于一帧缓存,最好两帧以上,并且是 2 的幂次方。

3、如果内存有限制,无法开更大缓存,那么可以开启 DMA的半传输中断,这样也可以及时取走 DMA缓存的数据(或者使用定时更新的方式)。

4、用户缓存 buff_read 可以随意设置,没有限制,但为了节省内存,一般小于等于 DMA 的接收缓存 usart_buff_rx。另外在该例子中,buff_read 并没有清除数据,可以按需清除。

5、fifo_read_buff 返回值为实际接收到的数据长度,如果等于 0,代表没有接收到任何数据,并且读取完之后,会自动清除 DMA缓存的数据,用户不需要清除它(实际上,缓存的数据还在,只是用户读取不了,并最终会被后面接收的数据所覆盖)

6、串口中断一般可以设置为最低优先级,因为是 DMA后台自动接收的,所以中断优先级最低并不会丢失数据(前提是缓存足够大)。

7、如果使用串口不为空(USART_IT_RXNE)中断,一般接收会出现 ORE错误,此时如果不清除该错误会导致死机现象,但一般 DMA总是能及时接收数据,应该不会产生该错误,但为了发现这种情况,鱼鹰也设置了错误标志。至于为什么要做这些检查,请看鱼鹰的笔记《许久以后,你会感谢自己写的异常处理代码》

总结一下,终极串口接收的关键就是 DMA循环接收,和接收索引的更新。

1.png2.png

其他的和网络上的 DMA 串口接收没什么区别。

当然,还有最重要的一点,你看这些代码会比较舒服,最后皮一下,哈哈。

感谢道友看到最后,咱们下期再见。

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客