很多时候,我们需要从串口无损传输一个文件。但是对于串口来说,经常会发生丢失数据或者是数据错误的情况。因此,需要有一个协议来保证传输的正确性。这里设计一个简单的协议。
首先,打开接收端等待传输,发送端传输一个4字节的文件长度。之后的数据都是以N为单位(比如:16字节或者128字节等等)进行发送。
数据包的长度为 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 直接就可以调用串口非常方便。
我传输了一个200K的内容,之后对比过,发送和接收到的内容是相同的。我将代码直接打包,有兴趣的朋友可以研究一下(Delphi XE2编译)。
这是上位机的发送部分,每次收到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 左右的文件:
上面的方法优点是:
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