飞道的博客

STM32串口通信详解以及通信异常或者卡死常见问题分析

436人阅读  评论(0)

STM32串口通信详解以及通信异常或者卡死常见问题分析


一、常见的异常问题

◉异常一:数据传输中会出现乱码
◉异常二:程序卡在中断函数里面无法跳出执行主函数的逻辑
◉异常三:数据传输中间歇性数据异常
◉异常四:数据发送时会出现漏发的现象

这些问题对小编带了了巨大的困扰,本文也因这些问题应时而生。小编将自己的见解分享大家,也希望各路大神指导改正。欢迎交流


二、STM32的串口简介

1.串口的通讯方式

◉并行通讯
-传输原理:数据各个同时传输
-优点:速度快
-缺点:占用引脚资源多

◉串行通讯
-传输原理:数据安位顺序传输
-优点:占用引脚资源少
-缺点:传输速度较慢

①按数据传输方向

◉单工:
-数据传输只支持数据一个方向传输

◉半双工
-允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,他是实际是一种切换方向的单工通讯。
◉全双工
-允许数据同时在两个方向上传输,因此,全双工通讯是两单工通讯方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

串行三种通讯方式如图所示:

②串行通讯的通信方式

◉同步通信
-带时钟同步信号传输(接口:SPI,IIC,USART(通用同步异步首发器)

◉异步通信

-不带时钟同步信号(接口:UART(通用异步收发器),单总线

-常见的串口通信接口如图:

③UART异步通信方式引脚连接方法

◉RXD: 数据输入引脚,数据接收。

◉TXD: 数据发送引脚,数据接收。

接线如图:

④STM32F103系列串口对应引脚

串口号 TXD RXD 重定义TXD 重定义RXD
USART1 PA9 PA10 PA6 PA7
USART2 PA2 PA3 PD5 PD6
USART3 PB10 PB11 PD8/PC10 PD9 /PC11
USART4 PC10 PC11 / /
USART5 PC12 PD3 / /

⑤串口通讯过程

2.串口的部分寄存器以及库函数的应用

①USART_SR状态寄存器

作用:状态寄存器USART_SR,描述串口寄存器的一些状态

获取状态标志位函数-操作USART_SR寄存器

// 获取状态标志位
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 清除状态标志位
void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
// 获取中断状态标志位
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
// 清除中断状态标志位
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);

②USART_DR数据寄存器

USART_DR实际是包含了两个寄存器,一个专门用于发送的TDR,一个专门用于接收的RDR。进行发送数据操作时,往USART_DR写入数据会自动存储在TDR内;当进行读取数据操作时,向USART_DR读取数据会自动提取RDR数据。

接收发送数据函数-操作USART_DR寄存器

// 发送数据到串口(通过写USART_DR寄存器发送数据)
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
// 接收数据(从USART_DR寄存器读取接收到的数据)
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

③重要的标志位

1、USART_FLAG_RXNE 串口信号接收完成

2、USART_FLAG_TXE 数据预发送准备

3、USART_FLAG_TC数据发送完成

常用的代码应用:


USART_ITConfig(USART1, USART_IT_TXE, ENABLE); //清除溢出中断

USART_ClearFlag(USART1, USART_FLAG_TC);//清除传输完成标志位

USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空中断标志位

三、实用的代码讲解分析

1.发送功能函数应用详解

①字符发送

u8 Uart1_PutChar(u8 ch) //发送一个字符,输入参数为发送的字符
{
   
	USART_SendData(USART1, (u8) ch);//向串口1,发送一个字符
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){
   }//等待发送完成
    return ch;//返回字符
}

②字符串发送

表达①

void Uart1_PutString(u8* buf , u8 len)//发送一个字符串,输入参数为要发送的数组,发送的字符串个数
{
    
    u8 i;
	for(i=0;i<len;i++)
	{
   
		Uart1_PutChar(*buf++);//发送一个字符
	}
}

表达②

void Uart1_SendStr(u8 *SendBuf)//串口1打印数据
{
   
	while(*SendBuf)
	{
   
	   while((USART1->SR&0X40)==0){
   };//等待发送完成 
       USART1->DR = (u8) *SendBuf; 
	   SendBuf++;
	}
}

③C语言printf应用

#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{
    
	int handle; 
}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
_sys_exit(int x) 
{
    
	x = x; 
} 
//重定义fputc函数 ,重定义到串口1
int fputc(int ch, FILE *f)
{
         
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

2.接收功能函数应用详解

①解析数据帧头的通讯接收方式

-说明:通过分析帧头来确定一包数据的到来,还需要确认数据的长度。
-例如:如果帧头确定第一个字符是0X5A,第二个字符是0X5A。串口1接收到的数据从串口2发送出去
-代码函数如下:

u8 old_ch;
u8 oold_ch;
u8 send_distant = 0; //发送字符的指针
void USART2_IRQHandler(void)                	//串口2中断服务程序
{
   
	u8 ch;
	if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)  //是否发送中断事件
		{
   
			ch =USART_ReceiveData(USART2);//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
			if(oold_ch == 0x5A && old_ch == 0x5A)
			{
   
				send_distant = 2;
			}
			if(send_distant)
			{
   
				if(send_distant == 1)
				{
   
					ch = JuLi_L;//改变第二个字符
				}else if(send_distant == 2)
				{
   
					ch = JuLi_H;//改变第二个字符
				}
				send_distant--;
			}
			oold_ch = old_ch;
			old_ch = ch;
			USART_SendData(USART1,ch);//将接收到的数据从串口1发送出去
    } 

②解析数据帧尾的通讯接收方式

-说明:通过分析帧尾来确定一包数据的结束,不需要确认数据的长度。
-例如:如果帧尾确定倒数第一个字符是0X0A,倒数第二个字符是0X0D。串口1接收到的数据从串口2发送出去
-代码函数如下:

	 
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
   
  u8 Res;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)//接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
   
			Res =USART_ReceiveData(USART1);	//读取接收到的数据
			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;//接收数据错误,重新开始接收	  
					}		 
				}
			}   		 
		} 
} 

③解析接收超时的通讯接收方式

-说明:该方法不需要判断帧头帧尾,也不需要确认好数据的长度。是通过判断接收数据帧的时间间隔来判断一包数据是否接收完成。

-该方法需要开启定时器来计算时间,接收超时的时间需要计算出数据帧的接收时间间隔来判定,不同的波特率数据帧的接收时间间隔不一样,比如:波特率为115200,时间间隔为:10/115200=0.08ms。

串口中断函数代码如下:

extern unsigned char star_time_led ;  //计时开始变量
unsigned char recv_flag = 0;//定义接受标志位
unsigned long recv_cnt = 0;//串口1接收数据缓存
unsigned char recv_buf[MAX_REV_NUM];//串口1接收数据缓存
extern unsigned char star_time;
extern unsigned char recv_time_cnt;

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
   
	static char ch;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //是否发送中断事件
		{
   
			ch = USART_ReceiveData(USART1);//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
			star_time = 1;         //接受到一帧数据的时候,打开软件定时器,去计数
			if(recv_cnt < MAX_REV_NUM)//数组长度是否超过缓存区
			{
   
				recv_buf[recv_cnt] =ch;//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
				recv_cnt++;
			}
			else
			{
   
				recv_cnt = MAX_REV_NUM
				;	//限制数组长度,超过缓存区则不再接收
			}
			recv_time_cnt = 0; //每接收到一帧数据,把定时计数器清零,相当于喂狗
			                   //但是在定时器中断里面会不断的累加
			USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空中断标志位
		}
} 

定时器中断函数代码如下:

unsigned char star_time = 0 ;  //计时开始变量
unsigned char recv_time_cnt;  //定时计数器
extern unsigned long recv_cnt;
extern unsigned char recv_flag;
unsigned char star_time_led = 0 ;  //计时开始变量
void TIM4_IRQHandler(void)   //TIM1中断函数
{
   
	if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) //检查指定的TIM1中断发生与否:TIM1中断源 
		{
   	
     star_time_led++;
		if(star_time == 1)
		{
    
			recv_time_cnt++; //1、累加定时时间计数器
      if(recv_time_cnt>MAX_REV_TIME)//2、判断时间是否超过了设定的最大的时间阈值,
				                            //   超过则说明等待一段时间后没有新的数据到来
			                              //   则判断一包数据接收完毕
			{
   
				recv_time_cnt = 0;//3、清除定时计数器,处理数据 清楚nuffer(放在数据处理之后)
				recv_cnt = 0;
				recv_flag = 1; //接收完成标志位置1
			}
		}
  		TIM_ClearITPendingBit(TIM4, TIM_IT_Update  );  //清除TIM1的中断待处理位:TIM1中断源 
		}	
}

四、对第一章的问题分析

◉异常一:数据传输中会出现乱码
数据传输中会出现乱码,很有可能是数组溢出,或者定义的数组长度不够。或者中断被打断。

◉异常二:程序卡在中断函数里面无法跳出执行主函数的逻辑
中断标志位没有被清除,在这里要注意一点,串口中断标志位自动清空的前提是软件需要先读USART_SR寄存器,然后读USART_DR寄存器来自动清除。即串口中断事件发生后,如果使能的接收中断,而中断函数里面什么都不执行的话,接收中断标志位是无法自动清空的,故而,函数会一直卡在中断函数里面。

比如一下这个函数,该函数没有逻辑问题,但会引发以上问题,代码如下

extern unsigned char star_time_led ;  //计时开始变量
unsigned char recv_flag = 0;//定义接受标志位
unsigned long recv_cnt = 0;//串口1接收数据缓存
unsigned char recv_buf[MAX_REV_NUM];//串口1接收数据缓存
extern unsigned char star_time;
extern unsigned char recv_time_cnt;
/*
以下写法有严重问题
如果没有这句函数→USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空中断标志位
串口接收中断标志位将文法被清空,会导致函数卡在中断函数里面一直循环,无法正常运行主函数

原因分析:
中断条件成立后,中断标志位将会标记,程序将会进入中断函数运行,软件自动轻触中断标志位的条件是
先读USART_SR寄存器,再读USART_DR寄存器。
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //是否发送中断事件
		{
			star_time = 1;         //接受到一帧数据的时候,打开软件定时器,去计数
			if(recv_cnt < MAX_REV_NUM)//数组长度是否超过缓存区
			{
				recv_buf[recv_cnt] =USART_ReceiveData(USART1);//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
				recv_cnt++;
			}
			else
			{
				recv_cnt = MAX_REV_NUM
				;	//限制数组长度,超过缓存区则不再接收
			}
			recv_time_cnt = 0; //每接收到一帧数据,把定时计数器清零,相当于喂狗
			                   //但是在定时器中断里面会不断的累加
			USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空中断标志位
		}
} 
*/

上述代码优化后如下

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
   
	static char ch;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //是否发送中断事件
		{
   
			ch = USART_ReceiveData(USART1);//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
			star_time = 1;         //接受到一帧数据的时候,打开软件定时器,去计数
			if(recv_cnt < MAX_REV_NUM)//数组长度是否超过缓存区
			{
   
				recv_buf[recv_cnt] =ch;//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
				recv_cnt++;
			}
			else
			{
   
				recv_cnt = MAX_REV_NUM
				;	//限制数组长度,超过缓存区则不再接收
			}
			recv_time_cnt = 0; //每接收到一帧数据,把定时计数器清零,相当于喂狗
			                   //但是在定时器中断里面会不断的累加
			USART_ClearFlag(USART1,USART_FLAG_RXNE); //清空中断标志位
		}
} 

◉异常三:数据发送中间歇性数据异常漏发乱发等

对于这些奇奇怪怪的问题,首先要了解一下发送函数是怎么发送的

USART_DR 包含了已发送的数据或者接收到的数据。 USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR读取数据会自动提取 RDR 数据。

TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的,发送时把 TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。

当 TDR 内容转移到发送移位寄存器,还没有发送出去的,就再次把TDR 内容转移到发送移位寄存器里,就会出现少发的现象。

什么时候会有这种情况呢?错误操作代码如下:

void USART2_IRQHandler(void)                	//串口2中断服务程序
{
   
	if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)  //是否发送中断事件
		{
   
			Usart1RecBuf[RxCounter] =USART_ReceiveData(USART2);//将接收到的数据存在数组Usart1RecBuf[RxCounter]里
			RxCounter++;//指向数组地址自加
			if(RxCounter==2) 
			{
   	
				USART_SendData(USART1, Usart1RecBuf[0]);//发送Usart1RecBuf[0]
                USART_SendData(USART1, Usart1RecBuf[1]);//发送Usart1RecBuf[1]
				USART_SendData(USART1, Usart1RecBuf[2]);//发送Usart1RecBuf[2]
			}
		}
}

上述代码连续运行了3次USART_SendData(USART1, Usart1RecBuf);这个函数,这种情况一般都会出现只有最后一个数据发送成功出去。原因可能就是数据还没有发送出去,发送移位寄存器就更新了。

四、总结

◉上述内用的一些程序源码连接如下,包括解析数据帧头的通讯接收方式,解析数据帧尾的通讯接收方式,解析接收超时的通讯接收方式。创作实属不易

链接:
https://download.csdn.net/download/weixin_43281206/19342432


转载:https://blog.csdn.net/weixin_43281206/article/details/116279189
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场