YMODEM
它分为YMODEM-1K
与YMODEM-g
,平时说的YMODEM
传输指是YMODEM-1K
传输。
YMODEM-1K
可以一次传输1024字节
的信息块,同时支持传输多个文件。数据的发送会使用CRC校验
,保证数据传输的正确性。它每传输一个信息块数据时,就会等待接收端回应ACK
信号,接收到回应后才会继续传输下一个信息块,保证数据已经全部接收。符号 | 数值 | 含义 |
SOH | 0x01 |
128字节数据包 |
STX | 0x02 |
1024字节数据包 |
EOT | 0x04 |
结束传输 |
ACK | 0x06 |
回应 |
NAK | 0x15 |
不回应 |
CA | 0x18 |
传输中止 |
C | 0x43 |
请求数据包 |
SOH | 0x00 | 0xFF | File_Name | File_Size | NUL | CRC |
0x01 |
帧序号 | 帧反序号 | 文件名(Hex ) |
文件大小 | 填充0x00 区 |
校验位 |
起始帧帧长 = 3Bytes
帧头 + 128Bytes
数据域 + 2Bytes
的CRC16校验 = 133Bytes
SOH = 0x01
表示数据域为128Bytes
,起始帧帧序号为0x00
,0xFF
为0x00
取反,通过判断这两个字节是否为取反关系,来确定数据是否出错。
File_Name + File_Size + NUL
属于数据域
FileName
表示文件名,如main.bin
,它在数据帧中存放的格式为16进制:6D 61 69 6E 2E 62 69 6E 00
,需要在文件名最后添加00
,表示文件名结束。
FileSize
表示文件大小,如132KB
,转为16进制为0x21000H
,它在数据帧中的格式就是32 31 30 30 30 00
,同样需要在最后添加0x00
表示结束。
NUL
是数据域128Bytes
减去文件名
和文件大小
后剩下的字节都用0x00
填充。
CRC
为校验部分,高8位在前,低8位在后
,采用CRC16-CCITT。
STX | 0x01 | 0xFE | dat_pkt[1024] | CRC |
0x02 |
帧序号 | 帧反序号 | 数据包 | 校验位 |
STX = 0x02
表示数据包为1024Bytes
。
如果文件数据最后剩余的数据内容大小在128~1024之间,则还使用STX
的1024Bytes
传输,但剩余的空间使用0x1A
填充。
如果文件大小<=128Bytes
或者文件数据最后剩余数据内容大小 <128Bytes
,则Ymodem
会选择SOH
数据帧用128Bytes来传输数据,如果数据不满128Bytes,剩余的数据用0x1A
填充。
文件大小小于128Bytes
的结构为:SOH 01 FE data[ ] 1A ...1A CRCH CRCL
;
文件最后剩余数据小于128Bytes
的结构为:SOH [NUM] [~NUM] data[ ] 1A...1A CRCH CRCL
。
SOH | 0x00 | 0xFF | NUL[128] | CRC |
0x01 |
帧序号 | 帧反序号 | 填充0x00 |
校验位 |
API
部分,调用的就是Ymodem_Receive
函数(只需要ymodem.c
& common.c
两个文件),上传API
实际中用的少,就忽略了。
#define APP_FLASH_START_ADDR (0x08002800U) //APP程序跳转运行地址
#define IMAGE_UPDATA_FLASH_SIZE (MCU_FLASH_APP_MAIN_AREA_SIZE) //
unsigned char recv_buff[PACKET_1K_SIZE] ={0};//开辟数据包缓存区
unsigned char FileName[] = {0}; //文件名缓存区
/*
**app跳转引导函数
*/
void iap_load_app(const unsigned int app_addr)
{
//app_addr为新程序的起始地址,检查栈顶地址是否合法,即栈顶地址是否为0x2000xxxx(内置SRAM)
if (0x20000000 == ((*(volatile unsigned int*)app_addr) & 0x2FFE0000))
{
//__set_PRIMASK(1);//有中断,需要先关闭所有中断
const unsigned int jump_addr = *(volatile unsigned int*)(app_addr + 4);
__set_MSP(*(volatile unsigned int*)app_addr);
((void (*)(void))jump_addr)();// 设置PC指针为新程序复位中断函数的地址
}
}
/*
** Ymodem OTA升级执行函数
*/
void ymodem_ota_task(void)
{
//在执行的过程中,随时都可以按下 `A`按键主动结束升级动作。
printf(">> IAP already,you can pressed `A` key to end ota....\r\n");
//返回值携带升级信息
const int file_sz = Ymodem_Receive(&recv_buff[0],APP_FLASH_START_ADDR);//API改造了一下,增加APP跳转地址形参,便于全局维护。
if (0 < file_sz)
{
printf("\r\n--------- updata sucess --------\r\n");
printf("file name: %s\r\n", FileName);
printf("file size: %d bytes\r\n",file_sz);
printf("--------------------------------\r\n");
iap_load_app(APP_FLASH_START_ADDR);
}
else
{
printf("ota failed ...\r\n\r\n");
if (-1 == file_sz)//升级文件大于分配的FLASH大小
{
printf("\r\n alloc flash size is small ...\r\n");
}
else if (-2 == file_sz)//升级文件校验出错
{
printf("\r\n verif failed ...\r\n");
}
else if (-3 == file_sz)//用户主动结束升级(即按下了`A`按键)
{
printf("\r\n aborted by user ...\r\n");
}
else//升级超时或者其它
{
printf("\r\n receive failed ....\r\n");
}
//NVIC_SystemReset();
}
}
/*
**
*/
int Ymodem_Receive(unsigned char *buf, unsigned int app_addr)
{
unsigned char packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD] ,
file_size[FILE_SIZE_LENGTH] ,
*file_ptr, //
*buf_ptr;
int i,
packet_length,
session_done,
file_done,
packets_received,
errors,
session_begin,
image_size = 0;
volatile unsigned int flash_dst_addr, bin_data;
flash_dst_addr = app_addr;
//双循环
for (session_done = 0, errors = 0, session_begin = 0;;)//等价 while(1)
{
for (packets_received = 0, file_done = 0, buf_ptr = buf;;)//等价 while(1)
{
/* 0x00: 正常返回 | -1:时间溢出或数据包错误 | 0x01:用户终止*/
//分类处理接收包数据
switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
{
case 0://收到完整协议包
errors = 0;//清除累计错误
switch (packet_length)//判断包长
{
case -1:/* 连续的两个CA信号终止传输 */
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);//包序号不一致
}
else//包序号一致
{
if (packets_received == 0)//起始帧
{
if (packet_data[PACKET_HEADER] != 0)
{
//注:file_ptr = packet_data + PACKET_HEADER 等价 file_ptr = &packet_data[PACKET_HEADER],即从第PACKET_HEADER字节开始取数据指针
for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
{
FileName[i++] = *file_ptr++;//获取文件名
}
FileName[i++] = '\0';//补\0字符串结束符操作。
for (i = 0, file_ptr++; (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH);)
{
file_size[i++] = *file_ptr++;//获取文件大小
}
file_size[i++] = '\0';//补\0字符串结束符操作。
Str2Int(file_size, &image_size);//将字符串转为hex数据
if (IMAGE_UPDATA_FLASH_SIZE < image_size )//判断将要升级的文件是否超过预留的FALSH升级空间大小
{
/* End session */
Send_Byte(CA);//超过FALSH升级空间大小,发送传输中止应答符'CA',结束升级
Send_Byte(CA);
return -1; //返回错误码
}
/* Erase the needed pages where the user application will be loaded */
/* Define the number of page to be erased */
const unsigned int nbr_of_pg = mcu_flash_page_alloc(image_size);//计算当前文件大小要擦除的页数
/* Erase the FLASH pages */
const unsigned int erase_pgn = mcu_flash_page_num_calc(flash_dst_addr);//计算APP起始地址所在的页序号(因为后面擦除FALSH的API是按页序号来操作的)
//#define SIMLATE_DEBUG
#ifdef SIMLATE_DEBUG //仿真接收数据信息
printf("image size:%uBytes |erase page num :%d | erase page nbr:%d\r\n",image_size,erase_pgn,nbr_of_pg);
#else
mcu_flash_page_erase_cc_num(erase_pgn,nbr_of_pg);//按页序号来擦除FALSH的API
#endif
Send_Byte(ACK);
Send_Byte(CRC16);//发送正常应答符 + CRC校验
}
else //结束帧
{
Send_Byte(ACK);
file_done = 1; //
session_done = 1;//传输完毕
break;
}
}
else //数据帧
{
//注同样:packet_data + PACKET_HEADER 等价 &packet_data[PACKET_HEADER]
memcpy(buf_ptr,packet_data + PACKET_HEADER,packet_length);//取有效程序数据(去掉前三个帧头)
bin_data = (volatile unsigned int )buf;
//注:packet_length/4:表示下面写FLASH函数是按照4字节(字长度)
//若是按2字节写入,则用packet_length/2
//1字节则按packet_length
for (unsigned int j = 0; (j < packet_length/4);j++)
{
#ifdef SIMLATE_DEBUG //先仿真交互信息
printf("image dating addr :0x%08X | image data:%08X\r\n",flash_dst_addr,*(unsigned int *)bin_data);
#else
/* Program the data received into STM32F10x Flash */
mcu_flash_word_write(flash_dst_addr,*(volatile unsigned int *)bin_data);//每次按4字节写入(1包则按packet_length/4次写完)
if (*(volatile unsigned int *)flash_dst_addr != *(volatile unsigned int *)bin_data)//每次写入后再读取出来进行对比。
{
/* End session */
Send_Byte(CA);
Send_Byte(CA);
return -2; //写入出错(校验失败)
}
#endif
flash_dst_addr += 4;//每次地址按写入的字节长度偏移,当前API是4字节。
bin_data +=4;//同样数据源(来自包数据)一起同步偏移。
}
Send_Byte(ACK);//写完后一包发送ACK.
}
packets_received++;//升级包数目计数
session_begin = 1;//接收一包标记
// printf("received packets :%d\r\n",packets_received);
}
}
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;
}//end Receive_Packet...
if (file_done != 0)
{
break;
}
}//end 接收while(1);
if (session_done != 0)
{
break;
}
}
return (int)image_size;
}
/*
**获取输入\接收单字节数据
*/
unsigned int SerialKeyPressed(unsigned char *key)
{
//串口查询方式接收数据
if (RESET!=USART_GetFlagStatus(USART1,USART_FLAG_RXNE))
{
*key = (uint8_t)USART_ReceiveData(USART1);
return 1;
}
return 0;
}
/*
** 接收字节函数
*/
static int Receive_Byte(unsigned char *c, unsigned int timeout)
{
volatile unsigned int count = timeout;
while (count-- > 0)
{
//收到数据立马退出
if (SerialKeyPressed(c) == 1)
{
return 0;
}
}
return -1;
}
/*
** 接收包数据函数
*/
int Receive_Packet(unsigned char *data, int *length, unsigned int timeout)
{
unsigned short int i, packet_size;
unsigned char c;
unsigned short int crc;
*length = 0;
/* 接收一个字符 */
if (Receive_Byte(&c, timeout) != 0) //没有收到数据,退出
{
return -1;
}
switch (c)
{
/* SOH表示数据区有128字节 */
case SOH:
packet_size = PACKET_SIZE;
break;
/* STX表示数据区有1k字节 */
case STX:
packet_size = PACKET_1K_SIZE;
break;
/* 传输结束 end of transmission */
case EOT:
return 0;
/* 连续的两个CA信号终止传输 */
case CA:
/* 收到两个连续的CA信号 */
if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
{
*length = -1;
return 0;
}
else/* 只收到一个CA信号 */
{
return -1;
}
/* 用户终止传输 */
case ABORT1:
case ABORT2:
return 1;
default:
return -1;
}
*data = c;
for (i = 1; i < (packet_size + PACKET_OVERHEAD); i++)
{
//data + i 等价 &data[i]
if (Receive_Byte(data + i, timeout) != 0)//没有收到数据,退出
{
return -1;
}
}
/* 第PACKET_SEQNO_COMP_INDEX(数字2)字节是PACKET_SEQNO_INDEX(数字1)字节的反码 */
if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))//正反帧序号校验不通过
{
return -1;
}
/* 计算CRC */
crc = data[packet_size + PACKET_HEADER] << 8;
crc += data[packet_size + PACKET_HEADER + 1];
if (Cal_CRC16(&data[PACKET_HEADER], packet_size) != crc)//数据包校验不通过
{
return -1;
}
/* 取数据区长度 */
*length = packet_size;
return 0;
}
//FLASH擦写函数API参考
/*
** 根据提供的文件大小,计算所需要的FLASH页数。
*/
unsigned int mcu_flash_page_alloc(volatile unsigned int img_size)
{
unsigned int pg_num = 0x0;
unsigned int img_sz = img_size;
if ((img_sz % MCU_FLASH_PAGE_SIZE) != 0)
{
pg_num = (img_sz / MCU_FLASH_PAGE_SIZE) + 1;
}
else
{
pg_num = img_sz / MCU_FLASH_PAGE_SIZE;
}
return pg_num;
}
/*
**页所在的FLASH地址计算当前页序号
*/
unsigned int mcu_flash_page_num_calc(const unsigned int flash_addr)
{
/* calculate the number of page to be programmed/erased */
return (flash_addr - MCU_FLASH_START_ADDR) / MCU_FLASH_PAGE_SIZE;
}
/*
**(按序号)连擦除FLASH函数
*/
void mcu_flash_page_erase_cc_num(const unsigned int erase_pgn,unsigned int erase_pg_nbr)
{
unsigned int erase_pgaddr_ofs=mcu_flash_page_addr_calc(erase_pgn);/*计算当前页所在FLASH地址*/
/* Unlocks the FLASH Program Erase Controller */
FLASH_Unlock();
/* clear all pending flags */
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_WRPRTERR | FLASH_FLAG_PGERR);
/* erase the flash pages */
for(unsigned int i=0;i<erase_pg_nbr;i++)
{
while(FLASH_COMPLETE != FLASH_ErasePage(erase_pgaddr_ofs));//擦除当前FLASH地址所在页
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_WRPRTERR | FLASH_FLAG_PGERR);
erase_pgaddr_ofs+=MCU_FLASH_PAGE_SIZE;
}
/* Locks the FLASH Program Erase Controller */
FLASH_Lock();
}
1.网上大多推荐的是SecureCRT终端工具,我用的是Tera Term这款终端工具,里面有支持Xmodem
,Ymodem
,Zmodem
等多协议文件发送功能,
2.Ymodem
升级文件使用: 打开终端
3.后来在gitee
上找到了作者biglu制作的专门用于Ymodem
升级的上位机Ymodem_tool,其支持发送包大小是128B/1024B
的,界面简洁好用。