本次来分享板子YModem串口协议通信,可以通用协议发送接收文件。Ymodem协议是一种常用的文件传输协议,广泛用于嵌入式系统串口传输。它支持文件名、文件大小、块128或1024字节大小可选,并采用CRC16校验,可靠性高。
Ymodem的数据包分为三种:开始包(文件名和文件大小)、数据包(文件内容)、结束包(空包)。每种包都包含包头、序号、序号补码、数据区、CRC校验 。
协议字符:
| 字符 |
值(十六进制) |
含义 |
| SOH |
0x01 |
128字节数据包头 |
| STX |
0x02 |
1024字节数据包头 |
| EOT |
0x04 |
传输结束 |
| ACK |
0x06 |
确认正确接收 |
| NAK |
0x15 |
接收错误,要求重传 |
| CAN |
0x18 |
取消传输 |
| C |
0x43 |
字符'C',表示接收方准备就绪 |
字段说明 :
- 包头 :
SOH(128字节)或 STX(1024字节)。
- 序号 :从0开始递增,每个包序号加1(模256)。开始包序号为0,数据包从1开始。
- 补码 :
0xFF - 序号,用于简单校验。
- 数据区 :包体内容。对于开始包,为文件名字符串 + 空格 + 文件大小(十进制字符串),不足长度用
0x00填充;对于数据包,为文件数据;对于结束包,全为0x00。
- CRC :对数据区 计算得到的CRC16-CCITT值(多项式
0x1021),先高字节后低字节。
协议传输流程:
接收方通常首先发送字符C,通知发送方已准备好使用CRC校验。
- 接收方发送
C (可重复发送直到收到第一个包或超时)。
- 发送方发送开始包 (包序号0,数据区包含
文件名 文件大小,末尾填充0x00)。
- 接收方收到后,若CRC正确且序号匹配,回复
ACK,然后发送C请求下一个包。
- 若CRC错误或序号错,回复
NAK,发送方重发。
- 发送方发送数据包 (序号从1开始,每包数据长度128或1024,最后一个包可能不足)。
- 接收方每正确接收一包,回复
ACK,序号递增。
- 若出错,回复
NAK要求重发当前包。
- 发送方发送EOT (表示文件数据发送完毕)。
- 发送方发送结束包 (包序号0,数据区全0x00)。
板子上有2个串口,一个用于打印LOG日志,另一个用于YModem协议通信测试。下面是YModem协议实现过程,首先定义一些常量和结构。
#define ymodem_printf(fmt, args...) printf(fmt, ##args)
/* Ymodem 协议常量 */
#define YMODEM_SOH 0x01
#define YMODEM_STX 0x02
#define YMODEM_EOT 0x04
#define YMODEM_ACK 0x06
#define YMODEM_NAK 0x15
#define YMODEM_CAN 0x18
#define YMODEM_C 0x43
/* 接收端状态 */
typedef enum {
YMODEM_RX_STATE_IDLE,
YMODEM_RX_STATE_WAIT_START, /* 等待开始(发送'C') */
YMODEM_RX_STATE_RECEIVE_PACKET, /* 接收数据包中 */
YMODEM_RX_STATE_PROCESS_PACKET, /* 处理数据包 */
YMODEM_RX_STATE_WAIT_EOT, /* 等待EOT */
YMODEM_RX_STATE_SEND_EOT_ACK, /* 发送EOT的ACK */
YMODEM_RX_STATE_WAIT_END, /* 等待结束包 */
YMODEM_RX_STATE_SEND_END_ACK, /* 发送结束包ACK */
YMODEM_RX_STATE_COMPLETE,
YMODEM_RX_STATE_ERROR
} ymodem_rx_state_t;
/* 发送端状态 */
typedef enum {
YMODEM_TX_STATE_IDLE,
YMODEM_TX_STATE_WAIT_START_C, /* 等待开始字符C */
YMODEM_TX_STATE_SEND_START_PACKET, /* 发送开始包 */
YMODEM_TX_STATE_WAIT_START_ACK, /* 等待开始包ACK */
YMODEM_TX_STATE_WAIT_DATA_C, /* 等待请求数据包字符C */
YMODEM_TX_STATE_SEND_DATA, /* 发送数据包 */
YMODEM_TX_STATE_WAIT_DATA_ACK, /* 等待数据包ACK/NAK */
YMODEM_TX_STATE_SEND_EOT, /* 发送EOT */
YMODEM_TX_STATE_WAIT_EOT_ACK, /* 等待EOT的ACK */
YMODEM_TX_STATE_SEND_END_PACKET, /* 发送结束包(空包) */
YMODEM_TX_STATE_WAIT_END_ACK, /* 等待结束包ACK */
YMODEM_TX_STATE_COMPLETE,
YMODEM_TX_STATE_ERROR
} ymodem_tx_state_t;
/* 包接收子状态 */
typedef enum {
PKT_RX_STATE_WAIT_HEADER,
PKT_RX_STATE_WAIT_SEQ,
PKT_RX_STATE_WAIT_SEQ_COMP,
PKT_RX_STATE_WAIT_DATA,
PKT_RX_STATE_WAIT_CRC_H,
PKT_RX_STATE_WAIT_CRC_L
} ymodem_rx_packet_state_t;
/* 处理结果 */
typedef enum {
YMODEM_RESULT_IN_PROGRESS,
YMODEM_RESULT_SUCCESS,
YMODEM_RESULT_ERROR_TIMEOUT,
YMODEM_RESULT_ERROR_CRC,
YMODEM_RESULT_ERROR_SEQ,
YMODEM_RESULT_ERROR_CANCEL,
YMODEM_RESULT_ERROR_WRITE /* 文件写入错误 */
} ymodem_result_t;
/* Ymodem 句柄(同时支持发送和接收) */
typedef struct {
uint8_t mode; /* 模式标志(0:接收,1:发送) */
uint8_t eot_transmit; /* 传输完成标志 */
uint8_t seq; /* 当前接收包/发送包的序号 */
uint8_t expected_; /* 期望的下一个包序号 */
uint8_t buff[1032]; /* 数据缓冲区 1024 + 8 */
uint16_t pkt_len; /* 当前发送包数据长度(128/1024) */
uint16_t data_idx;
uint16_t crc;
uint16_t retry_count; /* 包重试次数 */
uint32_t expected_seq; /* 期望的下一个包序号 */
uint32_t file_size; /* 文件总大小 */
uint32_t transmit_size; /* 已发送字节数 */
uint32_t last_tick;
uint32_t timeout_ms;
uint32_t state;
char filename[256]; /* 接收的文件名 */
const char* tx_filename; /* 要发送的文件名 */
/* 接收专用变量 */
ymodem_rx_packet_state_t rx_pkt_state;
uint32_t last_c_tick; /* 上次发送'C'的时间(接收端专用) */
/* 硬件抽象回调函数 */
uint8_t (*uart_read)(uint8_t *ch);
uint8_t (*uart_write)(uint8_t ch);
uint32_t (*get_tick)(void);
uint32_t (*file_open_read)(const char* name,uint32_t *filesize); /* 打开文件读取(发送端用) */
uint32_t (*file_read)(uint8_t* buf, uint32_t len); /* 读取文件数据,返回实际读取字节数 */
uint32_t (*file_close_read)(void); /* 关闭读取文件 */
uint32_t (*file_open_write)(const char* name, uint32_t size); /* 打开文件写入(接收端用) */
uint32_t (*file_write)(const uint8_t* data, uint32_t len);
uint32_t (*file_close_write)(void);
} ymodem_handle_t;
协议解析过程使用状态机方式,非阻塞通信过程。串口发送接收数据使用之前的中断模式,FIFO发送接收。
/ CRC16-CCITT 表 /
static const uint16_t crc16_table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
0xEDAE, 0xFD8F, 0xCDEC, 0CD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 06C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
};
static uint16_t crc16_update(uint16_t crc, uint8_t data) {
return (crc << 8) ^ crc16_table[((crc >> 8) ^ data) & 0xFF];
}
static uint16_t crc16_block(const uint8_t* data, uint16_t len) {
uint16_t crc = 0x0000;
for (uint16_t i = 0; i < len; i++) {
crc = crc16_update(crc, data[i]);
}
return crc;
}
/ 发送一个字节(忽略返回值) /
static void send_byte(ymodem_handle_t* handle, uint8_t byte) {
handle->uart_write(byte);
}
/ 发送 ACK /
static void send_ack(ymodem_handle_t* handle) {
send_byte(handle, YMODEM_ACK);
}
/ 发送 NAK /
static void send_nak(ymodem_handle_t* handle) {
send_byte(handle, YMODEM_NAK);
handle->retry_count++;
}
/ 发送 'C' /
static void send_c(ymodem_handle_t* handle) {
send_byte(handle, YMODEM_C);
}
/ 发送 CAN /
static void send_can(ymodem_handle_t handle) {
send_byte(handle, YMODEM_CAN);
send_byte(handle, YMODEM_CAN); / 发送两个CAN表示取消 */
}
串口YModem协议接收处理代码:
/* ======================= 接收端处理 ======================= */
static void rx_handle_received_byte(ymodem_handle_t* handle, uint8_t byte)
{
switch (handle->rx_pkt_state) {
case PKT_RX_STATE_WAIT_HEADER:
if (byte == YMODEM_SOH) {
handle->pkt_len = 128;
handle->data_idx = 0;
handle->crc = 0x0000;
handle->rx_pkt_state = PKT_RX_STATE_WAIT_SEQ;
} else if (byte == YMODEM_STX) {
handle->pkt_len = 1024;
handle->data_idx = 0;
handle->crc = 0x0000;
handle->rx_pkt_state = PKT_RX_STATE_WAIT_SEQ;
} else if (byte == YMODEM_EOT) {
handle->eot_transmit = 1;
handle->state = YMODEM_RX_STATE_SEND_EOT_ACK;
} else if (byte == YMODEM_CAN) {
handle->state = YMODEM_RX_STATE_ERROR;
} else {
/* 非法字符,发送NAK要求重传 */
send_nak(handle);
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
ymodem_printf("NAK-ErrData:%02X.\r\n",byte);
if (handle->expected_seq == 0)
{
handle->state = YMODEM_RX_STATE_WAIT_START;
}
}
break;
case PKT_RX_STATE_WAIT_SEQ:
handle->seq = byte; /* 暂存接收到的序号 */
handle->rx_pkt_state = PKT_RX_STATE_WAIT_SEQ_COMP;
ymodem_printf("SEQ:%02X.\r\n",handle->seq);
break;
case PKT_RX_STATE_WAIT_SEQ_COMP:
if (handle->seq != (byte^0xff)) {
send_nak(handle);
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
handle->state = YMODEM_RX_STATE_ERROR;
ymodem_printf("SEQ-ErrData:%02X-%02X.\r\n",handle->seq,byte);
} else {
handle->rx_pkt_state = PKT_RX_STATE_WAIT_DATA;
}
break;
case PKT_RX_STATE_WAIT_DATA:
handle->buff[handle->data_idx++] = byte;
handle->crc = crc16_update(handle->crc, byte);
if (handle->data_idx >= handle->pkt_len) {
handle->rx_pkt_state = PKT_RX_STATE_WAIT_CRC_H;
}
break;
case PKT_RX_STATE_WAIT_CRC_H:
handle->crc = crc16_update(handle->crc, byte);
handle->rx_pkt_state = PKT_RX_STATE_WAIT_CRC_L;
break;
case PKT_RX_STATE_WAIT_CRC_L:
handle->crc = crc16_update(handle->crc, byte);
if (handle->crc != 0x0000) {
send_nak(handle);
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
handle->state = YMODEM_RX_STATE_ERROR;
ymodem_printf("CRC-ErrData:%04X.\r\n",handle->crc);
} else {
/* 包接收完成,进入处理状态 */
handle->state = YMODEM_RX_STATE_PROCESS_PACKET;
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
ymodem_printf("CRC:%04X.\r\n",handle->crc);
}
break;
}
}
static void rx_process_packet(ymodem_handle_t* handle)
{
if (handle->seq != (handle->expected_seq & 0xff)) {
send_nak(handle);
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
handle->state = YMODEM_RX_STATE_RECEIVE_PACKET;
ymodem_printf("EXPSEQ-ErrData:%02X-%02X.\r\n",handle->seq,handle->expected_seq);
return;
}
if (handle->expected_seq == 0)
{
/* 开始包:解析文件名和大小 */
char* p = (char*)handle->buff;
char* filename = p;
char* size_str = NULL;
while (*p && *p != ' ') p++;
if (*p == ' ') {
*p = '\0';
}
p++;
size_str = p;
while (*p && *p != 0x00) p++;
*p = '\0';
if (filename[0] == '\0') {
/* 空包,直接结束 */
handle->state = YMODEM_RX_STATE_SEND_END_ACK;
send_ack(handle);
return;
}
send_ack(handle);
handle->file_size = size_str ? (uint32_t)strtoul(size_str, NULL, 10) : 0;
handle->transmit_size = 0;
strncpy(handle->filename, filename, sizeof(handle->filename) - 1);
handle->filename[sizeof(handle->filename) - 1] = '\0';
if (handle->file_open_write) {
handle->file_open_write(handle->filename, handle->file_size);
}
send_c(handle); /* 请求下一个包 */
handle->state = YMODEM_RX_STATE_WAIT_START;
} else
{
/* 数据包 */
uint16_t data_len = handle->pkt_len;
uint32_t remain = handle->file_size - handle->transmit_size;
if (data_len > remain) data_len = remain;
if (data_len > 0 && handle->file_write) {
handle->file_write(handle->buff, data_len);
handle->transmit_size += data_len;
}
handle->state = YMODEM_RX_STATE_RECEIVE_PACKET; // 直接等待下一包
send_ack(handle);
}
handle->last_tick = handle->get_tick();
handle->expected_seq++; /* 下一个期望序号为1 */
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
if (handle->transmit_size >= handle->file_size) {
handle->state = YMODEM_RX_STATE_WAIT_EOT;
handle->expected_seq = 0;
}
ymodem_printf("PACKET:%d.\r\n",handle->expected_seq);
}
static ymodem_result_t ymodem_receive_process(ymodem_handle_t* handle) {
uint32_t now = handle->get_tick();
uint8_t byte;
if ((now - handle->last_tick) > handle->timeout_ms) {
handle->state = YMODEM_RX_STATE_ERROR;
return YMODEM_RESULT_ERROR_TIMEOUT;
}
switch (handle->state)
{
case YMODEM_RX_STATE_WAIT_START:
/* 每隔1秒重发'C' */
if ((now - handle->last_c_tick) >= 1000) {
send_c(handle);
ymodem_printf("C-");
handle->last_c_tick = now;
}
/* 检查是否有数据到达 */
if (handle->uart_read(&byte) == 0) {
handle->state = YMODEM_RX_STATE_RECEIVE_PACKET;
rx_handle_received_byte(handle, byte);
}
break;
case YMODEM_RX_STATE_RECEIVE_PACKET:
while (handle->uart_read(&byte) == 0) {
rx_handle_received_byte(handle, byte);
if (handle->state != YMODEM_RX_STATE_RECEIVE_PACKET) break;
}
break;
case YMODEM_RX_STATE_PROCESS_PACKET:
rx_process_packet(handle);
break;
case YMODEM_RX_STATE_WAIT_EOT:
if (handle->uart_read(&byte) == 0) {
if (byte == YMODEM_EOT) {
handle->eot_transmit = 1;
handle->state = YMODEM_RX_STATE_SEND_EOT_ACK;
} else {
send_nak(handle);
}
}
break;
case YMODEM_RX_STATE_SEND_EOT_ACK:
send_ack(handle);
handle->state = YMODEM_RX_STATE_WAIT_END;
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
break;
case YMODEM_RX_STATE_WAIT_END:
if (handle->uart_read(&byte) == 0) {
handle->state = YMODEM_RX_STATE_RECEIVE_PACKET;
rx_handle_received_byte(handle, byte);
}
break;
case YMODEM_RX_STATE_SEND_END_ACK:
send_ack(handle);
if (handle->file_close_write) handle->file_close_write();
handle->state = YMODEM_RX_STATE_COMPLETE;
return YMODEM_RESULT_SUCCESS;
case YMODEM_RX_STATE_COMPLETE:
return YMODEM_RESULT_SUCCESS;
case YMODEM_RX_STATE_ERROR:
return YMODEM_RESULT_ERROR_CANCEL;
default:
break;
}
return YMODEM_RESULT_IN_PROGRESS;
}
串口YModem协议发送处理代码:
/* ======================= 发送端处理 ======================= */
/* 发送一个数据包(SOH/STX + 序号 + 补码 + 数据 + CRC) */
static void tx_send_packet(ymodem_handle_t* handle, uint8_t pkt_type, uint8_t seq, uint16_t len)
{
int lenght = (len > 128) ? 1024 : 128;
handle->data_idx = 0;
handle->buff[handle->data_idx++] = pkt_type; /* SOH 或 STX */
handle->buff[handle->data_idx++] = seq; /* 序号 */
handle->buff[handle->data_idx++] = ~seq; /* 补码 */
if(lenght > len) memset(&handle->buff[3+len],0,lenght-len);
handle->data_idx += lenght;
handle->crc = crc16_block(&handle->buff[3], lenght);
handle->buff[handle->data_idx++] = (handle->crc >> 8) & 0xFF;
handle->buff[handle->data_idx++] = handle->crc & 0xFF;
for (uint16_t i = 0; i < handle->data_idx; i++) {
handle->uart_write(handle->buff[i]);
}
handle->last_tick = handle->get_tick();
}
/* 读取下一个要发送的数据块,返回实际长度(0表示无数据) */
static uint16_t tx_read_next_block(ymodem_handle_t* handle)
{
uint32_t remain = handle->file_size - handle->transmit_size;
if (remain == 0) return 0;
uint16_t block_size = (remain >= 1024) ? 1024 : 128;
if (block_size > 128 && handle->transmit_size + 128 >= handle->file_size) {
block_size = 128; /* 最后一个包使用SOH(128字节) */
}
uint16_t read_len = (block_size < remain) ? block_size : remain;
if (handle->file_read) {
read_len = handle->file_read(&handle->buff[3], read_len);
}
return read_len;
}
static ymodem_result_t ymodem_send_process(ymodem_handle_t* handle)
{
uint8_t byte;
ymodem_result_t result = YMODEM_RESULT_IN_PROGRESS;
uint32_t now = handle->get_tick();
if ((now - handle->last_tick) > handle->timeout_ms) {
handle->state = YMODEM_TX_STATE_ERROR;
return YMODEM_RESULT_ERROR_TIMEOUT;
}
switch (handle->state)
{
case YMODEM_TX_STATE_WAIT_START_C:
if (handle->uart_read(&byte) == 0)
{
if (byte == YMODEM_C) {
handle->state = YMODEM_TX_STATE_SEND_START_PACKET;
ymodem_printf("RECV 'C'.\r\n");
}
}
break;
case YMODEM_TX_STATE_SEND_START_PACKET:
/* 构建开始包:文件名 + 空格 + 文件大小 + 0x00 */
{
#if 0
int len = snprintf(handle->tx_data, sizeof(handle->tx_data), "%s %lu",
handle->tx_filename, (unsigned long)handle->tx_file_size);
#else
int len = snprintf((char *)&handle->buff[3], 1024, "%s",handle->tx_filename);
handle->buff[len+3] = 0;
len += snprintf((char *)&handle->buff[len+1+3], 1024-len, "%lu",(unsigned long)handle->file_size);
#endif
/* 填充剩余部分为0x00 */
memset(handle->buff + len + 1 + 3, 0, 128 - len - 1 - 3);
tx_send_packet(handle, YMODEM_SOH, 0, 128);
}
handle->state = YMODEM_TX_STATE_WAIT_START_ACK;
handle->retry_count = 0;
break;
case YMODEM_TX_STATE_WAIT_START_ACK:
if (handle->uart_read(&byte) == 0) {
if (byte == YMODEM_ACK) {
// handle->state = YMODEM_TX_STATE_SEND_DATA; //handle->state = YMODEM_TX_STATE_WAIT_DATA_C
handle->seq = 1;
handle->transmit_size = 0;
handle->retry_count = 0;
} else if (byte == YMODEM_NAK) {
/* 重传开始包 */
handle->retry_count++;
if (handle->retry_count >= 5) {
handle->state = YMODEM_TX_STATE_ERROR;
result = YMODEM_RESULT_ERROR_CANCEL;
} else {
handle->state = YMODEM_TX_STATE_SEND_START_PACKET;
}
}
else if (byte == YMODEM_C)
{
if(1 == handle->seq)
{
handle->state = YMODEM_TX_STATE_SEND_DATA;
ymodem_printf("RECV-D 'C'.\r\n");
}
}
}
break;
// case YMODEM_TX_STATE_WAIT_DATA_C:
// if (handle->uart_read(&byte) == 0)
// {
// if (byte == YMODEM_C) {
// handle->state = YMODEM_TX_STATE_SEND_DATA;
// ymodem_printf("RECV-D 'C'.\r\n");
// }
// }
// break;
case YMODEM_TX_STATE_SEND_DATA:
{
uint16_t data_len = tx_read_next_block(handle);
if (data_len == 0) {
handle->state = YMODEM_TX_STATE_SEND_EOT;
} else {
uint8_t pkt_type = (data_len > 128) ? YMODEM_STX : YMODEM_SOH;
tx_send_packet(handle, pkt_type, handle->seq, data_len);
handle->transmit_size += data_len;
handle->state = YMODEM_TX_STATE_WAIT_DATA_ACK;
handle->expected_seq = handle->seq;
handle->retry_count = 0;
}
}
break;
case YMODEM_TX_STATE_WAIT_DATA_ACK:
if (handle->uart_read(&byte) == 0) {
if (byte == YMODEM_ACK) {
handle->seq++;
handle->state = YMODEM_TX_STATE_SEND_DATA;
} else if (byte == YMODEM_NAK) {
handle->retry_count++;
if (handle->retry_count >= 5) {
handle->state = YMODEM_TX_STATE_ERROR;
result = YMODEM_RESULT_ERROR_CANCEL;
} else {
/* 重发当前包 */
handle->state = YMODEM_TX_STATE_SEND_DATA;
handle->transmit_size -= handle->pkt_len; /* 回退已发送计数 */
/* 注意:需要重新读取数据,但简单起见重新调用 send_data 时会重新读取 */
handle->seq = handle->expected_seq; /* 保持序号不变 */
}
} else if (byte == YMODEM_CAN) {
handle->state = YMODEM_TX_STATE_ERROR;
result = YMODEM_RESULT_ERROR_CANCEL;
}
}
break;
case YMODEM_TX_STATE_SEND_EOT:
send_byte(handle, YMODEM_EOT);
handle->state = YMODEM_TX_STATE_WAIT_EOT_ACK;
handle->retry_count = 0;
break;
case YMODEM_TX_STATE_WAIT_EOT_ACK:
if (handle->uart_read(&byte) == 0)
{
if (byte == YMODEM_ACK)
{
/* 发送结束包(空包) */
handle->eot_transmit = 1;
memset(&handle->buff[3], 0, 128);/* 全0 */
tx_send_packet(handle, YMODEM_SOH, 0, 128);
handle->state = YMODEM_TX_STATE_WAIT_END_ACK;
} else if (byte == YMODEM_NAK)
{
handle->retry_count++;
if (handle->retry_count >= 5) {
handle->state = YMODEM_TX_STATE_ERROR;
result = YMODEM_RESULT_ERROR_CANCEL;
} else {
handle->state = YMODEM_TX_STATE_SEND_EOT; /* 重发EOT */
}
}
}
break;
case YMODEM_TX_STATE_WAIT_END_ACK:
if (handle->uart_read(&byte) == 0) {
if (byte == YMODEM_ACK) {
if (handle->file_close_read) handle->file_close_read();
handle->state = YMODEM_TX_STATE_COMPLETE;
result = YMODEM_RESULT_SUCCESS;
} else if (byte == YMODEM_NAK) {
handle->retry_count++;
if (handle->retry_count >= 5) {
handle->state = YMODEM_TX_STATE_ERROR;
result = YMODEM_RESULT_ERROR_CANCEL;
} else {
/* 重发结束包 */
memset(&handle->buff[3], 0, 128);/* 全0 */
tx_send_packet(handle, YMODEM_SOH, 0, 128);
}
}
}
break;
case YMODEM_TX_STATE_COMPLETE:
return YMODEM_RESULT_SUCCESS;
case YMODEM_TX_STATE_ERROR:
if (handle->file_close_read) handle->file_close_read();
return YMODEM_RESULT_ERROR_CANCEL;
default:
break;
}
return result;
}
串口YModem协议API调用代码:
/* ======================= 公共API ======================= */
void ymodem_init(ymodem_handle_t* handle,
uint8_t (*uart_read)(uint8_t*),
uint8_t (*uart_write)(uint8_t),
uint32_t (*get_tick)(void)) {
memset(handle, 0, sizeof(ymodem_handle_t));
handle->uart_read = uart_read;
handle->uart_write = uart_write;
handle->get_tick = get_tick;
handle->mode = 0; /* 默认为接收模式 */
}
void ymodem_start_receive(ymodem_handle_t* handle, uint32_t timeout_ms,
uint32_t (*file_open)(const char*, uint32_t),
uint32_t (*file_write)(const uint8_t*, uint32_t),
uint32_t (*file_close)(void)) {
handle->mode = 0;
handle->state = YMODEM_RX_STATE_WAIT_START;
handle->rx_pkt_state = PKT_RX_STATE_WAIT_HEADER;
handle->seq = 0;
handle->expected_seq = 0;
handle->transmit_size = 0;
handle->file_size = 0;
handle->eot_transmit = 0;
handle->retry_count = 0;
handle->timeout_ms = timeout_ms;
handle->last_tick = handle->get_tick();
handle->last_c_tick = handle->get_tick();
handle->file_open_write = file_open;
handle->file_write = file_write;
handle->file_close_write = file_close;
send_c(handle); /* 立即发送第一个'C' */
}
void ymodem_start_send(ymodem_handle_t* handle, const char* filename, uint32_t file_size,
uint32_t timeout_ms,
uint32_t (*file_open)(const char*,uint32_t *),
uint32_t (*file_read)(uint8_t*, uint32_t),
uint32_t (*file_close)(void)) {
handle->mode = 1;
handle->state = YMODEM_TX_STATE_WAIT_START_C;
handle->tx_filename = filename;
handle->file_size = file_size;
handle->transmit_size = 0;
handle->seq = 0;
handle->eot_transmit = 0;
handle->timeout_ms = timeout_ms;
handle->last_tick = handle->get_tick();
handle->file_open_read = file_open;
handle->file_read = file_read;
handle->file_close_read = file_close;
if (handle->file_open_read) {
handle->file_open_read(filename,&handle->file_size);
}
}
ymodem_result_t ymodem_process(ymodem_handle_t* handle) {
if (handle->mode == 0) {
return ymodem_receive_process(handle);
} else {
return ymodem_send_process(handle);
}
}
协议移植接口处理,首先实现获取ms时间tick。再就是串口单个字节的发送和接收。
volatile uint32_t ymodem_tick;
static uint32_t get_tick(void) {
return ymodem_tick; //ms tick
}
uint8_t yModem_PutChar(uint8_t ch)
{
uart_send_char(1, ch);
return 0;
}
uint8_t yModem_GetChar(uint8_t *ch)
{
return uart_recv_char(1, ch);
}
再就是通信过程中文件打开,读,写回调函数。
static uint32_t file_open_write(const char* name, uint32_t size) {
/* 打开文件准备写入 */
ymodem_printf("ymodem recv file: %s,lenght:%u.\r\n",name,size);
delay_ms(10);
return 0;
}
static uint32_t file_write(const uint8_t* buff, uint32_t len) {
/* 写入文件 */
ymodem_printf("ymodem write file:%u.\r\n",len);
delay_ms(50);
return 0;
}
static uint32_t file_close_write(void) {
/* 关闭文件 */
ymodem_printf("ymodem write file over.\r\n");
delay_ms(10);
return 0;
}
static uint32_t file_open_read(const char* name,uint32_t *filesize) {
/* 打开文件读取 */
ymodem_printf("ymodem send file: %s.\r\n",name);
return 0;
}
static uint32_t file_read(uint8_t* buf, uint32_t len) {
/* 从文件读取数据,返回实际读取字节数 */
ymodem_printf("ymodem read file:%u.\r\n",len);
memset(buf,0x20,len);
return len;
}
static uint32_t file_close_read(void) {
/* 关闭文件 */
ymodem_printf("ymodem send file over.\r\n");
return 0;
}
使用shell命令实现YModem发送接收测试。
#include "lsy_shell_core.h"
ymodem_handle_t g_ymodem;
int ymodem_proc(int argc, char **argv)
{
uint8_t ch;
uint32_t num;
if (argc != 2) {
printf("Usage: ymodem <recv|send>\r\n");
return 1;
}
ymodem_tick = 0;
ymodem_init(&g_ymodem, yModem_GetChar, yModem_PutChar, get_tick);
if(!strcmp(argv[1], "recv"))
{
ymodem_start_receive(&g_ymodem, 25000, file_open_write, file_write, file_close_write);
}else if(!strcmp(argv[1],"send"))
{
ymodem_start_send(&g_ymodem, "test.bin", 123450, 25000, file_open_read, file_read, file_close_read);
}else
{
printf("cmd para error!\n");
printf("Usage: ymodem <recv|send>\r\n");
return 1;
}
while (1)
{
ymodem_result_t res = ymodem_process(&g_ymodem);
if (res == YMODEM_RESULT_SUCCESS) {
printf("\n\nsuccess!\n");
break;
} else if (res != YMODEM_RESULT_IN_PROGRESS) {
printf("\n\nerror: %d\n", res);
break;
}
uart_flush_data(1);
}
num=0;
while(!uart_recv_char(1, &ch))
{
num++;
}
printf("Flush rx=%u!\n",num);
return 0;
}
LSY_SHELL_CMD_EXPORT(ymodem, ymodem_proc, "YModem transmit file");
编译下载之后就可以通过串口shell测试YModem协议发送接收。

测试软件使用python实现的,代码如下:
import sys
import serial
import serial.tools.list_ports
import logging
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QComboBox, QPushButton,
QProgressBar, QTextEdit, QFileDialog, QMessageBox,
QGroupBox, QGridLayout)
from PyQt5.QtCore import QThread, pyqtSignal, QObject, Qt
from ymodem.Socket import ModemSocket # 修正导入路径
logging.basicConfig(level=logging.DEBUG)
class SerialWorker(QObject):
"""串口工作线程"""
log_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int, int) # 当前包数,总包数
finished_signal = pyqtSignal(bool)
cancel_signal = pyqtSignal()
def __init__(self, ser, is_send, file_path, save_dir=None):
super().__init__()
self.ser = ser
self.is_send = is_send
self.file_path = file_path
self.save_dir = save_dir
self._cancel = False
def run(self):
try:
# 定义读写函数
def read_func(size, timeout=1):
return self.ser.read(size) or None
def write_func(data, timeout=1):
return self.ser.write(data)
# 创建 ModemSocket 对象(使用 YMODEM 协议,包大小 1024 字节)
modem = ModemSocket(
read_func,
write_func,
protocol_type=1,
packet_size=1024
)
# 进度回调函数(注意参数格式与官方文档一致)
def progress_callback(task_idx, filename, total_packets, success_packets):
"""
task_idx: 当前任务索引
filename: 当前文件名
total_packets: 总包数
success_packets: 已成功包数
"""
if self._cancel:
return False # 返回 False 取消传输
# 更新进度条
self.progress_signal.emit(success_packets, total_packets)
return True
if self.is_send:
# 发送文件(send 方法接收文件路径列表)
self.log_signal.emit(f"开始发送文件: {self.file_path}")
success = modem.send([self.file_path], callback=progress_callback)
if success:
self.log_signal.emit("文件发送成功")
else:
self.log_signal.emit("文件发送失败或被取消")
self.finished_signal.emit(success)
else:
# 接收文件(recv 方法接收保存目录)
self.log_signal.emit(f"开始接收文件到目录: {self.save_dir}")
received_files = modem.recv(self.save_dir, callback=progress_callback)
if received_files:
# 兼容列表或字符串返回值
if isinstance(received_files, list):
files_str = ', '.join(received_files)
else:
files_str = str(received_files) # 直接转字符串
self.log_signal.emit(f"文件接收成功: {files_str}")
self.finished_signal.emit(True)
else:
self.log_signal.emit("文件接收失败或被取消")
self.finished_signal.emit(False)
except Exception as e:
self.log_signal.emit(f"传输出错: {str(e)}")
self.finished_signal.emit(False)
def cancel(self):
self._cancel = True
self.cancel_signal.emit()
# MainWindow 类保持不变,和之前完全一样
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("串口 Ymodem 文件传输工具")
self.setMinimumSize(600, 500)
self.ser = None
self.thread = None
self.worker = None
self.init_ui()
self.refresh_ports()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# ----- 串口设置区域 -----
group_serial = QGroupBox("串口设置")
serial_layout = QGridLayout()
serial_layout.addWidget(QLabel("串口号:"), 0, 0)
self.combo_port = QComboBox()
serial_layout.addWidget(self.combo_port, 0, 1)
btn_refresh = QPushButton("刷新")
btn_refresh.clicked.connect(self.refresh_ports)
serial_layout.addWidget(btn_refresh, 0, 2)
serial_layout.addWidget(QLabel("波特率:"), 1, 0)
self.combo_baud = QComboBox()
self.combo_baud.addItems(["9600", "19200", "38400", "57600", "115200"])
self.combo_baud.setCurrentText("115200")
serial_layout.addWidget(self.combo_baud, 1, 1)
self.btn_toggle = QPushButton("打开串口")
self.btn_toggle.clicked.connect(self.toggle_serial)
serial_layout.addWidget(self.btn_toggle, 1, 2)
group_serial.setLayout(serial_layout)
main_layout.addWidget(group_serial)
# ----- 文件操作区域 -----
group_file = QGroupBox("文件操作")
file_layout = QHBoxLayout()
self.btn_send = QPushButton("发送文件")
self.btn_send.clicked.connect(self.send_file)
self.btn_send.setEnabled(False)
file_layout.addWidget(self.btn_send)
self.btn_recv = QPushButton("接收文件")
self.btn_recv.clicked.connect(self.recv_file)
self.btn_recv.setEnabled(False)
file_layout.addWidget(self.btn_recv)
self.btn_cancel = QPushButton("取消传输")
self.btn_cancel.clicked.connect(self.cancel_transfer)
self.btn_cancel.setEnabled(False)
file_layout.addWidget(self.btn_cancel)
group_file.setLayout(file_layout)
main_layout.addWidget(group_file)
# ----- 进度条 -----
self.progress = QProgressBar()
self.progress.setRange(0, 100)
self.progress.setValue(0)
main_layout.addWidget(self.progress)
# ----- 日志区域 -----
group_log = QGroupBox("日志")
log_layout = QVBoxLayout()
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
log_layout.addWidget(self.log_text)
group_log.setLayout(log_layout)
main_layout.addWidget(group_log)
def refresh_ports(self):
self.combo_port.clear()
ports = [port.device for port in serial.tools.list_ports.comports()]
if ports:
self.combo_port.addItems(ports)
self.combo_port.setCurrentIndex(0)
else:
self.combo_port.addItem("无可用串口")
def toggle_serial(self):
if self.ser is None or not self.ser.is_open:
port = self.combo_port.currentText()
if port == "无可用串口":
QMessageBox.warning(self, "警告", "没有可用的串口")
return
try:
baud = int(self.combo_baud.currentText())
self.ser = serial.Serial(port, baud, timeout=1)
self.btn_toggle.setText("关闭串口")
self.btn_send.setEnabled(True)
self.btn_recv.setEnabled(True)
self.log(f"打开串口 {port} 成功,波特率 {baud}")
except Exception as e:
QMessageBox.critical(self, "错误", f"打开串口失败: {str(e)}")
self.ser = None
else:
self.ser.close()
self.ser = None
self.btn_toggle.setText("打开串口")
self.btn_send.setEnabled(False)
self.btn_recv.setEnabled(False)
self.log("串口已关闭")
def send_file(self):
if self.ser is None or not self.ser.is_open:
QMessageBox.warning(self, "警告", "请先打开串口")
return
file_path, _ = QFileDialog.getOpenFileName(self, "选择要发送的文件")
if not file_path:
return
self.start_transfer(is_send=True, file_path=file_path, save_dir=None)
def recv_file(self):
if self.ser is None or not self.ser.is_open:
QMessageBox.warning(self, "警告", "请先打开串口")
return
save_dir = QFileDialog.getExistingDirectory(self, "选择保存目录")
if not save_dir:
return
self.start_transfer(is_send=False, file_path=None, save_dir=save_dir)
def start_transfer(self, is_send, file_path, save_dir):
self.btn_toggle.setEnabled(False)
self.btn_send.setEnabled(False)
self.btn_recv.setEnabled(False)
self.btn_cancel.setEnabled(True)
self.progress.setValue(0)
self.thread = QThread()
self.worker = SerialWorker(self.ser, is_send, file_path, save_dir)
self.worker.moveToThread(self.thread)
self.worker.log_signal.connect(self.log)
self.worker.progress_signal.connect(self.update_progress)
self.worker.finished_signal.connect(self.transfer_finished)
self.worker.cancel_signal.connect(self.on_cancel)
self.thread.started.connect(self.worker.run)
self.thread.start()
def cancel_transfer(self):
if self.worker:
self.worker.cancel()
self.log("正在取消传输...")
def on_cancel(self):
self.log("传输已被取消")
self.transfer_finished(False)
def transfer_finished(self, success):
if self.thread and self.thread.isRunning():
self.thread.quit()
self.thread.wait()
self.thread = None
self.worker = None
self.btn_toggle.setEnabled(True)
self.btn_cancel.setEnabled(False)
if self.ser and self.ser.is_open:
self.btn_send.setEnabled(True)
self.btn_recv.setEnabled(True)
if success:
QMessageBox.information(self, "完成", "文件传输成功")
else:
QMessageBox.warning(self, "失败", "文件传输失败或被取消")
def update_progress(self, current, total):
if total > 0:
percent = int(current / total * 100)
self.progress.setValue(percent)
def log(self, message):
self.log_text.append(f"{message}")
self.log_text.ensureCursorVisible()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
YModem协议库核心代码