[DSP] YMODEM协议与实现
1.协议简介及控制字符
1.1 协议简介
YMODEM = XMODEM-1K的扩展,在XMODEM的基础上增加了:文件名/长度包(块0)、1K打包(STX)以及批量文件传输功能。
核心思想很简单,就是停等协议(Stop-and-Wait)——发完一包数据,等对方ACK再发下一包数据;收到NAK就重发。简单、可靠、就是比较慢。
1.2 控制字符
| 字符 | 值 | 发起者 | 含义 |
| SOH | 0x01 | 发送方 | 后面跟128字节数据包 |
| STX | 0x02 | 发送方 | 后面跟1024字节数据包 |
| EOT | 0x04 | 发送方 | 传输结束 |
| ACK | 0x06 | 接收方 | 确认,包正常接收 |
| NAK | 0x15 | 接收方 | 否认,要求重传 |
| CAN | 0x18 | 双方 | 取消传输 |
| C | 0x43 | 接收方 | 握手请求 |
2. 帧格式
2.1 数据帧
- seq:包序号,从0开始(块0=文件名包),数据包从1开始
- ~seq:序号取反,校验用。要求seq+~seq == 0xFF
- data:有效载荷,不满填0x1A
- CRC16:CCITT多项式0x1021,处置0,只算data部分(不含包头/CRC)
2.2 文件名包
文件包与数据包格式一样,只不过data段放的内容是文件名和文件大小。文件名后跟一个\0间隔,文件大小后也跟一个\0间隔,不够128字节补0x00。
2.3 单字节控制帧
3. 传输时序
- 接收方发送C:启动会话,指定 CRC16 校验
- 发送方收到C,发送SOH 00 FF的块 0 包:携带文件名、文件大小信息(128 字节)
- 接收方收到块 0 包,校验成功,接收方回复ACK(0x06)
- 接收方再次发送C:通知发送方,可以开始传输文件有效数据
- 发送方收到该C,发送STX 01 FE块 1(1024 字节 Ymodem‑1K 数据)
- 接收方收到数据块,校验成功则接收方回复ACK;校验失败则接收方回复NAK,触发发送方重传本数据块
- 发送方继续依次发送块 2、块 3……,每发送一个数据包,等待接收方回复ACK
- 文件全部发送完成,发送方发送第一个EOT(0x04)
接收方收到第一个EOT,接收方回复NAK(0x15)
发送方收到NAK,重发第二个EOT
接收方收到第二个EOT,接收方回复ACK:当前单个文件传输完毕
接收方发送C:批量模式,询问发送方是否还有待传输的下一个文件
发送方收到C,若无更多文件,发送方发送结束包:SOH 00 FF,128 字节全 0
接收方收到结束包,接收方回复ACK,整个 Ymodem 会话彻底结束
4. 代码实现
代码要分为协议层和硬件层,协议层主要包括crc校验函数、读取函数、解析第0包函数、接收状态机、发送逻辑等;硬件层包括字符发送、字符接收、延时函数以及一些对外接口函数等。
4.1 硬件层
putc 字符发送函数
static void u0_putc(unsigned char c) { UARTCharPut(SOC_UART_0_REGS, c); }getc 字符提取函数
static int u0_getc(unsigned char *c, unsigned int timeout_ms) { int us_ticks = 0; while (1) { int ch = UARTCharGetNonBlocking(SOC_UART_0_REGS); if (ch >= 0) { *c = (unsigned char)ch; return 1; } _delay_us(20); if (++us_ticks >= 50) { /* 50 * 20us = 1ms */ us_ticks = 0; if (timeout_ms == 0) return 0; timeout_ms--; } } }delay函数(使用定时器实现_delay_ms)
static void u0_delay(unsigned int ms) { unsigned int i; for (i = 0; i < ms; i++) _delay_ms(1); }接收端口
int uart0_ymodem_recv(unsigned char *buf, unsigned int size) { return ymodem_receive(u0_putc, u0_getc, u0_delay, buf, size); }发送端口
int uart0_ymodem_send(ymodem_read_fn read_fn, void *arg, unsigned int datalen, const char *filename) { return ymodem_send(u0_putc, u0_getc, u0_delay, read_fn, arg, datalen, filename); }4.2 协议层
crc校验函数
static unsigned short crc16_ccitt(const unsigned char *buf, int len) { unsigned short crc = 0; while (len-- > 0) { crc ^= (unsigned short)(*buf++) << 8; int i; for (i = 0; i < 8; i++) crc = (crc & 0x8000) ? (crc << 1) ^ CRC_POLY : (crc << 1); } return crc; }读取函数
static int read_body(int (*getc)(unsigned char *, unsigned int), unsigned char header, unsigned char *data_buf, int *pkt_seq) { int pkt_size = (header == SOH) ? PKT_128 : PKT_1024; unsigned char raw[PKT_1024 + 2]; unsigned char seq, seq_inv; int i; if (!getc(&seq, 2000)) return -2; if (!getc(&seq_inv, 2000)) return -2; if ((unsigned char)(seq + seq_inv) != 0xFF) return -1; *pkt_seq = seq; for (i = 0; i < pkt_size + 2; i++) if (!getc(&raw[i], 2000)) return -2; unsigned short rx_crc = (raw[pkt_size] << 8) | raw[pkt_size + 1]; unsigned short calc_crc = crc16_ccitt(raw, pkt_size); if (rx_crc != calc_crc) { g_ym_crc_fail++; g_ym_last_rx = rx_crc; g_ym_last_calc = calc_crc; int j; for (j = 0; j < 16 && j < pkt_size; j++) g_ym_dbg_pkt[j] = raw[j]; return -1; } g_ym_crc_pass++; memcpy(data_buf, raw, pkt_size); /* 整包拷贝,含填充 */ return pkt_size; }解析块0
static void parse_block0(const unsigned char *data, int len, unsigned long *filesize) { int i = 0; while (i < len && data[i] != 0) i++; /* 跳过 filename */ if (i >= len) return; i++; /* 跳过 \0 */ unsigned long v = 0; int got = 0; while (i < len && data[i] >= '0' && data[i] <= '9') { v = v * 10 + (data[i] - '0'); i++; got = 1; } if (got) *filesize = v; }接收状态机函数
typedef enum { YMODEM_STATE_HANDSHAKE, YMODEM_STATE_WAIT_HEADER, YMODEM_STATE_READ_PACKET, YMODEM_STATE_PROCESS_PACKET, YMODEM_STATE_WAIT_EOT, YMODEM_STATE_DONE, YMODEM_STATE_ABORT, YMODEM_STATE_ERROR } ymodem_state_t; int ymodem_receive(void (*putc)(unsigned char), int (*getc)(unsigned char *, unsigned int), void (*delay)(unsigned int), unsigned char *buf, unsigned int size) { ymodem_state_t state = YMODEM_STATE_HANDSHAKE; unsigned char header = 0; unsigned char packet[PKT_1024]; int pkt_seq = 0; int rc = 0; int retry = 0; unsigned int total = 0; unsigned int expected_seq = 0; unsigned long filesize = 0; int have_block0 = 0; (void)delay; if (putc == 0 || getc == 0 || buf == 0 || size == 0) { return -1; } while (state != YMODEM_STATE_DONE && state != YMODEM_STATE_ABORT && state != YMODEM_STATE_ERROR) { switch (state) { case YMODEM_STATE_HANDSHAKE: { int started = 0; int i; for (i = 0; i < 60; i++) { putc('C'); if (getc(&header, 1000)) { if (header == SOH || header == STX) { started = 1; state = YMODEM_STATE_READ_PACKET; break; } if (header == CAN) { state = YMODEM_STATE_ABORT; break; } if (header == EOT) { putc(ACK); state = YMODEM_STATE_DONE; break; } } } if (!started && state == YMODEM_STATE_HANDSHAKE) { state = YMODEM_STATE_ERROR; } break; } case YMODEM_STATE_WAIT_HEADER: { if (!getc(&header, 3000)) { retry++; if (retry > 10) { state = YMODEM_STATE_ERROR; } else { putc(NAK); } break; } retry = 0; if (header == SOH || header == STX) { state = YMODEM_STATE_READ_PACKET; } else if (header == EOT) { /* * 发送端第一次发 EOT。 * 按常见 YMODEM 流程回复 NAK, * 等待发送端再次发送 EOT。 */ putc(NAK); state = YMODEM_STATE_WAIT_EOT; } else if (header == CAN) { state = YMODEM_STATE_ABORT; } else { putc(NAK); } break; } case YMODEM_STATE_READ_PACKET: { rc = read_body(getc, header, packet, &pkt_seq); if (rc < 0) { retry++; if (retry > 10) { state = YMODEM_STATE_ERROR; } else { putc(NAK); state = YMODEM_STATE_WAIT_HEADER; } } else { retry = 0; state = YMODEM_STATE_PROCESS_PACKET; } break; } case YMODEM_STATE_PROCESS_PACKET: { /* * 第0包:文件信息包 * * 只有在传输刚开始时,序号0才表示块0。 * 当 expected_seq 达到256以后, * 序号0是正常的数据包,不能误认为块0。 */ if (pkt_seq == 0 && expected_seq == 0) { parse_block0(packet, rc, &filesize); if (filesize > 0) { have_block0 = 1; if (filesize > size) { putc(CAN); putc(CAN); state = YMODEM_STATE_ERROR; break; } } expected_seq = 1; putc(ACK); putc('C'); state = YMODEM_STATE_WAIT_HEADER; break; } /* * 如果发送端没有发送块0, * 则允许第一个数据包直接从序号1开始。 */ if (expected_seq == 0 && pkt_seq == 1) { expected_seq = 1; } /* * 新数据包。 */ if (pkt_seq == (int)(expected_seq & 0xFF)) { unsigned int copy_len = (unsigned int)rc; /* * 已知真实文件长度时, * 最后一个包只保存有效数据, * 不保存0x1A填充。 */ if (have_block0) { if ((unsigned long)total >= filesize) { copy_len = 0; } else if ((unsigned long)copy_len > filesize - total) { copy_len = (unsigned int) (filesize - total); } } else { /* * 没有块0时不知道真实文件长度, * 不能允许写出缓冲区。 */ if (copy_len > size - total) { putc(CAN); putc(CAN); state = YMODEM_STATE_ERROR; break; } } if (copy_len > 0) { memcpy(&buf[total], packet, copy_len); total += copy_len; } expected_seq++; putc(ACK); state = YMODEM_STATE_WAIT_HEADER; break; } /* * 块0重复发送。 * * 可能是接收端之前发送的 ACK 或 C 丢失。 */ if (pkt_seq == 0 && expected_seq == 1 && total == 0) { putc(ACK); putc('C'); state = YMODEM_STATE_WAIT_HEADER; break; } /* * 重复数据包。 * * 只回复ACK,不能再次保存。 */ if (pkt_seq == (int)((expected_seq - 1) & 0xFF)) { putc(ACK); state = YMODEM_STATE_WAIT_HEADER; break; } /* * 其他情况视为乱序包。 */ putc(NAK); state = YMODEM_STATE_WAIT_HEADER; break; } case YMODEM_STATE_WAIT_EOT: { /* * 等待发送端的第二个 EOT。 */ if (getc(&header, 2000) && header == EOT) { putc(ACK); if (!have_block0) { while (total > 0 && buf[total - 1] == 0x1A) { total--; } } state = YMODEM_STATE_DONE; } else { retry++; if (retry > 10) { state = YMODEM_STATE_ERROR; } else { state = YMODEM_STATE_WAIT_HEADER; } } break; } default: state = YMODEM_STATE_ERROR; break; } } if (state == YMODEM_STATE_DONE) { return (int)total; } if (state == YMODEM_STATE_ABORT) { return 0; } return -1; }发送逻辑函数
int ymodem_send(void (*putc)(unsigned char), int (*getc)(unsigned char *, unsigned int), void (*delay)(unsigned int), ymodem_read_fn read_fn, void *read_arg, unsigned int datalen, const char *filename) { unsigned char pkt[PKT_1024 + 5]; unsigned char c; unsigned int seq, offset; int i, retry; /* ---- 1. 等接收方发 'C' ---- */ for (retry = 0; retry < 60; retry++) { if (getc(&c, 1000)) { if (c == 'C') break; if (c == CAN) return 0; } } if (retry >= 60) return -1; /* ---- 2. 发块0:文件名 + 文件大小(SOH 128字节包) ---- */ memset(pkt, 0, PKT_128 + 5); pkt[0] = SOH; pkt[1] = 0x00; pkt[2] = 0xFF; { int n = 0; const char *s = filename; while (*s && n < 60) pkt[3 + n++] = (unsigned char)*s++; pkt[3 + n++] = 0; /* 十进制文件大小 */ { char tmp[12]; int t = 0; unsigned int v = datalen; if (v == 0) tmp[t++] = '0'; while (v && t < 11) { tmp[t++] = '0' + (v % 10); v /= 10; } while (t) pkt[3 + n++] = (unsigned char)tmp[--t]; pkt[3 + n++] = 0; } { unsigned short crc = crc16_ccitt(&pkt[3], PKT_128); pkt[3 + PKT_128] = (unsigned char)(crc >> 8); pkt[3 + PKT_128 + 1] = (unsigned char)crc; } } for (retry = 0; retry < 10; retry++) { for (i = 0; i < PKT_128 + 5; i++) putc(pkt[i]); if (getc(&c, 3000)) { if (c == ACK) break; if (c == CAN) return 0; } } if (retry >= 10) return -1; /* ---- 3. 等 'C'(接收方请求数据) ---- */ for (retry = 0; retry < 10; retry++) { if (getc(&c, 3000)) { if (c == 'C') break; if (c == CAN) return 0; } } if (retry >= 10) return -1; /* ---- 4. 发数据包(STX 1024字节) ---- */ seq = 1; offset = 0; while (offset < datalen) { unsigned int chunk = datalen - offset; if (chunk > PKT_1024) chunk = PKT_1024; pkt[0] = STX; pkt[1] = (unsigned char)(seq & 0xFF); pkt[2] = (unsigned char)(~seq & 0xFF); /* 流式读取:只从数据源取本包需要的 chunk 字节 */ if (read_fn(read_arg, offset, &pkt[3], chunk) != (int)chunk) { putc(CAN); putc(CAN); /* 读数据失败,通知对方取消 */ return -1; } for (i = chunk; i < PKT_1024; i++) pkt[3 + i] = 0x1A; /* 0x1A 填充 */ { unsigned short crc = crc16_ccitt(&pkt[3], PKT_1024); pkt[3 + PKT_1024] = (unsigned char)(crc >> 8); pkt[3 + PKT_1024 + 1] = (unsigned char)crc; } for (retry = 0; retry < 10; retry++) { for (i = 0; i < PKT_1024 + 5; i++) putc(pkt[i]); if (getc(&c, 3000)) { if (c == ACK) break; if (c == CAN) return 0; } } if (retry >= 10) return -1; offset += chunk; seq++; } /* ---- 5. EOT 双握手 ---- */ for (retry = 0; retry < 10; retry++) { putc(EOT); if (getc(&c, 3000)) { if (c == NAK || c == ACK) break; /* 第一个EOT,标准是NAK,有的实现直接ACK */ if (c == CAN) return 0; } } putc(EOT); for (retry = 0; retry < 10; retry++) { if (getc(&c, 3000)) { if (c == ACK) break; if (c == CAN) return 0; } } return (int)datalen; }5. 实验结果
传输成功