记录一次对STM32G4串口硬件FIFO的调试

记录一次对STM32G4串口硬件FIFO的调试

前言:通常我们使用串口接收多字节数据会使用中断和DMA两种方式。使用中断方式,每接收到一个字节就会触发一次中断,我们可以在中断函数里将接收到的这一字节保存在内存中然后等待其他程序处理,也可以直接在中断函数里处理。使用DMA方式,需要事先开辟一块内存,每当接收到一个字节,DMA会自动将数据保存在开辟的内存中而不需要CPU的参与。

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

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

一、功能配置

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

在这里插入图片描述

在这里插入图片描述

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

1、Fifo Mode

这里选择使能。

2、Txfifo Threshold

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

3、Rxfifo Threshold

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

说明:

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

然后我们生成工程即可。

二、硬件FIFO的使用

1、配置正常的中断接收

我们在初始化过程中加上这一句:

__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);

然后编写中断函数:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);	
	USART1->TDR = USART1->RDR;
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
  /* USER CODE END USART1_IRQn 1 */
}

再用串口助手发送"12345\r\n",观看逻辑分析仪抓到的波形:

在这里插入图片描述

通道0连接的是PA0,通道4连接的是RX,通道3连接的是TX。发现串口每接收到一个数据就进入一次中断,并且把接收到的数据原封不动的发送出去。符合我们之前的设想。

2、FIFO的运行机制探究

下面,我们把RXNE中断改为RXFT中断:

//	__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
	
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXFT);

修改中断函数:

uint8_t rxBuf[32];
uint8_t rxCnt;

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
	{	
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);					
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
	}
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

并且在主循环里加上这一段:(仅用来验证FIFO功能,切勿用于其他用途)

void softDelay()
{
	for(int i = 0; i < 30000; i ++)
	{}
}

int main(void)
{
    init();
    
    while(1)
    {
 		if(rxCnt)
        {
            lastRxCnt = rxCnt;
            softDelay();

            if(rxCnt == lastRxCnt)
            {
                HAL_UART_Transmit(&huart1, rxBuf, rxCnt, 10);
                lastRxCnt = rxCnt = 0;
            }
        }                
    }    
}

然后使用串口助手同样发送"12345\r\n",观看逻辑分析仪抓到的波形:

在这里插入图片描述

神奇的事情发送了,串口居然只返回了"1234",丢了3字节,而且我们发现仅在接收到第4个字节的时候进入了一次中断,这是为什么?

正如我们前面分析的,因为Rxfifo Threshold设置为1/2的深度,所以在第4次接收到数据时触发了一次RXFT中断,但后面的"5\r\n"只有三字节,不足以再次触发RXFT中断。而进不了中断就没有从FIFO中取出数据,因此最终只返回了"1234",剩下的"5\r\n"依旧保存在FIFO中。那么如何证明?

在现有基础上,我们再次发送"abcdefg\r\n",观看逻辑分析仪抓到的波形:

在这里插入图片描述

我们发现当接收完"a"的时候进入了一次中断,之后每收到4次数据就进入一次中断。而串口返还的数据是"5\r\nabcdefg\r\n",到了这里,相信大家都明白是怎么回事了吧。

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

看到这里可能有人要吐槽了,这功能也太鸡肋了吧。先别着急,后面有办法解决。

3、如何正确的使用FIFO进行数据接收

不知道大家是否还记得空闲中断,对,就是用于DMA接收不定长数据用到的那个空闲中断。有了它,我们就可以把FIFO中不足4字节的数据给取出来了。

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

在这里插入图片描述

然后我们修改初始化代码:

	__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXFT);
	
	HAL_UART_ReceiverTimeout_Config(&huart1, 2);
	HAL_UART_EnableReceiverTimeout(&huart1);
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_RTO);

修改中断函数:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
	{
		__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF);
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);					
		
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}					
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);	
	}	
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
	{	
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);			
				
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);	
	}
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

到了这里,基本的功能就已经实现了,如果不想继续深究,看到这边就可以了,后面我会分享自己的debug经历以及疑问。


问题1: 之前的代码没有手动清除RTOF标志位,导致HAL库自带的HAL_UART_IRQHandler(&huart1); 函数将 RXFT 中断使能给清除了。

问题代码:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
	{	
		SET_TEST0;		
				
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}
		RESET_TEST0;
	}
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
	{
		//__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF); 之前没写这句
		SET_TEST2;				
		
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}					
		RESET_TEST2;
    }
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

结果导致一次性接收不超过9字节能正常接收,超过9字节只能接收前9字节:

在这里插入图片描述

在这里插入图片描述

问题2: 如果一次性接收到的数据刚好是4的倍数,那么最后一轮数据发完触发的是RTO中断还是RXFT中断还是都有?

经过多次验证,接收完最后一个数据后只能产生一个中断,如果先判断有没有产生RTO标志就不会再处理RXFT的内容(但前提是RTOR寄存器的RTO是0,不然最后只可能是RXFT中断),如果先判断有没有产生RXFT标志就不会再处理RTO的内容。

先判断RTORTOR寄存器的RTO是0的例子:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
	{
		__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF);
		SET_TEST2;				
		
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}					
		RESET_TEST2;
	}	
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
	{	
		SET_TEST0;		
				
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}
		RESET_TEST0;
	}
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

在这里插入图片描述

先判断RXFT的例子:

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFT))
	{	
		SET_TEST0;		
				
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}
		RESET_TEST0;
	}
	if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RTOF))
	{
		__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_RTOF); 
		SET_TEST2;				
		
		for(int i = 0; i < 8; i ++)
		{			
			if(!__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXFNE))
			{
				break;
			}
			rxBuf[rxCnt++] = USART1->RDR;
		}					
		RESET_TEST2;
    }
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

在这里插入图片描述

这个问题仅会出现在字节数刚刚好是4的倍数的情况,如果不是则两种代码的结果都是一样的。或许ST是有意这么设计的,防止最后出现两次中断,还是说我哪里没有考虑到,如果有朋友能解释一下,鄙人不胜感激。

三、总结

随着我们工作、学习的不断深入,会接触到越来越高级的芯片、越来越复杂的外设功能,不再像以前那样随便看两篇教程、看两集视频、复制一段代码就能调通,即使是使用CubeMX加HAL库也不会那么容易。往后需要我们自己熟读手册,了解每一位寄存器的作用,要是遇到手册说得不清楚或者自己无法理解的情况,还需要我们动手实践,自己去摸索它们大致的功能和作用。

要不然以后遇到没有参考的时候,就只能干瞪眼了。就算有参考,也建议大家自己调一遍,不要复制黏贴,不要怕浪费时间。就好比这篇文章,我相信会有人看完后拿着逻辑分析仪一遍一遍的尝试,但恐怕更多的还是复制代码,能用就行。

自己调通自己写驱动代码,我觉得这才是做BSP的核心和魅力所在。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/33801.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ruoyi-vue版本(三十)Spring Security 安全框架中token的生成与解析

目录 1 使用2 写工具类3 使用工具类 1 使用 1 项目里面添加依赖 <!-- Token生成与解析--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency>2 写工具类 package com.ruoyi.framework.we…

ethtool 原理介绍和解决网卡丢包排查思路

前言 之前记录过处理因为 LVS 网卡流量负载过高导致软中断发生丢包的问题&#xff0c;RPS 和 RFS 网卡多队列性能调优实践[1]&#xff0c;对一般人来说压力不大的情况下其实碰见的概率并不高。这次想分享的话题是比较常见服务器网卡丢包现象排查思路&#xff0c;如果你是想了解…

【数据库四】MySQL备份与恢复

MySQL备份与恢复 1.数据库备份的分类1.1 数据备份的重要性1.2 数据库备份的分类1.3 常见的备份方法 2.MySQL完全备份与恢复2.1 MySQL完全备份2.2 数据库完全备份分类2.3 MySQL物理冷备份及恢复2.4 数据迁移DST2.5 mysqldump进行逻辑备份2.5.1 mysqldump备份数据库2.5.2 mysqldu…

Nvidia官方视频编解码性能

NVIDIA VIDEO CODEC SDK | NVIDIA Developer 1080P解码性能&#xff1a; 720P解码性能&#xff1a; 详细的参见官方的链接地址&#xff0c;对于GPU的解码fps能力&#xff0c;可以作为评估参照&#xff01;

视频与AI,与进程交互(二) pytorch 极简训练自己的数据集并识别

目标学习任务 检测出已经分割出的图像的分类 2 使用pytorch pytorch 非常简单就可以做到训练和加载 2.1 准备数据 如上图所示&#xff0c;用来训练的文件放在了train中&#xff0c;验证的文件放在val中&#xff0c;train.txt 和 val.txt 分别放文件名称和分类类别&#xff…

【JavaSE】初步认识

目录 【1】Java语言概述 【1.1】Java是什么 【1.2】Java语言重要性 【1.3】Java语言发展简史 【1.4】Java语言特性 【1.5】 Java开发环境安装 【2】初识Java的main方法 【2.1】main方法示例 【2.2】运行Java程序 【3】注释 【3.1】基本规则 【3.2】注释规范 【4】…

C语言--消失的数字

文章目录 1.法一&#xff1a;映射法2.法二&#xff1a;异或法3.法三&#xff1a;差值法4.法四&#xff1a;排序查找 1.法一&#xff1a;映射法 时间复杂度&#xff1a;O&#xff08;N&#xff09; 空间复杂度&#xff1a;O&#xff08;N&#xff09; #include<stdio.h>…

Tree树形控件做权限时,保持选项联动的同时,解决数据无法回显的问题

项目需求&#xff1a; 要求树形控件要有父子联动&#xff0c;也就是选择父级选项&#xff0c;子级也要选中&#xff0c;那么check-strictly属性就不能设置死,我的是 :check-strictlycheckStrictly,在data中赋值有变量。我之前设置check-strictly&#xff0c;就没了联动效果&…

补码的定义

补码的定义 补码的概念引入 补码的定义 例题

智能相机的功能介绍

智能视觉检测相机主要是应用在工业检测领域图像分析识别、视觉检测判断。相机具有颜色有无判别、颜色面积计算、轮廓查找定位、物体特征灰度匹配、颜色或灰度浓淡检测、物体计数、尺寸测量、条码二维码识别读取、尺寸测量、机械收引导定位、字符识别等功能。相机带有HDMI高清视…

设计模式3:单例模式:volatile关键字能不能解决多线程计数问题?

先说结论不能&#xff1a; 代码实测下&#xff1a; public class Counter {public volatile static int count 0;public static void inc() {//这里延迟1毫秒&#xff0c;使得结果明显try {Thread.sleep(1);} catch (InterruptedException e) {}count;}public static void ma…

6.4.4 观察文件类型:file

如果你想要知道某个文件的基本数据&#xff0c;例如是属于 ASCII 或者是 data 文件&#xff0c;或者是 binary&#xff0c; 且其中有没有使用到动态函数库 &#xff08;share library&#xff09; 等等的信息&#xff0c;就可以利用 file 这个指令来检阅。举例来说&#xff1a;…

Linux vs windows 他们之间的区别

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 前言 一.windows与Linux区别 二.Linux与Windows操作对比 三.Linux与Windows命令 …

如何克服自动化测试中的壁垒和问题?

随着自动化测试技术的快速发展和普及&#xff0c;自动化测试已经成为各个行业广泛应用的重要测试手段。然而&#xff0c;自动化测试中仍然存在壁垒和问题&#xff0c;这些问题可能对测试效果产生影响&#xff0c;甚至会影响整个项目的进程。在本文中&#xff0c;我们将探讨如何…

Mysql批量插入1000条数据

使用mysql的存储过程 1.现有如下一张表&#xff1a;site_row 2.创建存储过程 CREATE PROCEDURE p01 () BEGIN declare i int; set i1;while i<1000 doINSERT INTO site_row(row_id,row_num) VALUES ( i,i);set ii1; end WHILE;END; 3.执行存储过程 CALL p01(); 4.查看效…

UE4/5动画系列(3.通过后期处理动画蓝图的头部朝向Actor,两种方法:1.通过动画层接口的look at方法。2.通过control rig的方法)

目录 蓝图 点积dot Yaw判断 后期处理动画蓝图 动画层接口 ControlRig: 蓝图 首先我们创建一个actor类&#xff0c;这个actor类是我们要看的东西&#xff0c;actor在哪&#xff0c;我们的动物就要看到哪里&#xff08;同样&#xff0c;这个我们也是做一个父类&#xff0…

爆肝整理,性能测试-测试工具选型(各个对比)卷起来...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试和功能测…

Elasticsearch:跨集群复制应用场景及实操 - Cross Cluster Replication

通过跨集群复制&#xff08;Cross Cluster Replication - CCR&#xff09;&#xff0c;你可以跨集群将索引复制并实现&#xff1a; 在数据中心中断时继续处理搜索请求防止搜索量影响索引吞吐量通过在距用户较近的地理位置处理搜索请求来减少搜索延迟 跨集群复制采用主动 - 被…

2核4G服务器_4M带宽_CPU性能测评_60G系统盘

阿里云2核4G服务器297元一年、4M公网带宽、60G系统盘&#xff0c;阿里云轻量应用服务器2核4G4M带宽配置一年297.98元&#xff0c;2核2G3M带宽轻量服务器一年108元12个月&#xff0c;如下图&#xff1a; 目录 阿里云2核4G4M轻量应用服务器 2核4G服务器限制条件 轻量服务器介…