YMODEM 协议介绍以及通信流程分析和Lua语言实现

目录

概述

[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
}
相关推荐
flysnow0101 年前
Qt实现XYModem协议(三)
数据库·qt·ymodem·xmodem
flysnow0101 年前
Qt实现XYModem协议(六)
开发语言·qt·ymodem·xmodem
flysnow0101 年前
Qt实现XYModem协议(一)
开发语言·qt·ymodem·xmodem
Tosonw2 年前
YModem协议总结
服务器·网络·网络协议·ymodem