设计一个文件传输的协议

很多时候,我们需要从串口无损传输一个文件。但是对于串口来说,经常会发生丢失数据或者是数据错误的情况。因此,需要有一个协议来保证传输的正确性。这里设计一个简单的协议。
首先,打开接收端等待传输,发送端传输一个4字节的文件长度。之后的数据都是以N为单位(比如:16字节或者128字节等等)进行发送。

image001

数据包的长度为 N字节。其中有2个字节是用于校验。一个是 Checksum: 将0到N-2加到一起,要等于0;另一个是Index取值从0-255,比如一个包是0,第二个发送过来的就是1,这样一直下去,到255后再返回到0。
在传输阶段,接收端用“N”来表示请发送下一个数据包,接收端对收到的数据进行校验,如果出现错误,发送“R”表示要求发送端重新发送。
设计完成就是代码的实现。我编写了一对Windows 程序用来发送和接受,使用的是 128字节的数据包;此外就是一个Windows发送端,还有接受的Arduino代码。
在Windows下,推荐使用Virtual Serial port Driver 这个软件调试。他能够虚拟出来连通的串口,这样无需在外部的Loopback 直接就可以调用串口非常方便。
image010

我传输了一个200K的内容,之后对比过,发送和接收到的内容是相同的。我将代码直接打包,有兴趣的朋友可以研究一下(Delphi XE2编译)。
image011

这是上位机的发送部分,每次收到N或者R后会根据情况进行发送

procedure TForm2.ComPort1RxChar(Sender: TObject; Count: Integer);
var
  s,t: String;

  aSize,i:integer;
  checksum:byte;
begin
  ComPort1.ReadStr(s, Count);
  if s[Count]='N' then
    begin
      Memo1.Lines.Add('Received N');
      if BytesSend<stream.size then
         begin
           fillchar(Buffer,BUFFERSIZE,0);
           // 读取文件并取得实际读取出来的长度
           aSize:=stream.Read(Buffer,BUFFERSIZE-2);
           //计算Checksum, 就是 Buffer 第一个到倒数第二个加起来要求为0
           checksum:=0;
           for i := 0 to BUFFERSIZE-3 do
             begin
               checksum:=checksum-Buffer[i];
             end;
           //放置Checksum
           Buffer[BUFFERSIZE-2]:=checksum;
           //放置顺序号
           Buffer[BUFFERSIZE-1]:=Index;
           inc(Index);
           ComPort1.Write(Buffer,BUFFERSIZE);
           {t:='';
           for i := 0 to BUFFERSIZE-1 do
             begin
               t:=t+ IntToHex(Buffer[i],2)+' ';
             end;
            Memo1.Lines.Add(t);
           }
           BytesSend:=BytesSend+aSize;
           Form2.Caption:=IntToStr(BytesSend)+'/'+IntToStr(stream.size);
           Form2.Refresh;
         end
      else
         Memo1.Lines.Add('Completed!');
    end;
  //如果收到 R 就再次发送
  if s[Count]='R' then
    begin
      ComPort1.Write(Buffer,BUFFERSIZE);
      {     t:='';
           for i := 0 to BUFFERSIZE-1 do
             begin
               t:=t+ IntToHex(Buffer[i],2)+' ';
             end;
            Memo1.Lines.Add(t);
      }
      Memo1.Lines.Add('R');
    end;

end;

 

Arduino代码如下 (Arduino我没有进行Index的校验)

#include <SoftwareSerial.h>

#define BUFFERSIZE 16
#define TIMEOUT 3000UL
#define DEBUG (1)

//用来 DEBUG 的软串口,用额外的USB转串口线接在Pin11即可
SoftwareSerial mySerial(10, 11); // RX, TX

char buffer[BUFFERSIZE];

unsigned long filesize=0;  //文件大小
byte *p=(byte *)&filesize;
unsigned long total=0;
  
void setup() {
  //收到的文件大小字节数,一共4位
  byte c=0;
  //每次收到的数值
  byte r;
  
  Serial.begin(115200);

  if DEBUG {
      // set the data rate for the SoftwareSerial port
      mySerial.begin(115200);
      mySerial.println("Hello, world?");
  }     

  //如果没有收够4字节,则一直接收
  while (c<4) {
    while (Serial.available() > 0) 
      {
        //比如文件大小为 0x10000, 那么上位机发出来的顺序是
        // 00 00 01 00, 因此这里需要一个顺序的调换
        r=Serial.read();
        *(p+c)=r;
        c++;
        if DEBUG {
            mySerial.print(r);
            mySerial.println("  ");
        }    
      } //while (Serial.available() > 0) 
  } //while (c<4)   

  if DEBUG {
    mySerial.print("filesize=");
    mySerial.println(filesize);
  }
    
  //通知上位机继续发送
  Serial.print('N');
}

void loop() {
  byte buffer[BUFFERSIZE];
  int i;
  byte checksum;
Next:  
  //接收数据的字节数
  int counter=0;
  //接收超时的变量
  unsigned long elsp=millis();
  //如果接收数量小于缓冲区大小并且未超时,那么继续接收
  while ((counter<BUFFERSIZE)&&(millis()-elsp<TIMEOUT))
    {
      while (Serial.available()>0)
        { 
          buffer[counter]=Serial.read();
         if (DEBUG) {
            // mySerial.print(buffer[counter],HEX);
            // mySerial.print(" ");
            // mySerial.println(counter,HEX);
         }   
          counter++;
        } //while      
    }    
  //如果接收数量不足退出上面的循环  
  if (counter!=BUFFERSIZE) {
    //通知上位机重新发送
    Serial.print("R");
    if DEBUG {
        mySerial.print("R");
    }    
  }
  
  //检查接收到的数据校验和
  checksum=0; 
  for (i=0;i<BUFFERSIZE-1;i++) 
    {
      checksum=checksum+buffer[i];
      //Serial.print(buffer[i]);
      //Serial.print("   ");
      if DEBUG {
       //mySerial.print(buffer[i]);
       //mySerial.print("   ");
      } 
    }
  //校验失败通知上位机重新发送  
  if (checksum!=0)  {
       Serial.print("R");
       if DEBUG {
            mySerial.print("R");
       }     
       goto Next;
    }
  
  //如果当前收到的总数据小于文件大小,那么要求上位机继续发送  
  if (total<filesize) 
    {
      Serial.print('N');  
      //有效值只有 BUFFERSIZE-2
      total=total+BUFFERSIZE-2;
      if DEBUG {
            mySerial.print("N");
      }      
    }
    //否则停止
    else {
       if DEBUG {
            mySerial.print("Total received");
            mySerial.print(total);
       }     
      while (1==1) {}
    } //else

}

 

调试的方法是开一个SoftwareSeiral,然后用Pin11接到USB 转串口的RX 上,同时共地。我发送一个 400K 左右的文件:

image012

上面的方法优点是:
1.足够简单容易实现
2.对内存要求低

缺点也是很明显:
1.接收端只能等待外面过来的文件大小,如果这一步骤出现问题,那么后面都会乱掉,换句话说,如果你的通讯信道足够糟糕,那么整体还是不稳定;
2.效率不高,如果使用16位的Buffer,那么有效的数据只有 14字节,14 /16=87.5%。资料上说Uno 的串口默认是 64Byte,如果用这么大的Buffer,效率可以达到 (64-2)/64=96.9%。

附件下载:

Windows内部发送和接受代码和EXE(使用 128字节Buffer,速度挺快)
Receiver
Sender

Windows发送的EXE(16字节Buffer), Arduino 代码
Sender16
receivefile