STM32 USART串口通信

发布于:2025-05-25 ⋅ 阅读:(22) ⋅ 点赞:(0)

1、原理

1、USART串口协议

同步:有单独的时钟线,接收方可以在时钟信号的指引下进行采样

异步:双方需要约定一个采样频率,并添加一些帧头帧尾等进行采样位置的对齐

单端:引脚的高低电平都是相对于GND的电压差,需要接GND引脚

差分:抗干扰,靠两个差分引脚的电压差来传输信号,可以不需要GND

点对点:直接传输数据

多设备:需要寻址,来确定通信对象

串口通信TX、RX、GND必须要接

如果设备1、设备2都有独立供电,VCC可以不接

如果其中一个设备没有供电,如设备1为STM32,设备2为蓝牙串口模块,就需要将它们的VCC接在一起,STM32通过这根线向右边的子模块供电

波特率:异步通信通信速率,每秒传输码元的个数、bit/s。高电平表示1,低电平表示0。决定了每个多久发送一位

起始位:串口的空闲状态是高电平,起始位为低电平,通知接收设备这一帧数据要开始了

停止位:用于数据帧间隔,固定为高电平

2、USART串口外设

同步模式多了一个时钟输出,不支持时钟输入,并不支持两个USART之间的通信

波特率发生器:用来配置波特率,相当于分频器,最常用(9600,115200)。

数据位长度:8、9位

停止位长度:在进行连续发送时,帧的间隔

硬件流控制:防止接收方处理慢而导致数据丢失的问题

USART资源:USART1是APB2总线上的设备,USART2、3都是APB1总线上的设备 

TDR和RDR占用同一个地址,在程序上表示为一个寄存器,数据寄存器DR,在实际的硬件中,是两个寄存器,一个用于发送,一个用于接收,TDR只写,RDR只读。写操作,数据写道TDR;读操作,数据从RDR中读出

发送端:把一个字节的数据一位一位地向右移出去(低位先行),对应串口协议的数据位波形。        当硬件检测到写入数据,会检查当前移位寄存器是否有数据正在移位,如果没有该数据会立刻全部被发送到发送移位寄存器,准备发送。当数据从TDR发送到移位寄存器时,会置标志位TEX(TX Empty),TDR为空,TEX置1,就可以在TDR里写入下一个数据了,此时发送移位寄存器中的数据还没有发送出去。当数据移位完成后,新的数据就会再次自动地从TDR转移到发送移位寄存器里。可以保证连续发送数据时,数据帧之间不会有空闲。

接收端:从高位到低位方向移动,一个字节移位完成之后,这一个字节的数据就会整体移到接收数据寄存器RDR,转移过程中会置标志位RXNE(RX Not Empty),接收数据寄存器非空。RXNE置1时,就可以把数据读走了

硬件流控制:避免发送设备发的太快,接收设备来不及处理,导致丢弃或覆盖

nRTS:接对方的CTS,能接受的时候RTS置低电平,请求对方发送,对方的CTS接收到之后,就可以发送数据;当数据处理不过来,如接收数据寄存器一直没有读,又有数据过来了,RTS置高电平,对方CTS接收到之后,暂停发送数据,直到RTS置低电平。

nCTS:清除发送,用于接收其他设备的nRTS。

SCLK:用于产生同步的时钟信号,配合发送移位寄存器输出,发送寄存器移位一次,同步时钟电平就跳变一个周期,时钟告诉对方,移出去一位数据。只支持输出,不支持输入,两个USART之间不能实现同步的串口通信。用于兼容别的协议,如SPI,和自适应波特率,如接收设备不确定发送设备给的是什么波特率,可以测量时钟周期,计算得出波特率。

唤醒单元:实现串口挂载多设备,一条总线上接多个从设备,每个设备分配一个地址,想跟某个设备通信,就先进行寻址,确定通讯对象。如给设备分配一个地址,当发送指定地址时,此设备唤醒开始工作;没收到地址就保持沉默。

状态寄存器:TEX和RXNE是判断发送状态和接收状态的必要标志位。

USART中断控制:配置中断是否能通向NVIC。

波特率发生器:相当于分频器,对APB时钟进行分频,得到发送和接收移位的时钟。USART1挂载在APB2,PCLK2的时钟,72MHZ;其余USART挂载在APB1,PCLK1的时钟,36MHZ。之后这个时钟会进行分频,除USARTDIV的分频系数,分频完之后再除16。TE为1,发送器使能,RE为1接收器使能。

3、基本结构

4、数据帧

输出定时翻转TX引脚高低电平;输入要保证采样频率和波特率一致,还要保证每次输入采样的位置正好处于每一位的正中间,这样高低电平读进来才最可靠。

空闲帧、断开帧用于局域网协议

数据长度:8位(有、无校验)、9位(有、无校验),最好选择9位字长有校验,8位字长无校验。串口传输的数据类型一般为uint8_t

一个停止位和一个数据位时长一样,一般使用一个停止位

5、输入电路噪声处理

以波特率16倍频率采样,一位的时间进行16次采样。

为了避免噪声影响,接收电路在第一次遇到下降沿之后,第3、5、7次进行采样,第8、9、10次进行采样,这两批采样都至少有2个0。如果只用2个0,会在状态寄存器里置一个NE(Noise Error),噪声标志位。如果少于2个0,电路忽略前面的数据,重新开始捕捉下降沿。

6、数据采样

三次采样中,两次及以上为1,就认为收到了1;两次及以上为0,就认为收到了0。两次时,噪声标志位NE也会置1。

7、波特率发生器

DIV_Mantissa:DIV整数部分(二进制)

DIV_Fraction:DIV小数部分(二进制)

因为它内部还有一个16倍波特率的采样时钟,所以要多除16。

8、数据模式

可以以16进制数和字符的方式进行发送。

数据在线路中传输得形式是16进制数

2、代码

1、初始化

1、初始化时钟

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

2、初始化GPIO引脚

串口空闲状态是高电平,不使用下拉输入

GPIO输出模式使用复用推挽输出,因为在gpio的电路中,是由输出数据寄存器控制,外设无法干预,但使用复用推挽输出后,输出数据寄存器会被断开,由外设控制输出控制

	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	

3、初始化USART

	USART_InitTypeDef USART_InitStruct;
	USART_InitStruct.USART_BaudRate=9600;
	USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
	//想同时接收和发送可以使用按位或
	USART_InitStruct.USART_Mode=USART_Mode_Tx;
	USART_InitStruct.USART_Parity=USART_Parity_No;
	USART_InitStruct.USART_StopBits=USART_StopBits_1;
	USART_InitStruct.USART_WordLength=USART_WordLength_8b;
	
	USART_Init(USART1, &USART_InitStruct);
	
	USART_Cmd(USART1, ENABLE);

2、发送数据

1、发送一位数据

不需要对TXE手动清零,下一次使用USART_SendData()时,TXE自动清零。TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。对USART_DR 的写操作,将该位清零。
//串口一次移动8位数据
void Serial_SendData(uint8_t Byte)
{
	//数据写入发送数据寄存器TDR
	USART_SendData(USART1, Byte);
	//等待TDR中的数据转移到移位寄存器
	while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}

2、发送一个数组

void Serial_SendArray(uint8_t *Array, uint16_t Lenth)
{
	uint16_t i=0;
	for(i=0;i<Lenth;i++)
	{
		Serial_SendData(Array[i]);
	}
}

3、发送一个字符串

void Serial_SendString(char* string)
{
	uint16_t i;
	for(i=0;string[i]!='\0';i++)
	{
			Serial_SendData(string[i]);
	}
}

4、发送一个数字

一位一位地发送

uint32_t Serial_pow(uint32_t Num, uint16_t t)
{
	uint32_t result=1;
	while(t--)
	{
		result*=Num;
	}
	return result;
}
void Serial_SendNum(uint32_t Num, uint8_t Lenth)
{
	uint8_t i;
	for(i=0;i<Lenth;i++)
	{
		//Lenth-i-1 表示第一位数
		Serial_SendData(Num/Serial_pow(10,Lenth-i-1)%10+'0');
	}
	

5、重定向printf到串口

在工程设置->target里把Use MicroLib勾选上

#include <stdio.h>
//fputc是printf的底层,把fput重定向到串口,printf就重定向到串口
int fputc(int ch, FILE *f)
{
	Serial_SendData(ch);
	return ch;
}

3、接收数据

1、初始化RX

	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	USART_InitStruct.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;

2、串口接收

1、查询,在主函数里不断判断RXNE标志位
	while(1)
	{
		if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE))
		{
			RxData=USART_ReceiveData(USART1);
		}
		OLED_ShowHexNum(1,1,RxData,4);
	}
2、中断

开启RXNE标志位到NVIC的输出,RXNE一旦置1,就会向NVIC申请中断

	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
	
	//分配抢占优先级和响应优先级个数
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
		
	NVIC_InitTypeDef NVIC_InitStructure;
	//指定中断通道
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	//指定该通道是否为抢占优先级或响应优先级
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;);

//暂存数据,标志位
uint8_t RxData;
uint8_t RxFlag;

uint8_t Serial_GetRxData()
{
	return RxData;
}
//每次获取标志位后清零
uint8_t Serial_GetRxFlag()
{
	if(RxFlag==1)
	{
		RxFlag=0;
		return 1;
	}
	else
	{
		return 0;
	}
}
//收到USART的中断后,将标志位暂存
void USART1_IRQHandler()
{
	RxData = USART_ReceiveData(USART1);
	RxFlag = 1;
	USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}

4、收发一个数据包

1、HEX数据包

传输直接,解析数据简单,适合模块发送原始的数据如陀螺仪、温湿度传感器

2、文本数据包

数据直观易理解,灵活,适合输入指令进行人机交互的场合,如蓝牙模块AT指令

3、HEX数据包收发

//只存载荷数据
uint8_t TxPacket[4];
uint8_t RxPacket[4];
uint8_t RxFlag;

 中断函数内,用状态机表示状态

void USART1_IRQHandler()
{
	static uint8_t RxState=0;
	static uint8_t Rxnum=0;
	//每次从串口中接收一个字节
	uint8_t RxData = USART_ReceiveData(USART1);
	switch(RxState)
	{
		case 0: 
			//收到包头,进入转移状态
			if(RxData==0xFF)
			{
				RxState=1;
			}
			break;
		case 1:
			RxPacket[Rxnum]=RxData;
			Rxnum++;
			if(Rxnum>=4)
			{
				Rxnum=0;
				RxState=2;
			}
			break;
		case 2:
			if(RxData==0xFE)
			{
				RxState=0;
				//全部接收到,置接收标志位
				RxFlag=1;
			}
			break;
		default:
			break;
	}
	USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}

4、文本数据包接收

void USART1_IRQHandler()
{
	static uint8_t RxState=0;
	static uint8_t Rxnum=0;
	//每次从串口中接收一个字节
	uint8_t RxData = USART_ReceiveData(USART1);
	switch(RxState)
	{
		case 0: 
			//收到包头,进入转移状态
			//同时避免发送太快,第一组数据还没处理完就来第二组的情况
			if(RxData=='@' && RxFlag==0)
			{
				RxState=1;
				Rxnum=0;
			}
			break;
		case 1:
			//载荷字符数量不确定,先判断是不是包尾
			if(RxData=='\r')
			{
				RxState=2;
			}
			else
			{
				RxPacket[Rxnum]=RxData;
				Rxnum++;
			}
			break;
		case 2:
			//等待第二个包尾
			if(RxData=='\n')
			{
				RxState=0;
				//全部接收到,置接收标志位
				RxFlag=1;
				//字符数组最后,添加字符串结束标志位
				RxPacket[Rxnum]='\0';
			}
			break;
		default:
			break;
	}
	USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}

 

while(1)
	{
		if(Serial_GetRxFlag()==1)
		{
			OLED_ShowString(4,1,"                      ");
			OLED_ShowString(4,1,RxPacket);
            //判断传入文本是否和目标命令匹配
			if(strcmp(RxPacket, "LED_ON")==0)
			{
				LED1_ON();
				Serial_SendString("LED1_ON_OK\r\n");
				OLED_ShowString(2,1,"                      ");
				OLED_ShowString(2,1,"LED1_ON_OK\r\n");
			}
			else if(strcmp(RxPacket, "LED_OFF")==0)
			{
				LED1_OFF();
				Serial_SendString("LED1_ON_OFF\r\n");
				OLED_ShowString(2,1,"                      ");
				OLED_ShowString(2,1,"LED1_ON_OFF\r\n");
			}
			else
			{
				Serial_SendString("CommandError\r\n");
				OLED_ShowString(2,1,"                      ");
				OLED_ShowString(2,1,"CommandError\r\n");
			}
		}
	}


网站公告

今日签到

点亮在社区的每一天
去签到