找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 10890|回复: 2
打印 上一主题 下一主题
收起左侧

安全可靠的串口通讯协议之校验

[复制链接]
跳转到指定楼层
楼主
ID:128229 发表于 2016-6-27 22:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
(1)开场白:

串口通讯的协议各种各样,五花八门。有的协议没有校验,有的协议有校验,它们的存在都有它们的道理。没有校验的协议,感觉更简单直接,传输与处理的速度更 加快。有校验的协议,感觉用起来更加放心点。这节将要介绍我常用校验的三道防火墙。它们分别是提取数据头,判断数据总数,判断所有字节的累加和。在上一节 中略加修改即可完成本节的实验。

(2)功能需求:

   无论是单片机还是上位机,最好在固定协议前多发送一个填充的无效字节0x00,因为硬件原因,第一个字节往往容易丢失。

           通讯协议: EB 00 55   XX YY YY YY YY CY

           其中前三位 EB 00 55为数据头,XX为数据长度,YY为数据,CY为前面所有字节的累加和。累加和的意思是所有数据相加,你只要定义一个字节变量用来接收所有数据的累加和,那么超过一个字节的部分会自动丢弃。比如以下数据:

           EB 00 55 04 02 12 56 A2 50

           其中 EB 00 55为数据头,04为数据总数,02 12 56  A2为数据,50为前面所有字节的累加和。累加和可以用电脑系统自带的计算器来验证。打开电脑上的计算器,点击“查看”下拉的菜单,选“科学型”,然后选 左边的“十六进制”,最后选右边的“字节”,然后把前面所有的字节相加,它们的和就是50,没错吧。



任意时刻,从电脑“串口调试助手”上位机收到的一堆数据中,只要此数据满足以上我介绍的三道防火墙,那么就往上位机发送“eb  00 aa”表示确认,同时蜂鸣器叫一声。如果校验出错,则往上位机发送“eb  00 55”表示出错。

(3)硬件原理:

把单片机串口通讯的那两个引脚经过一个MAX3232之后直接跟电脑的9针串口通讯。我发现很多朋友会选MAX232这个芯片,而我本人更加推荐用 MAX3232。因为MAX232只支持5V,不是宽压的,而MAX3232不但支持5V,还支持3V。每个人的记忆力都很宝贵,用232串口我只选 MAX3232,不管它是用5V工作还是3V工作。就像74系列的芯片,我的心中只有你(74HC)没有它(74LS),一样的道理,74HC是宽 压,74LS不是宽压。

(4)源码适合的单片机:PIC18f4520,晶振为22.1184MHz,波特率115200

(5)源代码讲解如下:

#include<pic18.h>         //包含芯片相关头文件

//补充说明:吴坚鸿程序风格是这样的,凡是输出IO后缀都是_dr,凡是输入的//IO后缀都//是_sr

#define  beep_dr  LATA2  //蜂鸣器输出

//补充说明:吴坚鸿程序风格是这样的,凡是做延时计数阀值的常量

//前缀都用cnt_表示。

#define cnt_voice_time   150  //蜂鸣器响的声音长短的延时阀值

#define cnt_send   300            //确保接收缓冲区没有继续接收数据,是变量

//send_cnt的溢出阀值

Void usart_service();        //串口通讯服务程序,放在main函数里

unsigned char asy_recieve();  //把串口缓冲区的数据一个个提取出来

void eusart_send(unsigned char t_data); //串口发送一个字节的数据

Void Buf_clear() ;  //把余下的缓冲区清零

void Delay11(unsigned int MS); //延时函数

//补充说明:吴坚鸿程序风格是这样的,凡是计数器延时的变量

//后缀都用_cnt表示。

unsigned int voice_time_cnt;        //蜂鸣器响的声音长短的计数延时

unsigned int send_cnt=0;            //一串数据从上位机发过来的时候,他们每个字节之间//的延时间隔很短,如果他们的延时间隔一旦超过了这个send_cnt变量的延时,那么就////认为他们的一串数据已经发送完毕

//补充说明:吴坚鸿程序风格是这样的,凡是涉及统计数量的变量

//后缀都用_total表示。

unsigned int RCREG_total;            //统计串口缓冲区已经收了多少个数据

unsigned int RCREG_read_total;   //统计已经从串口缓冲区读出了多少个数据

unsigned char valid_total;            //一串数据中的有效数据总量

//补充说明:吴坚鸿程序风格是这样的,凡是用来更新的标识变量,比如液晶刷屏,或者有新接收的串口数据更新等等,后缀一律用_update表示

Unsigned char  send_update=0;  //一旦有数据从上位机发送过来,就会引发串口接收中////断,在串口中断里,我把send_update=1表示目 前正在接收数据,警告单片机先不要//猴急,等串口中断把所有从上位机连续发送过来的一堆数据接收完,再处理。那么什么///时候才知道发送的数据已经发 送完毕了呢?用send_cnt识别。因为在串口中断里,我///每次都会把send_cnt=0,而在main函数里,一旦发现 send_update==1,send_cnt就//会开始自加,当它超过某个数值的时候,就会自动把send_update=0,表示目前已经没// 有数据发送了。而如果有数据不断从上位机传来,send_cnt永远也不会超过某个数值,//因为每个中断它都被清零,这个原理跟看门口狗喂狗的原理很 像。

//补充说明:吴坚鸿程序风格是这样的,凡是用来接收数据的缓冲区数组后缀都用_buf表//示

Unsigned char RCREG_buf[50];  //串口接收缓冲区,读者可以根据实际项目设置大小

Unsigned char RCREG_buf_temp[50];  //临时处理串口数据的缓冲区,可以不用那么大

//补充说明:吴坚鸿程序风格是这样的,凡是自锁变量名, 后缀都用_lock表示。

Unsigned char send_lock=0;

//补充说明:吴坚鸿程序风格是这样的,凡是在main函数中用的中间变量,前缀m_,后//缀用_char或者_int表示类型

Unsigned int m_int;      //中间变量,只要是用在main函数里,谁都可以重复用。

Unsigned char receive_sum;               //一串数据中的累加和,注意,必须定义成字节变//量,这样运算中它会自动丢弃超过一个字节的位。

//主程序

main()

{

ADCON0=0x00;  

ADCON1=0x0f;                               //全部为数字信号

    ADCON2=0xa1;                               //右对齐

    RBPU=0;                                      //上拉电阻

    SSPEN=0;                                    //决定RA5不作为串口

   TRISA2=0;  //蜂鸣器输出

    BRG16=0;         //设置串口通信寄存器

    BRGH=0;

    SPBRGH=0x00;

    SPBRG=0x02;     //22.1184MHz晶振,115200波特率

    SYNC=0;

    SPEN=1;

    TX9=0;

    TXEN=1;

    TXIF=1;

    RX9=0;

    CREN=1;

    RCIE=1;

    PEIE=1;

    GIE=1;

    T1CON=0x24;     //定时器中断配置

    TMR1H=0xFE;

TMR1L=0xEF;

    TMR1IF=0;

    TMR1IE=1;

    TMR1ON=1;

    TMR1IE=1;

//补充说明,以上的内容为寄存器配置,每种不同的单片机会有点差异,

//大家不用过度关注以上寄存器的配置,只要知道有这么一回事即可

    beep_dr=0;                               //关蜂鸣器,上电初始化IO

   while(1)   

   {

                     CLRWDT(); //喂看门狗,大家不用过度关注此行

                usart_service();        //串口通讯服务

}

}



//中断

void interrupt timer1rbint(void)

{

    if(RCIE==1&&RCIF==1)   //串口中断,一次只能接受一个字节

{

        RCIE=0;

        RCIF=0;

        ++RCREG_total;   //以下代码是鸿哥的在所有串口项目中用到的标准代码

        if(RCREG_total>50)  //超过缓冲区

        {

           RCREG_total=50;

        }

        RCREG_buf[RCREG_total-1]=RCREG;  //依次把上位机来的数据存入数组缓冲区

        send_update=1;    //通知单片机目前正在接收数据

        send_cnt=0;         //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数//据还没发送完毕,那么它永远也长不大,因为每个中断都被清零,很可怜。

        RCIE=1;

}

    if(TMR1IE==1&&TMR1IF==1)    //定时中断

        {



               TMR1IF=0;     //定时中断标志位关闭

                TMR1ON=0;    //定时中断开关关闭

      if(voice_time_cnt)                       //控制蜂鸣器声音的长短

                 {

                        beep_dr=1;         //蜂鸣器响

                      --voice_time_cnt;        //蜂鸣器响的声音长短的计数延时

                 }

                else

                {

                    Asm(“nop”);   //添加此行空指令为了使else的内容跟if的内容对称,意义////不大

beep_dr=0;      //蜂鸣器停止

                }

      TMR1H=0xFe;   //重新设置定时时间间隔

    TMR1L=0x00;

      TMR1ON=1;        //定时中断开关打开

    }

}

void usart_service()  //串口服务程序,在main函数里

{

     if(send_update==1)  //说明目前串口正在接收数据,不要读缓冲区数据

     {

        send_lock=1;     //开自锁标志

        ++send_cnt;    //只要有数据接收,send_cnt每次都被串口中断清零

        if(send_cnt>cnt_send)   //延时一段时间,确认缓冲区没有继续接受数据

        {

            send_cnt=0;

            send_update=0;  

        }

     }

     Else  //说明当前没有继续接收数据了

     {

         if(send_lock==1)    //在数据已经接收完毕,并且还没有处理过数据的情况下

         {

            send_lock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据

            while(RCREG_read_total<RCREG_total)   //说明还没有把缓冲区的数据读取完

            {

              CLRWDT();

RCREG_buf_temp[0]= RCREG_buf_temp[1];  //数据移动,方便截取关键字

RCREG_buf_temp[1]= RCREG_buf_temp[2];

               RCREG_buf_temp[2]=asy_recieve();

               if(RCREG_buf_temp[0]==0xeb&&  RCREG_buf_temp[1]==0x00&& RCREG_buf_temp[2]==0x55)  //数据头”eb 00  55”判断

               {

                                              //把余下有效的数据都取出来

                       RCREG_buf_temp[3]=asy_recieve();  //数据量

                       Valid_total= RCREG_buf_temp[3];

                       For(m_int=0;m_int< Valid_total;m_int++)  //提取有效数据

                       {

                                 RCREG_buf_temp[4+ m_int]=asy_recieve();  

}

RCREG_buf_temp[4+ Valid_total]=asy_recieve();  //自带校验和

Receive_sum=0;         //累加校验和计算

                       For(m_int=0;m_int< (Valid_total+4);m_int++)  //累加校验和计算

                       {

                                Receive_sum=Receive_sum+ RCREG_buf_temp[m_int];

}

If(Receive_sum== RCREG_buf_temp[4+ Valid_total])   //校验和正确

{

RCREG_buf_temp[0]=0;    //把临时处理数据清零,方便下次接收

RCREG_buf_temp[1]=0;

RCREG_buf_temp[2]=0;

                                eusart_send(0x00); //串口发送多一个填充无效字节,避免硬件原因引起第一个字节丢失

                                eusart_send(0xeb); //串口发送应答的数据

eusart_send(0x00); //串口发送应答的数据

eusart_send(0xaa); //串口发送应答的数据

                                 voice_time_cnt= cnt_voice_time;    //蜂鸣器响“滴”一声就停

}

Else    //校验和出错

{

RCREG_buf_temp[0]=0;    //把临时处理数据清零,方便下次接收

RCREG_buf_temp[1]=0;

RCREG_buf_temp[2]=0;

                                eusart_send(0x00); //串口发送多一个填充无效字节,避免硬件原因引起第一个字节丢失

                                eusart_send(0xeb); //串口发送应答的数据

eusart_send(0x00); //串口发送应答的数据

eusart_send(0x55); //串口发送应答的数据

}

                       break;   //退出循环

               }



           }

         Buf_clear();   //把余下的缓冲区清零,方便下一堆数据接收与处理



        }

     }

}

Void Buf_clear()   //把余下的缓冲区清零

{

         Unsigned char buf_clear_temp;

            while(RCREG_read_total<RCREG_total)   //说明还没有把缓冲区的数据读取完

            {

                 CLRWDT();

                 buf_clear_temp =asy_recieve();

             }

}

unsigned char asy_recieve()  //把串口缓冲区的数据一个个提取出来

{

      unsigned char RCREG_dt;

      ++RCREG_read_total;    //已经读出了多少个数据

      RCREG_dt=RCREG_buf[RCREG_read_total -1];

      if(RCREG_read_total >=RCREG_total) //只要把全部数据都读完,马上把缓冲区清零

         {

          RCREG_read_total =0;

          RCREG_total=0;

         }

      return RCREG_dt;

}

void eusart_send(unsigned char t_data) //串口发送一个字节的数据

{

unsigned int error_delay;

     TXREG=t_data;   //发送数据

     error_delay=0;//等待把数据发送完毕

     while(1)   //这里也可以省略,直接用延时替代

   {

        CLRWDT();

        if(TXIF==1)   //等待把数据发送完毕

        {

           break;

        }

        Else            

        {

          ++error_delay;

          if(error_delay>200)  //超时也要退出,不能死等

          {

              break;

          }

       }

     }

     Delay11(1);     //此处最玄机,要特别注意。每发送完一个字节,由于不同的项目,这//里的延时间隔都不一样,读者根据实际情况来改。这里最容易出问题,必须要延时,尤 其是连续发送一堆数据的时候。读者也可以自己把这种死延时改成计数延时的方式。

}

//延时函数

void Delay11(unsigned int MS)

{

     unsigned char us,usn;

     while(MS!=0)            //for 12M

           {

               CLRWDT();

                  usn = 2;

                 while(usn!=0)

                       {       CLRWDT();

                             us=0xf5;

                             while (us!=0){us--;};

                             usn--;

                       }

                 MS--;

           }

}

(6)小结:

          累加和就是把前面所有字节相加,然后把结果放在一个字节的变量上,超过一个字节范围的部分会自动丢弃。



分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏3 分享淘帖 顶 踩
回复

使用道具 举报

沙发
ID:83553 发表于 2016-6-29 11:32 | 只看该作者
“因为硬件原因,第一个字节往往容易丢失”------能否说一下为什么。
回复

使用道具 举报

板凳
ID:370386 发表于 2018-7-20 16:10 | 只看该作者
如果有个源程序下载该多好
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表