YModem协议是XModem的改进协议,它最常用于调制解调器之间的文件传输的协议,具有快速,稳定传输的优点。它的传输速度比XModem快,这是由于它可以一次传输1024字节的信息块,同时它还支持传输多个文件,也就是常说的批文件传输。
YModem分成YModem-1K与YModem-g。
YModem-1K用1024字节信息块传输取代标准的128字节传输,数据使用CRC校验,保证数据传输的正确性。它每传输一个信息块数据时,就会等待接收端回应ACK信号,接收到回应后,才会继续传输下一个信息块,保证数据已经全部接收。
YModem-g传输形式与YModem-1K差不多,只是它去掉了数据的CRC校验码,同时在发送完一个数据块信息后,它不会等待接收端的ACK信号,而直接传输下一个数据块。正是它没有涉及错误校验与等待响应,才使得它的传输速度比YModem-1K来得快。
此次我用的就是YModem-1K传输。
YModem协议简介
此篇文章对YModem协议介绍地很详细。
YModem的起始帧用于传输文件名与文件的大小,注意该数据包号为0,帧长=3字节的数据首部+128字节数据+2个字节CRC16校验码 = 133字节。数据结构为:
SOH 00 FF filename[ ] filezise[ ] NUL[ ] CRCH CRCL
YModem的数据帧从第二包数据开始,注意该数据包号为1。帧长 = 3字节的数据首部+1024字节数据+2字节的CRC16校验码 = 1029字节。数据结构为:
STX [num] [~num] data[ ] 1A …1A CRCH CRCL
其中的第二个字节为传输的数据包包号,第三个字节为数据包号取反组成。若文件数据的最后一包数据在128~1024之间,则数据部分剩余空间全部用0x1A填充。
注意,存在一种特殊情况:如果文件的大小小于或等于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还会再传一包结束数据,只是数据内容为空。帧长=3字节首部+128字节的数据+2字节CRC16校验码 = 133字节,其数据帧结构为:
SOH 00 FF NUL[128] CRCH CRCL
#####4、文件传输过程
此处借用上面提到博客的图。以示说明。
特别注意的是,在文件传输结束时发送端发送了结束标识EOT之后待收到接收端的回复后,还会再发送一包空数据包以表示传输真正结束。
此次我使用的串口工具为 ttermpro.exe ,该串口工具支持Kermit、Xmodem、Ymodem、ZModem等通信协议,其中XModem和YModem协议采用的是CRC16_XModem校验法则。
运行该程序后先选择串口,如图:
然后再配置通信波特率,一般默认为9600,我们可以根据自己需要修改成相应波特率,如图:
紧接着就可以选择相应的通信协议进行文件传输了,如图:
此次我不再使用上篇文章中的代码,而是从官网上下的stm32f4_iap_using_usart官方F4xx的例程,刚好该例程中也是使用了YModem通信协议。
我将该例程移植到我的stm32F103的工程下,并且把外部按键触发升级程序修改为了软件触发。具体实现是:使用stm32中的备份寄存器作为标识位,当该位被修改,则重启程序进入Bootloader升级程序,在Bootloader程序中也根据备份寄存器中的值进行相应的升级操作。
而要使用备份寄存器,首先得使能该寄存器,查看数据手册后得到如下操作:
/**************************************************************************************************
** 函数名称 : BKP_Configuration
** 功能描述 : 使能BKP寄存器
** 入口参数 : 无
** 出口参数 : 无
** 返 回 值 : 无
** 其他说明 : 无
***************************************************************************************************/
void BKP_Configuration(void)
{
/* 使能PWR和BKP时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);
/* 使能对后备寄存器的访问 */
PWR->CR|=1<<8;//置PWR_CR寄存器的第八位DBP为1-->使能对后备寄存器和RTC的访问
}
在理解了YModem的数据结构和传输过程之后,再看例程中对YModem接收的处理就比较清晰了.
/**************************************************************************************************
** 函数名称 : Receive_Packet
** 功能描述 : 从发送方基于串口查询方式接收一个数据包
** 入口参数 : [in] 接收到的数据缓存区
[in] 接收的数据长度
[in] 最大等待接收时间
** 出口参数 : length
0 序号和补码校验不成功
-1 发送方中止传输
>0 正常的数据包长度
** 返 回 值 : 0 正常返回
-1 超时或者包错误
1 用户中止传输
** 作 者 :
** 日 期 :
** 其他说明 : 无
***************************************************************************************************/
int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
uint16_t i, packet_size;
uint8_t c =0;
*length = 0;
//printf("time_out = %x\r\n",timeout);
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 -2;
case CA: //发送方中止传输
if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
{
*length = -1;
return 0;
}
else
{
return -1; //中止传输返回-1
}
case ABORT1://A
case ABORT2://a
// case CAN:
//用户中止传输
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;//接收数据超时
}
}
uint8_t temp1 =0;
uint8_t temp2 =0;
temp1 = data[PACKET_SEQNO_INDEX];
temp2 = data[PACKET_SEQNO_COMP_INDEX];
if (temp1 != ((temp2 ^ 0xff) & 0xff))//校验序号和补码
{
printf("temp1 != ((temp2 ^ 0xff) & 0xff) \r\n");
return -1;
}
//序号和补码校验不成功则 length = 0;
*length = packet_size; //获取数据包长度
return 0;
}
/**************************************************************************************************
** 函数名称 : Receive
** 功能描述 : 基于Ymodem协议获取文件
** 入口参数 : [in] 文件首地址
** 出口参数 : 无
** 返 回 值 : 文件大小
** 作 者 :
** 日 期 :
** 其他说明 : 无
***************************************************************************************************/
int32_t 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;
FlashDestination = ApplicationAddress; //初始化闪存目标地址
memset(packet_data,0,PACKET_1K_SIZE + PACKET_OVERHEAD); //初始化为0
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))//0,-1,1
{
//---------case 0 正常返回
case 0:
case -2: //收到结束标志,读取结束包 SOH 00 FF 00…00[128个00] CRCH CRCL
errors = 0;
switch (packet_length)
{
/* 由发送方终止传输*/
case - 1:
Send_Byte(ACK);
return 0;
/* 数据包中序号和补码不匹配,终止数据发送*/
case 0:
Send_Byte(ACK);
file_done = 1;
break;
/* length>0 正常的数据包*/
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;
}
//擦除用户应用程序将被加载的所需的页面
//定义需要被擦除的页面号
NbrOfPage = FLASH_PagesMask(size);
// 擦除指定的flash页面
for (EraseCounter = 0; (EraseCounter < NbrOfPage) && (FLASHStatus == FLASH_COMPLETE); EraseCounter++)
{
FLASHStatus = FLASH_ErasePage(FlashDestination + (PageSize * EraseCounter));
}
//如果文件大小小于20K,一个页面大小2K
if(NbrOfPage < 10)
{
delay2_ms(100);
}
Send_Byte(ACK); //发送应答ACK
Send_Byte(CRC16); //发送“C”,等待接收下一包数据包
}
//文件名首字节为空
else
{
Send_Byte(ACK);
file_done = 1; //文件传输中止
session_done = 1; //传输中止
break;
}
}// if (packets_received == 0)
//packets_received > 0,1024B数据包开始传输
else
{
//取出数据
if(check(1,&packet_data[PACKET_DATA_INDEX],1024))//增加CRC校验验证
{
RamSource = (uint32_t)&packet_data[PACKET_DATA_INDEX];
for (j = 0;(j < packet_length) && (FlashDestination < ApplicationAddress + size);j += 4)
{
// BKP->DR6 = 2;//表示正在以Ymodem协议向Flash中写程序
/*将收到的数据写到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;
}//for
Send_Byte(ACK);
}
else
{ //CRC验证不通过
Send_Byte(NAK);
}
}//else
packets_received ++; //接收到的数据包加1
session_begin = 1;
}//else--序号一致
}//switch packet_length
memset(packet_data,0,PACKET_1K_SIZE + PACKET_OVERHEAD);//数据包清0
break;
//------case 1 由用户输入A(a)中止传输
case 1:
Send_Byte(CA);//发送字节CA
Send_Byte(CA);
return -3;
//------返回-1 超时或者包错误
// case -2:
// Send_Byte(ACK);
// break;
default:
if (session_begin > 0)
{
errors ++;
}
if (errors > MAX_ERRORS)
{
Send_Byte(NAK);
Send_Byte(CA);
Send_Byte(CA);
return 0;
}
Send_Byte(CRC16);//发送“C”
break;
}//switch Receive_Packet
if (file_done != 0) //文件传输中止
{
break;
}
}//for 2 内循环
if (session_done != 0) //传输中止
{
break;
}
}//for 1 外循环
return (int32_t)size;
}
以上两段代码为YModem协议接收数据及处理数据的过程,在此之前我们需先初始化系统以及结构,同时需要注意以下几点:
1、Bootloader中尽可能不使用中断,因此此处串口接收数据采用查询接收方式;
2、Bootloaderz中不要让程序卡死或者进入某个死循环,应在适当的地方进行软件复位;
3、在Keil环境下涉及内存拷贝时,尽量不用memcpy();
4、注意数组长度越界或者溢出错误;
5、注意YModem协议第一包数据的包号为00。
因为大家都留言寻求 ttermpro.exe 的软件包,所以我特意更新了博客,将下载链接放在这里,需要的小伙伴自取哟
百度网盘链接
提取码:jysq
YModem协议理解
YModem协议简介
Ymodem协议详解