STM32
直播中

江根磊

7年用户 1017经验值
私信 关注
[问答]

DMA串口空闲中断的实现流程是怎样的

串口空闲中断的实现流程是怎样的?
如何对STM32CubeMX进行配置呢?

回帖(1)

汤宇

2021-11-16 15:00:17
  STM32CubeMX 配置实现参考这里。
  1. 串口空闲中断
  1.1 UART_DMA方式接收数据
  STM32串口使用DMA方式接收数据可以减小CPU的开销。
  对于接收定长数据,可以将DMA接收缓冲区的长度设定为待接收数据的长度,这样利用DMA的传输完成中断DMAx_IT_TCy就可以知道已经接收了一帧数据。
  对于接收不定长数据,如何知道意见完成了数据的接收呢?
  1.2不定长数据接收的原理及其解决的问题
  在 STM32 中,UART是最为常见的通信方式——它每次接收一个字节,我们可以使用轮询的方式,但是对于某些数据不固定时间发送的数据,轮询的方式有时候不够灵活。也可以使用中断的方式,如每一个字节都中断一次,比较消耗系统资源。特别是HAL库中,从中断到回调函数运行了不少的程序,频繁的中断很可能造成数据溢出。
  为了避免这个问题,我们使用指定接收一定长度的数据,再调用回调函数,这会让我们可以接收大数据,但是这种情况则造成了,要求每次的包是固定长度。
  为了解决以上一些问题,网上最常用的办法是使用空闲中断,即在串口空闲的时候,触发一次中断,通知内核,本次运输完成了。串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发送,则认为串口空闲了。由于我们的内核在串口接收数据到空闲这段时间,是不受理串口数据的,所以我们还需要使用DMA来协助我们把数据传送到指定的地方,当数据传输完成后,通知内核去处理。
  2. 实现流程
  开启串口空闲中断: 在程序初始化时候,使能串口中断
  定义串口空闲中断处理函数: 在串口中断中添加串口空闲中断处理函数
  定义串口空闲中断回调函数: 用以标记数据接收完成,计算接收到数据的长度
  2.1 开启串口空闲中断
  基本与使用串口的时候一致,只不过一般我们是打开接收缓冲区非空中断,而现在是打开空闲中断
  首先,我们在初始化的时候,使能串口空闲中断,让串口在中断的时候,MCU可以调用串口中断函数。
  在 main.c 文件中的 MX_USART1_UART_Init(void) 函数中:
  static void MX_USART1_UART_Init(void)
  {
  // 省略了串口协议初始化部分
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能串口空闲中断
  }
  当添加这个函数到工程以后,每发送一次数据,都会调用一次USART1_IRQHandler() 中断服务函数,你可以在该函数中插入打印语句,来验证是否空闲中断正常。
  2.2 配置 DMA 接收
  把DMA配置完成,就可以直接打开DMA了,让它处于工作状态,当有数据的时候就能直接搬运了。DMA 初始化配置后面会有讲解。
  虽然我们使用的CubeMx来配置DMA,当然只是配置DMA模式为串口到内存(DMA初始化),但还需要在程序中进一步制定,DMA具体搬运到哪一个内存中,我们建立一个数组用以存放DMA搬运的串口数据,并使用HAL_UART_Receive_DMA()函数来配置,具体代码如下所示:
  。.. 。..
  uint8_t receive_buff[255]; //定义接收数组
  。.. 。..
  void main(void)
  {
  。. 。..
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
  HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, 255); //设置DMA传输,将串口1的数据搬运到recvive_buff中,每次255个字节
  while(1)
  {
  。. 。.
  }
  }
  。.. 。..
  2.3 添加中断处理函数和回调函数
  CubeMx 按上述操作后,生成的工程中,已经有了串口中断的处理函数 HAL_UART_IRQHandler(),但是其里面未发现该函数中对空闲中断的处理,所以我们额外添加一个函数: USER_UART_IRQHandler()(用户封装函数),添加后完整代码如下:
  stm32f0xx_it.c :
  void USART1_IRQHandler(void)
  {
  /* USER CODE BEGIN USART1_IRQn 0 */
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  // 新添加的函数,用来处理串口空闲中断
  USER_UART_IRQHandler(&huart1);
  /* USER CODE END USART1_IRQn 1 */
  }
  对 USER_UART_IRQHandler() 进行定义:
  usart.c:
  void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
  { // 判断是否是串口1
  if(USART1 == huart1.Instance)
  { // 判断是否是空闲中断
  if(RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
  { // 清除空闲中断标志(否则会一直不断进入中断)
  __HAL_UART_CLEAR_IDLEFLAG(&huart1);
  printf(“rnUART1 Idle IQR Detectedrn”);
  // 调用中断处理函数
  USAR_UART_IDLECallback(huart);
  }
  }
  }
  至此,我们已经可以正常的响应串口中断,并调用了一个新的函数:USAR_UART_IDLECallback(),它是专门用来处理空闲中断的一个回调函数,其定义如下(通样写在 usart.c 文件即可):
  // 声明外部变量
  extern uint8_t receive_buff[255];
  void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
  {
  // 停止本次DMA传输
  HAL_UART_DMAStop(&huart1);
  // 计算接收到的数据长度
  uint8_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
  // 测试函数:将接收到的数据打印出去
  printf(“Receive Data(length = %d): ”,data_length);
  HAL_UART_Transmit(&huart1,receive_buff,data_length,0x200);
  printf(“rn”);
  // 清零接收缓冲区
  memset(receive_buff,0,data_length);
  data_length = 0;
  // 重启开始DMA传输 每次255字节数据
  HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, 255);
  }
  关于计算数据长度可以具体了解HAL库函数的操作,简单来说__HAL_DMA_GET_COUNTER()函数将返回待接收的数据,讲设置需要接收的数据长度,减去待接收的数据,就得到了已经接收到的数据。
  至此,我们使用了DMA+串口空闲中断的方式来实现不定长数据的接收。
  3. DMA 软件配置
  3.1 DMA接收不定长数据
  关于串口接收数据的形式和DMA方式接收不定长数据的原理过程,下面这篇文章介绍的很清晰明了。
  在此,摘选接收完数据时处理这部分内容加深理解。
  暂时关闭串口接收DMA通道,有两个原因:1.防止后面又有数据接收到,产生干扰,因为此时的数据还未处理。2.DMA需要重新配置。
  清除DMA标志位。
  从DMA寄存器中获取接收到的数据字节数(可有可无)。
  重新设置DMA下次要接收的数据字节数,注意,数据传输数量范围为0至65535。这个寄存器只能在通道不工作(DMA_CCRx的EN=0)时写入。通道开启后该寄存器变为只读,指示剩余的待传输字节数目。寄存器内容在每次DMA传输后递减。数据传输结束后,寄存器的内容或者变为0;或者当该通道配置为自动重加载模式时,寄存器的内容将被自动重新加载为之前配置时的数值。当寄存器的内容为0时,无论通道是否开启,都不会发生任何数据传输。
  给出信号量,发送接收到新数据标志,供前台程序查询。
  开启DMA通道,等待下一次的数据接收。注意,对DMA的相关寄存器配置写入,如重置DMA接收数据长度,必须要在关闭DMA的条件进行,否则操作无效。
  注意事项
  STM32的IDLE的中断在串口无数据接收的情况下,是不会一直产生的,产生的条件是这样的,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一断接收的数据断流,没有接收到数据,即产生IDLE中断。如果中断发送数据帧的速率很快,MCU来不及处理此次接收到的数据,中断又发来数据的话,这里不能开启,否则数据会被覆盖。有两种方式解决:
  在重新开启接收DMA通道之前,将Rx_Buf缓冲区里面的数据复制到另外一个数组中,然后再开启DMA,然后马上处理复制出来的数据。
  建立双缓冲,重新配置DMA_MemoryBaseAddr的缓冲区地址,那么下次接收到的数据就会保存到新的缓冲区中,不至于被覆盖。
  3.2 DMA初始化
  在上面的介绍中,只说明了串口空闲中断的整体流程,但关于此过程中的 DMA 初始化等并未涉及,因此在这里补充下
  main.c:
  static void MX_DMA_Init(void)
  {
  // 使能 DMA1 时钟
  __HAL_RCC_DMA1_CLK_ENABLE();
  // 中断优先级配置和中断使能
  HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
  }
  stm32f0xx_hal_msp.c:
  hdma_usart4_tx.Instance = DMA1_Channel2;
  hdma_usart4_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; // 存储器到外设
  hdma_usart4_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设非增量
  hdma_usart4_tx.Init.MemInc = DMA_MINC_ENABLE; // 存储器增强
  hdma_usart4_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
  hdma_usart4_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
  hdma_usart4_tx.Init.Mode = DMA_NORMAL; // 正常模式
  hdma_usart4_tx.Init.Priority = DMA_PRIORITY_LOW; // 优先级低
  if (HAL_DMA_Init(&hdma_usart4_tx) != HAL_OK)
  {
  _Error_Handler(__FILE__, __LINE__);
  }
  __HAL_DMA1_REMAP(HAL_DMA1_CH2_USART4_TX);
  __HAL_LINKDMA(huart,hdmatx,hdma_usart4_tx); // 与串口引脚相连接
  初始化结构体成员的含义参考下图
  
举报

更多回帖

发帖
×
20
完善资料,
赚取积分