STM8学习笔记---串口通信中如何自己定义通信协议

在单片机刚开始学习的时候,串口通信是经常要用到的,但是实际产品中串口通信是需要通信协议的。好多人不明白为什么要用通信协议,如何定义通信协议,带通信协议的程序要怎么写。今天就来说一下如何串口通信协议是如何定义出来的。
先看一段最简单的串口程序。


void Uart1_Init( unsigned int baudrate )
{
    unsigned int baud;
    baud = 16000000 / baudrate;
    Uart1_IO_Init();				//IO口初始化
    UART1_CR1 = 0;
    UART1_CR2 = 0;
    UART1_CR3 = 0;
    UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );
    UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );

    UART1_CR2_bit.REN = 1;        //接收使能
    UART1_CR2_bit.TEN = 1;        //发送使能
    UART1_CR2_bit.RIEN = 1;       //接收中断使能
}
//阻塞式发送函数
void SendChar( unsigned char dat )
{
    while( ( UART1_SR & 0x80 ) == 0x00 ); //发送数据寄存器空
    UART1_DR = dat;
}
//接收中断函数 
#pragma vector = 20            
__interrupt void UART1_Handle( void )
{
    unsigned char res;
    res = UART1_DR;  
    return;
}

主要函数有三个,一个初始化函数,一个发送函数,一个接收中断。先定义一个简单的协议,比如:接收到1点亮LED灯,接收到0熄灭LED灯。那么接收中断函数就可以修改为:


#pragma vector = 20             
__interrupt void UART1_Handle( void )
{
    unsigned char res;
    res = UART1_DR; 
    if( res == 0x01 )
			LED = 1;	
	else if( res == 0 )
			LED = 0;
    return;
}

直接判断接收到的数据值,根据数据值来控制LED灯的状态。
如果需要控制两个LED灯怎么办呢?需要发送两个数据来控制LED灯的状态。
下来将协议改复杂点,第一位数据控制LED1,第二个数据控制LED2,同样1是点亮LED,0是熄灭LED。如果是这样的话接收数据的时候就不能像上面那样,接收一个数据就去控制LED的状态,因为这次发送的数据有两位,必须区分开第一个数据和第二个数据,于是可以考虑用数组接收数据。修改程序如下:

#pragma vector = 20             
unsigned char res[2];
unsigned char cnt=0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if( res[ 0 ] == 0x01 )
			LED1 = 1;	
	else if( res[ 0 ] == 0 )
			LED1 = 0;
	 if( res[ 1 ] == 0x01 )
			LED2 = 1;	
	else if( res[ 1 ] == 0 )
			LED2 = 0;
    return;
}

这样通过数组将接收的数据存起来,然后用下标来判断第几个数据,再去控制LED灯的状态。
这是如要需要控制三个LED的话,发送的数据就要在增加一位,加上第三个LED灯,可以用同样的方式来接收数据。修改程序如下:

#pragma vector = 20             
unsigned char res[3];
unsigned char cnt=0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if( res[ 0 ] == 0x01 )
			LED1 = 1;	
	else if( res[ 0 ] == 0 )
			LED1 = 0;
	 if( res[ 1 ] == 0x01 )
			LED2 = 1;	
	else if( res[ 1 ] == 0 )
			LED2 = 0;
	 if( res[ 2 ] == 0x01 )
			LED3 = 1;	
	else if( res[ 2 ] == 0 )
			LED3 = 0;
    return;
}

这样的话就算在多加几个LED控制,通信起来也一样适用。看起来这样的协议就可以满足使用了。但是仔细想想,这种协议看起来没什么问题,唯一的缺点就是他会将每个接收到数据都作为有效命令对待。如果说上位机没发送点亮LED的命令,但是串口线上出现了干扰,如果干扰信号刚好是0或者1,那么程序就有可能误动作。就需要在接收数据的时候用一个标志来判断当前发送的数据是真正的命令还是干扰。用一个特殊的值来做为接收指令的开始,这里选用0XA5做为数据的开始,为什么选0xA5呢,因为0xA5的 二进制数位 1010 0101 刚好是一个1一个0,间隔开,用这个数字做为开始可以很大程度上的避免干扰信号,因为干扰信号一般不会是这种高低高低很有规律的信号。于是协议就改为 0xA5 为第一个数据,做为上位机发送命令的标志,然后后面用0和1来代表LED的状态,0为LED熄灭,1为LED点亮。假如我们需要点亮3个LED,那么上位机发送的指令就是 0xA5 0x01 0x01 0x01 一个起始标志,后面跟着三个控制信号。程序修改为:

#pragma vector = 20             
unsigned char res[4];
unsigned char cnt=0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if( res[ 0 ] == 0xA5   )
    {
		    if( res[ 1 ] == 0x01 )
					LED1 = 1;	
			else if( res[ 1 ] == 0 )
					LED1 = 0;
			 if( res[ 2 ] == 0x01 )
					LED2 = 1;	
			else if( res[ 2 ] == 0 )
					LED2 = 0;
			 if( res[ 3 ] == 0x01 )
					LED3 = 1;	
			else if( res[ 3 ] == 0 )
					LED3 = 0;
	}
    return;
}

这样接收到第一个数据的时候先判断是不是0xA5,如果是0xA5说明是发送的指令,就执行后面的命令,如果第一个数据不是0xA5,就说明是干扰信号,就不执行命令。这样就可以避免干扰信号,导致程序误动作。这样是不是就可以了,仔细分析分析,如果干扰信号没有发生在数据开始位置,而是发生在了数据结束位置,比如我现在只需要控制两个LED灯,发生的指令为0xA5 0x01 0x01,但是接收完前面几个数据后发生了干扰,数据多了一个0x01,那么单片机接收到的数据就成了0xA5 0x01 0x01 0x01 导致第三个灯被误打开,为了避免这种干扰情况,可以再增加一个结束标志,代表发送数据结束,用0x5A作为结尾,刚好和开始标志相反。那么此时如果要控制两个灯的话,发送的数据就变为 0xA5 0x01 0x01 0x5A 。代码修改为:

#pragma vector = 20             
unsigned char res[4];
unsigned char cnt=0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if( ( res[ 0 ] == 0xA5   )&&( res[ 3 ] == 0x5A ) )
    {
		    if( res[ 1 ] == 0x01 )
					LED1 = 1;	
			else if( res[ 1 ] == 0 )
					LED1 = 0;
			 if( res[ 2 ] == 0x01 )
					LED2 = 1;	
			else if( res[ 2 ] == 0 )
					LED2 = 0;
	}
    return;
}

这样通过同时判断发送数据的开始标志和结束标志,确保接收到的数据是真正的命令,避免了干扰数据。但是仔细观察后又发现了一个新的问题。结束标志的数据位置下标是固定的,也就是说每次发送数据只能发送4个字节,也就是每次只能控制两个LED灯,如果要增加控制LED灯的数量就要修改程序,这样在实际操作中很不方便,能不能可以动态的识别发送了几个数据?于是想到,在发送指令的时候,告诉单片机我要控制LED的数量,单片机根据数量值,自动去判断当前需要点亮几个LED灯,于是协议修改为在开始标志后面再添加一位,代表要控制的LED灯数量,后面是点亮或者熄灭命令,最后为结束标志。假如现在要点亮2个LED灯,发送的数据为:0xA5 0x02 0x01 0x01 0x5A,开始标志后面的0x02就代表要控制两个LED灯。如果要点亮3个灯发送的数据就为0xA5 0x03 0x01 0x01 0x01 0x5A。那么如何确定结束标志在哪个位置呢?通过观察上面两组数据可以发现控制2个LED灯的话结束标志在第4位,控制3个LED灯的话结束标志在第5位,结束标志的位置刚好比控制lED灯数量多2,于是程序修改为:

#pragma vector = 20             
unsigned char res[5];
unsigned char cnt=0;
unsigned char num=0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if(  res[ 0 ] == 0xA5   )
    {
    		num = res[ 1 ];
    		if( res [ num + 2] == 0x5A )
    		{
    		    if( num == 3 ) 
    		    {
				    if( res[ 2 ] == 0x01 )
							LED1 = 1;	
					else if( res[ 2 ] == 0 )
							LED1 = 0;
					 if( res[ 3 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 3 ] == 0 )
							LED2 = 0;
					 if( res[ 4 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 4 ] == 0 )
							LED2 = 0;
				}
			
			  if(  num == 2 ) 
			  {
				    if( res[ 2 ] == 0x01 )
							LED1 = 1;	
					else if( res[ 2 ] == 0 )
							LED1 = 0;
					 if( res[ 3 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 3 ] == 0 )
							LED2 = 0;		
				}			
			}
	}
    return;
}

这样通过在协议中增加一个数量判断,程序就可以动态的设置LED灯的状态了。但是感觉串口中断中的代码太多了,数据接收和数据处理都放在一个函数中了,这样程序读起来比较费劲。能不能把接收数据和处理数据分开呢?那么就可以在串口中断函数中只接收数据。数据接收完成之后设置标志位,然后在主函数中去处理接收到的数据。于是修改程序为:

#pragma vector = 20             
unsigned char res[5];
unsigned char cnt = 0;
unsigned char num = 0;
unsigned char receive_ok = 0;
__interrupt void UART1_Handle( void )
{
    res[ cnt++ ] = UART1_DR; 
    if(  res[ 0 ] == 0xA5   )
    {
    		num = res[ 1 ];
    		if( res [ num + 2] == 0x5A )
    		{
    		     receive_ok = 1;					//接收完成
    		     cnt = 0;
			}
	}
    return;
}
void LED_Show( void )
{
		if( receive_ok )
		{
		     receive_ok = 0;
				  if( res[ 1 ] == 3 ) 
    		     {
				    if( res[ 2 ] == 0x01 )
							LED1 = 1;	
					else if( res[ 2 ] == 0 )
							LED1 = 0;
					 if( res[ 3 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 3 ] == 0 )
							LED2 = 0;
					 if( res[ 4 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 4 ] == 0 )
							LED2 = 0;
				}			
			  if( res[ 1 ]  == 2 ) 
			  {
				    if( res[ 2 ] == 0x01 )
							LED1 = 1;	
					else if( res[ 2 ] == 0 )
							LED1 = 0;
					 if( res[ 3 ] == 0x01 )
							LED2 = 1;	
					else if( res[ 3 ] == 0 )
							LED2 = 0;		
				}			
		}
}

void main ( void )
{
		while( 1 )
		{
				LED_Show();
		}
}

这样通过一个标志位,将串口接收代码和数据处理代码分开。在接收数据过程中不会因为处理数据导致串口接收异常。程序看起来也比较简洁明了。
在实际项目应用中有时候为了确保接收数据的正确性还需要增加校验位。校验位一般在结束标志的前一位,我们在这里也增加一个校验位,校验位在结束标志前面,校验方式为前面所有数据的累加和。比如 要发送的数据为 0xA5 0x02 0x01 0x01 校验 0x5A
校验 = 0xA5 + 0x02 + 0x01+ 0x01 校验 = 0xA9 所以发送的数据就为 :0xA5 0x02 0x01 0x01 0xA9 0x5A 。串口接收到数据后,也将校验位前面的所有数据累加,然后累加的结果和校验位的数据对比,如果计算的校验结果和校验位的数值相等,说明接收的数据是正确的。否则说明接收的数据错误。修改串口接收部分代码为:

#pragma vector = 20             
unsigned char res[6];
unsigned char cnt = 0;
unsigned char num = 0;
unsigned char receive_ok = 0;
__interrupt void UART1_Handle( void )
{
unsigned char  i=0;
unsigned char  check=0;
    res[ cnt++ ] = UART1_DR; 
    if(  res[ 0 ] == 0xA5   )									//接收到开始标志
    {
    		num = res[ 1 ];									//存储命令个数
    		if( res [ num + 3] == 0x5A )				//接收到结束标志
    		{
    			for( i = 0; i <  num+2; i++)
    			{
    				   check  += res[ i ];						// 计算校验位前面所有数据累加和
    		   }
    		    if( check == res [ num + 2] )			//如果计算的校验位和接收到的校验位想等     	
    		    {	    		   
    		       receive_ok = 1;							//接收完成
    		     }
    		     else 
    		     {
    		        receive_ok = 0;							//接收失败
    		        cnt = 0;
				}
    		     	
			}
	}
    return;
}

通过增加校验位来判断接收数据的正确性,这样通过增加开始标志、结束标志、命令数量、校验位这些措施来保证数据传输的可靠性和完整性。如果对数据正确性要求更高的话,可以使用更复杂的校验方法,或者使用更复杂的开始标志或者结束标志。比如开始标志使用两位 如0xA5 0x5A 结束标志也使用两位 0x0D 0x0A ,校验方式使用CRC校验。这样数据的可靠性就会更高。在项目使用中根据不同情况自己定义协议的复杂性,如果要求不高就可以使用简单点的协议,如果对数据要求高,自己根据实际情况设计复杂一点的协议。
通过上面的例子可以看出来,协议主要是用来保证通信过程中数据的安全性和可靠性。协议可以很简单,也可以很复杂,主要决定于应用场合和环境。搞清楚了为什么要定义协议,为什么有的协议很简单,有的协议却很复杂的原因之后,以后在项目中如果再遇到串口通信时,就再也不会发慌了。

发布了76 篇原创文章 · 获赞 30 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_20222919/article/details/99644560
今日推荐