对 YModem 的接触,只是在无网络的情况下,使用SecureCRT软件输入loady命令进行串口传输,烧写内核和文件系统。
参看:Hi3516A开发-- 板卡串口烧写
但你要问我 YModem 是个什么东西,我还真说不上来。
接下来,就开始对YModem的一步步剖析详解!!
一、YModem简介
参看:XMODEM/YMODEM PROTOCOL REFERENCE
。。。。 网上查了老半天,怎么感觉没有一篇讲的特别清楚的。
1、YMODEM最低要求
声称支持YMODEM的所有程序必须满足以下最低要求
1) 发送程序应在块0中发送路径名(文件名)。
2) 路径名应为空终止的ASCII字符串,如下所述。
对于那些懒得阅读整篇文档的人:
3) 除非特别要求,否则仅发送文件名部分。
4) 没有发送驱动器号。
5) 不区分大小写的系统,文件名中的字母只能以小写形式发送路径名。
6) 接收程序应使用此路径名作为接收文件名称,除非明确覆盖。
7) 当接收程序成功接收到此块时打开输出文件,它应该用ACK确认该块字符,然后继续正常的XMODEM文件传输以接收器发送的“C”或NAK开头。
8) 发送程序应使用CRC-16响应“C”路径名nak,否则使用8位校验和。
9) 接收程序必须接受128和1024字节的任何混合。它接收的每个文件中的块。发送节目可能会任意切换1024和128字节块。
10) 发送程序不得更改未确认的长度块。
11) 在每个文件的末尾,发送程序应发送EOT最多十个直到收到ACK字符为止。 (这是其中的一部分XMODEM规范。)
12) 传输会话的结束应由null(空)表示pathname,此路径名块应与其他路径名相同路径名块。
1、简介
参看:YModem协议简介
最常用的几种通信传输协议有:XModem、YModem、ZModem等。
XModem是最早的协议之一,几乎所有的通讯程序支持的文件传输协议,它传输128字节信息块。
YModem协议是XModem的改进协议,它最用于调制解调器之间的文件传输的协议,具有快速,稳定传输的优点。它的传输速度比XModem快,这是由于它可以一次传输1024字节的信息块,同时它还支持传输多个文件,也就是常说的批文件传输。
ZModem速度快于XModem和YModem,而且可以更好地断开后恢复传输。
如今,XModem基本已经被淘汰,最常用的就是YModem与ZModem。为了后面YModem升级程序实现做铺垫,下面就简单介绍下YModem协议。
YModem分成YModem-1K与YModem-g。
YModem-1K用1024字节信息块传输取代标准的128字节传输,数据的发送回使用CRC校验,保证数据传输的正确性。它每传输一个信息块数据时,就会等待接收端回应ACK信号,接收到回应后,才会继续传输下一个信息块,保证数据已经全部接收。
YModem-g传输形式与YModem-1K差不多,但是它去掉了数据的CRC校验码,同时在发送完一个数据块信息后,它不会等待接收端的ACK信号,而直接传输下一个数据块。正是它没有涉及错误校验,才使得它的传输速度比YModem-1K来得块。
一般都会选择YModem-1K传输,平时所说的YModem也是指的是YModem-1K。
二、YModem传输协议
1、起始帧的数据格式
YModem的起始帧并不直接传输文件的数据,而是将文件名与文件的大小放在数据帧中传输,它的帧长=3字节数据首部+128字节数据+2字节CRC16校验码=33字节。它的数据结构如下:
SOH 00 FF filename[ ] filezise[ ] NUL[ ] CRCH CRCL
其中SOH=0x01,表示这个数据帧中包含着128字节的数据部分;在SOH后面的00 FF,00表示数据帧序号,因为是起始帧,所以它的帧序为00,至于FF,它是帧序的取反,YModem特地这么做是为了给数据是否正确提供一种判断依据,通过判断这两个字节是否为取反关系,就可以知道数据是否传输出错;filename[ ]就是文件名,如文件名foo.c,它在数据帧中存放格式为:66 6F 6F 2E 63 00,一定要在文件名最后跟上一个00,表示文件名结束;filesize[ ]就是文件大小,如上面的foo.c的大小为1KByte,即1024Byte,需要先将它转化成16进制,即0x400,所以它在数据帧的存放格式为:34 30 30 00,即“400”,同样的文件大小最后需要跟上00,表示结束;NUL[ ]表示剩下的字节都用00填充,数据部分大小为128字节,除去文件名与文件大小占用的空间外,剩余的字节全部用00填充;CRCH CRCL分别表示16位CRC校验码的高8位与低8位。
扩展,什么是补码?
参看:C语言再学习 – 负数
2、数据帧的数据格式
YModem的数据帧中会预留1024字节空间用来传输文件数据,它跟起始帧接收差不多,如下:
STX 01 FE data[1024] CRCH CRCL
其中STX=0x02,表示这帧数据帧后面包含着1024字节的数据部分;STX后面的01 FE,01表示第一帧数据帧,FE则是它的取反,当然如果是第二帧数据的话就是:01 FD;data[1024]表示存放着1024字节的文件数据;CRCH与CRCL是CRC16检验码的高8位与低8位。
如果文件数据的最后剩余的数据在128~1024之前,则还是使用STX的1024字节传输,但是剩余空间全部用0x1A填充,如下结构:
STX [num] [~num] data[ ] 1A …1A CRCH CRCL
有一种特殊的情况:如果文件大小小于等于128字节或者文件数据最后剩余的数据小于128字节,则YModem会选择SOH数据帧用128字节来传输数据,如果数据不满128字节,剩余的数据用0x1A填充这是数据正的结构就变成了:
文件大小小于128字节:
SOH 01 FE data[ ] 1A …1A CRCH CRCL
文件最后剩余数据小于128字节:
SOH [num] [~~num] data[ ] 1A…1A CRCH CRCL
3、结束帧数据结构
YModem的结束帧数据也采用SOH的128字节数据帧,它的结构如下:
SOH 00 FF NUL[128] CRCH CRCL
结束帧同样以SOH开头,表示后面跟着128字节大小的数据;结束帧的帧序也认为是00 FF;结束帧的128字节的数据部分不存放任何信息,即NUL[128]全部用00填充。
4、文件传输过程
YMODEM批量传输会话(1个文件)
YMODEM批量传输会话(2个文件)
YMODEM批量传输Session-1k块
YMODEM-g传输会话(扩展)
上面传输过程中存在许多通信信号,它们的数值与意义如下表所示:
CRC的计算:
参看:STM32开发 – CRC校验码
手册CRC代码:
/* update CRC */
unsigned short
updcrc(c, crc)
register c;
register unsigned crc;
{
register count;
for (count=8; --count>=0;) {
if (crc & 0x8000) {
crc <<= 1;
crc += (((c<<=1) & 0400) != 0);
crc ^= 0x1021;
}
else {
crc <<= 1;
crc += (((c<<=1) & 0400) != 0);
}
}
return crc;
}
传输过程文字描述
- 接收方发送信号C启动传输会话,然后进入等待(SOH)状态,如果没有回应,就会超时退出。
- 发送方开始时处于等待C过程中。收到C以后,发送携带文件名和文件长度的起始帧(SOH)数据包开始信号。进入等待(ACK)状态
- 接收方收到SOH起始帧以后,CRC校验满足,则发送ACK。发送方接收到ACK,又进入等待“文件传输开启”信号,即重新进入等待“C”的状态
- 接收方发送C,表示可以开始数据的传输
- 于是发送方发送数据帧、接收方接收到后回复ACK,如此循环进行数据接收(过程中双方因为任何异常,如人工终端、通讯故障等都可能造成传输中断).
- 文件传输完毕后,发送方发送EOT信号,接收方收到后,回应NAK
- 发送方再次发送EOT,接收方回应ACK。
- 接收方发送C,准备再次文件传输
- 如果是单次文件传输,发送方发送传输结束帧,接收方回应ACK后,整个传输会话结束
接收方和发送方应实现的处理策略
参看:通信协议之YMODEM
共通策略
出错后需要重复尝试10次
协议流程应由接收方驱动
使用CAN或ASCII的^X字符来取消传输
接收方策略
接收方应该有10s超时机制
只要接收到数据开始,接收方使用1s超时机制来接收每个数据,直到该数据包接收完成。
同步:1.接收到想要的那个数据包并且没有任何错误后才算接收成功,并返回ACK; 2.能够处理两条重复的数据包(ACK丢失引起的问题); 3.因为一些特殊原因造成同步出错应终止传输,发送CAN
发送方策略
等待传输开始的这段时间,发送方应该设置一个比较长的超时时间,也可以不设置超时时间
文件传输结束应该发送EOT到接收方,直到收到一条ACK响应,否则重复发送
三、C语言实现YModem传输协议
上面简单的介绍了一下YModem的传输协议。那么问题来了,我想在STM32上用实现,代码该怎么写呢?
参看:ST官方代码——YModem协议部分c代码分析
参看:STM32F103代码远程升级(三)基于YModem协议串口升级程序的实现
下载工程:相关参考工程
网上相关的工程有很多的,接下来就逐一分析一下相关代码。
1、接收数据
可耻的,直接复制了。。。该博主注释的相当不错。我没什么可说的了。。。
/*******************************************************************************
* @函数名称 Receive_Packet
* @函数说明 从发送端接收一个数据包
* @输入参数 data :数据指针
length:长度
timeout :超时时间
* @输出参数 无
* @返回参数 接收的结果
0: 正常返回
-1: 超时或者数据包错误
1: 用户取消
*******************************************************************************/
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
uint16_t i, packet_size;
uint8_t c;
*length = 0;
if (Receive_Byte(&c, timeout) != 0)
{
return -1;//超时返回-1
}
switch (c) //c表示接收到的数据的第一个字节
{
case SOH: //数据包开始
packet_size = PACKET_SIZE;
break;
case STX: //正文开始
packet_size = PACKET_1K_SIZE;
break;
case EOT: //数据包结束
return 0;
case CA: //发送方中止传输
if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
{
*length = -1;
return 0;
}
else
{
return -1; //中止传输返回-1
}
case ABORT1: //A
case ABORT2: //a
return 1;
default:
return -1;
}
*data = c;
for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++) //获取剩下的数据(以字节为单位)
{
if (Receive_Byte(data + i, timeout) != 0)
{
return -1; //接收数据超时
}
}
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
{
return -1;
}
*length = packet_size;
return 0;
}
/*******************************************************************************
* @函数名称 Ymodem_Receive
* @函数说明 通过 ymodem协议接收一个文件
* @输入参数 buf: 首地址指针
* @输出参数 无
* @返回参数 文件长度
*******************************************************************************/
int32_t Ymodem_Receive (uint8_t *buf)
{
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
int32_t i, j, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
//初始化Flash地址变量
FlashDestination = ApplicationAddress;
for (session_done = 0, errors = 0, session_begin = 0; ;)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
{
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
{
case 0:
errors = 0;
switch (packet_length)
{
//发送端终止
case - 1:
Send_Byte(ACK);
return 0;
//结束传输
case 0: /* 数据包中序号和补码不匹配,终止数据发送*/
Send_Byte(ACK);
file_done = 1;
break;
//正常的数据包
default:
if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
{
Send_Byte(NAK); //发送应答NAK,接收失败要求重发
}
else
{
if (packets_received == 0) //第一包,包含文件名,文件大小
{
//文件名数据包
if (packet_data[PACKET_HEADER] != 0) //去除3个字节的首部,读取128B的数据包
{
//文件名数据包有效数据区域
//取出文件名--32B用于存储
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
file_name[i++] = *file_ptr++;
}
file_name[i++] = '\0';
//取出文件大小--2B用于存储
for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
file_size[i++] = *file_ptr++;
}
file_size[i++] = '\0';
Str2Int(file_size, &size); //字符转整型
//测试数据包是否过大
//文件大小是否超出flash存储大小
if (size > (FLASH_SIZE - 1))
{
//结束
Send_Byte(CA); //中止通信
Send_Byte(CA);
return -1;
}
//计算需要擦除Flash的页
//擦除用户应用程序将被加载的所需的页面
//定义需要被擦除的页面号
NbrOfPage = FLASH_PagesMask(size);
//擦除Flash
for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
{
FLASHStatus = FLASH_ErasePage(FlashDestination + (PageSize * EraseCounter));
}
Send_Byte(ACK); //发送应答ACK
Send_Byte(CRC16); //发送“C”,等待接收下一包数据包
}
//文件名数据包空,结束传输
else
{
Send_Byte(ACK);
file_done = 1; //文件传输中止
session_done = 1; //传输中止
break;
}
}
//数据包
else
{
memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
RamSource = (uint32_t)buf;
for (j = 0; (j < packet_length) && (FlashDestination < ApplicationAddress + size); j += 4)
{
//把接收到的数据编写到Flash中
FLASH_ProgramWord(FlashDestination, *(uint32_t*)RamSource);
if (*(uint32_t*)FlashDestination != *(uint32_t*)RamSource)
{
//结束
Send_Byte(CA);
Send_Byte(CA);
return -2;
}
FlashDestination += 4;
RamSource += 4;
}
Send_Byte(ACK);
}
packets_received ++;
session_begin = 1;
}
}
break;
case 1:
Send_Byte(CA);
Send_Byte(CA);
return -3;
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
Send_Byte(CA);
Send_Byte(CA);
return 0;
}
Send_Byte(CRC16);
break;
}
if (file_done != 0)
{
break;
}
}
if (session_done != 0)
{
break;
}
}
return (int32_t)size;
}
2、发送数据
/*******************************************************************************
* @函数名称 Ymodem_SendPacket
* @函数说明 通过ymodem协议传输一个数据包
* @输入参数 data :数据地址指针
length:长度
* @输出参数 无
* @返回参数 无
*******************************************************************************/
void Ymodem_SendPacket(uint8_t *data, uint16_t length)
{
uint16_t i;
i = 0;
while (i < length)
{
Send_Byte(data[i]);
i++;
}
}
/*******************************************************************************
* @函数名称 Ymodem_Transmit
* @函数说明 通过ymodem协议传输一个文件
* @输入参数 buf :数据地址指针
sendFileName :文件名
sizeFile:文件长度
* @输出参数 无
* @返回参数 是否成功
0:成功
*******************************************************************************/
uint8_t Ymodem_Transmit (uint8_t *buf, const uint8_t* sendFileName, uint32_t sizeFile)
{
uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD];
uint8_t FileName[FILE_NAME_LENGTH];
uint8_t *buf_ptr, tempCheckSum ;
uint16_t tempCRC, blkNumber;
uint8_t receivedC[2], CRC16_F = 0, i;
uint32_t errors, ackReceived, size = 0, pktSize;
errors = 0;
ackReceived = 0;
for (i = 0; i < (FILE_NAME_LENGTH - 1); i++)
{
FileName[i] = sendFileName[i];
}
CRC16_F = 1;
//准备第一个数据包
Ymodem_PrepareIntialPacket(&packet_data[0], FileName, &sizeFile);
do
{
//发送数据包
Ymodem_SendPacket(packet_data, PACKET_SIZE + PACKET_HEADER);
//发送CRC校验
if (CRC16_F)
{
tempCRC = Cal_CRC16(&packet_data[3], PACKET_SIZE);
Send_Byte(tempCRC >> 8);
Send_Byte(tempCRC & 0xFF);
}
else
{
tempCheckSum = CalChecksum (&packet_data[3], PACKET_SIZE);
Send_Byte(tempCheckSum);
}
//等待响应
if (Receive_Byte(&receivedC[0], 10000) == 0)
{
if (receivedC[0] == ACK)
{
//数据包正确传输
ackReceived = 1;
}
}
else
{
errors++;
}
} while (!ackReceived && (errors < 0x0A));
if (errors >= 0x0A)
{
return errors;
}
buf_ptr = buf;
size = sizeFile;
blkNumber = 0x01;
//1024字节的数据包发送
while (size)
{
//准备下一个数据包
Ymodem_PreparePacket(buf_ptr, &packet_data[0], blkNumber, size);
ackReceived = 0;
receivedC[0]= 0;
errors = 0;
do
{
//发送下一个数据包
if (size >= PACKET_1K_SIZE)
{
pktSize = PACKET_1K_SIZE;
}
else
{
pktSize = PACKET_SIZE;
}
Ymodem_SendPacket(packet_data, pktSize + PACKET_HEADER);
//发送CRC校验
if (CRC16_F)
{
tempCRC = Cal_CRC16(&packet_data[3], pktSize);
Send_Byte(tempCRC >> 8);
Send_Byte(tempCRC & 0xFF);
}
else
{
tempCheckSum = CalChecksum (&packet_data[3], pktSize);
Send_Byte(tempCheckSum);
}
//等待响应
if ((Receive_Byte(&receivedC[0], 100000) == 0) && (receivedC[0] == ACK))
{
ackReceived = 1;
if (size > pktSize)
{
buf_ptr += pktSize;
size -= pktSize;
if (blkNumber == (FLASH_IMAGE_SIZE/1024))
{
return 0xFF; //错误
}
else
{
blkNumber++;
}
}
else
{
buf_ptr += pktSize;
size = 0;
}
}
else
{
errors++;
}
} while (!ackReceived && (errors < 0x0A));
//如果没响应10次就返回错误
if (errors >= 0x0A)
{
return errors;
}
}
ackReceived = 0;
receivedC[0] = 0x00;
errors = 0;
do
{
Send_Byte(EOT);
//发送 (EOT);
//等待回应
if ((Receive_Byte(&receivedC[0], 10000) == 0) && receivedC[0] == ACK)
{
ackReceived = 1;
}
else
{
errors++;
}
} while (!ackReceived && (errors < 0x0A));
if (errors >= 0x0A)
{
return errors;
}
//准备最后一个包
ackReceived = 0;
receivedC[0] = 0x00;
errors = 0;
packet_data[0] = SOH;
packet_data[1] = 0;
packet_data [2] = 0xFF;
for (i = PACKET_HEADER; i < (PACKET_SIZE + PACKET_HEADER); i++)
{
packet_data [i] = 0x00;
}
do
{
//发送数据包
Ymodem_SendPacket(packet_data, PACKET_SIZE + PACKET_HEADER);
//发送CRC校验
tempCRC = Cal_CRC16(&packet_data[3], PACKET_SIZE);
Send_Byte(tempCRC >> 8);
Send_Byte(tempCRC & 0xFF);
//等待响应
if (Receive_Byte(&receivedC[0], 10000) == 0)
{
if (receivedC[0] == ACK)
{
//包传输正确
ackReceived = 1;
}
}
else
{
errors++;
}
} while (!ackReceived && (errors < 0x0A));
//如果没响应10次就返回错误
if (errors >= 0x0A)
{
return errors;
}
do
{
Send_Byte(EOT);
//发送 (EOT);
//等待回应
if ((Receive_Byte(&receivedC[0], 10000) == 0) && receivedC[0] == ACK)
{
ackReceived = 1;
}
else
{
errors++;
}
} while (!ackReceived && (errors < 0x0A));
if (errors >= 0x0A)
{
return errors;
}
return 0;//文件传输成功
}