一、前言
串口作为STM32的重要外设,对程序调试具有不可替代的作用。通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。其主要具备以下特性:
- 支持全双工的异步通信
- 支持LIN(局部互联网)
- 支持智能卡协议和IrDA(红外数据组织)SIR ENDEC规范
- 支持调制解调器(CTS/RTS)操作
- 支持多处理器通信
- 支持使用多缓冲器配置的DMA方式,实现高速数据通信。
STM32F103ZET6具备多达5路串口,本文将使用USART1(PA9/PA10)实现串口回环测试DEMO。
二、串口通信原理与时序
2.1 USART和UART的区别
STM32F103ZET6具备5个串口,其中串口1,2,3均为USART(通用同步异步收发传输器),串口4,5为UART(通用异步收发传输器)。他们的区别如下:
- 同步与异步:UART仅支持异步通信,数据在没有时钟信号的情况下进行传输,发送和接收双方必须在波特率等设置上保持一致;USART支持同步和异步两种通信模式。在同步模式下,它使用额外的时钟信号来同步数据的发送和接收。
- 信号引脚:USART1、USART2和USART3接口具有硬件的CTS和RTS信号管理、兼容ISO7816的智能卡模式和类SPI通信模式,除了UART5之外所有其他接口都可以使用DMA操作。
- 数据传输可靠性:UART数据传输仅依赖于预设的波特率,没有时钟信号,因此数据传输的可靠性可能受到干扰;USART在同步模式下,数据传输更加可靠,因为有时钟信号来确保数据的正确接收和发送。USART1接口通信速率可达4.5兆位/秒,其他接口的通信速率可达2.25兆位/秒。
2.2 串口通信的硬件组成
USART1由6根线组成,如下所示:
| USART1线束 | 功能 | 
|---|---|
| USART1_TX | 发送数据引脚 | 
| USART1_RX | 接受数据引脚 | 
| USART1_CK | 用于输出同步信号传输的时钟 | 
| USART1_CTS | 用于流量控制的清除发送引脚,若是高电平,在当前数据传输结束时阻断下一次的数据发送。 | 
| USART1_RTS | 用于流量控制的发送请求引脚,若是低电平,表明USART准备好接收数据。 | 
| GND | 接地 | 
其中,同步模式下的CK引脚以及用于流量控制的CTS/RTS引脚不常用,而TX/RX/GND引脚是串口通信中不可或缺的,因此本文只关注这三个引脚。
2.3 串口通信的通讯方式
从传输数据的方向性和同时性上划分,串口通信主要被分为全双工、半双工和单工模式:
- 全双工模式:允许数据在两个通信设备之间同时双向传输。
- 半双工模式:数据可以在两个通信设备之间双向传输,但不能同时进行。
- 单工模式:数据只能单向传输,不能同时发送和接收。
STM32F103ZET6的USART1通讯方式是全双工的。
2.4 串口通信的时序要求和数据格式
串口通信的时序要求如下所示:

每一个字符帧都满足以下数据格式:
| 字符帧的位 | 含义 | 
|---|---|
| 起始位 | 每个数据帧的开始部分,通常是一个低电平信号。 | 
| 数据位 | 实际传输的数据部分,可以是8位、7位或9位,具体取决于配置。每个位依次从最低有效位(LSB)到最高有效位(MSB)传输(小端传输)。 | 
| 校验位 | 在数据位后添加一个校验位,可以帮助检测数据在传输过程中是否发生了错误。校验方式有奇偶校验。 | 
| 停止位 | 数据帧的结束部分,通常是一个高电平信号。一般来讲,停止位有1,1.5,2个单位时间三种长度。 | 
| 空闲位 | 处于逻辑1状态, 表示当前线路上没有数据传送。 | 
2.5 串口通信的奇校验和偶校验
在标准ASCII码中,其最高位(b7)用作奇偶校验位。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。
2.6 串口通信的波特率
波特率(Baud Rate)是串行通信中衡量数据传输速度的一个重要参数,定义为每秒钟传输的符号(信号变化)次数,通常以bps(bits per second,位/秒)为单位。
三、时钟树配置
从《STM32中文参考手册》可知,USART1是归属APB2总线下的外设,其时钟来源为APB2的外设时钟PCLK2。APB2时钟来源于AHB时钟,AHB时钟来源于经锁相环PLL倍频后的外部8MHz高速时钟HSE。具体时钟树配置流程如下所示。

时钟树配置代码在正点原子官方例程SYSTEM/sys.c下实现:
void Stm32_Clock_Init(u8 PLL)
{
	unsigned char temp=0;   
	MYRCC_DeInit();		  //复位并配置向量表
 	RCC->CR|=0x00010000;  //外部高速时钟使能HSEON
	while(!(RCC->CR>>17));//等待外部时钟就绪
	RCC->CFGR=0X00000400; //APB1=DIV2;APB2=DIV1;AHB=DIV1;
	PLL-=2;				  //抵消2个单位(因为是从2开始的,设置0就是2)
	RCC->CFGR|=PLL<<18;   //设置PLL值 2~16
	RCC->CFGR|=1<<16;	  //PLLSRC ON 
	FLASH->ACR|=0x32;	  //FLASH 2个延时周期
	RCC->CR|=0x01000000;  //PLLON
	while(!(RCC->CR>>25));//等待PLL锁定
	RCC->CFGR|=0x00000002;//PLL作为系统时钟	 
	while(temp!=0x02)     //等待PLL作为系统时钟设置成功
	{   
		temp=RCC->CFGR>>2;
		temp&=0x03;
	}    
}		    调用该函数时,函数传参PLL倍频为9, 最终串口1获得的基础时钟频率PCLK2为72MHz。
四、寄存器介绍
实现USART1的功能主要涉及以下寄存器:
| 寄存器 | 功能 | 
|---|---|
| APB2ENR |  
     APB2 外设时钟使能寄存器 
     | 
|  
      GPIOx_CRH 
      | 端口配置高寄存器 | 
| APB2RSTR |  
     APB2 外设复位寄存器 
     | 
| USART_BRR |  
     波特比率寄存器 
     | 
| USART_CR1 |  
     控制寄存器 1 
     | 
| USART_SR |  
     状态寄存器 
     | 
| USART_DR |  
     数据寄存器 
     | 
下面将对这些寄存器进行一一介绍。
4.1 APB2ENR外设时钟使能寄存器
《STM32中文参考手册》对APB2ENR寄存器的描述如下:



毋庸置疑,本文需要将USART1时钟使能,即将位14置1。
除此以外,本文用到的串口1的TX和RX分别是PA9和PA10,因此需要使能IO端口A的时钟,即将位2置1。
4.2 GPIOx_CRH端口配置高寄存器
《STM32中文参考手册》对GPIOx_CRH寄存器的描述如下:

本文使用PA9和PA10作为串口1的功能端口,因此需要按照如下要求进行配置:
| GPIO端口 | 复用功能 | 配置 | GPIO配置 | 
|---|---|---|---|
| PA9 |  
     USART1_TX 
     | 全双工模式 | 推挽复用输出 | 
| PA10 | USART1_RX | 全双工模式 | 浮空输入或上拉输入 | 
因此,需要按照如下进行配置:
GPIO端口 寄存器配置 PA9 GPIOA_CRH[7:6] = 0b10 GPIOA_CRH[5:4] = 0b11 PA10 GPIOA_CRH[11:10] = 0b10 GPIOA_CRH[9:8] = 0b00 
4.3 APB2RSTR外设复位寄存器
《STM32中文参考手册》对APB2RSTR寄存器的描述如下:


本次DEMO仅需将USART1复位即可,故仅需将 APB2RSTR的第14位 置1 后再 置0 即可复位。
4.4 USART_BRR波特比率寄存器
《STM32中文参考手册》对USART_BRR寄存器的描述如下:

STM32F103ZET6的USART1波特率计算方法如下:

若设置USART1的波特率为115200bps,按照如下方式求得USART_BRR寄存器的值:
- 由第三节时钟树配置可知,USART1的时钟来源PCLK2为72MHz。
- USARTDIV=fPCLK2/(16*bound)=72000000/(16*115200)=39.0625
- 故DIV_Mantissa[11:0]=39=0X27
- 故DIV_Fraction[3:0]=0.0625*16=1=0X1
- USART_BRR寄存器的值为(DIV_Mantissa[11:0]<<4)|(DIV_Fraction[3:0])=0X0271
4.5 USART_SR状态寄存器
《STM32中文参考手册》对USART_SR寄存器的描述如下:


其中,仅重点关注TXE、TC和RXNE标志位。当发送寄存器为空时,TXE为1;当发送完成时,TC为1,;当读寄存器非空,即接收到数据时,RXNE为1。
4.6 USART_CR1控制寄存器 1
《STM32中文参考手册》对USART_CR1寄存器的描述如下:




其中,仅需重点关注以下几位:
| 标志位 | 功能 | 用法 | 
|---|---|---|
| 13:UE | 串口使能位 | 置1 | 
| 12:M | 字长选择位,当该位为 0 的时候设置串口为 8 个字长外加 n 个停止  
     位,停止位的个数(n)是根据 USART_CR2 的[13:12]位设置来决定的,默认为 0。 
     | 置0 | 
| 10:PCE |  
     校验使能位,设置为 0,则禁止校验,否则使能校验。 
       
     | 置0 | 
| 9:PS |  
     校验位选择,设置为 0 则为偶校验,否则为奇校验 
     | / | 
| 7:TXEIE |  
     发送缓冲区空中断使能位,设置该位为 1,当 USART_SR 中的 TXE 位为1 时,将产生串口中断。 
     | 置1 | 
| 6:TCIE |  
     发送完成中断使能位,设置该位为 1,当 USART_SR 中的 TC 位为 1 时,将产生串口中断。 
     | 置1 | 
| 5:RXNEIE |  
     RXNEIE 为接收缓冲区非空中断使能,设置该位为 1,当USART_SR 中的 ORE 或者 RXNE 位为1时,将产生串口中断。 
     | 置1 | 
| 3:TE |  
     发送使能位 
     | 置1 | 
| 2:RE |  
     接收使能位 
     | 置1 | 
4.7 USART_DR数据寄存器
《STM32中文参考手册》对USART_DR寄存器的描述如下:


将数据放入TDR中即可发送,同理,可以从RDR中读出接收到的数据。
五、硬件连接

- 将以上跳线帽接好,即将USART1接到板载CH340 USB转串口芯片上。
- 将USB线接到USB_SLAVE端口。
六、程序设计
6.1 USART1初始化
本函数位于SYSTEM/usart.c/uart_init(),该函数主要进行串口波特率设置(参考4.4)、使能A口和串口1外设时钟(参考4.1),设置端口复用(参考4.2),复位串口1(参考4.3)、使能接收中断(参考4.6)。具体代码如下所示:
void uart_init(u32 pclk2,u32 bound)
{  	 
	float temp;
	u16 mantissa;
	u16 fraction;	   
	temp=(float)(pclk2*1000000)/(bound*16);//得到USARTDIV
	mantissa=temp;				 //得到整数部分
	fraction=(temp-mantissa)*16; //得到小数部分	 
    mantissa<<=4;
	mantissa+=fraction; 
	RCC->APB2ENR|=1<<2;   //使能PORTA口时钟  
	RCC->APB2ENR|=1<<14;  //使能串口时钟 
	GPIOA->CRH&=0XFFFFF00F;//IO状态设置
	GPIOA->CRH|=0X000008B0;//IO状态设置 
	RCC->APB2RSTR|=1<<14;   //复位串口1
	RCC->APB2RSTR&=~(1<<14);//停止复位	   	   
	//波特率设置
 	USART1->BRR=mantissa; // 波特率设置	 
	USART1->CR1|=0X200C;  //1位停止,无校验位.
#if EN_USART1_RX		  //如果使能了接收
	//使能接收中断 
	USART1->CR1|=1<<5;    //接收缓冲区非空中断使能	    	
	MY_NVIC_Init(3,3,USART1_IRQn,2);//组2,最低优先级 
#endif
}6.2 USART1接收中断函数
本函数位于SYSTEM/usart.c/USART1_IRQHandler(),主要通过定义一个标志USART_RX_STA,其bit15为接收完成标志位,bit14为接收到0X0d标志位,bit13-bit0为Buf计数。接收到的数据需要以0X0d 0X0a结尾。具体流程如下图所示:

具体实现如下所示:
#if EN_USART1_RX   //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误   	
u8 USART_RX_BUF[USART_REC_LEN];     //接收缓冲,最大USART_REC_LEN个字节.
//接收状态
//bit15,	接收完成标志
//bit14,	接收到0x0d
//bit13~0,	接收到的有效字节数目
u16 USART_RX_STA=0;       //接收状态标记	  
  
void USART1_IRQHandler(void)
{
	u8 res;	
	if(USART1->SR&(1<<5))	//接收到数据
	{	 
		res=USART1->DR; 
		if((USART_RX_STA&0x8000)==0)//接收未完成
		{
			if(USART_RX_STA&0x4000)//接收到了0x0d
			{
				if(res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				else USART_RX_STA|=0x8000;	//接收完成了 
			}else //还没收到0X0D
			{	
				if(res==0x0d)USART_RX_STA|=0x4000;
				else
				{
					USART_RX_BUF[USART_RX_STA&0X3FFF]=res;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收	  
				}		 
			}
		}  		 									     
	}
}6.3 printf函数重定向
本函数位于SYSTEM/usart.c/fputc(),主要是将fputc重定向到USART1上。
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//等待上一次串口数据发送完成  
	USART1->DR = (u8) ch;      	//写DR,串口1将发送数据
	return ch;
}6.4 轮询主函数
本函数位于USER/test.c,主要是通过USART_RX_STA获得接收到的信息长度,并将信息按照字节通过USART1->DR发送出去,利用USART_SR->TC判断是否发送完成。进而形成USART回环。具体代码如下所示:
#include "sys.h"
#include "usart.h"		
#include "delay.h"	
#include "led.h" 
#include "beep.h" 
#include "key.h"	 	 
int main(void)
{								  
	u16 t; 
	u16 len;	
	u16 times=0;    
	Stm32_Clock_Init(9);	//系统时钟设置
	uart_init(72,115200); 	//串口初始化为115200
	delay_init(72);	   	 	//延时初始化 
	LED_Init();		  		//初始化与LED连接的硬件接口 
 	while(1)
	{
		if(USART_RX_STA&0x8000)
		{					   
			len=USART_RX_STA&0x3FFF;//得到此次接收到的数据长度
			printf("\r\n您发送的消息为:\r\n\r\n");
			for(t=0;t<len;t++)
			{
				USART1->DR=USART_RX_BUF[t];
				while((USART1->SR&0X40)==0);//等待发送结束
			}
			printf("\r\n\r\n");//插入换行
			USART_RX_STA=0;
		}else
		{
			times++;
			if(times%5000==0)
			{
				printf("\r\n串口USART1寄存器实验\r\n");
			}
			if(times%200==0)printf("请输入数据,以回车键结束\r\n");  
			if(times%30==0)LED0=!LED0;//闪烁LED,提示系统正在运行.
			delay_ms(10);   
		}
	}		 
}
完整版代码见正点原子官方:【正点原子】精英STM32F103开发板\【正点原子】精英STM32F103开发板资料 资料盘(A盘)\4,程序源码\1,标准例程-寄存器版本\1,标准例程-寄存器版本\实验4 串口实验
七、上机测试
使用XCOM查看串口发送的数据,并测试串口回环:

至此测试成功!



















