很多时候,我们需要从串口无损传输一个文件。但是对于串口来说,经常会发生丢失数据或者是数据错误的情况。因此,需要有一个协议来保证传输的正确性。这里设计一个简单的协议。 首先,打开接收端等待传输,发送端传输一个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后会根据情况进行发送 [mw_shl_code=pascal,true]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;
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,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,2)+' ';
end;
Memo1.Lines.Add(t);
}
Memo1.Lines.Add('R');
end;
end;[/mw_shl_code]
Arduino代码如下 (Arduino我没有进行Index的校验) [kenrobot_code]#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;
//Serial.print(buffer);
//Serial.print(" ");
if DEBUG {
//mySerial.print(buffer);
//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
}[/kenrobot_code] 调试的方法是开一个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,速度挺快) Windows发送的EXE(16字节Buffer), Arduino 代码
|