OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 2674|回复: 2

HAL库版DMA循环模式数据收发方案分享

[复制链接]

2

主题

15

帖子

0

精华

新手上路

积分
43
金钱
43
注册时间
2019-11-17
在线时间
12 小时
发表于 2020-2-3 07:53:04 | 显示全部楼层 |阅读模式
本帖最后由 Machinor 于 2020-2-3 17:18 编辑

  STM32CubeMX生成的HAL库中,提供了三类串口数据收发的接口,分别为阻塞模式,非阻塞模式和DMA模式,文本主要对DMA模式进行了分析并依据提供的接口提出了更加实用的串口数据收发方案。通过对网上资料的查找和分析,主要存在下述两个问题:
  1、在数据接收过程中,采用了串口的空闲中断实现了DMA模式下不定长数据的接收,但存在的限制是单次接收的数据长度必须小于DMA缓冲区的长度,如果接收的数据长度大于DMA缓冲区的长度,就数据丢失了。
  2、在数据发送过程中,DMA在发送阶段是不能再使能DMA发送的,即调用HAL库中的HAL_UART_Transmit_DMA接口后,在DMA传输数据完成之前是不能再调用该接口的,常用的方式是通过一个标志变量来确定是否可发送,但程序中若存在等待标志变量等逻辑的话,会降低程序运行的效率。
本文通过采用DMA的循环模式配合DMA传输完成中断和串口空闲中断解决DMA缓冲区长度对串口接收数据量的限制问题。采用类递归的逻辑解决串口发送数据等待发送完成标志的问题。
1 硬件配置
  首先,应用STM32CubeMX对串口进行配置,
20200202174633.png
  在Connectivity中勾选usart1,具体引脚根据硬件确定。重点注意的是,勾选USART1 globalinterrupt 使能;将收发的DMA添加上,将USART1_RX的DMA设置为Circular模式,将USART1_TX的DMA设置为Normal模式。
2 软件实现方案
2.1 初始化准备
  将程序内部做驱动层和应用层的区分,串口数据收发接口的封装属于驱动层面,应用层面调用驱动层的接口实现数据的发送和获取,通过收发两个缓冲区实现数据的交互,本文中采用一组循环FIFO实现数据的缓冲。新建一组文件提供对串口收发数据的中间件接口,在头文件中定义串口中间件属性:
  1. typedef struct
  2. {
  3.          UART_HandleTypeDef  *handle;    /*HAL库提供的串口句柄*/
  4.          int16_t TransFlag;              /*数据发送标志位*/
  5.          int32_t DmaSize;                /*DMA缓冲区的大小*/
  6.          int32_t DamOffset;              /*获取数据在DMA缓冲区的偏移量*/
  7.          uint8_t *pReadDma;              /*指向接收DMA缓冲区的首地址*/
  8.          uint8_t *pWriteDma;             /*指向发送DMA缓冲区的首地址*/
  9.          CFIFO ReadCFifo;                /*接受数据的循环缓冲区*/
  10.          CFIFO WriteCFifo;               /*发送数据的循环缓冲区*/
  11. }MW_UART_ATTR;
复制代码
上述属性值中包括了缓冲区,DMA等参数,后续实现中具体讲述各参数的用途。本文仅提供对一个串口进行配置用作演示,实际中在属性中添加了id参数实现多串口的管理,或者采用其他方式。
  1. #define MW_TRANS_IDLE        0
  2. #define MW_TRANS_BUSY        1

  3. #define MW_UART_BUFFER_LEN   1024
  4. #define MW_UART_DMA_LEN      256

  5. static uint8_t Uart1TxBuff[MW_UART_BUFFER_LEN] = {0};
  6. static uint8_t Uart1RxBuff[MW_UART_BUFFER_LEN] = {0};

  7. static uint8_t Uart1TxDma[MW_UART_DMA_LEN] = {0};
  8. static uint8_t Uart1RxDma[MW_UART_DMA_LEN] = {0};

  9. static MX_UART_ATTR sUartAttr;

  10. int8_t MW_UART_Init(UART_HandleTypeDef *handle)
  11. {
  12.         /*为属性的参数附初值*/
  13.         MX_UART_ATTR *pUartAttr = &sUartAttr;
  14.         pUartAttr->handle = handle;
  15.         pUartAttr->DamOffset = 0;
  16.         pUartAttr->TransFlag = MW_TRANS_IDLE;
  17.         pUartAttr->DmaSize = MW_UART_DMA_LEN;
  18.         pUartAttr->pReadDma = Uart1RxDma;
  19.         pUartAttr->pWriteDma = Uart1TxDma;
  20.         CFIFO_Init(&pUartAttr->ReadCFifo, Uart1RxBuff, MW_UART_BUFFER_LEN);
  21.         CFIFO_Init(&pUartAttr->WriteCFifo, Uart1TxBuff, MW_UART_BUFFER_LEN);

  22.         /*使能串口空闲中断*/
  23.         __HAL_UART_ENABLE_IT(handle, UART_IT_IDLE);
  24.         /*配置DMA参数并使能中断*/
  25.         if(HAL_OK != HAL_UART_Receive_DMA(pUartAttr->handle, pUartAttr->pReadDma, MW_UART_DMA_LEN))
  26.         {
  27.                return MW_FAIL;
  28.         }

  29.         return MW_SUCCESS;
  30. }
复制代码
  MW_UART_Init接口实现了对串口初始化的功能,HAL库提供的MX_USART1_UART_Init接口仅对硬件进行了配置,在该接口后调用MW_UART_Init对串口进行使能。因为CFIFO初始化的循环缓冲区用于对DMA接收数据的缓存,因此DMA缓冲区的长度要小于数据缓存区的长度。通过对串口空闲中断的使能和DMA的使能,使能串口数据的接收。
2.2 串口数据接收方案
  本文中提供了采用串口空闲中断和DMA接收完成中实现数据接收的方案。假设DMA缓冲区的大小是16,若一次接收的数据长度小于DMA缓冲区的长度,则本次传输通过串口空闲中断获取数据;若一次接收数据的长度大于16,假设为20,则前16字节由DMA完成中断从DMA缓冲区中获取,剩下的4字节数据通过串口空闲中断从DMA缓冲区中获取。
基于上述的方案,在DMA缓冲区满后应自动重新装载缓冲区,因此采用了DMA循环模式。在循环模式中,需要注意DMA缓冲满后,再接收的数据存放的位置。举例,串口要接收的一串字符串为"1234567890abcdefghij"共20字节,当DMA缓冲区接收了16字符后,DMA缓冲区里的数据为"1234567890abcdef"并产生DMA完成中断,我们可以利用这个中断将获取数据。由于是循环模式,不需要重新配置,在DMA缓冲区满后继续接收余下的4字节数据,当接收完这四字节数据后,DMA缓冲区中的16字节数据为"ghij567890abcdef"并产生串口空闲中断,我们可以利用这个中断获取后四节的数据。值得注意的是,需要获取的4字节数据在DMA缓冲区的后四个字节,因此获取的时候需要格外注意。因此在属性中定义了DmaOffset这个参数来声明在DMA缓冲中获取数据的偏移位置。具体实现方式如下:
  HAL库提供的中断处理中,并没有对空闲中断进行处理,因此需要在中断中提供对空闲中断处理的接口:
  1. void USART1_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN USART1_IRQn 0 */

  4.   /* USER CODE END USART1_IRQn 0 */
  5.   HAL_UART_IRQHandler(&huart1);
  6.   /* USER CODE BEGIN USART1_IRQn 1 */
  7.   MW_UART_IRQHandler(&huart1);
  8.   /* USER CODE END USART1_IRQn 1 */
  9. }
复制代码
  在HAL库提供的USART1_IRQHandler接口中添加MW_UART_IRQHandler接口实现对串口空闲中断的处理(HAL_UART_IRQHandler为HAl库提供的对中断处理的接口)。
  1. void MW_UART_IRQHandler(UART_HandleTypeDef *huart)
  2. {
  3.         int32_t RecvNum = 0;
  4.         int32_t WriteNum = 0;
  5.         int32_t DmaIdleNum = 0;

  6.         MX_UART_ATTR *pUartAttr = &sUartAttr;

  7.         if((__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET))
  8.         {
  9.                /*清除空闲中断标识位,重新接受串口空闲中断*/
  10.                __HAL_UART_CLEAR_IDLEFLAG(huart);

  11.                /*计算在DMA缓冲区需要获取的数据长度*/
  12.                DmaIdleNum = __HAL_DMA_GET_COUNTER(huart->hdmarx);
  13.                RecvNum = pUartAttr->DmaSize - DmaIdleNum - pUartAttr->DamOffset;
  14.                /*将获取到的数据放到数据接收缓冲区中*/
  15.                WriteNum = CFIFO_Write(&pUartAttr->ReadCFifo,pUartAttr->pReadDma + pUartAttr->DamOffset,RecvNum);
  16.                if(WriteNum != RecvNum)
  17.                {
  18.                        loge("Uart ReadFifo is not enough\r\n");
  19.                }
  20.               /*计算获取数据位置的偏移量*/
  21.                pUartAttr->DamOffset += RecvNum;
  22.         }
  23. }
复制代码
  通过对HAL库中HAL_UART_RxCpltCallback这个弱函数的重写可以实现对DMA完成中断的处理,这个函数虽然声明在stm32f4xx_hal_uart.c中,其实DMA完成中断最后调用的是串口接收完成的回调函数。
  1. void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
  2. {                     
  3.         int32_t DmaLen = 0;
  4.         int32_t WriteNum = 0;

  5.         MX_UART_ATTR *pUartAttr = &sUartAttr;
  6.         /*计算需要获取数据的长度*/
  7.         DmaLen = pUartAttr->DmaSize - pUartAttr->DamOffset;
  8.         /*将获取的数据存放到数据缓冲区中*/

  9.         WriteNum = CFIFO_Write(&pUartAttr->ReadCFifo,pUartAttr->pReadDma + pUartAttr->DamOffset,DmaLen);
  10.         if(WriteNum != DmaLen)
  11.         {
  12.                loge("Uart ReadFifo is not enough\r\n");
  13.         }
  14.         /*复位DMA偏移量*/
  15.         pUartAttr->DamOffset = 0;
  16. }
复制代码
  该接口中也需要计算接收数据的长度的原以为:若上次串口空闲中断接收了4个字节的数据,则再接收12个字节的数据就会产生DMA传输完成中断,因此DMA缓冲区的长度16减去串口中断为DmaOffset赋值的4,得到实际接收的数据长度为12。
两个中断里将DMA缓冲区的数据搬移到数据循环缓冲区中,以供应用去获取和处理:
  1. int32_t MW_UART_Receive(uint8_t* buffer,int32_t len)
  2. {
  3.         int32_t RecvNum = 0;
  4.         MX_UART_ATTR *pUartAttr = &sUartAttr;
  5.         /*从数据循环缓冲区中获取数据*/
  6.         RecvNum = CFIFO_Read(&pUartAttr->ReadCFifo, buffer, len);

  7.         return RecvNum;
  8. }
复制代码
  应用中,调用MW_UART_Receive接口获取数据并对数据进行处理等后续操作。上述的方案避免了DMA缓冲区长度对单次接收数据长度的限制,如果出现了“Uart ReadFifo is not enough”,说明应用中调用MW_UART_Receive接口的频率不够快,或者接收循环缓冲区不够大。总之,数据接收的完整性应取决于应用上逻辑的实现,而不是需要DMA缓冲区申请的足够大,这也是本文串口数据接收方案的原则。
2.3 串口数据发送方案
  串口数据发送中,采用了接收中类似的方案,先将数据暂时保存到循环缓冲区中,然后通过DMA将数据发送出去。在应用中调用MW_UART_Transmit将数据发送出去,传统方案中一次发送的数据量必须小于DMA缓冲区的大小,本方案中一次发送的数据量应小于发送循环缓冲区的余量。
  1. int32_t MW_UART_Transmit(uint8_t* buffer,int32_t len)
  2. {
  3.         int32_t TransNum = 0;
  4.         int32_t TransLen = 0;
  5.         MX_UART_ATTR *pUartAttr = &sUartAttr;
  6.         /*write data in cfifo for temporary storage*/
  7.         TransNum = CFIFO_Write(&pUartAttr->WriteCFifo, buffer, len);

  8.         /*if dam is not in using,get data form cfifo to transmit*/
  9.         if(pUartAttr->TransFlag == MW_TRANS_IDLE)
  10.         {
  11.                 TransLen = CFIFO_Read(&pUartAttr->WriteCFifo,pUartAttr->pWriteDma,pUartAttr->DmaSize);
  12.                 if(TransLen > 0)
  13.                 {
  14.                         pUartAttr->TransFlag = MW_TRANS_BUSY;
  15.                         if(HAL_OK != HAL_UART_Transmit_DMA(pUartAttr->handle,pUartAttr->pWriteDma,TransLen))
  16.                         {
  17.                                 loge("Uart Trans_DMA failed\r\n");
  18.                         }
  19.                 }                                       
  20.         }

  21.         return TransNum;
  22. }
复制代码
  该接口中,若TransFlag为MW_TRANS_IDLE,则从循环缓冲区中获取最长不超过DMA缓冲区长度的数据,调用HAL库提供的HAL_UART_Transmit_DMA接口将数据发送出去,将TransFlag置为MW_TRANS_BUSY,发送完成之后响应串口发送完成中断,重写HAL库中提供的HAL_UART_TxCpltCallback弱函数接口实现串口发送完成中断的操作。
  1. void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
  2. {
  3.         int32_t TransNum = 0;
  4.         MX_UART_ATTR *pUartAttr = &sUartAttr;

  5.         /*从发送循环缓冲区中获取数据*/
  6.         TransNum = CFIFO_Read(&pUartAttr->WriteCFifo,pUartAttr->pWriteDma,pUartAttr->DmaSize);
  7.         if(TransNum > 0)
  8.         {               
  9.                 if(HAL_OK != HAL_UART_Transmit_DMA(pUartAttr->handle,pUartAttr->pWriteDma,TransNum))
  10.                 {
  11.                         loge("Uart Trans_DMA failed\r\n");
  12.                 }
  13.         }
  14.         else
  15.         {
  16.                 pUartAttr->TransFlag = MW_TRANS_IDLE;
  17.         }
  18. }
复制代码
  当一组数据发送完成后,继续从缓冲区中获取数据,若获取到数据,继续调用HAL_UART_Transmit_DMA将数据发送出去,若获取不到数据了,则将TransFlag置为MW_TRANS_IDLE,这样再调用MW_UART_Transmit就能使能重新使能发送。本方案中,如果正处于DMA传输过程,则将要发送的数据放到数据循环缓冲区中,在发送完成中断中去获取数据将其发送,避免了等待DMA传输完成的逻辑。本文中提及的串口数据接收发送方案最初是因为采用的MCU的RAM很小,想尽量缩减各类缓冲区的长度,出于这样的考虑完成了整体的方案设计。


正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

15

主题

1061

帖子

0

精华

资深版主

Rank: 8Rank: 8

积分
3624
金钱
3624
注册时间
2019-8-14
在线时间
1054 小时
发表于 2020-2-3 14:58:48 | 显示全部楼层
回复 支持 反对

使用道具 举报

2

主题

15

帖子

0

精华

新手上路

积分
43
金钱
43
注册时间
2019-11-17
在线时间
12 小时
 楼主| 发表于 2020-2-3 17:21:20 | 显示全部楼层

不好意思,之前有一个地方写错了,
... ...当接收完这四字节数据后,DMA缓冲区中的16字节数据为"ghij567890abcdef"并产生串口空闲中断。
缓冲区里的数据内容我笔误写错了,更改提交审核了
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-5-6 12:46

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表