智能小车6:串口协议

在智能小车四《串口通信》中讲解了串口的通信原理,它就是一个直接把信息转为电信号的工具,透明传输。接着这篇文章我们来解决一下没有协议而发生信息错乱的情况。比如在我们的小车里,收到字符u表示要前进。我们用实际手机给小车发一条蓝牙串口命令。



从上面你拼出什么了么?CONNECT ...... 这些是蓝牙协议的内容,他可能会与我们的命令重合,使我们的小车发生错乱。于是我想自己定义一个协议,我参考TCP协议的结构来定义的。关于TCP协议也是大学里网络课程有的,我简单描述一下。
TCP包结构:



TCP协议是基于端口的,所以它有源端口、目的端口,而串口协议不存在这个。其它的字段的含义可以网上查到,我这里面不再赘述了。最后我把协议定义成如下结构:



如何实现这个协议呢?需要分别在小车(arduino c语言)和控制端(android java)各实现一套数据包的解析和生成程序。
首先是小车端:
一、发送数据包:


ZZProtocol zzp;
/**
cmd是要发送的命令
*/
void sendCmd(String cmd){
  
    int len=cmd.length();
    char data[len+3];
    char cmdArray[len];
    for(int i=0;i<len;i++){
      cmdArray[i]=cmd.charAt(i);
    }
   
    zzp.sendMsg(cmdArray,len,data);
    for(int i=0;i<len+3;i++){
      Serial.print(data[i]);
    }
    Serial.println();
}

/*
msg:要发送的内容
len:数据长度
data:最后发送数据包
*/
void ZZProtocol::sendMsg(char msg[], int& len, char data[]) {
    if (len <= 0) {
        return;
    }
    data[0] =  STARTFLAG;
    data[1] = (char) len;
    data[3] = msg[0];
    char tmpCode = data[3];
    for (int i = 1; i < len; i++) {
        data[i + 3] = msg[i];
        tmpCode = tmpCode^data[i + 3];
    }
    data[2] = tmpCode;
}




二、接收数据包:
/**
得到命令
*/
int getCmd(){
    int receiveData[64];  
  int rstData[64];
  int rstNum=0;
  receiveMsg(receiveData,rstData,rstNum);
 
  if(rstNum>0){ 
      if(rstNum==1){
        int cmd=(int)rstData[0];
        return cmd;
      }
  }
  return 0;
}


/**
接收命令
*/
void receiveMsg(int receiveData[],int rstData[],int &rstNum){
   int readNum=Serial.available();   
   if(readNum>0){   
       int startIndex=0;
       readHead(receiveData,startIndex);     
       int rstFlag=zzp.checkFullPackage(receiveData,startIndex,rstData,rstNum);
       if(rstNum>0){
         //正常命令
         /*
         Serial.println("cmd:");
         printMsg(rstData,rstNum);             
        */
       }else{
         //错误信息,调试时用,Serial.print会再次传给蓝牙,造成arduino死机      
         Serial.print("error:");  
         Serial.println(rstFlag);            
         if(rstFlag==-1){
           char str[100]="";
           sprintf(str, "[(head error) byte0 btye1  char0 char1 length]:%d,%d,%c,%c,%d",receiveData[0],receiveData[1],receiveData[0],receiveData[1],readNum);     
           Serial.println(str);        
         }else if(rstFlag==-2){
             for(int i=1;i<startIndex;i++){
                Serial.print((byte)receiveData[i]);
                Serial.print(" ");
             }
             Serial.println();
         }   
       }
      
     }else{
       //读到的数,调试用
      /*
       if(readNum>0){
         Serial.print("readNum:");
         Serial.println(readNum);
       }
       */
     } 
}


里面的包头识别函数(readHead)与检查包函数(checkFullPackage)我要用伪代码了。因为代码太多了,再粘下去,文章不能看了。

包头识别函数(readHead):
一个字符一个字符的读取,只到读到首部标识字符。第二位是长度,所以用一个循环while读取,直到读取到长度或超时退出。如果读到长度,则可以计算出还需多少空间来存储包。当然第二位也有可能是首部标识,这种情况,就舍弃第一个字符,重新从头计算。之后就可以开始读取数据了,但数据里还有可能是首部字符,这时又舍弃前面的字符,重新执行本函数。

检查包函数(checkFullPackage)比较简单:
/**
 *
 * @param receiveData 接收字符串
 * @param dataLen 接收字符串长度
 * @param rstData 返回字符串
 * @param rstLen 返回字符串长度
 * @return  -1:head或len验证没通过。-2 checkCode验证没通过 。1正常返回
 */
int ZZProtocol::checkFullPackage(int receiveData[],int dataLen,int rstData[],int &rstLen) {
    int head= receiveData[0];
    int len = receiveData[1];
    int checkCode = receiveData[2];
    int dataCheck=-1;
    if(head==STARTFLAG&&dataLen==len+3){
         for (int i=3; i < dataLen ; i++) {
            if (dataCheck == -1) {
                dataCheck = receiveData[i];
            } else {
                dataCheck =  dataCheck^receiveData[i];
            }
        }
    }else{
        return -1;
    }
    if(checkCode==dataCheck){
        int index = 0;
        while (index < len ) {
           rstData[index]=receiveData[index+3];
           index++;
        }      
        rstLen=len;
        return 1;
    }else{
        return -2;
    }
}

最后得到这个命令,可以看得出,getCmd的返回值是个int,并不是一个字符串。嗯,这是因为我的小车功能还比较简单,一个int就能表示所有的命令了,这样也方便调试。另外还有一个问题就是int占用空间少,比较适合arduino这样的硬件受限的设备,字符串可能效率非常低。不过只要我一直玩下去,这就能变成一个字符串。

好累,android部分的协议我还是放到讲android的时候再讲吧。
文/程忠 浏览次数:0次   2017-08-12 09:22:20

相关阅读


评论: