YMODEM类似于XMODED-1K模式(实际在SecureCRT等实现中会混合使用128包长模式),固定使用crc校验。和XMODEM的主要区别是,他会在正式传输文件前先使用ID为0的包进行文件信息交互,文件传输完成之后使用ID为0的包来结束整个传输。可以连续传输多个文件。
YMODEM还有个变种即YMODEM-g,流式无ACK,一般很少使用。
xmodem实现参考 《https://mp.weixin.qq.com/s/QSYKxND3DTyQZ0JtnSqkzQ XMODEM协议介绍与高效高可移植非阻塞版本实现》
Xmodem和ymodem协议参考xymodem.pdf(http://www.blunk-electronic.de/train-z/pdf/xymodem.pdf)。
基于Xmodem-1K,所以包格式和Xmodem一样。但是Ymodem在正式传输文件之前有一个包ID0的交互用于传输文件信息,中间可以传输多个文件,所有文件传输完成之后用一个ID为0的包交互结束。一个文件的内容传输过程就对应Xmodem的传输,一个文件传输之后有两个EOT,接收方对第一个EOT进行NAK,对第二个进行ACK。详见传输过程示意。
整体上看流程为:
1.启动0 ID包交互
2.文件传输
3.文件传输
....
4.结束0 ID包交互
文件传输即对应Xmodem的过程,但是最后传输多了一个EOT-NAK的过程。
0 ID包的内容见下一节
如下图所示,有背景部分是ymodem相对xmodem增加,图示只描述正常流程,为了简洁不包括取消,NAK重发等过程。
包格式参考前言的文档,这里不再赘述,只描述ydmodem不一样的0 ID包。
注意启动包,结束包都是ID为0,所以数据传输从包ID1开始,但是绕到255之后还是会从0继续,而不是从1开始。所以不能以包ID为0来判断是否为传输数据的包还是启动结束包。
按照文档的介绍格式为如下,即包ID为0, 1024字节包长即STX,但是实际应用一般可以配置所以也可以SOH,128字节包长。
文档规定只需要文件名以空字符结束,如下所示文件名x.xxx长度为5字节加上结束字符6字节,所以后面填充1024或者128-6字节的0. 最后是2字节的CRC校验值,大端存储。
固定使用CRC校验,不使用checksum。
STX|SOH 00 FF "x.xxx" NUL[1024|128-6] (CRCCRC)
实际在应用中一般会扩展文件长度,时间,属性等信息,比如以SecureCRT为例,抓取格式如下
STX|SOH 00 FF "x.xxx" NUL”LEN”0x20”TIME”0x20”ATT” (CRCCRC)
即长度LEN和时间戳TIME和ATT属性之间以空格分割。
而文件名后和ATT之后以空字符0结束。
即文件名x.xxx和NUL结束符之后增加了10进制的文件长度信息LEN,如下图的14113字节,
然后空格0x20之后是8进制的UTC时间戳,
如下图8进制数14 620 606 052转为十进制为1715670058
https://tool.lu/timestamp/换算,对应时间戳为2024-05-14 15:00:58和文件修改时间一致。
然后空格0x20之后应该是属性,这里是一个固定的字符’0’. 最后全部填充0x00.
开始0 ID包也可能是128长包格式,比如SecureCRT除了第一个文件使用1024的启动0 DI包,后续文件都使用128包长。
按照文档描述格式为
STX|SOH 00 FF NUL[1024|128] CRC CRC
SecureCRT为如下,即长度固定为128字节,即SOH头,
然后文件名是空为0x00
然后文件长度为0x30即字符’0’ 后面加0x20空格
时间戳也是0x30即字符’0’后面加0x20空格
属性为0x30即字符’0’后面加0x00结束。
实际就是文件名为NUL,文件长度为’0’, 时间戳为’0’,属性为’0’
使用状态机的方式实现,非阻塞。
状态机示意如下
接收状态机
发送状态机
代码实现如下
ymodem.c
#include
#include "ymodem.h"
/* 符号定义 */
#define SOH 0x01
#define STX 0x02
#define EOT 0x04
#define ACK 0x06
#define NAK 0x15
#define CAN 0x18
#define CTRLZ 0x1A
/**
* crc计算参考https://www.iar.com/knowledge/support/technical-notes/general/checksum-generation/
*/
static const uint16_t t[] =
{
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
};
static uint16_t crc_nibble_rom(uint16_t sum, uint8_t* p, uint32_t len)
{
while (len--)
{
// hi nibble
sum = t[(sum>>12)^(*p >> 4)]^(sum<<4);
// lo nibble
sum = t[(sum>>12)^(*p++ & 0xF)]^(sum<<4);
}
return sum;
}
/**
* \fn ymodem_check
* \paran[in] crc 0使用checksum 1使用crc
* \paran[in] buf 待计算数据
* \paran[in] sz 待计算数据长度
* \retval 0 校验正确
* \retval 其他值校验错误
*/
static int ymodem_check(uint16_t crc, uint8_t *buf, uint32_t sz)
{
if (crc != 0)
{
uint16_t crc = crc_nibble_rom(0, buf, sz);
uint16_t tcrc = ((uint16_t)(buf[sz]) << 8) + (uint16_t)(buf[sz + 1]);
if (crc == tcrc)
{
return 0;
}
}
else
{
uint8_t cks = 0;
for (uint32_t i = 0; i < sz; i++)
{
cks += buf[i];
}
if (cks == buf[sz])
{
return 0;
}
}
return -1;
}
#if YMODEM_RX_ENABLE
static ymodem_rx_cfg_st* s_rx_cfg_pst = 0; /* 接口指针,ymodem_rx_init初始化 */
static unsigned int ymodem_atoi(uint8_t* str, uint32_t len)
{
unsigned int i = 0U;
while(len--)
{
if ((*str >= '0') && (*str <= '9'))
{
i = i * 10U + (unsigned int)(*str++ - '0');
}
else
{
break;
}
}
return i;
}
static uint32_t ymodem_get_len(uint8_t* str,uint32_t len)
{
uint8_t* p = str;
if(*p== 0)
{
/* 无效设置最大值 */
return 0xFFFFFFFF;
}
while(len--)
{
/* 略去前面的文件名 */
if(*p++ == 0)
{
break;
}
}
if(len >= 2)
{
return ymodem_atoi(p,len);
}
else
{
/* 后面整数至少也要2个字符,一个数字一个NULL,否则无效设置最大值 */
return 0xFFFFFFFF;
}
}
/**
* \fn ymodem_rx
* 接收处理, 在此之前必须要先调用xymodem_rx_init初始化.
* 返回XYMODEM_RX_ERR_NEED_CONTINUE则需要继续重复调用,直到返回其他值.
* \return \ref xymodem_rx_err_e
*/
ymodem_rx_err_e ymodem_rx(void)
{
ymodem_rx_err_e res = YMODEM_RX_ERR_NEED_CONTINUE;
uint8_t tmp = NAK;
uint32_t t;
uint8_t* buf;
uint16_t len;
uint32_t getlen;
#if YMODEM_CHECK_PARAM
if(s_rx_cfg_pst == (ymodem_rx_cfg_st*)0)
{
return YMODEM_RX_ERR_UNINIT;
}
if((s_rx_cfg_pst->io_read == 0) || \
(s_rx_cfg_pst->io_write == 0) || \
(s_rx_cfg_pst->mem_write == 0) || \
(s_rx_cfg_pst->mem_start == 0) || \
(s_rx_cfg_pst->mem_done == 0) || \
(s_rx_cfg_pst->buffer == 0) || \
(s_rx_cfg_pst->getms == 0))
{
return YMODEM_RX_ERR_UNINIT;
}
#endif
t = s_rx_cfg_pst->getms();
switch(s_rx_cfg_pst->state.state_e)
{
case YMODEM_STATE_RX_IDLE:
s_rx_cfg_pst->state.getlen_u32 = 0;
s_rx_cfg_pst->state.pnum_u8 = 0;
s_rx_cfg_pst->xferlen = 0;
s_rx_cfg_pst->totallen = 0;
/* YMODEM使用'C' 启动, 使用CRC16
*/
tmp = 'C';
s_rx_cfg_pst->io_write(&tmp,1);
/**
* 等待接收P0的头
*/
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_P0_HEAD;
s_rx_cfg_pst->state.ms_u32 = t; /* 状态切换更新时间 */
break;
case YMODEM_STATE_RX_WAIT_P0_HEAD:
/* 这里初始化,接收多个文件时,新的文件从0开始,已经接收长度也重新开始计算 */
s_rx_cfg_pst->state.pnum_u8 = 0;
s_rx_cfg_pst->xferlen = 0;
buf = s_rx_cfg_pst->buffer;
buf[0] = 0; /* 必须初始化,否则加入未收到数据, 后面超时判断要判断该值,则判断的可能是上一帧的值,导入不进入超时判断 */
if(0 != s_rx_cfg_pst->io_read(buf,1))
{
/* 读到头,切换状态到读数据 */
if((buf[0] == SOH) || (buf[0] == STX))
{
s_rx_cfg_pst->plen = (buf[0] == SOH) ? (uint16_t)128 : (uint16_t)1024; /* 根据头决定接收包长 */
s_rx_cfg_pst->state.ms_u32 = t;
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_P0_DATA;
s_rx_cfg_pst->state.getlen_u32 = 1;
s_rx_cfg_pst->is_start = 1;
}
else
{
/* 其他数据忽略 */
}
}
else
{
/* 没有读到数据 */
}
if((buf[0] != SOH) && (buf[0] != STX))
{
/* 未收到头,进行超时判断 */
if((t - s_rx_cfg_pst->state.ms_u32) >= s_rx_cfg_pst->ack_timeout)
{
s_rx_cfg_pst->state.ms_u32 = t; /* 更新时间以便下一次继续判断超时 */
/* 如果是传输第一个文件时,即刚开始超时未收到响应,继续重复发送启动字符,如果超过了设置的超时时间则退出
* 只有第一次is_start=0时,才等待start_timeout超时.
*/
if(s_rx_cfg_pst->is_start == 0)
{
if(s_rx_cfg_pst->start_timeout <= 1)
{
/* 超时start_timeout次后退出 */
res = YMODEM_RX_ERR_WAIT_P0_HEAD_TO;
}
else
{
/* 回到IDLE重新发送启动 */
s_rx_cfg_pst->start_timeout--;
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_IDLE;
}
}
else
{
/* 非最开始启动时,超时即退出 */
res = YMODEM_RX_ERR_WAIT_P0_HEAD_TO;
}
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_RX_WAIT_P0_DATA:
buf = s_rx_cfg_pst->buffer;
len = s_rx_cfg_pst->plen + 3 + 2; /* 总共需要接收的长度 */
getlen = s_rx_cfg_pst->io_read(buf+s_rx_cfg_pst->state.getlen_u32, len - s_rx_cfg_pst->state.getlen_u32); /* 尝试接收剩余需要接收的长度 */
s_rx_cfg_pst->state.getlen_u32 += getlen;
if(s_rx_cfg_pst->state.getlen_u32 >= len)
{
/* 接收完,准备判断合法性 */
if(((uint8_t)0 == buf[1]) && ((buf[1] + buf[2]) == (uint8_t)255))
{
/* 校验正确 */
if(0 == ymodem_check(1, buf+3, s_rx_cfg_pst->plen))
{
s_rx_cfg_pst->state.pnum_u8 = buf[1];
s_rx_cfg_pst->state.ms_u32 = t;
if(buf[3] == 0)
{
/* 传输结束 回ACK */
tmp = ACK;
s_rx_cfg_pst->io_write(&tmp,1);
res = YMODEM_RX_ERR_NONE;
}
else
{
/* 文件开始 解析文件大小 */
s_rx_cfg_pst->totallen = ymodem_get_len(&(buf[3]),s_rx_cfg_pst->plen);
/* 调用启动接口 */
uint8_t* fname = &(buf[3]);
if(0 == s_rx_cfg_pst->mem_start(s_rx_cfg_pst->addr,&fname,0))
{
/* 回ACK */
tmp = ACK;
s_rx_cfg_pst->io_write(&tmp,1);
/* 回'C' */
tmp = 'C';
s_rx_cfg_pst->io_write(&tmp,1);
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_DATA_HEAD;
s_rx_cfg_pst->state.ms_u32 = t;
}
else
{
/* 不成功回取消 */
for(int i=0;i<10;i++)
{
tmp = CAN;
s_rx_cfg_pst->io_write(&tmp,1);
}
res = YMODEM_RX_ERR_START;
}
}
}
else
{
/* NAK让对方重发 @todo需要flush输入? */
tmp = NAK;
s_rx_cfg_pst->io_write(&tmp,1);
res = YMODEM_RX_ERR_WAIT_P0_DATA_TO;
}
}
}
else
{
}
/* 超时处理 */
if((t - s_rx_cfg_pst->state.ms_u32) >= s_rx_cfg_pst->packet_timeout)
{
/* 超时,NAK让对方重发 */
tmp = NAK;
s_rx_cfg_pst->io_write(&tmp,1);
res = YMODEM_RX_ERR_WAIT_P0_DATA_TO;
}
else
{
/* 未超时继续等待*/
}
break;
case YMODEM_STATE_RX_WAIT_DATA_HEAD:
buf = s_rx_cfg_pst->buffer;
buf[0] = 0; /* 必须初始化,否则加入未收到数据, 后面超时判断要判断该值,则判断的可能是上一帧的值,导入不进入超时判断 */
if(0 != s_rx_cfg_pst->io_read(buf,1))
{
/* 读到头,切换状态到读数据 */
if((buf[0] == SOH) || (buf[0] == STX))
{
s_rx_cfg_pst->plen = (buf[0] == SOH) ? (uint16_t)128 : (uint16_t)1024; /* 根据头决定接收包长 */
s_rx_cfg_pst->state.ms_u32 = t;
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_DATA_DATA;
s_rx_cfg_pst->state.getlen_u32 = 1;
}
else if(buf[0] == EOT)
{
/* NAK等待下一个EOT */
tmp = NAK;
s_rx_cfg_pst->io_write(&tmp,1);
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_EOT;
}
else if(buf[0] == CAN)
{
/* 提前取消 */
res = YMODEM_RX_ERR_CAN;
s_rx_cfg_pst->mem_done();
}
else
{
/* 其他数据忽略 */
}
}
else
{
/* 没有读到数据 */
}
if((buf[0] != SOH) && (buf[0] != STX))
{
/* 未收到头,进行超时判断 */
if((t - s_rx_cfg_pst->state.ms_u32) >= s_rx_cfg_pst->ack_timeout)
{
s_rx_cfg_pst->state.ms_u32 = t; /* 更新时间以便下一次继续判断超时 */
res = YMODEM_RX_ERR_WAIT_DATA_HEAD_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_RX_WAIT_DATA_DATA:
buf = s_rx_cfg_pst->buffer;
len = s_rx_cfg_pst->plen + 3 + 2; /* 总共需要接收的长度 */
getlen = s_rx_cfg_pst->io_read(buf+s_rx_cfg_pst->state.getlen_u32, len - s_rx_cfg_pst->state.getlen_u32); /* 尝试接收剩余需要接收的长度 */
s_rx_cfg_pst->state.getlen_u32 += getlen;
if(s_rx_cfg_pst->state.getlen_u32 >= len)
{
/* 接收完,准备判断合法性 */
if(((uint8_t)((s_rx_cfg_pst->state.pnum_u8+(uint8_t)1)) == buf[1]) && ((buf[1] + buf[2]) == (uint8_t)255))
{
/* 校验正确 */
if(0 == ymodem_check(1, buf+3, s_rx_cfg_pst->plen))
{
s_rx_cfg_pst->state.pnum_u8 = buf[1];
s_rx_cfg_pst->state.ms_u32 = t;
/* 调用写接口 */
if(s_rx_cfg_pst->xferlen >= s_rx_cfg_pst->totallen)
{
/* 上一次已经接收了足够的数据,本次无需再写入, 取消 */
for(int i= 0; i<10; i++)
{
tmp = CAN;
s_rx_cfg_pst->io_write(&tmp,1);
}
res = YMODEM_RX_ERR_NONE;
}
else
{
/* 本次可能是最后一包,有可能有填充,如果超过长度,则只接收指定长度 */
if((s_rx_cfg_pst->xferlen + s_rx_cfg_pst->plen) > s_rx_cfg_pst->totallen)
{
s_rx_cfg_pst->mem_write(s_rx_cfg_pst->addr,&(buf[3]),s_rx_cfg_pst->totallen - s_rx_cfg_pst->xferlen);
s_rx_cfg_pst->xferlen += s_rx_cfg_pst->totallen - s_rx_cfg_pst->xferlen;
}
else
{
s_rx_cfg_pst->mem_write(s_rx_cfg_pst->addr,&(buf[3]),s_rx_cfg_pst->plen);
s_rx_cfg_pst->xferlen += s_rx_cfg_pst->plen;
}
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_DATA_HEAD;
tmp = ACK;
s_rx_cfg_pst->io_write(&tmp,1);
s_rx_cfg_pst->state.ms_u32 = t;
}
}
}
}
else
{
}
/* 未接收完判断超时 */
if((t - s_rx_cfg_pst->state.ms_u32) >= s_rx_cfg_pst->packet_timeout)
{
/* 超时退出 */
for(int i= 0; i<10; i++)
{
tmp = CAN;
s_rx_cfg_pst->io_write(&tmp,1);
}
res = YMODEM_RX_ERR_WAIT_DATA_DATA_TO;
}
else
{
/* 未超时继续等待*/
}
break;
case YMODEM_STATE_RX_WAIT_EOT:
buf = s_rx_cfg_pst->buffer;
buf[0] = 0; /* 必须初始化,否则加入未收到数据, 后面超时判断要判断该值,则判断的可能是上一帧的值,导入不进入超时判断 */
if(0 != s_rx_cfg_pst->io_read(buf,1))
{
/* 读到头,切换状态到读数据 */
if((buf[0] == EOT))
{
tmp = ACK;
s_rx_cfg_pst->io_write(&tmp,1);
tmp = 'C';
s_rx_cfg_pst->io_write(&tmp,1);
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_WAIT_P0_HEAD;
s_rx_cfg_pst->state.ms_u32 = t; /* 切换状态更新时间 */
s_rx_cfg_pst->mem_done();
}
else
{
/* 其他数据忽略 */
}
}
else
{
/* 没有读到数据 */
}
if((buf[0] != EOT))
{
/* 未收到头,进行超时判断 */
if((t - s_rx_cfg_pst->state.ms_u32) >= s_rx_cfg_pst->ack_timeout)
{
/* 超时退出 */
res = YMODEM_RX_ERR_WAIT_EOT_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
default:
return YMODEM_RX_ERR_UNINIT;
break;
}
return res;
}
/**
* \fn ymodem_rx_init
* 接收初始化
* \param[in] cfg \ref ymodem_rx_cfg_st 配置信息
* \retval 0 成功
* \retval 其他值 失败
*/
int ymodem_rx_init(ymodem_rx_cfg_st* cfg)
{
#if YMODEM_CHECK_PARAM
if(cfg == (ymodem_rx_cfg_st*)0)
{
return -1;
}
#endif
s_rx_cfg_pst = cfg;
s_rx_cfg_pst->state.state_e = YMODEM_STATE_RX_IDLE;
return 0;
}
#endif
#if YMODEM_TX_ENABLE
static ymodem_tx_cfg_st* s_tx_cfg_pst = 0; /* 接口指针,ymodem_tx_init初始化 */
/**
* \fn ymodem_tx
* 发送处理, 在此之前必须要先调用xymodem_tx_init初始化.
* 返回XYMODEM_TX_ERR_NEED_CONTINUE则需要继续重复调用,直到返回其他值.
* \return \ref xymodem_tx_err_e
*/
ymodem_tx_err_e ymodem_tx(void)
{
ymodem_tx_err_e res = YMODEM_TX_ERR_NEED_CONTINUE;
uint8_t tmp = 0;
uint32_t t;
uint8_t* buf;
#if YMODEM_CHECK_PARAM
if(s_tx_cfg_pst == (ymodem_tx_cfg_st*)0)
{
return YMODEM_TX_ERR_UNINIT;
}
if((s_tx_cfg_pst->io_read == 0) || \
(s_tx_cfg_pst->io_write == 0) || \
(s_tx_cfg_pst->mem_read == 0) || \
(s_tx_cfg_pst->mem_start == 0) || \
(s_tx_cfg_pst->mem_done == 0) || \
(s_tx_cfg_pst->buffer == 0) || \
(s_tx_cfg_pst->getms == 0))
{
return YMODEM_TX_ERR_UNINIT;
}
#endif
t = s_tx_cfg_pst->getms();
switch(s_tx_cfg_pst->state.state_e)
{
case YMODEM_STATE_TX_IDLE:
break;
case YMODEM_STATE_TX_WAIT_P0_START:
/* 这里初始化,发多个文件时,新的文件包从ID0开始 */
s_tx_cfg_pst->state.pnum_u8 = 0;
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
/* 读到头,切换状态到读数据 */
if(tmp == 'C')
{
uint8_t* str;
uint32_t strlen;
s_tx_cfg_pst->state.ms_u32 = t;
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_P0_ACK;
s_tx_cfg_pst->is_start = 1;
buf = s_tx_cfg_pst->buffer;
uint16_t crc;
if(0 == s_tx_cfg_pst->mem_start(0,&str,&strlen))
{
/* 填充P0包 */
memset(buf,0,s_tx_cfg_pst->plen+5);
buf[0] = (s_tx_cfg_pst->plen == 128) ? SOH : STX;
buf[1] = 0;
buf[2] = 0xFF;
memcpy(&(buf[3]),str,strlen);
crc = crc_nibble_rom(0,&(buf[3]),s_tx_cfg_pst->plen);
buf[s_tx_cfg_pst->plen+4] = crc&0xFF;
buf[s_tx_cfg_pst->plen+3] = (crc>>8)&0xFF;
s_tx_cfg_pst->io_write(buf,s_tx_cfg_pst->plen+5);
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_P0_ACK;
s_tx_cfg_pst->state.ms_u32 = t;
}
else
{
/* 发送结束P0包 */
memset(buf,0,128+5);
buf[0] = SOH;
buf[1] = 0;
buf[2] = 0xFF;
buf[3] = 0;
buf[4] = 0x30;
buf[5] = 0x20;
buf[6] = 0x30;
buf[7] = 0x20;
buf[8] = 0x30;
crc = crc_nibble_rom(0,&(buf[3]),128);
buf[128+4] = crc&0xFF;
buf[128+3] = (crc>>8)&0xFF;
s_tx_cfg_pst->io_write(buf,s_tx_cfg_pst->plen+5);
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_P0_DONE_ACK;
s_tx_cfg_pst->state.ms_u32 = t;
}
}
else if(tmp == CAN)
{
/* 取消 */
res = YMODEM_TX_ERR_CAN;
}
else
{
}
}
else
{
/* 没有读到数据 */
}
if(tmp != 'C')
{
/* 未收到头,进行超时判断 */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
s_tx_cfg_pst->state.ms_u32 = t; /* 更新时间以便下一次继续判断超时 */
/* 如果是传输第一个文件时,即刚开始超时未收到启动'C',继续等待,如果超过了设置的超时时间则退出
* 只有第一次is_start=0时,才等待start_timeout超时.
*/
if(s_tx_cfg_pst->is_start == 0)
{
if(s_tx_cfg_pst->start_timeout <= 1)
{
/* 超时start_timeout次后退出 */
res = YMODEM_TX_EER_WAIT_P0_START_TO;
}
else
{
/* 递减超时次数,继续等待 */
s_tx_cfg_pst->start_timeout--;
}
}
else
{
/* 非最开始启动时,超时即退出 */
res = YMODEM_TX_EER_WAIT_P0_START_TO;
}
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_P0_ACK:
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
/* 读到头,切换状态到读数据 */
if(tmp == ACK)
{
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_DATA_START;
s_tx_cfg_pst->state.ms_u32 = t;
}
}
if(tmp != ACK)
{
/* 未收到ACK */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
res = YMODEM_TX_EER_WAIT_P0_ACK_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_DATA_START:
buf = s_tx_cfg_pst->buffer;
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
/* 读到头,切换状态到发送数据 */
if(tmp == 'C')
{
uint32_t rdlen;
uint16_t crc;
/* 发送一包数据 */
s_tx_cfg_pst->state.pnum_u8++;
buf[0] = (s_tx_cfg_pst->plen == 128) ? SOH : STX;
buf[1] = s_tx_cfg_pst->state.pnum_u8;
buf[2] = ~buf[1];
rdlen = s_tx_cfg_pst->mem_read(s_tx_cfg_pst->addr,&(buf[3]),s_tx_cfg_pst->plen);
if(rdlen > 0)
{
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_DAT_ACK;
if(rdlen
plen) {
memset(&(buf[3+rdlen]),CTRLZ,s_tx_cfg_pst->plen-rdlen);
}
crc = crc_nibble_rom(0,&(buf[3]),s_tx_cfg_pst->plen);
buf[s_tx_cfg_pst->plen+4] = crc&0xFF;
buf[s_tx_cfg_pst->plen+3] = (crc>>8)&0xFF;
s_tx_cfg_pst->io_write(buf,s_tx_cfg_pst->plen+5);
}
else
{
/* 未读到数据可能是读失败胡总和读完,发EOT */
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_EOT_NACK;
tmp = EOT;
s_tx_cfg_pst->io_write(&tmp,1);
}
s_tx_cfg_pst->state.ms_u32 = t;
}
}
if(tmp != ACK)
{
/* 未收到'C' */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
res = YMODEM_TX_EER_WAIT_DATA_START_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_DAT_ACK:
buf = s_tx_cfg_pst->buffer;
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
/* 读到ACK,继续发送数据 */
if(tmp == ACK)
{
uint32_t rdlen;
uint16_t crc;
/* 发送一包数据 */
s_tx_cfg_pst->state.pnum_u8++;
buf[0] = (s_tx_cfg_pst->plen == 128) ? SOH : STX;
buf[1] = s_tx_cfg_pst->state.pnum_u8;
buf[2] = ~buf[1];
rdlen = s_tx_cfg_pst->mem_read(s_tx_cfg_pst->addr,&(buf[3]),s_tx_cfg_pst->plen);
if(rdlen > 0)
{
if(rdlen
plen) {
memset(&(buf[3+rdlen]),CTRLZ,s_tx_cfg_pst->plen-rdlen);
}
crc = crc_nibble_rom(0,&(buf[3]),s_tx_cfg_pst->plen);
buf[s_tx_cfg_pst->plen+4] = crc&0xFF;
buf[s_tx_cfg_pst->plen+3] = (crc>>8)&0xFF;
s_tx_cfg_pst->io_write(buf,s_tx_cfg_pst->plen+5);
}
else
{
/* 未读到数据可能是读失败或者读完,发EOT */
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_EOT_NACK;
tmp = EOT;
s_tx_cfg_pst->io_write(&tmp,1);
}
s_tx_cfg_pst->state.ms_u32 = t;
}
}
if(tmp != ACK)
{
/* 未收到ACK */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
res = YMODEM_TX_EER_WAIT_DATA_ACK_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_EOT_NACK:
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
if(tmp == NAK)
{
/* 读到NAK 发送EOT继续等待ACK */
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_EOT_ACK;
s_tx_cfg_pst->state.ms_u32 = t;
tmp = EOT;
s_tx_cfg_pst->io_write(&tmp,1);
}
}
if(tmp != NAK)
{
/* 未收到NAK */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
/* 超时返回错误 */
res = YMODEM_TX_EER_WAIT_EOT_NACK_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_EOT_ACK:
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
if(tmp == ACK)
{
/* 读到ACK 等待下一个启动'C' */
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_P0_START;
s_tx_cfg_pst->state.ms_u32 = t;
}
}
if(tmp != ACK)
{
/* 未收到ACK */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
/* 超时返回错误 */
res = YMODEM_TX_EER_WAIT_EOT_ACK_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
case YMODEM_STATE_TX_WAIT_P0_DONE_ACK:
if(0 != s_tx_cfg_pst->io_read(&tmp,1))
{
if(tmp == ACK)
{
/* 读到ACK 正常结束 */
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_DATA_START;
res = YMODEM_TX_ERR_NONE;
}
}
if(tmp != ACK)
{
/* 未收到ACK */
if((t - s_tx_cfg_pst->state.ms_u32) >= s_tx_cfg_pst->ack_timeout)
{
/* 超时返回错误 */
res = YMODEM_TX_EER_WAIT_P0_DONE_ACK_TO;
}
else
{
/* 未超时继续等待*/
}
}
break;
default:
return YMODEM_TX_ERR_UNINIT;
break;
}
return res;
}
/**
* \fn ymodem_tx_init
* 发送初始化
* \param[in] cfg \ref ymodem_tx_cfg_st 配置信息
* \retval 0 成功
* \retval 其他值 失败
*/
int ymodem_tx_init(ymodem_tx_cfg_st* cfg)
{
#if YMODEM_CHECK_PARAM
if(cfg == (ymodem_tx_cfg_st*)0)
{
return -1;
}
#endif
s_tx_cfg_pst = cfg;
s_tx_cfg_pst->state.state_e = YMODEM_STATE_TX_WAIT_P0_START;
return 0;
}
#endif
ymodem.h
extern "C" {
typedef uint32_t (*ymodem_io_read_pf)(uint8_t* buffer, uint32_t len); /**< 通讯接收接口,返回实际读到的长度返回0表示未读到 */
typedef uint32_t (*ymodem_io_write_pf)(uint8_t* buffer, uint32_t len); /**< 通讯发送接口 */
typedef int (*ymodem_mem_start_pf)(uint32_t addr, uint8_t** name, uint32_t* len); /**< 启动文件传输回调接口,返回非0表示失败 */
typedef int (*ymodem_mem_done_pf)(void); /**< 结束文件传输回调接口 */
typedef uint32_t (*ymodem_getms_pf)(void); /**< 获取mS时间戳 */
typedef uint32_t (*ymodem_mem_write_pf)(uint32_t addr, uint8_t* buffer, uint32_t len); /**< 写存储接口,接收才需要 */
/**
* \enum ymodem_rx_state_e
* 接收状态枚举
*/
typedef enum
{
YMODEM_STATE_RX_IDLE = 0, /**< 初始状态 */
YMODEM_STATE_RX_WAIT_P0_HEAD = 1, /**< 等待接收P0包的头 */
YMODEM_STATE_RX_WAIT_P0_DATA = 2, /**< 等待接收P0包的数据 */
YMODEM_STATE_RX_WAIT_DATA_HEAD = 3, /**< 等待接收数据包的头 */
YMODEM_STATE_RX_WAIT_DATA_DATA = 4, /**< 等待接收数据包的数据 */
YMODEM_STATE_RX_WAIT_EOT = 5, /**< 等待接收第二个EOT 数据传完发送端发两个EOT,接收端对第一个NAK,对第二个ACK */
} ymodem_rx_state_e;
/**
* \enum ymodem_rx_err_e
* 接收错误枚举
*/
typedef enum
{
YMODEM_RX_ERR_NEED_CONTINUE = 0, /**< 需要继续处理 */
YMODEM_RX_ERR_NONE, /**< 成功结束无错误 */
YMODEM_RX_ERR_UNINIT, /**< 未初始化 */
YMODEM_RX_ERR_WAIT_P0_HEAD_TO, /**< 等待接收P0的包头超时 */
YMODEM_RX_ERR_WAIT_P0_DATA_TO, /**< 等待接收P0包的数据超时 */
YMODEM_RX_ERR_WAIT_DATA_HEAD_TO, /**< 等待接收数据包的头超时 */
YMODEM_RX_ERR_WAIT_DATA_DATA_TO, /**< 等待接收数据包的数据超时 */
YMODEM_RX_ERR_WAIT_EOT_TO, /**< 等待ID第二个EOT超时 */
YMODEM_RX_ERR_CAN, /**< 发送方取消 */
YMODEM_RX_ERR_START, /**< 启动文件传输回调接口返回失败 */
} ymodem_rx_err_e;
/**
* \struct ymodem_rx_state_st
* 接收状态机结构体
*/
typedef struct
{
ymodem_rx_state_e state_e; /**< 状态机的状态 */
uint32_t getlen_u32; /**< 本包接收到的数据个数 */
uint8_t pnum_u8; /**< 上一包的包ID */
uint32_t ms_u32; /**< 上一个状态mS时间戳 */
} ymodem_rx_state_st;
/**
* \struct ymodem_rx_cfg_st
* 接收参数配置结构体
*
* 设计思想是模块本身除非必要,尽量不使用全局(静态)变量,这样模块不使用时不占用对应的RAM资源.
* 需要的资源全部由调用者提供,即本结构体提供.
*
*/
typedef struct
{
/* 以下需要用实例提供 */
ymodem_io_read_pf io_read; /**< 通讯接收接口 */
ymodem_io_read_pf io_write; /**< 通讯发送接口 */
ymodem_mem_start_pf mem_start; /**< 启动回调接口 */
ymodem_mem_done_pf mem_done; /**< 停止回调接口 */
ymodem_mem_write_pf mem_write; /**< 写存储接口 */
ymodem_getms_pf getms; /**< 获取mS时间戳接口 */
uint32_t start_timeout; /**< 等待启动超时次数,单位每次超时为ack_timeout */
uint32_t packet_timeout; /**< 等待数据包超时时间mS */
uint32_t ack_timeout; /**< 等待响应(头)超时时间mS*/
uint8_t* buffer; /**< 包缓冲区,用户提供,1024字节包需要1024+5 */
uint32_t addr; /**< 存储地址 */
uint32_t totallen; /**< 传输长度 */
uint32_t xferlen; /**< 已经传输长度 */
/* 以下调用rx_init接口初始化 */
ymodem_rx_state_st state; /**< 状态机 */
/* 以下内部使用无需初始化 */
uint16_t plen; /**< 包长:128或1024 */
uint8_t is_start; /**< 是否已经启动标志,第一次启动时等待start_timeout次超时时间 */
} ymodem_rx_cfg_st;
/**
* \fn ymodem_rx
* 接收处理, 在此之前必须要先调用ymodem_rx_init初始化.
* 返回YMODEM_RX_ERR_NEED_CONTINUE则需要继续重复调用,直到返回其他值.
* \return \ref ymodem_rx_err_e
*/
ymodem_rx_err_e ymodem_rx(void);
/**
* \fn ymodem_rx_init
* 接收初始化
* \param[in] cfg \ref ymodem_rx_cfg_st 配置信息
* \retval 0 成功
* \retval 其他值 失败
*/
int ymodem_rx_init(ymodem_rx_cfg_st* cfg);
/**
* \enum ymodem_tx_state_e
* 发送状态枚举
*/
typedef enum
{
YMODEM_STATE_TX_IDLE = 0, /**< 初始状态 */
YMODEM_STATE_TX_WAIT_P0_START = 1, /**< 等待发送P0之前的'C' */
YMODEM_STATE_TX_WAIT_P0_ACK = 2, /**< 等待发送P0之后的ACK */
YMODEM_STATE_TX_WAIT_DATA_START = 3, /**< 等待发送DATA之前的'C' */
YMODEM_STATE_TX_WAIT_DAT_ACK = 4, /**< 等待发送数据之后的ACK */
YMODEM_STATE_TX_WAIT_EOT_NACK = 5, /**< 等待发送EOT之后的NACK */
YMODEM_STATE_TX_WAIT_EOT_ACK = 6, /**< 等待发送EOT之后的ACK */
YMODEM_STATE_TX_WAIT_P0_DONE_ACK = 7, /**< 等待发送结束P0之后的ACK */
} ymodem_tx_state_e;
/**
* \enum ymodem_tx_err_e
* 发送错误枚举
*/
typedef enum
{
YMODEM_TX_ERR_NEED_CONTINUE = 0, /**< 需要继续处理 */
YMODEM_TX_ERR_NONE, /**< 成功结束无错误 */
YMODEM_TX_ERR_UNINIT, /**< 未初始化 */
YMODEM_TX_EER_WAIT_P0_START_TO,
YMODEM_TX_EER_WAIT_P0_ACK_TO,
YMODEM_TX_EER_WAIT_DATA_START_TO,
YMODEM_TX_EER_WAIT_DATA_ACK_TO,
YMODEM_TX_EER_WAIT_EOT_NACK_TO,
YMODEM_TX_EER_WAIT_EOT_ACK_TO,
YMODEM_TX_EER_WAIT_P0_DONE_ACK_TO,
YMODEM_TX_ERR_CAN, /**< 接收方取消 */
YMODEM_TX_ERR_START, /**< 启动文件传输回调接口返回失败 */
} ymodem_tx_err_e;
/**
* \struct ymodem_tx_state_st
* 发送状态机结构体
*/
typedef struct
{
ymodem_tx_state_e state_e; /**< 状态机的状态 */
uint8_t pnum_u8; /**< 上一包的包ID */
uint32_t ms_u32; /**< 上一个状态mS时间戳 */
} ymodem_tx_state_st;
/**
* \struct ymodem_tx_cfg_st
* 发送参数配置结构体
*
* 设计思想是模块本身除非必要,尽量不使用全局(静态)变量,这样模块不使用时不占用对应的RAM资源.
* 需要的资源全部由调用者提供,即本结构体提供.
*
*/
typedef struct
{
/* 以下需要用实例提供 */
ymodem_io_read_pf io_read; /**< 通讯接收接口 */
ymodem_io_read_pf io_write; /**< 通讯发送接口 */
ymodem_mem_start_pf mem_start; /**< 启动回调接口 */
ymodem_mem_done_pf mem_done; /**< 停止回调接口 */
ymodem_mem_write_pf mem_read; /**< 读存储接口 */
ymodem_getms_pf getms; /**< 获取mS时间戳接口 */
uint32_t start_timeout; /**< 等待启动超时次数,单位每次超时为ack_timeout */
uint32_t packet_timeout; /**< 等待数据包超时时间mS */
uint32_t ack_timeout; /**< 等待响应(头)超时时间mS*/
uint8_t* buffer; /**< 包缓冲区,用户提供,1024字节包需要1024+5 */
uint32_t addr; /**< 存储地址 */
uint16_t plen; /**< 包长:128或1024 */
/* 以下调用rx_init接口初始化 */
ymodem_tx_state_st state; /**< 状态机 */
/* 以下内部使用无需初始化 */
uint8_t is_start; /**< 是否已经启动标志,第一次启动时等待start_timeout次超时时间 */
} ymodem_tx_cfg_st;
typedef uint32_t (*ymodem_mem_read_pf)(uint32_t addr, uint8_t* buffer, uint32_t len); /**< 读存储接口,发送才需要 */
/**
* \fn ymodem_tx
* 接收处理, 在此之前必须要先调用ymodem_tx_init初始化.
* 返回YMODEM_TX_ERR_NEED_CONTINUE则需要继续重复调用,直到返回其他值.
* \return \ref ymodem_tx_err_e
*/
ymodem_tx_err_e ymodem_tx(void);
/**
* \fn ymodem_tx_init
* 发送初始化
* \param[in] cfg \ref ymodem_tx_cfg_st 配置信息
* \retval 0 成功
* \retval 其他值 失败
*/
int ymodem_tx_init(ymodem_tx_cfg_st* cfg);
}
测试以SecureCRT进行,有一些需要注意的地方
1.结束0 ID包总以128包长格式进行
2.最后数据不足1024,会拆分成128包传输,比如14113长的文件,前面按照1024字节传输,最后余801字节,会转为7个128长包,最后的一个128包会填充,如下所示
3.0 ID结束包总是以128包传输。
4.第一个文件以1024包传输,后面的文件会以128包长传输,导致速率下降,如下所示
115200波特率串口第一个文件1024包传输可以稳定达到9kB/s,后面128包传输只能达到4kB/S
Shell实现参考《https://mp.weixin.qq.com/s/XLmbJn0SKoDT1aLdxHDrbg 一个超级精简高可移植的shell命令行C实现》
测试发送
uint8_t sfile_name[2][64];
static int tx_file_open_flag = 0;
static uint32_t tx_file_num = 0;
static void syfilefunc(uint8_t* param);
{ (uint8_t*)"syfile", syfilefunc, (uint8_t*)"syfile"},
void syfilefunc(uint8_t* param)
{
(void)param;
int plen = 0;
int res;
int num = 0;
ymodem_tx_cfg_st cfg=
{
.buffer = rxtx_buf,
.getms = getms,
.io_read = io_read,
.io_write = io_write,
.start_timeout = 60,
.packet_timeout = 2000,
.ack_timeout = 2000,
.plen = 1024,
.mem_start = tx_file_start,
.mem_read = tx_file_read,
.mem_done = tx_file_done,
};
memset(sfile_name,0,sizeof(sfile_name));
tx_file_num = 0;
num = sscanf((const char*)param, "%*s %d %s %s", &plen, sfile_name[0],sfile_name[1]);
if((num == 2) || (num == 3))
{
cfg.plen = plen;
ymodem_tx_init(&cfg);
while((res = ymodem_tx()) == YMODEM_TX_ERR_NEED_CONTINUE);
xprintf("\r\nres:%d\r\n",res);
}
}
static uint32_t getms(void)
{
return os_boot_time32();
}
static uint32_t io_read(uint8_t* buffer, uint32_t len)
{
return fifo_cdc_rx_read(buffer, len);
}
static uint32_t io_write(uint8_t* buffer, uint32_t len)
{
fifo_cdc_tx_write(buffer, len);
return len;
}
static uint8_t sfile_name_len_att[128];
/* 获取文件名 */
static int tx_file_start(uint32_t addr, uint8_t** name, uint32_t* len)
{
(void)addr;
(void)len;
uint32_t flen;
uint32_t fnamelen;
int res = 0;
if(tx_file_open_flag != 0)
{
lfs_file_close(&lfs, &tx_file);
tx_file_open_flag = 0;
}
if(tx_file_num >= sizeof(sfile_name)/sizeof(sfile_name[0]))
{
return -1;
}
if(0 == (res = lfs_file_open(&lfs, &tx_file, (const char*)sfile_name[tx_file_num], LFS_O_RDONLY)))
{
flen = lfs_file_size(&lfs, &tx_file);
fnamelen = strlen((const char*)sfile_name[tx_file_num]);
memcpy(sfile_name_len_att,sfile_name[tx_file_num],fnamelen);
sfile_name_len_att[fnamelen]=0;
*len = fnamelen + 1 + snprintf((char*)(&sfile_name_len_att[fnamelen+1]),sizeof(sfile_name_len_att)-(fnamelen+1),"%ld %o %d",flen,1715670058,0);
*name = sfile_name_len_att;
tx_file_open_flag = 1;
}
tx_file_num++;
return res;
}
static uint32_t tx_file_read(uint32_t addr, uint8_t* buffer, uint32_t len)
{
(void)addr;
return lfs_file_read(&lfs, &tx_file, buffer, len);
}
static int tx_file_done(void)
{
if(0 != tx_file_open_flag)
{
tx_file_open_flag = 0;
return lfs_file_close(&lfs, &tx_file);
}
return 0;
}
添加以上命令行实现,测试发送两个文件
测试接收
static void ryfilefunc(uint8_t* param);
{ (uint8_t*)"ryfile", ryfilefunc, (uint8_t*)"ryfile"},
void ryfilefunc(uint8_t* param)
{
(void)param;
int res = 0;
ymodem_rx_cfg_st cfg=
{
.buffer = rxtx_buf,
.getms = getms,
.io_read = io_read,
.io_write = io_write,
.start_timeout = 60,
.packet_timeout = 2000,
.ack_timeout = 1000,
.mem_start = rx_file_start,
.mem_write = rx_file_write,
.mem_done = rx_file_done,
};
ymodem_rx_init(&cfg);
while((res = ymodem_rx()) == YMODEM_RX_ERR_NEED_CONTINUE);
xprintf("\r\nres:%d\r\n",res);
}
static int rx_file_start(uint32_t addr, uint8_t** name, uint32_t* len)
{
(void)addr;
(void)len;
int res = 0;
if(rx_file_open_flag == 0)
{
if(0 == (res = lfs_file_open(&lfs, &rx_file, (const char*)(*name), LFS_O_RDWR | LFS_O_CREAT)))
{
rx_file_open_flag = 1;
}
}
return res;
}
static uint32_t rx_file_write(uint32_t addr, uint8_t* buffer, uint32_t len)
{
(void)addr;
return lfs_file_write(&lfs, &rx_file, buffer, len);
}
static int rx_file_done(void)
{
if(0 != rx_file_open_flag)
{
rx_file_open_flag = 0;
return lfs_file_close(&lfs, &rx_file);
}
return 0;
}
实现以上命令行,测试接收两个文件
XMODEM,YMODEM本身没有标准化,只有前言部分贴出的设计者写的一个文档xymodem.pdf,但是描述太罗嗦,逻辑性不强,介绍的也不是很详细,并且对于异常处理等也规范不细。所以不同的实现或扩展导致了一些差异,使用中最好是配合上位机工具进行调试,比如我们使用SecureCRT进行测试。
XMODEM,YMODEM本身设计上不能保证可靠性,因为使用了单字节包,单字节包没法确认其正确性,只有带头和CRC的包才能确认其正确性(依赖CRC的检错能力)。所以XMODEM,YMODEM只适合于简单场景,可靠性要求高,容易丢数据,误码,通讯不稳定等场景不适用(否则需要考虑复杂的重发,错误检测等机制,太麻烦)。
我们上述实现基于状态机实现,非阻塞,只考虑了正常传输的情况,也没有进行过多异常情况的处理,因为其本身设计就不可靠,异常处理无详细规范等,所以过多考虑本身也没太大意义。
代码仅作演示,不保证可靠性和充分测试,如果移植可能,需要实际进行调试修改。