【STM32F4】五、串口通信2——软件部分(以USART为例)


参考正点原子的视频教程,本文我们将编写一段以USART作为通信串口、接收到数据后立即引发中断、并执行中断处理函数将数据发送给MCU的程序。

源码请参考正点原子-实验4-串口实验。

一、什么是USART

1. USART简介

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)的全称为通用同步/异步串行接收/发送器,它与普通串口UART的不同在于,USART有同步、异步两种工作模式;而UART是经过裁剪后的USART,只有异步工作模式。

2. STM32F4中的USART

2.1 USART的发送/接收引脚

STM32F4中有两个USART(USART1、USART2),其中,我们以USART1为例,它的发送、接收PA9、PA10引脚相连,从如下的GPIO引脚复用图中可以看出,PA9可以复用为USART1的发送(TX)功能,而PA10可以复用为USART1的接收(RX)功能。
在这里插入图片描述

2.2 USART转为USB接口

单片机通常需要与电脑互相传输数据,但是电脑没有USART接口,这该怎么办呢?设计者通常通过一个芯片把USART接口转换为USB接口,这样就可以与电脑通信了。

能实现USART转USB的芯片有很多,STM32F4中使用的是CH340G
在这里插入图片描述
STM32F4中USART1USB的原理图如下所示:
在这里插入图片描述
其中,TXD/RXD 是相对于 CH340G 来说的,也就是 USB 串口的发送和接受脚。而 USART1_RX/USART1_TX 则是相对于 STM32F407ZGT6 来说的。这样,通过对接,就可以实现 USB 串口和 STM32F407ZGT6 的串口通信了。

注: 在本文的实验中,就是用USB串口把USB信号转换为串口信号,进而通过PA9和PA10的复用功能来与单片机进行通信。

二、常用的串口相关寄存器

常用的串口相关寄存器有三个

  • USART_SR 状态寄存器,用来记录一些状态,如是否接收到数据,是否要发送数据等
  • USART_DR 数据寄存器,用来存储数据,包括要接收的数据和要发送的数据等
  • USART_BRR 波特率寄存器,用来调整波特率的大小。

其中,USART_SR和USART_DR的各个位的含义在 《STM32F4xx中文参考手册》26.6节可以查到, 本文不再赘述。这里只简要介绍一下USART_BRR波特率寄存器的内部结构:

在上篇博文【STM32F4】四、串口通信1——硬件部分中我们在第部分列出过,下面我们只把波特率发生器的硬件部分展示在下图中:
在这里插入图片描述
首先由USART_BRR寄存器产生初始的时钟信号,假设频率为** f ;输入到分频系数为USARTDIV的分频器后,输出信号频率变为 f / USARTDIV**;在经过采样除法器后,最终输出信号的波特率为** f / USARTDIV / [8 x (2 - OVER8)] **,其中,OVER8可人为设置。

其中,初始频率 f 通常是固定的,OVER8 通常设为0;那么公式就简化为波特率 = f / USARTDIV / 16。而为了得到最终的波特率,我们要求的其实就是唯一的可变参数USARTDIV,实际上它也是由USART_BRR寄存器决定的。

但在程序中,我们不需自己计算USART_BRR的配置。我们只要把想要的波特率(如115200)直接写入程序、传给相应函数即可,STM32F4提供的库函数会帮我们计算并对USART_BRR进行配置。

三、程序编写

1. 串口配置的一般步骤

根据正点原子课程,列出串口配置(带中断响应)的一般过程如下:

必要的时钟使能

  • 串口时钟使能:RCC_APBxPeriphClockCmd();
  • GPIO时钟使能:RCC_AHB1 PeriphClockCmd();
    注:要想使用一个外设,必须要对【外设】、以及【连接外设的GPIO引脚】的时钟进行使能。

引脚复用映射:GPIO_PinAFConfig():
GPIO端口模式设置:GPIO_Init(); //模式设置为GPIO_Mode_AF
串口参数初始化:USART_Init(); //配置波特率等参数
开启中断并且初始化NVIC(如果需要开启中断才需要这个步骤):

  • NVIC_Init();
  • USART_ITConfig();

使能串口:USART_Cmd();
编写中断处理函数:USARTx_IRQHandler();
串口数据收发:

  • void USART_SendData(); //从DR寄存器中将数据发送出去
  • unit16_t USART_ReceiveData(); //从DR寄存器读取接收到的数据

:串口传输状态获取:

  • FlagStatus USART_GetFlagStatus();
  • void USART_ClearITPendingBit();
    在这里插入图片描述

下面我们也将按照上述步骤,一一编写程序。

2. 编写程序

下面我们把经过详细注释的代码放上来,全都是按照上述九个步骤来写的:

#include "stm32f4xx.h"
#include "usart.h"
#include "delay.h"

void My_USART1_Init(void) //配置和初始化的程序,除中断处理函数外,其他的配置都在这里面
{
    
    
	GPIO_InitTypeDef  GPIO_InitStructure; //用于GPIO配置的结构体
	USART_InitTypeDef* USART_InitStruct; //用于USART配置的结构体
	NVIC_InitTypeDef* NVIC_InitStruct; //用于NVIC配置的结构体
	
//===============================一、串口时钟使能===================================
	//使能USART1,由于USART1挂载在APB2总线下,所以要去RCC相关的库函数中搜索APB2的时钟使能函数
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

//=================================GPIO时钟使能=====================================
	//因为要通过PA9和PA10的复用功能来使用UART1,所以也要使能GPIOA的时钟,GPIOA挂载在AHB1总线下
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	
//===============================二、引脚复用映射===================================
	//通过下面这个函数,把PA9配置为复用功能——USART1_TX,PA10配置为复用功能——USART1_RX
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
	
//=============================三、端口模式设置==================================
	//下面要配置PA9和PA10配置为复用模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //GPIO_Mode_OUT;  //AF即复用模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

//================================四、串口参数初始化=================================	
	//初始化USART1的配置
	USART_InitStruct->USART_BaudRate = 115200; //波特率
	USART_InitStruct->USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用硬件流控制
	USART_InitStruct->USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //把发送和接收功能都进行使能
	USART_InitStruct->USART_Parity = USART_Parity_No;//不使用奇偶校验
	USART_InitStruct->USART_StopBits = USART_StopBits_1;//使用1个停止位
	USART_InitStruct->USART_WordLength = USART_WordLength_8b;//因为没有奇偶检验,所以可以使用8位字长
	USART_Init(USART1, USART_InitStruct);
	
	//====================================================================
	//=============如果不使用中断,那么这个程序到这里就可以结束了=============
	//====================================================================
	
//=============================五、开启中断并且初始化NVIC============================
	//配置NVIC
	//首先设置中断优先级分组
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//再初始化NVIC
	NVIC_InitStruct->NVIC_IRQChannel = USART1_IRQn;//不同的通道定义在顶层头文件stm32f4xx.h中   //设置NVIC通道为USART1的通道
	NVIC_InitStruct->NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级为1
	NVIC_InitStruct->NVIC_IRQChannelSubPriority = 1;//设置响应优先级为1
	NVIC_Init(NVIC_InitStruct);

//==================================六、使能串口====================================
	//使能USART1
	USART_Cmd(USART1, ENABLE);
	//使能USART1的某种中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//把接收非空中断USART_IT_RXNE使能,即一旦接收到了信息,就引发中断,且执行相应的中断函数

	//下一步就要在下面定义USART1的中断服务函数,函数名是固定的,在官方的系统系统文件中startup_stm32f40_41xxx.s已经给出:USART1_IRQHandler
}

//================================七、编写中断处理函数===============================
void USART1_IRQHandler(void)
{
    
    
	u8 res; //用来记录接收到的数据,因为我们在上面设置的8位字长代表一个数据,所以这里可以用u8来记录?

//==============================九、串口传输状态获取=================================
	if(USART_GetITStatus(USART1, USART_IT_RXNE)) //读取USART_IT_RXNE标志位的状态)
	{
    
    
//================================八、串口数据收发==================================
		res = USART_ReceiveData(USART1); //读取接收到的数据
		USART_SendData(USART1, res); //把接收到的数据发出去
	}
}


//主函数
int main(void)
{
    
    
	My_USART1_Init();
	while(1); //在这里无限循环即可,程序会自动执行 中断 和 中断处理函数
	
	return 0;
}

注: 如果编译时程序出错,可能是因为在官方库文件usart.c里已经定义过一个中断处理函数USART1_IRQHandler(),把它注释掉或者把这个文件删掉即可。
在这里插入图片描述

四、更复杂的程序(USART_RX_STA寄存器的应用)

我们在上面第三部分写的程序中,在中断处理函数里我们只是简单地把每个接收到的字符发送出去,没有做其他处理。但是,这往往不能满足工程师们的需求。

工程师们在使用串口时,通常要求在写完一段话后才进行发送(而不是每收到一个字符就发送出去),而且一定要有一个结束标志(如回车键Enter),当遇到结束标志时才进行发送,而不是像第三部分中机械地每收到一个字符就发送一个。

这就要求在以上程序的基础上,加入如下功能:

  • 自己设计一个结束标志,以让单片机明白待发送的信息到哪里就结束了;
  • 有一个buffer,用来存储我们输入的这一段话(这个buffer通常时我们自己创建的一个数组),知道遇到结束标志,才把这段话发送出去。

为了实现以上能存储一段话、且有结束标志的功能,STM32F4为我们提供了一个很方便的寄存器:USART_RX_STA

1. USART_RX_STA 寄存器简介

USART_RX_STA寄存器共有16位,即0 ~ 15,每个位的作用如下图所示:
(下图源自正点原子《STM32F4开发指南——库函数版本》 5.3.3小节——USART1_IRQHandler 函数)
在这里插入图片描述
其中,0 ~ 13位用来表示接收到的有效数据个数,由于其有14个二进制位,因此最大的可保存数据为 2 ^14 -1,也就是说我们一段话最多可以包含 2 ^14 -1 个字符;

14 位用来表示是否接收到了某个标志符(图中的 0X0D 表示回车符,也就是说当接收到回车符时,要把第** 14** 位置 1),我们通常将回车符或者是回车符和某个字符的组合设为结束标志。

15 位用来表示是否接收完成,也就是要根据我们人为设计的结束标志(如回车+ c) 来判断,如果遇到了结束标志,则需要把这一位置1,对接收的信息处理完成后再把这一位置0

2. 程序编写

2.1 程序思路

从上面的描述中可以看出,要使用USART_RX_STA寄存器,我们需要始终对 此寄存器的几个标志位串口接收到的字符 进行监控更新

现在假设我们把标志为设为 ** 0X0D + 0X0A**,即回车键 + 换行键(其实只要在键盘上按下一次回车键就够了),那么我们要做的监控更新操作如下:

  • 每接收到一个字符,就把字符添加用以保存句子的buffer中,并把 0 ~ 13 位保存的数字加1,用以记录有效数据的个数;
  • 监控接收到的字符,如果接收到了0X0D(回车符),则把第 14位置1;
  • 监控接收到的字符,如果接收到了0X0A,则判断第 14 位是否为1,如果是1,表示上一个接收到的字符是回车符,当前接收到的字符是换行符,表示句子结束,这时候我们就把buffer中的信息拿来处理,并把寄存器的所有位都置0,重新开始工作;而如果当前接收到了0X0A,但第 14 位并不是1,那么表示出现了错误,则把buffer清空,且寄存器的所有位0,重新开始工作。

2.2 编写程序

上述所有工作,都与我们的初始化函数My_USART1_Init没有关系,初始化只是一个初始化功能,没有监督更新的作用;

而上述所有监督更新的工作,我们都在中断处理函数主(main)函数中完成,如下,先展示出中断处理函数

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
    
    
	u8 Res;
#if SYSTEM_SUPPORT_OS 		//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntEnter();    
#endif
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //根据USART_IT_RXNE标志位来确认是否接收到了新字符
	{
    
    
		Res =USART_ReceiveData(USART1);//(USART1->DR);	//读取接收到的新字符
		
		if((USART_RX_STA&0x8000)==0)//USART_RX_STA&0x8000,即读取USART_RX_STA的第15位(1000 0000 0000 0000,其实是第16位,但是用 0 ~ 15 来编号的话,我们把它叫做第15位)
		{
    
    
			if(USART_RX_STA&0x4000)//USART_RX_STA&0x4000,即读取USART_RX_STA的第14位,判断上一次接收到的字符是否是0x0d
			{
    
    
				if(Res!=0x0a)USART_RX_STA=0;//如果上一次是0x0d,但这次不是0x0a,则表示接收错误,USART_RX_STA全部清0,重新开始
				else USART_RX_STA|=0x8000;	//如果上一次是0x0d,且这一次是0x0a,表示接收到了结束标志位,当前的句子接收完成,把第15位置1
			}
			else //还没收到0X0D
			{
    
    	
				if(Res==0x0d)USART_RX_STA|=0x4000;//如果这次接收到的是0x0d,则把USART_RX_STA的第14位置1
				else
				{
    
    
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;//如果之前没有接收到0x0d,且这一个接收到的字符也不是0x0d,则表示当前这个字符是普通消息,则把它存在buffer(USART_RX_BUF)的第USART_RX_STA&0X3FFF中。其中,USART_RX_STA&0X3FFF是我们当前记录的字符个数,即USART_RX_STA的0 ~ 13位(0011 1111 1111 1111)
					USART_RX_STA++; //当没有接收到0x0d时,USART_RX_STA的第14、15位都是0,只有前13位有数据,用以存储接收到的字符个数,因此这里直接把USART_RX_STA加1,用来记录字符个数,且不会影响到14、15位(因为它们都是0)
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//如果接收到的字符个数比可接收的最多数量USART_REC_LEN多了,则表示溢出、接收错误,重新开始接收	  
				}		 
			}
		}   		 
  } 
#if SYSTEM_SUPPORT_OS 	//如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
	OSIntExit();  											 
#endif
} 
#endif	

从以上程序中可以看到,中断处理函数主要做了监督并更新第14、15位,以及保存字符、且监督并更新 0 ~ 13 位

另外,关于程序中的#if SYSTEM_SUPPORT_OS OS,我在这篇帖子中找到了相关的解释:

我感觉它的作用是保护传输数据不被打断
如果有操作系统(对于STM32,一般是UCOS)的话,可能会涉及比串口中断优先级更高的中断,会打断串口传输,所以在进入串口中断后干脆就关掉总中断,等传完了再开中断,如果是这样的话中断结尾估计还得有个OSIntExit()。
这是一种保护临界段的手段。

那么,主(main)函数中需要做什么工作呢?

int main(void)
{
    
     
 
	u8 t;
	u8 len;	
	u16 times=0;  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
	delay_init(168);		//延时初始化 
	uart_init(115200);	//串口初始化波特率为115200
	LED_Init();		  		//初始化与LED连接的硬件接口  
	while(1)
	{
    
    
		if(USART_RX_STA&0x8000) //若检测到USART_RX_STA的第15位、即结束标志位为1,则表示当前信息传输结束
		{
    
    					   
			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
			printf("\r\n您发送的消息为:\r\n"); //把这句话通过串口打印到屏幕上
			for(t=0;t<len;t++)
			{
    
    
				USART_SendData(USART1, USART_RX_BUF[t]);         //向串口1发送数据
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
			}
			printf("\r\n\r\n");//插入换行
			USART_RX_STA=0; //把当前信息处理完后,将USART_RX_STA清0,一切重新开始
		}else //如果结束标志位为0.则表示当前信息传输还未结束
		{
    
    
			times++;
			if(times%5000==0)
			{
    
    
				//打印信息以提示工程师继续输入信息
				printf("\r\nALIENTEK 探索者STM32F407开发板 串口实验\r\n");
				printf("正点原子@ALIENTEK\r\n\r\n\r\n");
			}
			if(times%200==0)printf("请输入数据,以回车键结束\r\n");  
			if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
			delay_ms(10);   
		}
	}
}

整个主函数中,最主要的就是``while(1)`循环,它负责监督USART_RX_STA寄存器的结束标志位,如果结束标志位被置1,则表示当前信息传输结束,主函数就把从串口USART1接收到的信息再通过USART1发送出去;

如果当前USART_RX_STA的结束标志位不是1,则表示传输还未结束,主函数则打印提示信息,以提示工程师继续输入信息。

源码请参考正点原子-实验4-串口实验

猜你喜欢

转载自blog.csdn.net/qq_39642978/article/details/112135021