SECS/GEM 协议栈开发笔记 · 第 3 篇
基于代码库:Libsecs(一个 C++ 实现的 SEMI 协议栈)
说在前面:由于工作的原因,本人早些年开发了一套SEMI标准的协议栈用于公司的SECS标准实现,为了防止经验遗忘,特写下本专辑以免后续忘记。
上两篇把"为什么要有 SECS/GEM"和"SEMI 那一大堆标准怎么分层"梳理了下。
往下沉一层,来看下SECS-I这个东西是怎么玩的:SECS-I(标准编号 E4)。
本人刚接触半导体设备通信时,第一个被派去调的就是串口。当时心里还犯嘀咕:都 21 世纪了,怎么还在用 RS-232 这种老古董?后来才明白,车间环境里串口皮实、便宜、干扰小,而且很多老设备一用就是十几年,标准定下来就得认。所以这层虽然老,但绕不开,而且它里头那个"半双工 + 分块 + 重传"的设计,思路非常漂亮,值得单独唠一篇。
下面先讲标准在折腾什么,再对照看看 libsecs讲下代码的实现思路。
一、SECS-I 到底在管什么
一句话:SECS-I 管的是"两根线(RX/TX)上,字节怎么排着队飞过去,丢了怎么要回来"。
它不关心你发的是 S1F1 还是 S6F11,也不关心数据类型是 List 还是 ASCII。在它眼里,一切都只是一串要分段发出去的字节。它解决三件事:
- 一条长消息太大,怎么切成小块(block)发出去;
- 串口是半双工的(同一时刻只能一方说、一方听),怎么避免两个人同时抢着说话;
- 某一块在路上丢了或者坏了,怎么发现并重传。
这三条,就是 E4 标准的全部重心。你把它想成"串口上的可靠传输协议"就行,跟 TCP 在以太网上的角色有点像,只是手段土得多。
二、块(block)长什么样
SECS-I 规定:消息在线路上被切成若干个块,单个块最长 254 字节。这就是它的"MTU"。
我在libsecs这个协议栈里把这个值直接写死在类里了:
cpp
static const int MAX_MESSAGE_LEN = 254; // Secs1Message
那 254 字节里都装了什么?拿一条真正的 SECS-II 消息来说,它本来由"10 字节消息头 + 数据体"组成。在 SECS-I 上,这 10 字节头被原封不动地带在每一个块里,剩下的空间装数据体的一个片段。
libsecs 把那个 10 字节头定义成一个紧凑结构体,如下 所示:
cpp
struct Secs1Header {
uint16_t session_id; // 设备/会话 ID,最高位 R-bit
uint8_t stream_byte; // Stream,最高位 W-bit(要不要对方回)
uint8_t func_byte; // Function
uint8_t block_h; // Block 序号高字节,最高位 E-bit(是否末块)
uint8_t block_l; // Block 序号低字节
uint32_t system; // 事务号(System Bytes),4 字节
};
几个 bit 的含义得记一下,后面有用:
- W-bit(stream 字节最高位):置 1 表示"我这条需要你回一条"。SECS 里很多消息是"问---答"结构,靠这个位区分。
- R-bit(session_id 最高位):传输的方向标志。
- E-bit(block 序号最高位):End of Block,置 1 表示"我是最后一块"。接收端看到它,就知道这一堆块拼完了。
这块头里有个很关键的设计:每块都带着完整的 10 字节消息头,包括那个 4 字节的 system(事务号)。也就是说,不管切成多少块,每一块都认得出自己属于哪条消息、是该消息的第几块。接收端即便块乱序到达(串口上一般不乱,但设计上每块自描述),也能正确归位。这是 SECS-I 分块比很多老协议聪明的地方。
下面这张图就是一条 3 块消息在线路上的样子:
text
10字节头 数据片段
块1: [ session | S/F | blk#1 | system ] [ ....数据.... ] (E-bit=0)
块2: [ session | S/F | blk#2 | system ] [ ....数据.... ] (E-bit=0)
块3: [ session | S/F | blk#3+EOB | system ] [ ..剩余数据.. ] (E-bit=1, 末块)
每块头里的 block 序号从 1 往上走,末块把 E-bit 拉高。接收端从块 1 收起,攒到看见 E-bit 为止,拼成原消息交给上层。
三、半双工:为什么不能直接狂发
以太网是全双工的,你发你的我发我的,互不耽误。串口(RS-232)虽然也是全双工的,但是如果要描述事务的话,通常也是做半双工使用------同一根线上,要么你在说,要么我在说,不能同时,这其实是一种降低速率但是提高通讯稳定性的方法。
这就带来一个麻烦:设备想发消息前,得先确认线路空闲、对方愿意听,否则两边同时开口就成了一团噪音。
SECS-I 用一套很轻量的握手解决这个问题,核心就两个字符:
- ENQ(ASCII 0x05):"我想用线路,申请一下。"
- ACK(ASCII 0x06):"准了,你说吧。"
流程大概是:发送方先发 ENQ,等对方回 ACK,然后才开始把块一个一个发过去;每发一块,接收方校验没问题就回 ACK,发送方再发下一块。全部发完,末块的 ACK 也就意味着这次传输结束。
libsecs 里这套握手机制我其实是做成状态机的,我把握手的时序分成了不通的状态:
text
S_SEND_ENQ -> S_WAIT_ACK -> ...(发块、等 ACK)...
也就是"先发 ENQ、再等 ACK、然后进发块循环"。至于收方,则是"收到 ENQ 回 ACK、收到块校验 OK 再回 ACK"。哪一方是主动申请线路的那一方,由角色决定:libsecs 里 EQ(设备)在 SECS-I 下默认扮演 MASTER,也就是由设备主动发起 ENQ 去抢线路(注意:HSMS 里是 host 主动建 TCP 连接,方向正好反过来,这点后面会点出来)。
这套握手看着啰嗦,但好处是稳:任何一步没收到应答,发送方就知道"线路出问题了或者对方忙",可以重来,不会 blindly 把数据往死里灌。
四、超时与重传:丢了怎么办
串口环境脏,块在路上丢了、被干扰坏掉、或者 ACK 没回来,都是常事。SECS-I 靠"超时 + 重传"兜底。
标准里定义了几个超时参数,libsecs 在 Secs1Param 里把它们都收在一个结构体里:
cpp
typedef struct {
uint16_t T1, T2, T3, T4;
uint16_t rty; // retry 次数上限
enum Role { EQ, HOST } role;
} Secs1Param;
这里有几个超时时间的参数:
- T1(块间超时):发完一块、到收到下一块允许的最大间隔。默认 500ms。意思就是"一块接一块,中间卡太久就当你丢了"。
- T2(等应答超时) :发完一块、等多久没收到 ACK 就认为失败。libsecs 协议层默认 10 秒,而设备类
Secs1Equipment在初始化时把它压成了 2000ms(更快失败、更快重传,适合车间节奏)。
还有 rty(重传次数),libsecs 默认设成 3。重传逻辑很朴素:
发一块 → 等 ACK → 超时没收到 / 收到 NAK(校验不过)→ 带着 R-bit 把这块重发一次 → 还不行就再发 → 直到 rty 次用尽 → 判定这次通信失败,上报异常。
T3、T4 这俩在 SECS-I 语境里含义和 HSMS 略有区别,篇幅所限先不展开,你只要记住:T1/T2 管"块级"快慢,rty 管"最多试几次",这一套组合起来就是 SECS-I 的可靠性来源。
五、libsecs 里大概是怎么搭的
讲一下我在Libsecs里面的实现, SECS-I 这一层在 libsecs 里主要由三个角色配合完成):
Secs1Equipment:设备对象,继承自通用的SecsEquipment。它干两件事:把上层的一条SecsMessage序列化成字节流(serializeMessage),以及把收到的字节流喂给解析器。它内部持有Secs1Protocol和SecsParser。Secs1Protocol:真正的传输引擎,管串口、管 ENQ/ACK 握手状态机、管 T1/T2 超时和重传。它对外暴露的接口很干净:addMessage()把要发的块丢进发送队列,setMessageHandler()注册一个回调,收到完整消息时自动调你。Secs1Parser:解析器,把串口上零碎到达的字节一点点攒起来,按块头里的 E-bit 判断一条消息收齐没有,收齐了就组装回Message交上去。
分块这件事具体怎么做,看 serializeMessage 的思路就很清楚:
text
id = 1
循环:
取本块能装的数据片段(最多 MAX_BLOCK_ITEM_SIZE 字节)
如果是最后一块(剩余 <= 上限):
block_id 最高位 |= 0x8000 // 打上 EOB 标志
拼 10 字节头 + 这段数据
推进偏移量
直到整条消息发完
接收端反着来:parser 每收一块,检查 block_h & 0x80 是不是 1(EOB),不是就暂存,是就拼齐交付。就是这么直接。
这里有个实现上的小聪明值得说:发送端是按"每块固定大小"机械切的,但每块都带完整 10 字节头(含 system 事务号和 block 序号),所以接收端根本不需要知道对方切了多少块、怎么切的,纯靠每块自带的元数据就能还原。这种"每块自描述"的设计,让收发两端解耦得很干净。
六、小结
SECS-I(E4)这一层差不多就是这样。记住三句话够了:
- 它管串口上的可靠传输:分块、半双工握手、超时重传;
- 块是 254 字节上限,每块带 10 字节头,靠 E-bit 标末块、W-bit 标要不要回、R-bit 标代表方向;
- 可靠性来自 T1/T2 超时 + rty 次重传,而不是"发出去就不管了"。
下一篇聊 HSMS(E37)。你会发现,现代设备基本不用串口了,改跑 TCP/IP,而 HSMS 就是"把 SECS-II 这套消息原样搬到网线上"的标准。它不用 ENQ/ACK 那套握手了(TCP 自己保证可靠),而是用 Select/Deselect 建链、用 Linktest 当心跳。为什么现在都爱用 HSMS 而不用 SECS-I?个人认为和历史原因有关,这个后面再说。
本人不是 SEMI 专家,上面这些也是当年啃 E4 文档一点点尝试出来的,哪里理解有偏差,评论区直接拍我。