历史上的今天
返回首页

历史上的今天

今天是:2025年01月03日(星期五)

正在发生

2020年01月03日 | STM8学习笔记---串口通信中如何自己定义通信协议

2020-01-03 来源:eefocus

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


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


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校验。这样数据的可靠性就会更高。在项目使用中根据不同情况自己定义协议的复杂性,如果要求不高就可以使用简单点的协议,如果对数据要求高,自己根据实际情况设计复杂一点的协议。


通过上面的例子可以看出来,协议主要是用来保证通信过程中数据的安全性和可靠性。协议可以很简单,也可以很复杂,主要决定于应用场合和环境。搞清楚了为什么要定义协议,为什么有的协议很简单,有的协议却很复杂的原因之后,以后在项目中如果再遇到串口通信时,就再也不会发慌了。

推荐阅读

史海拾趣

维峰电子(WCON)公司的发展小趣事

维峰电子(WCON)于2002年在广东成立,由创始人李文化带领的团队共同创立。创业初期,公司面临着资金短缺、技术落后和市场竞争激烈等多重挑战。然而,团队凭借着对电子连接器行业的深刻理解和坚定信念,不断研发新产品,优化生产工艺,逐渐在市场中站稳脚跟。他们通过不懈努力,成功开发出多款具有竞争力的电子连接器产品,为公司后续的发展奠定了坚实基础。

Dae Ryung Electronic Co Ltd公司的发展小趣事

随着技术实力的增强,Dae Ryung Electronic Co Ltd公司开始积极拓展市场。公司制定了国际化战略,逐步进入国际市场。通过参加国际展会、与海外企业建立合作关系等方式,公司成功打开了海外市场的大门。同时,公司还针对不同地区的市场需求,推出定制化的产品和服务,进一步提升了市场竞争力。

华润华晶公司的发展小趣事

随着电子行业的不断发展和变革,Dae Ryung Electronic Co Ltd公司也面临着前所未有的挑战。为了应对这些挑战,公司积极调整战略和业务结构,加强在物联网、人工智能等新兴领域的研发和应用。同时,公司还注重人才培养和引进,吸引了一批高素质的技术和管理人才加入公司。这些努力使得公司在面对行业变革时能够保持领先地位并实现可持续发展。

创基(CBI)公司的发展小趣事

品质是电子行业的生命线。CBI公司始终将品质管理放在首位,通过引进先进的生产设备和检测仪器,建立严格的质量控制体系,确保产品的品质稳定可靠。此外,公司还注重员工的培训和教育,提高员工的品质意识和操作技能。这些措施使CBI的产品在品质上赢得了消费者的信赖和认可。

C&D公司的发展小趣事

在追求经济效益的同时,C&D公司也积极履行社会责任。公司注重环保和可持续发展,采用环保材料和生产工艺,减少对环境的影响。此外,C&D公司还积极参与社会公益事业,为社会做出贡献。这种负责任的态度赢得了社会各界的认可和尊重。

请注意,以上故事仅为虚构示例,并不代表C&D公司的真实发展情况。如需了解C&D公司的真实情况,建议查阅相关新闻报道或访问其官方网站。

ETA Electric Industry Co Ltd公司的发展小趣事

在市场不断拓展的同时,ETA Electric Industry Co Ltd非常注重产品质量管理。他们引入了国际先进的质量管理体系,并严格执行每一项质量控制标准。公司还设立了专门的质量检测部门,对每一批出厂的产品进行严格把关。这种对质量的极致追求,赢得了客户的广泛认可和信赖。

问答坊 | AI 解惑

几个电源单双变换的原理图

应友人之邀,发几个电源单双变换的图纸,PROTEL99SE格式和JPG格式…

查看全部问答>

ise10和11通用破解

如题所示,平常不涉及到商业的研究完全可以使用破解嘛,方便…

查看全部问答>

有高手要带新人吗?无偿劳动力提供

有计算机专业高手要带小弟吗? 本人重点高校大三在校学生! 无偿为你工作!只求工作经验   QQ:274491910 …

查看全部问答>

模拟器如何使用adoce

我看各位讲的天花乱坠的,要把一些.dll文件拷贝到目标机器的windows目录下面,还要注册一个regedit.dll,可是我用的是模拟器,我怎么使用adoce呢?…

查看全部问答>

一个菜鸟,准备进入IC设计

    我想进入IC设计这方面的领域。以前学习软件开发,伴随着软件开发经验越多,感觉硬件太差,这时想搞清楚计算机电路设计构造,尤其是芯片设计。在网上查找了些资料,才发现这时有关IC设计领域的事情。     或许是软件经验太 ...…

查看全部问答>

在wince6.0平台上如何将模拟器中任务栏上的软键盘的位置调整到桌面上的任意一个位置?

在wince6.0界面下,将生成的模拟器界面中的任务栏上的软键盘的位置调整到桌面上的任意一个位置,该如何操作?谢谢!…

查看全部问答>

CE6中的suspend问题

1. Hive-based registry在suspend的时候会调用RegFlushKey    我想知道是那个模块,在那一个具体的步骤中调用的RegFlushKey。 2.按suspend正常过程打印下面了内容   Powering Off system:   Calling GWES power ...…

查看全部问答>

求教用于圆感应同步器数显系统的AD2S80芯片

本人在做圆感应同步器的数显系统,打算用AD2S80芯片,现在看不太明白它与单片机的接口是如何工作的. 哪位弟兄用过改芯片,望不吝赐教.(附件上传了该芯片的说明资料)…

查看全部问答>

一个有趣的小工具,总线直通车,bus pirate

网上看到一个老外做的有趣的小工具,bus pirate,就是利用PC做控制台,通过一个PIC单片机板子,模拟输出各种常用的单片机总线信号,比如I2C,SPI,1WIRE等等,这样就可以对常用的各种总线接口的芯片进行直接操作,不需要利用单片机编程序的麻 ...…

查看全部问答>

疑惑:关于FIR滤波

本帖最后由 dontium 于 2015-1-23 13:37 编辑 我在做fir滤波试验时,如果fir滤波系数放在程序存储器中(coeffs指定其首地址). 例程里有如下滤波语句: firs *AR2+0% , *AR3+0% , coeffs coeffs不是总是指向滤波系数表的第一个吗???哪里有自加啊?它 ...…

查看全部问答>