Ymodem协议解析
Ymodem协议解析
2025-01-01
1. 交互流程
这里是以BootLoader为目的写的。
很多Shell工具都有自带的Xmodem、Ymodem传输文件的功能。这里以SecureCRT 9.2软件为例进行分析。不同的软件实际上可能有些许差异。

至此,流程结束。
2. STM32示例
这里已经将Ymodem协议接收部分已经封装好了。
- 头文件
/*
* ymodem.h
*
* Created on: 2024年9月23日
* Author: ZhangJinhao
*/
#ifndef PROTOCOL_YMODEM_H_
#define PROTOCOL_YMODEM_H_
#include "stdint.h"
/* 此协议流程参考:SecureCRT 9.2, Ymodem协议,其他上位机未经过测试 */
// 一包数据格式为:帧命令 + 索引 + 索引反码 + 数据(128/1024) + CRC16校验码
// 帧命令
#define YMODEM_SOH (0x01) // 128字节数据命令
#define YMODEM_STX (0x02) // 1024字节数据命令
#define YMODEM_EOT (0x04) // 发送结束命令
#define YMODEM_ACK (0x06) // 接收正确应答命令
#define YMODEM_NAK (0x15) // 接收错误应答命令
#define YMODEM_CA (0x18) // 取消传输
#define YMODEM_C (0x43) // 开始发送命令
#define YMODEM_ABORT1 (0x41) /* 'A' == 0x41, abort by user */
#define YMODEM_ABORT2 (0x61) /* 'a' == 0x61, abort by user */
// 长度定义
#define YMODEM_PACKET_HEADER_LEN (3) // 包头长度(命令+索引+索引反码)
#define YMODEM_PACKET_SIZE (128) // Ymodem包长度
#define YMODEM_PACKET_1K_SIZE (1024) // Ymodem-1K包长度
#define YMODEM_PACKET_TRAILER_LEN (2) // 包尾长度(CRC16)
#define YMODEM_PACKET_OVERHEAD (YMODEM_PACKET_HEADER_LEN + YMODEM_PACKET_TRAILER_LEN) // 除去数据外,另外的长度
// 一包数据中,索引和索引反码所处的位置
#define YMODEM_PACKET_SEQNO_INDEX (1)
#define YMODEM_PACKET_SEQNO_COMP_INDEX (2)
// 文件信息帧中文件长度
#define YMODEM_FILE_NAME_LENGTH (80) // 文件名长度
#define YMODEM_FILE_SIZE_LENGTH (16) // 文件大小长度
// 超时时间定义
#define NAK_TIMEOUT (5000) // 5秒
typedef struct _ymodem
{
uint8_t file_name[128]; // 需要接收的文件名
uint32_t file_size; // 需要接收的文件大小(单位:字节)
uint8_t buf[YMODEM_PACKET_1K_SIZE + YMODEM_PACKET_OVERHEAD]; // 最大:帧命令 + 索引 + 索引反码 + 数据(128/1024) + CRC16校验码 = 1029字节
uint8_t pack_cnt; // 下一包需要接收的索引号(0-FF之间循环)
uint8_t info_done; // 收到文件信息
uint8_t file_done; // 收到了文件数据
uint8_t null_done; // 收到空文件信息
uint32_t app_addr; // app程序的起始地址
uint32_t app_size; // app程序占用空间大小
uint32_t flash_cnt; // 已经写入到flash的字节数
} ymodem_t;
typedef ymodem_t *ymodem_ptr;
int32_t ymodem_receive(void);
#endif /* PROTOCOL_YMODEM_H_ */- 源文件
#include "ymodem.h"
#include "stdlib.h"
#include "iap.h"
#include "usart.h"
// ============================================ 以下函数需要用户做适配 ============================================ //
ymodem_t ymodem =
{
.file_name = {0},
.file_size = 0,
.buf = {0},
.pack_cnt = 0,
.info_done = 0,
.file_done = 0,
.null_done = 0,
.app_addr = 0x08004000, // APP起始地址
.app_size = 0x3C000, // APP空间大小
.flash_cnt = 0,
};
// 发送一个字符
static uint8_t ymodem_send_byte(uint8_t byte)
{
HAL_UART_Transmit(&huart1, &byte, 1, 0xFF);
return 0;
}
// 接收一个字符,成功返回0,失败返回1
static uint8_t ymodem_recv_byte(uint8_t *data, uint32_t timeout)
{
HAL_UART_Receive(&huart1, data, 1, timeout);
return 0;
}
// ymodem写flash函数,将接收到的数据写到flash中
static uint8_t ymodem_flash_write(uint32_t addr, uint8_t *data, uint32_t len)
{
stm32_flash_write(addr, data, len);
return 0;
}
// ymodem擦除app函数,擦除app区域flash
static uint8_t ymodem_flash_erase(uint32_t addr, uint32_t len)
{
stm32_flash_erase(addr, len);
}
// 调试信息输出
static uint8_t ymodem_printf(const char *format, ...)
{
return 0;
}
// ============================================ 以上函数需要用户做适配 ============================================ //
// CRC校验函数
static uint16_t CRC16_Xmodem(uint8_t *data, uint32_t len)
{
uint8_t i;
uint16_t crc = 0x0000;
while (len--)
{
crc ^= (*data++ << 8);
for (i = 0; i < 8; i++)
{
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc = crc << 1;
}
}
return crc;
}
// ymodem 接收一个包的数据
// return:
// -1: 接收失败(未收到 或 超时 或 错误命令 或 校验失败)
// 0:正常成功(收到完整数据length=128/1024 或 收到结束帧length=0 或 发送方要求停止length=-1)
// 1:主机要求取消传输
static int8_t ymodem_recv_packet(uint8_t *data, uint32_t *length, uint32_t timeout)
{
uint16_t i, packet_size, crc;
uint8_t c;
*length = 0;
// 接收第一个字符
if (ymodem_recv_byte(&c, timeout) != 0)
{
return -1;
}
// 判断命令类型
switch (c)
{
case YMODEM_SOH:
packet_size = YMODEM_PACKET_SIZE; // 128byte
break;
case YMODEM_STX:
packet_size = YMODEM_PACKET_1K_SIZE; // 1024byte
break;
case YMODEM_EOT:
return 0;
case YMODEM_CA:
if ((ymodem_recv_byte(&c, timeout) == 0) && (c == YMODEM_CA))
{
*length = -1;
return 0;
}
else
{
return -1;
}
case YMODEM_ABORT1:
case YMODEM_ABORT2:
return 1;
default:
return -1;
}
// 如果命令正确,继续接收够一包数据
// 第一个字节
*data = c;
// 剩下的字节
for (i = 1; i < (packet_size + YMODEM_PACKET_OVERHEAD); i++)
{
if (ymodem_recv_byte(data + i, timeout) != 0)
{
return -1;
}
}
// 校验索引和索引反码是否配对
if (data[YMODEM_PACKET_SEQNO_INDEX] != ((data[YMODEM_PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))
{
return -1;
}
// 校验CRC校验码是否正确(仅计算数据区域)
crc = CRC16_Xmodem(&data[YMODEM_PACKET_HEADER_LEN], packet_size);
if (((crc >> 8) != data[YMODEM_PACKET_HEADER_LEN + packet_size]) || ((crc & 0x00FF) != data[YMODEM_PACKET_HEADER_LEN + packet_size + 1]))
{
return -1;
}
// 记录本包中有效数据长度
*length = packet_size;
return 0;
}
// 阻塞式接收,一直到接收完成后返回。
// return:
// 1: 发送方要求停止
// 0: 接收成功
int32_t ymodem_receive(void)
{
uint32_t packet_length = 0;
// 擦除flash
ymodem_flash_erase(ymodem.app_addr, ymodem.app_size);
// 发送字符C,开始接收
ymodem_send_byte(YMODEM_C);
while (1)
{
// 先清空缓冲区,重新收
memset(ymodem.buf, 0, YMODEM_PACKET_1K_SIZE + YMODEM_PACKET_OVERHEAD);
// 接收一包数据
switch (ymodem_recv_packet(ymodem.buf, &packet_length, NAK_TIMEOUT))
{
// 成功接收到一包数据
case 0:
// 根据收到的数据长度判断收到的是什么命令
switch (packet_length)
{
// 发送方要求停止
case -1:
ymodem_send_byte(YMODEM_ACK);
return 1;
// 收到文件结束帧
case 0:
ymodem_send_byte(YMODEM_ACK);
ymodem_send_byte(YMODEM_C);
ymodem.file_done = 1;
ymodem.pack_cnt = 0;
break;
// 收到了正常的一包数据
default:
// 判断索引是否正确
if (ymodem.buf[YMODEM_PACKET_SEQNO_INDEX] != ymodem.pack_cnt)
{
ymodem_printf("index err, need_cnt=%x, real_cnt=%x \r\n", ymodem.pack_cnt, ymodem.buf[YMODEM_PACKET_SEQNO_INDEX]);
// 索引不对,发送错误应答,重新接收本包数据
ymodem_send_byte(YMODEM_NAK);
}
else
{
// 索引正确
// 第一包数据 和 文件数据结束之后的第一包数据 是文件信息
if ((ymodem.info_done == 0) || (ymodem.file_done == 1))
{
// 非空文件名,是第一包数据
if (ymodem.buf[YMODEM_PACKET_HEADER_LEN] != 0x00)
{
// 解析文件名:
uint8_t file_name_len = 0;
for (uint8_t i = YMODEM_PACKET_HEADER_LEN; i < YMODEM_FILE_NAME_LENGTH; i++)
{
if (ymodem.buf[i] == 0x00)
{
file_name_len = i - 3;
memcpy(ymodem.file_name, &(ymodem.buf[3]), file_name_len);
ymodem.file_name[file_name_len] = '\0';
ymodem_printf("file name = %s \r\n", ymodem.file_name);
break;
}
}
// 解析文件字节数:
for (uint8_t i = YMODEM_PACKET_HEADER_LEN + file_name_len + 1; i < YMODEM_PACKET_HEADER_LEN + file_name_len + 1 + YMODEM_FILE_SIZE_LENGTH; i++)
{
// 遇到分隔符、非法字符
if ((ymodem.buf[i] == 0x20) || (ymodem.buf[i] < '0') || (ymodem.buf[i] > '9'))
{
// 打印一下信息
ymodem_printf("file size = %d byte.\r\n", ymodem.file_size);
break;
}
else
{
ymodem.file_size = ymodem.file_size * 10 + (ymodem.buf[i] - '0');
}
}
// 解析成功
ymodem.info_done = 1;
ymodem.pack_cnt = 1;
// 发送应答开始接收数据
ymodem_send_byte(YMODEM_ACK);
ymodem_send_byte(YMODEM_C);
}
// 空文件名是最后一帧数据
else
{
ymodem_send_byte(YMODEM_ACK);
// TODO:延时半秒
ymodem_send_byte(YMODEM_ACK);
ymodem.null_done = 1;
break;
}
}
// 文件数据
else
{
// 将数据写入到flash
ymodem_flash_write(ymodem.app_addr + ymodem.flash_cnt, &(ymodem.buf[YMODEM_PACKET_HEADER_LEN]), packet_length);
// 记录已经写入到flash中的字节数
ymodem.flash_cnt = ymodem.flash_cnt + packet_length;
// 记录下一包应该收到的索引
if (ymodem.pack_cnt == 0xFF)
{
ymodem.pack_cnt = 0;
}
else
{
ymodem.pack_cnt++;
}
// 发送应答,接收下一帧数据
ymodem_send_byte(YMODEM_ACK);
}
}
}
break;
// 主机要求取消传输
case 1:
ymodem_send_byte(YMODEM_CA);
ymodem_send_byte(YMODEM_CA);
return 1;
// 接收失败(校验失败、接收超时、命令不对)
default:
// 如果是在等待文件信息
if (ymodem.info_done == 0)
{
// 重新发送C
ymodem_send_byte(YMODEM_C);
}
// 如果是接收数据过程出错
if ((ymodem.info_done == 1) && (ymodem.file_done == 0))
{
// 发送错误应答,重新接收数据
ymodem_send_byte(YMODEM_NAK);
}
// 如果是等待空文件信息出错
if (ymodem.file_done == 1)
{
// 重新发送C
ymodem_send_byte(YMODEM_C);
}
break;
}
// 判断接收流程是否全部结束
if (ymodem.null_done == 1)
{
ymodem_printf("recv over, need size=%d, real size=%d \r\n", ymodem.file_size, ymodem.flash_cnt);
return 0;
}
}
}其中:
- 头文件可以完全不用动;
- 源文件用户适配部分需要根据不同的单片机和接口进行修改。
- 使用案例
void main()
{
// init
while(1)
{
// 如果需要进行OTA
if(need_updata)
{
// 接收app程序
ymodem_receive();
// 跳转到app区域
jump_to_app();
}
}
}