目录
[1 YMODEM 协议介绍](#1 YMODEM 协议介绍)
[1.1 YMODEM vs XMODEM](#1.1 YMODEM vs XMODEM)
[1.2 协议实现](#1.2 协议实现)
[1.2.1 数据包格式](#1.2.1 数据包格式)
[1.2.2 通信流程](#1.2.2 通信流程)
[2 YMODEM的核心特征](#2 YMODEM的核心特征)
[2.1 关键特性介绍](#2.1 关键特性介绍)
[2.2 通信示例](#2.2 通信示例)
[3 YMODEM协议的Lua实现](#3 YMODEM协议的Lua实现)
概述
本文主要介绍YMODEM ,YMODEM协议是对XMODEM协议的扩展,主要用于文件传输。它由Chuck Forsberg在XMODEM的基础上改进而来,支持批量文件传输、文件名、文件大小、时间戳等元数据的传输,并使用CRC-16进行错误校验。YMODEM协议通常使用1024字节(1K)的数据块,但也可以回退到128字节(类似于XMODEM)的数据块。
1 YMODEM 协议介绍
YMODEM 是由 Chuck Forsberg 在 XMODEM 基础上开发的增强型文件传输协议,它结合了 XMODEM-1K 和 CRC 校验的优点,并增加了批量文件传输、文件信息传输等现代功能。
1.1 YMODEM vs XMODEM
特性 | XMODEM | YMODEM |
---|---|---|
数据块大小 | 128字节 | 128字节或1024字节 |
校验方式 | 校验和或CRC | 强制CRC-16 |
文件信息 | 不支持 | 支持文件名、大小、时间戳 |
批量传输 | 不支持 | 支持多文件连续传输 |
传输模式 | 单一模式 | 标准模式和流模式 |
传输效率 | 较低 | 较高 |
1.2 协议实现
1.2.1 数据包格式
YMODEM 使用两种主要的数据包类型:
1) .文件头包(启动传输)
bash
[SOH][0][0xFF][文件名][0][文件大小][0][修改时间][0][...][0][填充][CRC16]
包编号为 0:标识这是文件头包
包含文件信息:
文件名(以NULL结束)
文件大小(十进制ASCII字符串)
文件修改时间(Unix时间戳,可选)
其他文件属性
2) 数据包
bash
[STX/SOH][包编号][~包编号][数据][CRC16]
STX (0x02):1024字节数据包
SOH (0x01):128字节数据包
包编号从 1 开始递增
1.2.2 通信流程
1) 单文件传输流程:
bash
发送方 接收方
| |
| <------------- 'C' ----------- | (接收方请求CRC模式)
| |
| -------- 文件头包(包0) ------> |
| |
| <------------ ACK ------------ | (确认文件头)
| |
| -------- 数据包(包1) --------> |
| |
| <------------ ACK ------------ |
| |
| -------- 数据包(包2) --------> |
| |
| <------------ ACK ------------ |
| ... |
| |
| ------------ EOT ------------> | (文件结束)
| |
| <------------ ACK ------------ |
2) 多文件传输流程:
cpp
发送方 接收方
| |
| <------------- 'C' ----------- |
| |
| ----- 文件1头包(包0) --------> |
| |
| <------------ ACK ------------ |
| |
| ----- 文件1数据包... --------> |
| |
| <------------ ACK ------------ |
| |
| ------------ EOT ------------> | (文件1结束)
| |
| <------------ ACK ------------ |
| |
| ----- 文件2头包(包0) --------> | (开始下一个文件)
| |
| <------------ ACK ------------ |
| ... |
| |
| ----- 空文件头包(包0) ------> | (所有文件传输结束)
| |
| <------------ ACK ------------ |
2 YMODEM的核心特征
2.1 关键特性介绍
1) 文件信息传输
YMODEM 在文件头包中传输丰富的文件信息:
cpp
// 文件头包示例数据结构
struct ymodem_header {
uint8_t type; // SOH
uint8_t block_num; // 0
uint8_t block_num_comp;// 0xFF
char filename[128]; // "example.txt\0"
char filesize[16]; // "1024\0"
char timestamp[16]; // "1634567890\0" (Unix时间戳)
// ... 填充到128或1024字节
uint16_t crc16;
};
2) 批量文件传输
YMODEM 支持连续传输多个文件,无需重新建立连接:
bash
文件1 → ACK → 文件2 → ACK → ... → 空文件头包 → 传输结束
3) 动态块大小
YMODEM 可以根据需要选择数据块大小:
1024字节:大文件,提高效率
128字节:小文件或不可靠信道,减少重传开销
3) 增强的错误处理
强制使用CRC-16,提供更强的错误检测能力
完善的超时和重传机制
支持传输取消(CAN字符)
2.2 通信示例
bash
接收方: 'C' // 请求CRC模式
发送方: SOH 00 FF "test.txt" NUL "1024" NUL NUL ... [CRC16] // 文件头包
接收方: ACK // 确认文件头
发送方: STX 01 FE [1024字节数据] [CRC16] // 第一个数据块
接收方: ACK // 确认
发送方: STX 02 FD [1024字节数据] [CRC16] // 第二个数据块
接收方: ACK // 确认
发送方: SOH 03 FC [128字节数据] [CRC16] // 最后一个块(128字节)
接收方: ACK // 确认
发送方: EOT // 文件结束
接收方: ACK // 确认结束
// 如果是最后一个文件:
发送方: SOH 00 FF NUL NUL ... [CRC16] // 空文件头包
接收方: ACK // 确认传输完全结束
3 YMODEM协议的Lua实现
这里提供一个完整的 YMODEM 协议的 Lua 实现。由于 Lua 本身没有内置的串口和文件操作,这个实现使用了通用的接口,可以轻松适配到各种 Lua 环境(如嵌入式 Lua、Lua with serial libraries 等)。
完整的 YMODEM Lua 实现代码如下:
Lua
-- ymodem.lua
-- YMODEM 协议 Lua 实现
local bit = bit or bit32 or require("bit") -- 兼容不同的 bit 操作库
-- YMODEM 协议常量
local YMODEM = {
SOH = 0x01, -- 128字节数据包开始
STX = 0x02, -- 1024字节数据包开始
EOT = 0x04, -- 传输结束
ACK = 0x06, -- 确认
NAK = 0x15, -- 否认,请求重传
CAN = 0x18, -- 取消传输
C = 0x43, -- 'C' - CRC模式请求
-- 数据包大小
DATA_SIZE_128 = 128,
DATA_SIZE_1K = 1024,
-- 超时和重试配置
TIMEOUT = 3000, -- 超时时间(ms)
MAX_RETRIES = 10, -- 最大重试次数
-- 错误代码
ERROR = {
SUCCESS = 0,
TIMEOUT = 1,
CANCEL = 2,
IO_ERROR = 3,
INVALID_PACKET = 4,
TOO_MANY_RETRIES = 5,
EOF = 6,
FILE_ERROR = 7
}
}
-- CRC16 计算函数 (YMODEM 使用)
function YMODEM.crc16(data)
local crc = 0
for i = 1, #data do
crc = bit.bxor(crc, bit.lshift(data:byte(i), 8))
for _ = 1, 8 do
if bit.band(crc, 0x8000) ~= 0 then
crc = bit.bxor(bit.lshift(crc, 1), 0x1021)
else
crc = bit.lshift(crc, 1)
end
crc = bit.band(crc, 0xFFFF)
end
end
return crc
end
-- YMODEM 协议类
local YModem = {}
YModem.__index = YModem
-- 构造函数
function YModem.new(config)
local self = setmetatable({}, YModem)
-- 默认配置
self.config = {
use_1k = true, -- 使用1K数据块
timeout = YMODEM.TIMEOUT,
max_retries = YMODEM.MAX_RETRIES
}
-- 合并用户配置
if config then
for k, v in pairs(config) do
self.config[k] = v
end
end
-- 必须的回调函数
self.serial_read = config.serial_read
self.serial_write = config.serial_write
self.file_open = config.file_open
self.file_close = config.file_close
self.file_read = config.file_read
self.file_write = config.file_write
-- 可选的回调函数
self.get_char = config.get_char or self._default_get_char
self.file_seek = config.file_seek
self.progress_callback = config.progress_callback
self.get_time = config.get_time or os.time
-- 协议状态
self.packet_number = 0
self.retry_count = 0
self.current_file = nil
self.current_fd = nil
return self
end
-- 默认的获取字符实现(基于 serial_read)
function YModem:_default_get_char()
local buffer = self.serial_read(1)
if buffer and #buffer > 0 then
return buffer:byte(1)
end
return -1
end
-- 带超时的获取字符
function YModem:_get_char_timeout(timeout_ms)
local start_time = self.get_time()
local current_time = start_time
while (current_time - start_time) * 1000 < timeout_ms do
local char = self.get_char()
if char >= 0 then
return char
end
-- 这里可以添加小的延时
current_time = self.get_time()
end
return nil -- 超时
end
-- 发送数据包
function YModem:_send_data_packet(data, packet_num)
local header, data_size
-- 选择数据包头和大小
if #data == YMODEM.DATA_SIZE_1K and self.config.use_1k then
header = YMODEM.STX
data_size = YMODEM.DATA_SIZE_1K
else
header = YMODEM.SOH
data_size = YMODEM.DATA_SIZE_128
if #data > data_size then
data = data:sub(1, data_size)
end
end
-- 构建数据包
local packet = string.char(
header,
packet_num,
0xFF - packet_num -- 包编号补码
)
-- 添加数据(需要填充到标准大小)
if #data < data_size then
-- 用 0x1A 填充剩余空间
data = data .. string.rep(string.char(0x1A), data_size - #data)
end
packet = packet .. data
-- 计算并添加CRC16
local crc = YMODEM.crc16(data)
packet = packet .. string.char(
bit.rshift(bit.band(crc, 0xFF00), 8), -- CRC高字节
bit.band(crc, 0xFF) -- CRC低字节
)
-- 发送数据包
return self.serial_write(packet)
end
-- 发送文件头包
function YModem:_send_file_header_packet(file_info)
-- 构建文件信息字符串
local file_info_str = ""
if file_info and file_info.filename and file_info.filename ~= "" then
file_info_str = string.format("%s\0%d\0%d\0%d",
file_info.filename,
file_info.file_size or 0,
file_info.modify_time or 0,
file_info.mode or 0)
end
-- 如果文件名为空,表示传输结束
-- 构建数据包
local packet = string.char(
YMODEM.SOH, -- 头包总是使用SOH
0x00, -- 包编号0
0xFF -- 包编号补码
)
-- 添加文件信息(填充到128字节)
if #file_info_str < YMODEM.DATA_SIZE_128 then
file_info_str = file_info_str .. string.rep("\0", YMODEM.DATA_SIZE_128 - #file_info_str)
else
file_info_str = file_info_str:sub(1, YMODEM.DATA_SIZE_128)
end
packet = packet .. file_info_str
-- 计算并添加CRC16
local crc = YMODEM.crc16(file_info_str)
packet = packet .. string.char(
bit.rshift(bit.band(crc, 0xFF00), 8),
bit.band(crc, 0xFF)
)
-- 发送数据包
return self.serial_write(packet)
end
-- 接收数据包
function YModem:_receive_packet()
-- 读取包头
local header = self:_get_char_timeout(self.config.timeout)
if not header then
return nil, YMODEM.ERROR.TIMEOUT
end
local expected_data_size, is_1k_packet
-- 检查包头类型
if header == YMODEM.SOH then
expected_data_size = YMODEM.DATA_SIZE_128
is_1k_packet = false
elseif header == YMODEM.STX and self.config.use_1k then
expected_data_size = YMODEM.DATA_SIZE_1K
is_1k_packet = true
elseif header == YMODEM.EOT then
return {type = "EOT"}, YMODEM.ERROR.SUCCESS
elseif header == YMODEM.CAN then
return {type = "CAN"}, YMODEM.ERROR.CANCEL
else
return nil, YMODEM.ERROR.INVALID_PACKET
end
-- 读取包编号和补码
local packet_num = self:_get_char_timeout(self.config.timeout)
local packet_num_comp = self:_get_char_timeout(self.config.timeout)
if not packet_num or not packet_num_comp then
return nil, YMODEM.ERROR.TIMEOUT
end
-- 验证包编号
if bit.band(packet_num + packet_num_comp, 0xFF) ~= 0xFF then
return nil, YMODEM.ERROR.INVALID_PACKET
end
-- 读取数据
local data_buffer = ""
for _ = 1, expected_data_size do
local char = self:_get_char_timeout(self.config.timeout)
if not char then
return nil, YMODEM.ERROR.TIMEOUT
end
data_buffer = data_buffer .. string.char(char)
end
-- 读取CRC
local crc_high = self:_get_char_timeout(self.config.timeout)
local crc_low = self:_get_char_timeout(self.config.timeout)
if not crc_high or not crc_low then
return nil, YMODEM.ERROR.TIMEOUT
end
local received_crc = bit.bor(bit.lshift(crc_high, 8), crc_low)
local calculated_crc = YMODEM.crc16(data_buffer)
-- 验证CRC
if received_crc ~= calculated_crc then
return nil, YMODEM.ERROR.INVALID_PACKET
end
-- 去除填充字符,找到实际数据长度
local actual_data = data_buffer
local last_valid = 0
for i = 1, #actual_data do
local byte = actual_data:byte(i)
if byte ~= 0x1A and byte ~= 0x00 then
last_valid = i
end
end
if last_valid > 0 then
actual_data = actual_data:sub(1, last_valid)
else
actual_data = ""
end
return {
type = "DATA",
packet_num = packet_num,
data = actual_data,
data_size = #actual_data,
is_1k = is_1k_packet
}, YMODEM.ERROR.SUCCESS
end
-- 解析文件头包
function YMODEM.parse_file_header(data)
local file_info = {
filename = "",
file_size = 0,
modify_time = 0,
mode = 0
}
-- 检查是否是空文件头 (传输结束)
if #data == 0 or data:byte(1) == 0 then
return file_info, true -- 空文件头
end
-- 解析文件名
local null_pos = data:find("\0")
if not null_pos then
return file_info, false
end
file_info.filename = data:sub(1, null_pos - 1)
data = data:sub(null_pos + 1)
-- 解析文件大小
null_pos = data:find("\0")
if not null_pos then
return file_info, false
end
local size_str = data:sub(1, null_pos - 1)
file_info.file_size = tonumber(size_str) or 0
data = data:sub(null_pos + 1)
-- 解析修改时间
null_pos = data:find("\0")
if not null_pos then
return file_info, true -- 时间是可选的
end
local time_str = data:sub(1, null_pos - 1)
file_info.modify_time = tonumber(time_str) or 0
data = data:sub(null_pos + 1)
-- 解析文件模式
null_pos = data:find("\0")
if null_pos then
local mode_str = data:sub(1, null_pos - 1)
file_info.mode = tonumber(mode_str) or 0
end
return file_info, false
end
-- YMODEM 接收文件
function YModem:receive()
if not self.serial_read or not self.serial_write or
not self.file_open or not self.file_write or not self.file_close then
return 0, YMODEM.ERROR.IO_ERROR
end
-- 初始化状态
self.packet_number = 0
self.retry_count = 0
self.current_file = nil
self.current_fd = nil
-- 发送 'C' 启动CRC模式的传输
if not self.serial_write(string.char(YMODEM.C)) then
return 0, YMODEM.ERROR.IO_ERROR
end
local total_received = 0
local file_opened = false
while self.retry_count < self.config.max_retries do
local packet, err = self:_receive_packet()
if err ~= YMODEM.ERROR.SUCCESS then
-- 错误处理
if err == YMODEM.ERROR.TIMEOUT or err == YMODEM.ERROR.INVALID_PACKET then
self.retry_count = self.retry_count + 1
self.serial_write(string.char(YMODEM.NAK))
else
return total_received, err
end
else
-- 成功接收包
if packet.type == "EOT" then
-- 文件传输结束
self.serial_write(string.char(YMODEM.ACK))
if file_opened then
self.file_close(self.current_fd)
file_opened = false
end
elseif packet.type == "CAN" then
-- 取消传输
if file_opened then
self.file_close(self.current_fd)
end
return total_received, YMODEM.ERROR.CANCEL
elseif packet.packet_num == 0 then
-- 文件头包
local file_info, is_empty = YMODEM.parse_file_header(packet.data)
if is_empty then
-- 空文件头,所有传输结束
self.serial_write(string.char(YMODEM.ACK))
break
else
-- 新文件开始
self.current_file = file_info
self.current_fd = self.file_open(file_info.filename, "wb")
if not self.current_fd then
return total_received, YMODEM.ERROR.FILE_ERROR
end
file_opened = true
total_received = 0
-- 发送ACK确认文件头
self.serial_write(string.char(YMODEM.ACK))
self.retry_count = 0
-- 调用进度回调
if self.progress_callback then
self.progress_callback(0, file_info.file_size, "start")
end
end
else
-- 数据包
if not file_opened then
return total_received, YMODEM.ERROR.FILE_ERROR
end
-- 写入文件数据
if not self.file_write(self.current_fd, packet.data) then
self.file_close(self.current_fd)
return total_received, YMODEM.ERROR.FILE_ERROR
end
total_received = total_received + packet.data_size
-- 调用进度回调
if self.progress_callback and self.current_file.file_size > 0 then
self.progress_callback(total_received, self.current_file.file_size, "progress")
end
-- 发送ACK确认
self.serial_write(string.char(YMODEM.ACK))
self.retry_count = 0
-- 更新期望的下一个包编号
self.packet_number = packet.packet_num
end
end
end
if self.retry_count >= self.config.max_retries then
if file_opened then
self.file_close(self.current_fd)
end
return total_received, YMODEM.ERROR.TOO_MANY_RETRIES
end
if file_opened then
self.file_close(self.current_fd)
-- 最终进度回调
if self.progress_callback then
self.progress_callback(total_received, self.current_file.file_size, "complete")
end
end
return total_received, YMODEM.ERROR.SUCCESS
end
-- YMODEM 发送文件
function YModem:transmit(filename)
if not self.serial_read or not self.serial_write or
not self.file_open or not self.file_read or not self.file_close then
return 0, YMODEM.ERROR.IO_ERROR
end
-- 初始化状态
self.packet_number = 1
self.retry_count = 0
-- 打开文件
local fd, file_size = self.file_open(filename, "rb")
if not fd then
return 0, YMODEM.ERROR.FILE_ERROR
end
-- 获取文件信息(这里需要平台特定的实现)
local file_info = {
filename = filename,
file_size = file_size or 0,
modify_time = os.time(),
mode = 420 -- 0644 八进制
}
-- 等待接收方的启动信号 'C'
while self.retry_count < self.config.max_retries do
local response = self:_get_char_timeout(self.config.timeout)
if response == YMODEM.C then
break
elseif response == YMODEM.CAN then
self.file_close(fd)
return 0, YMODEM.ERROR.CANCEL
else
self.retry_count = self.retry_count + 1
end
end
if self.retry_count >= self.config.max_retries then
self.file_close(fd)
return 0, YMODEM.ERROR.TOO_MANY_RETRIES
end
-- 发送文件头包
if not self:_send_file_header_packet(file_info) then
self.file_close(fd)
return 0, YMODEM.ERROR.IO_ERROR
end
-- 等待文件头确认
local response = self:_get_char_timeout(self.config.timeout)
if not response or response ~= YMODEM.ACK then
self.file_close(fd)
return 0, YMODEM.ERROR.TIMEOUT
end
-- 发送文件数据
local total_sent = 0
self.retry_count = 0
while total_sent < file_info.file_size and self.retry_count < self.config.max_retries do
-- 读取文件数据
local to_read = self.config.use_1k and YMODEM.DATA_SIZE_1K or YMODEM.DATA_SIZE_128
if (total_sent + to_read) > file_info.file_size then
to_read = file_info.file_size - total_sent
end
local data = self.file_read(fd, to_read)
if not data or #data == 0 then
break
end
-- 发送数据包
if not self:_send_data_packet(data, self.packet_number) then
self.file_close(fd)
return total_sent, YMODEM.ERROR.IO_ERROR
end
-- 等待响应
response = self:_get_char_timeout(self.config.timeout)
if response == YMODEM.ACK then
-- 成功,移动到下一个数据包
total_sent = total_sent + #data
self.packet_number = self.packet_number + 1
self.retry_count = 0
-- 调用进度回调
if self.progress_callback then
self.progress_callback(total_sent, file_info.file_size, "progress")
end
elseif response == YMODEM.NAK then
-- 重传当前数据包
self.retry_count = self.retry_count + 1
-- 回退文件指针
if self.file_seek then
self.file_seek(fd, total_sent)
end
elseif response == YMODEM.CAN then
self.file_close(fd)
return total_sent, YMODEM.ERROR.CANCEL
else
self.retry_count = self.retry_count + 1
end
end
if self.retry_count >= self.config.max_retries then
self.file_close(fd)
return total_sent, YMODEM.ERROR.TOO_MANY_RETRIES
end
-- 发送EOT结束文件传输
self.serial_write(string.char(YMODEM.EOT))
-- 等待ACK
response = self:_get_char_timeout(self.config.timeout)
if not response or response ~= YMODEM.ACK then
self.file_close(fd)
return total_sent, YMODEM.ERROR.TIMEOUT
end
-- 发送空文件头包表示传输结束
self:_send_file_header_packet({})
-- 等待最终的ACK
response = self:_get_char_timeout(self.config.timeout)
if not response or response ~= YMODEM.ACK then
self.file_close(fd)
return total_sent, YMODEM.ERROR.TIMEOUT
end
self.file_close(fd)
-- 最终进度回调
if self.progress_callback then
self.progress_callback(total_sent, file_info.file_size, "complete")
end
return total_sent, YMODEM.ERROR.SUCCESS
end
return {
YMODEM = YMODEM,
YModem = YModem
}