[其他ST产品] 记录一次对STM32G4串口硬件FIFO的调试

[复制链接]
 楼主| 结合国际经验 发表于 2023-11-28 16:11 | 显示全部楼层 |阅读模式
记录一次对STM32G4串口硬件FIFO的调试
前言:通常我们使用串口接收多字节数据会使用中断和DMA两种方式。使用中断方式,每接收到一个字节就会触发一次中断,我们可以在中断函数里将接收到的这一字节保存在内存中然后等待其他程序处理,也可以直接在中断函数里处理。使用DMA方式,需要事先开辟一块内存,每当接收到一个字节,DMA会自动将数据保存在开辟的内存中而不需要CPU的参与。

中断方式的优点是可以在第一时间知道串口接收到了新数据,在一些对实时性要求特别高的情况下占优。而DMA方式则需要程序周期性的轮训接收内存,看看有没有收到新数据。但是中断方式每接收到一个字节都需要CPU去处理,在波特率比较高、数据量比较大的场合下会频繁进入中断,严重影响效率,甚至会干扰到其他程序的运行,这个时候使用DMA方式会更加合理。

现在,针对波特率高、数据量大的情况,又多了一种选择,那就是使用串口的硬件FIFO功能。串口硬件FIFO只有ST后面出的型号才有,比如H7、G4系列等,F1、F4系列应该是没有这个功能的。之前的中断方式,每收到一个字节都会触发一次中断,而使用了硬件FIFO,可以在收到n个字节后才触发一次中断,然后在中断函数里一次性地取出这n个字节。在连续接收大量数据的情况下可以大大降低进入中断的频率,提高效率。这里也多嘴一句,有了硬件FIFO不等于不再需要软件FIFO!!!

一、功能配置
这里使用CubeMX软件配置,使用硬件FIFO的前提是先正确配置好串口中断接收的功能。

484486565a0a73e02e.png
333876565a0b2c44cb.png

相比普通的功能,这里多了三个选项:

1、Fifo Mode

这里选择使能。

2、Txfifo Threshold

发送FIFO的阈值,因为这次仅调试了接收FIFO,没用到发送,所以选择默认值。后续可能会再出一篇关于发送的文章。

3、Rxfifo Threshold

接收FIFO的阈值,这里选择一半深度。


 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
说明:

FIFO的深度是多少?配置选项只说1/8、1/4、1/2、3/4、7/8这些。个人经过调试,推测深度是8字节,当然确切来讲不能说是8字节,因为位宽不一定是8位(数据字长、有无奇偶校验),应该说FIFO可以保存8次接收数据,再加上RDR寄存器也可以保存一次接收数据,所以最多可以保存9次接收数据而不溢出。
 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
阈值是干什么的,一般配成多少。这里的阈值意味着当我的接收FIFO收到n个数据了就可以产生一次中断。比如我设为1/2并且使能了相应的RXFT中断,那么每收到4次(这里的4就是8的1/2,后文所有的4都是这么来的,后面不再解释)数据就会产生一次RXFT中断。个人觉得阈值设为一半左右的深度比较好,设的太小了起不到降低CPU利用率的效果,设置的太大了容易溢出。要注意,这里设置的阈值仅仅影响中断的触发,并不会改变FIFO能保存多少数据。
然后我们生成工程即可。
 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
二、硬件FIFO的使用
1、配置正常的中断接收
我们在初始化过程中加上这一句:

__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
然后编写中断函数:
  1. void USART1_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN USART1_IRQn 0 */
  4.         HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);       
  5.         USART1->TDR = USART1->RDR;
  6.   /* USER CODE END USART1_IRQn 0 */
  7.   HAL_UART_IRQHandler(&huart1);
  8.   /* USER CODE BEGIN USART1_IRQn 1 */
  9.         HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
  10.   /* USER CODE END USART1_IRQn 1 */
  11. }
 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
再用串口助手发送"12345\r\n",观看逻辑分析仪抓到的波形:
249806565a101d63c9.png
 楼主| 结合国际经验 发表于 2023-11-28 16:12 | 显示全部楼层
通道0连接的是PA0,通道4连接的是RX,通道3连接的是TX。发现串口每接收到一个数据就进入一次中断,并且把接收到的数据原封不动的发送出去。符合我们之前的设想。

 楼主| 结合国际经验 发表于 2023-11-28 16:13 | 显示全部楼层
2、FIFO的运行机制探究
下面,我们把RXNE中断改为RXFT中断:
//        __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
       
        __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXFT);
 楼主| 结合国际经验 发表于 2023-11-28 16:13 | 显示全部楼层
修改中断函数:

  1. uint8_t rxBuf[32];
  2. uint8_t rxCnt;

  3. void USART1_IRQHandler(void)
  4. {
  5.   /* USER CODE BEGIN USART1_IRQn 0 */
  6.         if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
  7.         {       
  8.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);                                       
  9.                 for(int i = 0; i < 8; i ++)
  10.                 {                       
  11.                         if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
  12.                         {
  13.                                 break;
  14.                         }
  15.                         rxBuf[rxCnt++] = USART1->RDR;
  16.                 }
  17.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
  18.         }
  19.   /* USER CODE END USART1_IRQn 0 */
  20.   HAL_UART_IRQHandler(&huart1);
  21.   /* USER CODE BEGIN USART1_IRQn 1 */

  22.   /* USER CODE END USART1_IRQn 1 */
  23. }

 楼主| 结合国际经验 发表于 2023-11-28 16:13 | 显示全部楼层
并且在主循环里加上这一段:(仅用来验证FIFO功能,切勿用于其他用途)

  1. void softDelay()
  2. {
  3.         for(int i = 0; i < 30000; i ++)
  4.         {}
  5. }

  6. int main(void)
  7. {
  8.     init();
  9.    
  10.     while(1)
  11.     {
  12.                 if(rxCnt)
  13.         {
  14.             lastRxCnt = rxCnt;
  15.             softDelay();

  16.             if(rxCnt == lastRxCnt)
  17.             {
  18.                 HAL_UART_Transmit(&huart1, rxBuf, rxCnt, 10);
  19.                 lastRxCnt = rxCnt = 0;
  20.             }
  21.         }               
  22.     }   
  23. }

 楼主| 结合国际经验 发表于 2023-11-28 16:14 | 显示全部楼层
然后使用串口助手同样发送"12345\r\n",观看逻辑分析仪抓到的波形:

166246565a154cc922.png
 楼主| 结合国际经验 发表于 2023-11-28 16:14 | 显示全部楼层
神奇的事情发送了,串口居然只返回了"1234",丢了3字节,而且我们发现仅在接收到第4个字节的时候进入了一次中断,这是为什么?

正如我们前面分析的,因为Rxfifo Threshold设置为1/2的深度,所以在第4次接收到数据时触发了一次RXFT中断,但后面的"5\r\n"只有三字节,不足以再次触发RXFT中断。而进不了中断就没有从FIFO中取出数据,因此最终只返回了"1234",剩下的"5\r\n"依旧保存在FIFO中。那么如何证明?
 楼主| 结合国际经验 发表于 2023-11-28 16:14 | 显示全部楼层
在现有基础上,我们再次发送"abcdefg\r\n",观看逻辑分析仪抓到的波形:
423796565a170ed6b5.png
我们发现当接收完"a"的时候进入了一次中断,之后每收到4次数据就进入一次中断。而串口返还的数据是"5\r\nabcdefg\r\n",到了这里,相信大家都明白是怎么回事了吧。

没错,只有在FIFO存满4字节才会触发一次RXFT中断,不足4字节会保存下来,直到再一次存满4字节。

看到这里可能有人要吐槽了,这功能也太鸡肋了吧。先别着急,后面有办法解决。
 楼主| 结合国际经验 发表于 2023-11-28 16:15 | 显示全部楼层
3、如何正确的使用FIFO进行数据接收
不知道大家是否还记得空闲中断,对,就是用于DMA接收不定长数据用到的那个空闲中断。有了它,我们就可以把FIFO中不足4字节的数据给取出来了。

不过这里建议使用新的超时功能而不是idle。因为我们不知道发送方在发送多个字节时,中间的时间间隔能不能保持在很小,因为发送方有可能是USB转串口芯片、有可能是单片机硬件串口,甚至可能是IO模拟出来的串口。新功能RTO支持手动配置超时时间,而idle不行。

184746565a18883b24.png
 楼主| 结合国际经验 发表于 2023-11-28 16:15 | 显示全部楼层
然后我们修改初始化代码:
  1.         __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXFT);
  2.        
  3.         HAL_UART_ReceiverTimeout_Config(&huart1, 2);
  4.         HAL_UART_EnableReceiverTimeout(&huart1);
  5.         __HAL_UART_ENABLE_IT(&huart1, UART_IT_RTO);
 楼主| 结合国际经验 发表于 2023-11-28 16:15 | 显示全部楼层
修改中断函数:
  1. void USART1_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN USART1_IRQn 0 */
  4.         if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
  5.         {
  6.                 __HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF);
  7.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);                                       
  8.                
  9.                 for(int i = 0; i < 8; i ++)
  10.                 {                       
  11.                         if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
  12.                         {
  13.                                 break;
  14.                         }
  15.                         rxBuf[rxCnt++] = USART1->RDR;
  16.                 }                                       
  17.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);       
  18.         }       
  19.         if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
  20.         {       
  21.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);                       
  22.                                
  23.                 for(int i = 0; i < 8; i ++)
  24.                 {                       
  25.                         if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
  26.                         {
  27.                                 break;
  28.                         }
  29.                         rxBuf[rxCnt++] = USART1->RDR;
  30.                 }
  31.                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);       
  32.         }
  33.   /* USER CODE END USART1_IRQn 0 */
  34.   HAL_UART_IRQHandler(&huart1);
  35.   /* USER CODE BEGIN USART1_IRQn 1 */

  36.   /* USER CODE END USART1_IRQn 1 */
  37. }
 楼主| 结合国际经验 发表于 2023-11-28 16:15 | 显示全部楼层
到了这里,基本的功能就已经实现了,如果不想继续深究,看到这边就可以了,后面我会分享自己的debug经历以及疑问。
 楼主| 结合国际经验 发表于 2023-11-28 16:15 | 显示全部楼层
问题1: 之前的代码没有手动清除RTOF标志位,导致HAL库自带的HAL_UART_IRQHandler(&huart1); 函数将 RXFT 中断使能给清除了。

问题代码:
  1. void USART1_IRQHandler(void)
  2. {
  3.   /* USER CODE BEGIN USART1_IRQn 0 */
  4.         if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
  5.         {       
  6.                 SET_TEST0;               
  7.                                
  8.                 for(int i = 0; i < 8; i ++)
  9.                 {                       
  10.                         if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
  11.                         {
  12.                                 break;
  13.                         }
  14.                         rxBuf[rxCnt++] = USART1->RDR;
  15.                 }
  16.                 RESET_TEST0;
  17.         }
  18.         if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
  19.         {
  20.                 //__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF); 之前没写这句
  21.                 SET_TEST2;                               
  22.                
  23.                 for(int i = 0; i < 8; i ++)
  24.                 {                       
  25.                         if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
  26.                         {
  27.                                 break;
  28.                         }
  29.                         rxBuf[rxCnt++] = USART1->RDR;
  30.                 }                                       
  31.                 RESET_TEST2;
  32.     }
  33.   /* USER CODE END USART1_IRQn 0 */
  34.   HAL_UART_IRQHandler(&huart1);
  35.   /* USER CODE BEGIN USART1_IRQn 1 */

  36.   /* USER CODE END USART1_IRQn 1 */
  37. }
 楼主| 结合国际经验 发表于 2023-11-28 16:16 | 显示全部楼层
结果导致一次性接收不超过9字节能正常接收,超过9字节只能接收前9字节:

473606565a1c90993f.png
193746565a1cfaeeb5.png
 楼主| 结合国际经验 发表于 2023-11-28 16:16 | 显示全部楼层
结果导致一次性接收不超过9字节能正常接收,超过9字节只能接收前9字节:


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

本版积分规则

64

主题

773

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部