cathy 在 提交
这片文章主要是讲解原子给的代码里面的串口中断的中断函数,前面是我个人的学习路径。
关于STM32串口的学习,主要分为以下几个点:
1、USART的功能和内部结构。
功能包括同步、异步模式、双工通信、半工通信等。
内部结构,参考这张图片,去把各个部分了解清楚就差不多了。
2、USART的相关寄存器,看一看有个映像就可以了,主要了解一下这些寄存器对应上面的图,设置哪一部分的就ok了。
3、USART的收发格式,波特率的设置,了解硬件流控制。
4、USART的中断请求与模式配置,这个比较重要,要认真看一下,因为你要根据这个来配置串口发生什么中断。
下面附一篇文章,供大家参考学习,文章的博主还是讲得挺详细的。
剩下的,再翻一下参考手册,应该没什么问题。
再附上一张USART的引脚图
这是我学习的路径。
基本上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的高位设置成状态标志位,低位用来计数。如图所示
图中可以看到,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。
我问了老师,也可以定义\n\r的协议,但是在Windows中应该不行,键盘上每个字符都有个ascll码,\就有一个ascll码占一个字节,当你输入\n\r时,实际上不是两个字节,是4个字节。
当然,输入\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)。