×
嵌入式 > 技术百科 > 详情

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

发布时间:2021-01-22 发布时间:
|

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


先看一段最简单的串口程序。


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; //接收完



『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
NS推出采用第二代PowerWise技术的能源管理单元及先进电源控制器