嵌入式开发--STM32用DMA+IDLE中断方式串口接收不定长数据

回顾

之前讲过用 利用IDLE空闲中断来接收不定长数据 ,但是没有用到DMA,其实用DMA会更加的高效,MCU也可以腾出更多的性能去处理应该做的事情。

原理简介

IDLE顾名思义,就是空闲的意思,即当监测到串口空闲超过1个串口的数据帧时,会使状态寄存器(SR或ISR)的IDLE位置位,如果此时控制寄存器(CR或CR1)的IDLEIE为1,则会触发IDLE中断。

DMA搬运数据,则是一边接收数据,一边将串口接收到的数据搬运到内存中,这个过程不需要MCU参与,等到IDLE中断到来的时候,直接去内存中取数据即可。
DMA中断在CubeMX中是默认开启的,可以手动将其关闭,等IDLE中断到来的时候,直接操作读取数据即可。当DMA设置为NORMAL模式时,这个中断是完全用不到的,因为当IDLE到来时,置标志位,然后数据处理,此时需要重启DMA,而DMA的接收缓冲区又大于不定长的数据帧,这样DMA中断就永远不会触发了。

知道原理了,就好操作了。

CubeMX操作

开启调试接口
在这里插入图片描述

开启USART1异步方式,波特率、数据长度和校验方式根据需要设置
在这里插入图片描述
开启DMA,内存自增,循环方式
在这里插入图片描述
开启串口中断
在这里插入图片描述
设置好时钟
在这里插入图片描述
然后是 Generate Code生成代码

手动修改代码

中断函数中,置标志位,读取SR和DR寄存器,以清除IDLE中断标识,并获得本次接收的数据长度

//485发送数据
//由于485发送数据时,接收端会同步接收到发送的数据,这会造成数据解析的错乱。
//所在在485换向之前,先关闭DMA,等发送完成,再转换到输出状态以后,再开启DMA 
//也就是说,作为从机来说,不应当主动发起数据传输请求,否则当主机开始发送数据时,从机会丢失数据帧
void LL_myuart_send(u8 *pdata, u16 len)
{
    
    
  HAL_UART_DMAStop(&huart1);    //先关闭DMA
  
  RS485_OUT();
  HAL_Delay(1);
  HAL_UART_Transmit(&huart1, pdata, len, 1000);
  RS485_IN();
  HAL_Delay(1);

  rs485_receive_pos = 0;
  rs485_receive_len = 0;
  HAL_UART_Receive_DMA(&huart1,rs485_receive_data, RS485_BUF_LEN);  //等发送完成,再转换到输出状态以后,再开启DMA 
}
void USART1_IRQHandler(void)
{
    
    
  /* USER CODE BEGIN USART1_IRQn 0 */
  u8 temp;

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  //长时间未接收到数据时,会发生IDLE中断,此时意味着数据接收完成
  //不同的内核,清除IDLEIE的方式不同,请查阅手册
  if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
  {
    
    
    //读SR和DR寄存器,以清除IDLE和RXNE中断标志
    temp = huart1.Instance->SR;
    temp = huart1.Instance->DR;
    rs485_idle_flag = 1;
    
    rs485_receive_pos += rs485_receive_len;
    if(rs485_receive_pos >= RS485_BUF_LEN)  //如果起始位置超出缓冲区,则减去缓冲区长度,也就意味着数据从头开始循环记录
      rs485_receive_pos -= RS485_BUF_LEN;
    
    //DMA接收串口数据,
    //设置为NORMAL方式时,需要在每一次接收完成后,再次使能,否则DMA的接收缓冲区满了以后就不会再接收
    //设置为CIRCLAR方式时,不需要再次使能,但编程会麻烦些

    //如果长度大于缓冲区长度
    if(RS485_BUF_LEN < (__HAL_DMA_GET_COUNTER(&hdma_usart1_rx) + rs485_receive_pos))
      rs485_receive_len = RS485_BUF_LEN*2 - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx) - rs485_receive_pos; 
    else
      rs485_receive_len = RS485_BUF_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx) - rs485_receive_pos; 
    
  }
  /* USER CODE END USART1_IRQn 1 */
}

在主循环开始前进行初始设置
开启串口和DMA,使能IDLE中断

  HAL_UART_Receive_DMA(&huart1,rs485_receive_data,RS485_BUF_LEN);
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);	//启动IDLE中断

主循环内,清标志位,然后发送 receive OK,由于我用的是485总线,需要进行接收和发送方式的切换,故将整个发送过程做了封装,不再具体列出。

  while (1)
  {
    
    

    if(rs485_idle_flag != 0)  //接收到一帧数据
    {
    
    
      rs485_idle_flag = 0;
      //HAL_UART_Receive_DMA(&huart1,rs485_receive_data,RS485_BUF_LEN); //如果DMA采用NORMAL方式,需要激活本句,以使能DMA
      LL_myuart_send((u8*)"receive OK\r\n", 12); 
      rs485_idle_flag = 0;
    }

    /* USER CODE END WHILE */
  }

实测

在这里插入图片描述
在这里插入图片描述

下图为缓冲区写满时,从头开始循环写入的情况
在这里插入图片描述
在主程序中,从rs485_receive_pos开始,读取rs485_receive_len长度的数据即可,如果超出缓冲区,则减去缓冲区长度,从头开始读取,不再列出代码。

注意事项

1 DMA接收串口数据

设置为NORMAL方式时,需要在每一次接收完成后,再次使能,否则DMA只接收一次就不会再接收
设置为CIRCLAR方式时,不需要再次使能,但可能会由于各种延时导致数据来不及处理的问题

2 需要开启串口的总中断

在CubeMX中需要开启串口的总中断,否则会不进中断。

3 清中断标识

对于F1系列来说,清IDLE中断标志需要读取SR和DR寄存器,否则会一直进IDLE中断。
不同的内核,清IDLE标志的方法不同,这个需要查询芯片手册

4 非ST家的芯片,本代码不兼容

亲测航顺芯片就不兼容,需要改代码。

5 DMA操作

DMA的计数是连续的,每次接收都是在上次接收完成的位置,继续进行下一次接收,缓冲区装满时,继续从头开始接收,这给编程带来一些麻烦。

还有一种方式简单粗暴:首先接收缓冲区的长度 > 2倍的最大帧长度,然后在每次接收完成后,停止并重新开启DMA,这个过程可以放在主程序中,处理数据时进行。这样每次数据都是从缓冲区的0偏移开始接收数据,所以编程时缓冲区数据不会循环写入,编程上有便利,如果数据传输量不大,而且传输间隔也比较长,可以用这种方式。

猜你喜欢

转载自blog.csdn.net/13011803189/article/details/131694685