在智能小车四《串口通信》中讲解了串口的通信原理,它就是一个直接把信息转为电信号的工具,透明传输。接着这篇文章我们来解决一下没有协议而发生信息错乱的情况。比如在我们的小车里,收到字符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的时候再讲吧。

从上面你拼出什么了么?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的时候再讲吧。