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

news2024/9/25 13:23:13

记录一次对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的核心和魅力所在。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麦语言是什么东东?怎么学?

麦语言&#xff08;M Language&#xff09;是一种用于处理数据的编程语言&#xff0c;最初由微软公司开发。它是Power Query&#xff08;数据提取和转换工具&#xff09;和Power BI&#xff08;商业智能工具&#xff09;中的一部分。麦语言支持对各种数据源进行查询、转换和清理…

农业温室大棚数据监控系统的设计与实现

1.引言 农业温室大棚作为现在农业发展的必要条件&#xff0c;将高新技术融入农业温室大棚也愈发的重要&#xff0c;对农业温室大棚数据的监控&#xff0c;将温室大棚智能化。本设计对温室大棚实现远程数据监控&#xff0c;自动化控制&#xff0c;对温室内的环境数据进行巡回检…

解决Springboot在启动时报错:不支持发行版本17

今天在创建新项目时控制台出现如下错误&#xff1a; 最后经过排查发现问题出现如下几点。将以下几点进行修改问题得以解决。 1.将红色箭头地方由17改为11 2.将maven的pom文件中 的javaversion由17改为113.将spingboot的版本调为2.7.5 如果以上还没有解决问题&#xff0c;可以尝…

机器视觉(图像处理)入门金典之图像数字化及处理方法

图像的数字化 一般的图像(模拟图像)不能直接用计算机来处理,必须首先转化为数字图像 把模拟图像分割成一个个称为像素的小区域,每个像素的亮度或灰度值用一个整数表示 数字化的含义: 使模拟图像的灰度、亮度和色彩数据化 图像数字化的步骤: 两个步骤: 1、在空间坐标…

时间序列分解 | Matlab改进的自适应噪声完备集合经验模态分解ICEEMDAN

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列分解 | Matlab改进的自适应噪声完备集合经验模态分解ICEEMDAN 部分源码 %--------------------

【百日冲大厂】第十九篇,牛客网选择题+编程题汽水瓶+ 查找两个字符串a,b中的最长公共子串(动态规划问题)

前言&#xff1a; 大家好&#xff0c;我是良辰丫&#xff0c;第十九篇,牛客网选择题编程题汽水瓶 查找两个字符串a,b中的最长公共子串(动态规划问题).&#x1f49e;&#x1f49e;&#x1f49e;生活就像一只盲盒&#xff0c;藏着意想不到的辛苦&#xff0c;当然也有万般惊喜的可…

【自动化测试】——Selenium (基于java)

前言 小亭子正在努力的学习编程&#xff0c;接下来将开启软件测试的学习~~ 分享的文章都是学习的笔记和感悟&#xff0c;如有不妥之处希望大佬们批评指正~~ 同时如果本文对你有帮助的话&#xff0c;烦请点赞关注支持一波, 感激不尽~~ 目录 一、认识Selenium 1.什么是自动化测…

原码的表示

原码表示 定点整数源码与定点小数源码 源码表示例题 正数与负数转换直接将高位变为1即可 原码的性质 原码的优缺点 乘除法直接符号位异或&#xff0c;数值相乘除即可加法与减法需要先判断两个数值的大小然后确定符号位

软件开发项目延期就天天加班,你认为有效吗?

目录 一、软件开发项目延期的因素 1.1 客户需求变更 1.2 开发人员变动 1.3 技术瓶颈 1.4 对外沟通问题 二、相应的解决方案 2.1 需求变更管理机制 2.2 公司内部人员培训和团队建设 2.3 技术难题攻关 2.4 优化沟通流程 三、总结 软件开发项目延期时加班并不是一个长期…

C++之std::forward模板函数用法(一百四十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

2023.7.2-逆向显示键入的整数

功能&#xff1a;输入一个整数(多位)&#xff0c;逆向显示输入的结果。 程序&#xff1a; int main() {int a;printf("请输入一个整数&#xff1a;");scanf("%d",&a);if (a < 0)printf("请输入一个正整数");else{while (a>0){printf…

力扣 -- 931. 下降路径最小和

题目链接&#xff1a;931. 下降路径最小和 - 力扣&#xff08;LeetCode&#xff09; 下面是用动态规划的思想解决这道题的过程&#xff0c;相信各位小伙伴都能看懂并且掌握这道经典的动规题目滴。 参考代码&#xff1a; class Solution { public:int minFallingPathSum(vect…

动态规划 DP (四) 子序列问题

5.子序列问题 1&#xff09; 力扣https://leetcode.cn/problems/longest-increasing-subsequence/解题思路&#xff1a;因为子序列&#xff0c;其实就意味着要进行两层遍历了&#xff0c;分别列举子序列的结尾坐标和开始坐标&#xff0c;这样才能遍历完所有子序列。然后遍历的…

Linux学习-实操篇

Linux学习 实操篇 学自尚硅谷武晟然老师&#xff0c;结合老师课堂内容和自己笔记所写博文。 文章目录 Linux学习实操篇Shell介绍文件目录一、目录操作命令1.1 查看和切换工作目录1.2 列出目录内容1.3 创建和删除目录 二、文件操作命令2.1 创建文件2.2 复制文件或文件夹2.3 删除…

Django之ORM与MySQL对比

ORM 把类映射成数据库中的表&#xff0c;把类的一个实例对象映射成数据库中的数据行&#xff0c;把类的属性映射成表中的字段&#xff0c;通过对象的操作对应到数据库表的操作&#xff0c;实现了对象到SQL、SQL到对象的转换过程。 下面以一个商品库存明细表 myfirstapp_sku&…

spring boot/spring cloud项目中创建Moudel子模块出现 Ignored pom.xml

出现问题&#xff1a; 在 IDEA 项目的父工程中创建了子模块&#xff0c;但是由于子模块创建有误&#xff0c;删了后重新创建&#xff0c;出现了 Ignored pom.xml 的问题。 分析问题&#xff1a; 分析原因&#xff1a;应该是 IDEA 觉得这个项目已经被你删除了&#xff0c;所以…

pandas (十) 缺失值的处理:填充、删除、过滤、查询

Pandas使用函数处理缺失值 isnull和notnull&#xff1a;检测是否是空值&#xff0c;可用于df和seriesdropna&#xff1a;丢弃、删除余缺失值 axis: 删除行还是列&#xff0c;{0 or ‘index’, 1 or ‘columns’), default 0 how: 如果等于any则任何值为空都删除&#xff0c;如…

630到期,全面整改完成!

2023年6月30日对于消费金融行业的同学来讲&#xff0c;是一个大日子。 因为在2022年7月12日的时候&#xff0c;银监会特意下发了一个通知&#xff0c;《提升金融服务质效的通知》&#xff0c;原文见文末。 里面要求各金融机构进行相应整改&#xff0c;而整改的截止日期就是2023…

C++ 多线程

多线程是多任务处理的一种特殊形式&#xff0c;多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下&#xff0c;两种类型的多任务处理&#xff1a;基于进程和基于线程。 基于进程的多任务处理是程序的并发执行。基于线程的多任务处理是同一程序的片段的并发执行。…

Unity GameFramework StarForce 流程介绍

一、游戏总入口 GameEntry 1.内建好的GameEntry.Builtin 提供了各种框架的组件封装 2.自定义GameEntry.Custom 根据提供的案例参考即可实现自己的组件 3.游戏入口GameEntry 二、实现自己的组件并注册到管理类中 我们自己的组件只需要继承UnityGameFramework.Runtime.Gam…