STM32串口中断函数的逻辑理解

cathy的头像

这片文章主要是讲解原子给的代码里面的串口中断的中断函数,前面是我个人的学习路径。

关于STM32串口的学习,主要分为以下几个点:

1、USART的功能和内部结构。

功能包括同步、异步模式、双工通信、半工通信等。

内部结构,参考这张图片,去把各个部分了解清楚就差不多了。

“STM32串口中断函数的逻辑理解"

2、USART的相关寄存器,看一看有个映像就可以了,主要了解一下这些寄存器对应上面的图,设置哪一部分的就ok了。

“STM32串口中断函数的逻辑理解"

3、USART的收发格式,波特率的设置,了解硬件流控制。

4、USART的中断请求与模式配置,这个比较重要,要认真看一下,因为你要根据这个来配置串口发生什么中断。

下面附一篇文章,供大家参考学习,文章的博主还是讲得挺详细的。

STM32系统学习——USART(串口通信)

剩下的,再翻一下参考手册,应该没什么问题。

再附上一张USART的引脚图

“STM32串口中断函数的逻辑理解"

这是我学习的路径。

基本上STM32的串口配置都差不多,基本上都是直接参考的原子或者野火的代码,但是关于原子给的串口中断函数的理解,我找了找,基本没找到什么比较详细的文章。

我也是花了一点点时间才搞清楚这段代码的逻辑。

串口配置的代码直接用原子的,如果你已经很熟悉配置,可以直接跳过看后面串口中断函数解释。

头文件

#define USART1_REC_LEN		200  	//定义最大接收字节数 200
extern u8  USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 
extern u16 USART1_RX_STA;         		//接收状态标记
void USART1_Init(u32 bound);

配置代码

/*******************************************************************************
* 函 数 名         : USART1_Init
* 函数功能		   : USART1初始化函数
* 输    入         : bound:波特率
* 输    出         : 无
*******************************************************************************/ 
void USART1_Init(u32 bound)
{
   //GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
 
	
	/*  配置GPIO的模式和IO口 */
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;//TX			   //串口输出PA9
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;	    //复用推挽输出
	GPIO_Init(GPIOA,&GPIO_InitStructure);  /* 初始化串口输入IO */
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;//RX			 //串口输入PA10
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;		  //浮空输入
	GPIO_Init(GPIOA,&GPIO_InitStructure); /* 初始化GPIO */
	

	//USART1 初始化设置
	USART_InitStructure.USART_BaudRate = bound;//波特率设置
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
	USART_Init(USART1, &USART_InitStructure); //初始化串口1
	
	USART_Cmd(USART1, ENABLE);  //使能串口1 
	
	USART_ClearFlag(USART1, USART_FLAG_TC);
		
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启接收中断

	//Usart1 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器、	
}

如果其中的一些函数不懂,查固件库手册就好了,固件库中有函数详细的说明。

上面的配置代码中强调一点

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启接收中断

这行代码,这个串口中断函数第二个参数,要仔细去查一下固件库手册,它还有其他的参数,可以不配置成接收中断。

下面是中断函数

首先强调一点0x0d和0x0a在ASCII码表里面表示\r和\n,分别表示回车和换行

/*******************************************************************************
* 函 数 名         : USART1_IRQHandler
* 函数功能		   : USART1中断函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/ 
//0x0d和0x0a在ASCII码表里面表示\r和\n,分别表示回车和换行
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
1	u8 r;
2	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断
3	{
4		r =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的数据
5		if((USART1_RX_STA&0x8000)==0)//接收未完成
6		{
7			if(USART1_RX_STA&0x4000)//接收到了0x0d
8			{
9				if(r!=0x0a)USART1_RX_STA=0;//接收错误,重新开始
10				else USART1_RX_STA|=0x8000;	//接收完成了 
11			}
12			else //接收到0X0D之前或0x0D
13			{	
14				if(r==0x0d)USART1_RX_STA|=0x4000;
15				else
16				{
17					USART1_RX_BUF[USART1_RX_STA&0X3FFF]=r;
18					USART1_RX_STA++;
19					if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//接收数据错误,重新开始接收	  
				}		 
			}
		}   		 
	} 
} 

为了方便讲解,我在每行代码前加上了序号,移植的时候去掉就可以了。

其中

USART1_RX_BUF[],USART1_RX_STA在头文件中定义过了

#define USART1_REC_LEN		200  	//定义最大接收字节数 200
extern u8  USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 
extern u16 USART1_RX_STA;         		//接收状态标记

先说说这段代码的整体设计思路,这段代码是定义了一个协议,你所向单片机发送的字符必须以“/r/n”结尾(关于/r和/n,文章最后有补充说明),然后在中断函数里面通过判断\r和\n的ascll值使定义的 USART1_RX_STA 这个“状态寄存器”的相应位置1,然后在主函数里面判断 USART1_RX_STA这个“状态寄存器”的标志位的值,来发送数据。

看完之后是不是觉得很神奇,USART1_RX_STA明明是定义的一个变量,怎么就变成“寄存器了”,这就是设计师奇妙的设计,定义的USART1_RX_STA是两个字节的变量,有16个位可以用,但是我们的最大接收字节只要200个,就算有1000个字节我们也用不到2的15次方的大小,字节再多STM32就没这么大的存储空间了,所以200个字节已经足够我们一次接收的数据了。显然16个位我们是用不完的,所以我们可以把USART1_RX_STA的高位设置成状态标志位,低位用来计数。如图所示

“STM32串口中断函数的逻辑理解"

图中可以看到,0-13位用来记录字符的长度,高两位作为状态标志位。

下面开始正式讲解代码。

1、当接收到数据时,会产生接收中断,进入中断函数,将接收到的数据存入变量r中,注意,r只占一个字节。

2、代码第5行,判断最高位的状态,USART1_RX_STA最高位是接收到\n(0x0a),才会置1的状态标志位,当它置1时,标志着整个数据已经接收完毕,main中就是判断这个位是否为1来进行发送数据的。很显然,能够进入中断,那说明接收还未完成,所以接下来就是执行第5行下面的代码。

3、第7行是判断USART1_RX_STA次高位,USART1_RX_STA次高位是否置1,这要取决于是否接收到了倒数第2个字符0x0d。

  • 如果上一次接收中断接收到了0x0d,就会把次高位置1,代码第7行检测到了USART1_RX_STA次高位置1后,就会进入if里面(执行代码第9或者第10行),判断数据最后一位是否是0x0a(\n),如果最后一位不是0x0a,那说明这串字符不是按照程序所定义的协议发送给单片机的,单片机自然也就识别不了,这个数据就会被丢掉(代码第9行),USART1_RX_STA清零(包括里面的记录的数据长度和状态标志位),重新接收新的发送过来的数据;如果最后一位是0x0a,那这个字符串就是按照协议发送给单片机的,程序执行第10行,将USART1_RX_STA的最高位置1,标志这串字符串接收完毕。USART1_RX_STA最高位置1后,main函数中就可以检测它进行操作了。
  • 如果上一次接收中断未接收到了0x0d,那么USART1_RX_STA次高位就不会被置1,在程序走到第7行时,检测到USART1_RX_STA次高位为0,程序就会执行第12行的else。

4、程序第12行的else中,要判断接收到的数据是0x0d还是0x0d之前的数据。

  • 如果这个数据是0x0d,那么就将USART1_RX_STA次高位置1,在下一次进入接收中断时,就会执行上述3中的步骤了,程序第7行就能够检测到USART1_RX_STA次高位的状态了。
  • 如果这个数据是0x0d之前的数据,就执行17-19行代码,这三行代码的作用是将这个字节存入USART1_RX_BUF[USART1_REC_LEN]数组(头文件中定义过了)中,记录字符的长度,判断字符长度是否发生溢出(最大200字节)。
  • USART1_RX_BUF[USART1_RX_STA&0X3FFF]=r;说明一下这行代码中的USART1_RX_STA&0X3FFF,USART1_RX_STA的0-13位时用来记录数据长度的,这句话的意思就是去掉了USART1_RX_STA中的高两位状态位,剩下的数据与数组的值相对应。

5、看了上面4点解释脑子里是不是还是很乱,这也就是这段代码难的地方,不能够顺序的去看这个代码,不然还是理不清逻辑,这个串口中断,就相当于一个循环,要判断到接收数据完毕了才会结束这个循环,怎么判断结束是否完毕呢?就看USART1_RX_STA的高两位的状态。下面再来屡屡逻辑。

  • [0] 来了一串数据,假设有50个字符,占50个字节,再加上协议定义的结尾\r\n,一共是52个字节。
  • [1] 当接收到0x0d(\r,也是第51个字符)之前的字符,串口中断函数执行的过程是前4行执行完,第5行条件成立进入if,执行到第7行时,条件不成立,转至12行,此时14行进行判断,这个字符不是0x0d,条件不成立,执行17-19行,将数据存入数组中。
  • [2] 当0x0d前面的数据接收完时,下一个到来的字符是0x0d(\r)时,此时USART1_RX_STA的最高位和次高位都还是0,程序执行到第7行时,条件依然不会成立,程序会跳转置12行的else,此时程序14行的判断成立,会将USART1_RX_STA的次高位置1,然后结束这次中断。
  • [3] 接着上面的步骤,0x0d过了是最后一个字符0x0a(\n),当接收到这个字符时,第5行条件成立,进入if,执行到程序第7行时,由于上一步接收到了0x0d,已经把USART1_RX_STA的次高位置1了,程序就进入if,执行8-11行的代码,判断是否是0x0a,如果不是,执行程序第9行,如果是,执行第10行,将USART1_RX_STA最高位置1,此时整个数据接收就完成了,剩下的交给主函数处理了。

接下来是主函数的处理程序了,把这个串口中断函数理解清楚了,主函数也就好理解了。

int main()
{
	u16 t=0;
	u16 len=0;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
	USART1_Init(115200);
	
	while(1)
	{
		if(USART1_RX_STA&0x8000)
		{					   
			len=USART1_RX_STA&0x3fff;//得到此次接收到的数据长度
			for(t=0;t<len;t++)
			{
				USART_SendData(USART1, USART1_RX_BUF[t]);         //向串口1发送数据
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
			}
			USART1_RX_STA=0;
		}
		
}

1、主函数while里面,首先判断USART1_RX_STA的最高位,看数据是否接收完毕,再进行操作。
2、len是记下的数据长度,用于for循环中数据发送的次数(因为每次只能发送一个字节)。
3、for循环中的while是用的串口发送完成的标志位来判断的是否发送完毕。
4、最后一定要把USART1_RX_STA置0,方便下次的数据接收,这很重要!!!

补充,关于\r和\n

  • 回车 \r 本义是光标重新回到本行开头,r的英文return,控制字符可以写成CR,即Carriage Return。ASCII码13(0x0d)。
  • 换行 \n 本义是光标往下一行(不一定到下一行行首),n的英文newline,控制字符可以写成LF,即Line Feed。ASCII码10(0x0a)。

Windows系统中,我们键盘上的enter键,按下就是\r\n。所以也是为什么要在串口中定义这个协议。当输入一串字符之后按下回车键,点击发送,可以在右边的框中看到最后两个字符的十六进制码是0x0d和0x0a。

“STM32串口中断函数的逻辑理解"

我问了老师,也可以定义\n\r的协议,但是在Windows中应该不行,键盘上每个字符都有个ascll码,\就有一个ascll码占一个字节,当你输入\n\r时,实际上不是两个字节,是4个字节。

“STM32串口中断函数的逻辑理解"

当然,输入\r\n也不行,只要摁enter键。

最后,总结一下吧,这里面用得最好的就是定义了一个“状态寄存器USART1_RX_STA”,我也是第一次见到,原来变量还可以这么用,在其他的开发板,或者实现其他的功能中都可以用这种方式来优化程序。
————————————————
版权声明:本文为CSDN博主「轻染QAQ」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_47586883/article/details/110959012
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。