目录
1.协议介绍
Xmodem 协议最早是以 128 字节块的形式传输数据,并且每个块都使用校验和进行错误检测。后面衍生出使用循环冗余校验方式 (CRC16) 和支持 1024 字节块的传输协议 (Xmodem-1k)。
YMODEM 以三种方式扩展了 XMODEM。与 XMODEM-CRC 一样,YMODEM 用16 位循环冗余校验(CRC)替换了 8 位校验和,但使其成为默认的纠正形式而不是可选形式。从 TeLink 中,它添加了发送文件名和大小的“块 0”标头,这允许批量传输(单个会话中的多个文件)并消除了在文件末尾添加填充的需要。最后,YMODEM 允许将数据块大小从原始的 128 字节增加到 1024,如XMODEM-1k,这大大提高了更快调制解调器的吞吐量。
详细文档请查看:http://pauillac.inria.fr/~doligez/zmodem/ymodem.txt
2.协议解析
Xmodem 和 Ymodem 从控制符定义和帧包格式上是基本一致的。
2.1 控制字符定义
定义 | 取值 | 作用 |
---|---|---|
SOH | 0X01 | modem 128 字节头标志 |
STX | 0X02 | modem 1024字节头标志 |
EOT | 0X04 | 发送结束标志 |
ACK | 0X06 | 应答标志 |
NAK | 0X15 | 非应答标志 |
CAN | 0X18 | 取消发送标志 |
CRC16 | 0X43 | 使用CRC16校验标志 |
2.2 帧包格式
Byte1 | Byte2 | Byte3 | Byte4-Byte131 | Byte132-Byte133 |
---|---|---|---|---|
头标志 | 包序列 | ~包序列 | 包数据 | CRC16(2 Byte) |
说明: - 该帧是 Xmodem 使用 CRC16 校验方式,如果使用 Xmodem-1k 或者 Ymodem,帧格式 Byte 4 - Byte 131 (128 字节) 需要增大为 Byte 4 - Byte 1027 (1024字节)。 - Xmodem 如果使用校验和,帧格式 Byte 132 - Byte 133 只需要占用一个字节。 - Byte 3 是 Byte 2 按位取反,Byte 2 取值范围 0 - 255,超过 255 后从 0 递增。
2.3交互流程
Xmodem 校验和交互流程
Sender | Flow | Receiver |
---|---|---|
< | NAK | |
Timeout after 3 seconds | ||
< | Timeout after 3 seconds | |
SOH 0X01 0XFE DATA[0-127] Checksum | > | Packet ok |
< | ACK | |
SOH 0X02 0XFD DATA[0-127] Checksum | > | Miss the Packet |
< | NAK | |
SOH 0X02 0XFD DATA[0-127] Checksum | > | Packet ok |
< | ACK | |
EOT | > | Packet ok |
Finshed | < | ACK |
Xmodem CRC16 交互流程
说明: - 相比于 Xmodem 校验和, Xmodem CRC16 是发送控制字符 C,而校验和发送控制字符 NAK,并且 CRC16 校验字段占 2 Byte。 - 如果使用 Xmodem-1k 协议发送 1024 字节的数据,只需要将数据头标志由 SOH 替换为 STX,数据部分占 1024 字节。 - 如果发送的数据不满 128 字节或者 1024 字节,使用 0x1A 填充。
2.4 Ymodem 交互流程
Ymodem 协议的起始帧并不直接传输文件的数据,而是将文件名与文件的大小放在数据帧中传输。它的数据帧结构如下:
Byte1 | Byte2 | Byte3 | Byte4-Byte131 | Byte132-Byte133 |
---|---|---|---|---|
SOH | 0X00 | 0XFF | Filename/ ilesize/ NUL | CRC16(2 Byte) |
说明: - 头标志是 SOH,包序列固定是 0x00。 - Filename 是传输的文件名字,比如 hello_world.bin,它在起始帧中的格式为: 68 65 6c 6c 6f 5f 77 6f 72 6c 64 2e 62 69 6e 00,也就是把 ASCII 码转成十六进制,最后的 0x00 代表文件名结束。 - Filesize 是要传输的文件的大小,比如文件大小为 120 KB,转换为 120 * 1024 = 122880 Byte,转化为十六进制为 0x1E00,它在起始帧中的格式为: 31 45 30 30 00,对应 ASCII 1E00,最后的 0x00 代表文件长度结束。 - 最后 NUL 代表剩余不足 128 Byte 部分用 0x00 填充。
Ymodem 协议的结束帧与起始帧类似,结构如下:
Byte1 | Byte2 | Byte3 | Byte4-Byte131 | Byte132-Byte133 |
---|---|---|---|---|
SOH | 0X00 | 0XFF | NUL | CRC16(2 Byte) |
文件传输流程:
3.超时处理
1> 接收方等待一个信息包的到来所具有的超时时限为 3 秒,每个超时后发送 NAK
2> 当收到包时,接收过程中每个字符的超时间隔为 1 秒
3> 为保持“接收方驱动”,发送方在等待一个启动字节时不应该采用超时处理
4> 一旦传输开始,发送方采用单独的 1 分钟超时时限,给接收方充足的时间做发送
ACK ,NAK ,CAN 之前的必须处理
5> 所有的超时及错误事件至少重试 10 次
参考:https://zhuanlan.zhihu.com/p/349921713
3.状态机介绍
以下内容是我根据个人的理解对裸机思维公众号文章状态机系列文章的一个笔记,请务必以原作者文章内容为准。
状态机是一种思维方式、一种工具,同时它也是一种拥有极高自由度的语言。状态图是“新的源代码”,根据一定规则翻译状态图为C代码的过程就是”新的编译“。
状态机本身是一种编程语言;状态图是描述状态机的最常见方式之一;绘制状态图的图例规范有很多种,比如UML规范等等,本文是结合状态机的常见画法并针对嵌入式软件开发习惯简化后的图例规范,简单、明确、有效,并且可以毫无歧义的严格且无脑的翻译成包括switch状态机在内的多种C语言实现。
3.1 状态图的画图规范
使用状态图来设计状态机,其本意就是利用人类的视觉优于阅读能力的特性来降低设计难度。为了确保这一初衷能够贯彻始终,“逻辑清晰”是状态图设计的核心原则。
- 规范一:功能单一原则:
每个状态的功能要尽可能单一,要避免将多个功能复合在同一个状态上,从而产生所谓的“超级状态”的情况。
比如以下状态图实际上拥有两个功能:1. 通过serial_out()函数输出字符;2. 判断字符串的尾部
更好的设计如下:
-
规范二:状态机的起点和终点
一个状态机可以没有终点,但一定有一个起点,我们称之为 start。
START是状态机的起点、同时也兼任跃迁条件——换句话说:START 不是一个可以保持的状态,它也不能被看作一个特殊的状态;因此,翻译代码的时候,虽然START是0,但在对应的case分支中,一定要自动切换到下一个状态而绝对不能在此停留。 -
规范三:子状态机
如图所示:
-
子状态机是被圆角矩形包裹的
-
子状态机的右上角有一个自反的状态迁移,条件是“on going”意味子状态机正在执行,还未得出一个结果;
-
子状态机的右下角(或者别的什么位置)需要有一个标记有cpl条件的状态迁移,表示当子状态机内部达到了终点cpl以后,子状态机从这里退出并跃迁到指定的状态;
-
子状态机有一个标题栏,里面分别列举了状态机的名称以及传递给当前子状态机的形参列表。(状态机的返回值只能是类似cpl, on-going这样的状态,所以不需要特别标记)
- 规范四:简化公共条件
对于源自同一个状态的动作而产生的多个返回值,我们可以借助“公共条件”和“子条件”的方式加以简化:
比如:
- 规范五:合理使用虚线提升性能
我们知道,非必要的频繁任务切换会浪费大量的处理器时间,从而影响系统的实时性,这里由状态切换导致的频繁CPU出让(yield)实际上并非好事。我们把此类切换从实线箭头修改为虚线箭头——表示此类切换不“主动”出让CPU控制权。例如:
3.2 状态图的翻译规范
-
用圆圈来表示一个状态;
-
圆圈中心我们会写一些注释性质的内容用来帮助人们理解这个状态是做什么的;
-
图中有三个箭头,最左上角单纯“指向”状态的箭头表示从别的什么地方“跃迁”到了当前状态——我们称为“扇入”;下方从当前状态指向别的什么地方的箭头表示从当前状态离开;——我们成为“扇出”;右上角从当前状态“扇出”后又“返回到”当前状态的情况,我们称之为“自返”——也就是返回自己的意思。
针对上面的一个状态示意图,它可以简单的对应到下面的代码结构:
case <状态名称>:
状态具体执行了什么有返回值d的动作;
if (返回值 满足 跃迁条件1) {
s_tState = XXXXX; //!< 执行状态跃迁
执行对应的跃迁动作
} else if (返回值 满足 跃迁条件2) {
s_tState = XXXXX; //!< 执行状态跃迁
执行对应的跃迁动作
}
break;
状态图的所表达的逻辑是唯一的,但翻译它的方法从来都不是唯一的,一般来说,我们既可以用上面的公式无脑翻译代码,也可以进行必要的等效改编。比如:状态图可以根据需要翻译成阻塞的,或者非阻塞的代码
翻译成下面的C语言代码,就是阻塞的
#include <stdbool.h>
#include <stdint.h>
void print_hello(void)
{
//! 对应 start部分
uint8_t *s_pchSrc = "Hello";
do {
//! 对应 Print Hello 状态
while(!serial_out(*s_pchSrc));
//! serial_out返回值为true的状态迁移
s_pchSrc++;
//! 对应 "Is End of String"状态
if (*s_pchSrc == '\0') {
//! true分支,结束状态机
return ;
}
//! false分支,跳转到 "Print Hello" 状态
} while(true);
}
翻译成下面的C语言代码,就是非阻塞的
#include <stdbool.h>
#include <stdint.h>
typedef enum {
fsm_rt_err = -1,
fsm_rt_on_going = 0,
fsm_rt_cpl = 1,
} fsm_rt_t;
#define PRINT_HELLO_RESET_FSM() \
do {s_tState = START;} while(0)
fsm_rt_t print_hello(void)
{
static enum {
START = 0,
PRINT_HELLO,
IS_END_OF_STRING,
} s_tState = {START};
static const uint8_t *s_pchSrc = NULL;
switch (s_tState) {
case START:
//! 这个赋值写法只在嵌入式环境下“可能”是安全的
s_pchSrc = "Hello world";
s_tState++;
//break;
case PRINT_HELLO:
if (!serial_out(*s_pchSrc)) {
break;
};
s_tState = IS_END_OF_STRING;
s_pchSrc++;
//break;
case IS_END_OF_STRING:
if (*s_pchSrc == '\0') {
PRINT_HELLO_RESET_FSM();
return fsm_rt_cpl;
}
s_tState = PRINT_HELLO;
break;
}
return fsm_rt_on_going;
}
3.3 总结状态机设计步骤
-
按照状态功能单一原则,以逻辑清晰为基本目标,再完全不考虑优化的情况下,完成状态机的设计和调试;
-
在完成了状态机逻辑正确性验证的前提下,在必要的情况下,可以对状态图进行性能优化;
-
如果经过上述步骤,性能仍然达不到要求,可以对翻译后的代码进行进一步的等效优化。
4.使用状态机实现XMODEM和YMODEM协议
使用状态机思维编程的方式是对任务进行化整为零,逐个攻破。也就是说将某一复杂问题分解为若干部分,通过分析处理各个部分而最终解决整个问题的思维方法。
4.1 任务拆分
观察XMODEM和YMODEM的传输协议可以发现,它们都有握手,传输数据和传输完成三个阶段,所以就以这三个阶段分别建立状态图,然后组装起来,就是一个完整的协议,比如下图: