SECS/GEM 协议栈开发笔记 · 第 4 篇
基于代码库:Libsecs(一个 C++ 实现的 SEMI 协议栈)
说在前面:由于工作的原因,本人早些年开发了一套SEMI标准的协议栈用于公司的SECS标准实现,为了防止经验遗忘,特写下本专辑以免后续忘记。
上篇聊完 SECS-I,结尾说了为什么现在都爱用 HSMS 而不用串口。这一篇来分析和讨论下HSMS(标准编号 E37),它干的事本质上和 SECS-I 一样:把 SECS-II 那套消息从一端搬到另一端。区别在于,它下层网络采用的是 TCP/IP 网线,不是 RS-232了,天然这种方式通讯的速度会快不少。
我当年第一次把设备从串口挪到以太网上的时候,最直观的感受就是:不用再操心分块、不用再管 ENQ/ACK 那套啰嗦握手了。但后面才发现,高兴太早,HSMS 自己也是有一套"链接怎么建、心跳怎么跳、断了怎么办"的规矩,但是这套规矩比串口那套更贴近网络现实。
一、为什么要用 TCP 取代串口
串口那套在老车间里确实皮实,但随着设备变复杂,我猜老外在处理通讯问题时也是发现了很多的弊端,所以才催生了HSMS这个标准:
- 速率天花板低。9600、19200 波特率,碰上要传大量 trace 数据(后面 GEM 篇会讲)基本是便秘哈,这个速度完全扛不住。
- 距离短、拓扑死。RS-232 十几米就到头,想跨厂房、跨楼基本没戏。
- 半双工效率差。上篇说的 ENQ/ACK 轮流说话,本质是牺牲速率换稳定。
而现代化的工厂网络都是以太网,TCP 自带可靠传输(丢包重传、保序、流量控制全包了),带宽管够、距离随便拉。HSMS 的思路也很明确:既然 TCP 已经把"可靠"这件事干完了,那我就不重复造轮子,直接把 SECS-II 消息套个简单外壳扔到 TCP 流上就行。所以 HSMS 在 SEMI 分层里跟 SECS-I 是平级的------都是"传输层",二选一,上面都接 SECS-II。
这里要科普下,很多初次接触SEMI协议的人会误认为SECS-I和SECS-II是同一个东西,后者只是前者的升级版本,其实不然,大错特错了,兄弟😭
二、HSMS 的连接模型:谁拨号、连到哪
HSMS 跑在 TCP 上,先得有连接。这里有个概念叫"连接模式",我在Libsecs 里定义成一个枚举:
cpp
typedef enum{ PASSIVE, ACTIVE } ConnectMode;
- ACTIVE(主动):自己主动去连对方 IP:端口,像拨号。
- PASSIVE(被动):自己开个端口听着,等对方连过来,像接电话。
Libsecs 默认给的是 ACTIVE,也就是设备默认主动去拨 remote_ip:remote_port。
一般而言,现场部署的时候,更常见的搭配是 host(EAP/MES)当 ACTIVE 主动连设备,设备当 PASSIVE 监听。
HSMS 标准没规定非得谁拨号,这纯粹是个可配置项,只是行业习惯这么摆。你把两边角色配反了也能通,只要一端 active、一端 passive 对上就行。
顺带一个概念:session(会话) 。一条 TCP 连接上可以开多个 session,靠消息头里的 session_id 区分。也就是说,HSMS 支持"一根网线、多路逻辑通道",这是串口那种一对一物理链路做不到的。Libsecs 的 HsmsParam 里 device_id、remote_ip、remote_port、local_ip、local_port 就是把"连到哪、我是谁"全收在一个结构体里描述:
cpp
typedef struct {
ConnectMode role;
uint16_t device_id;
int remote_port;
std::string remote_ip;
int local_port;
std::string local_ip;
uint16_t T3, T5, T6, T7, T8;
uint16_t TestTime; // 心跳周期
}HsmsParam;
三、建链握手:TCP 连上 ≠ 能通信
连上 TCP 只是第一步。HSMS 规定连上之后还要走一个 SELECT 过程,设备才算"在线可通信"。状态机就三个:
text
NOT_CONNECTED ------ TCP 连上 ------> NOT_SELECTED
NOT_SELECTED ------ 收到 SELECT_REQ/回 SELECT_RSP ------> SELECTED
SELECTED ------ 收到 DESELECT_REQ ------> NOT_SELECTED
SELECTED ------ 收到 SEPARATE ------> NOT_CONNECTED(直接断开)
简单的来说,HSMS处理上面的状态机逻辑为:对方发来 SELECT_REQ,只要当前是 NOT_SELECTED,就回 SELECT_RSP 带上状态字 0x00(Communication Established),状态切到 SELECTED;如果我已经 SELECTED 了又收到 SELECT_REQ,就回 0x01(Communication Already Active,意思是"咱俩早通了,别重复建")。DESELECT 反过来,把状态退回 NOT_SELECTED。
下面这张图是一条典型的 active 设备通信时序:
text
[设备 ACTIVE] [Host/PASSIVE]
| |
|------ TCP 连接请求 ------->|
|<----- 连接建立 ------------|
|------ SELECT_REQ -------->|
|<----- SELECT_RSP(0x00) ---| 状态: SELECTED
| |
|==== 数据消息(SxFy) =======>| ← 真正干活从这里开始
|<=== 数据消息(回复) ========|
| |
|<----- LINKTEST_REQ -------| 心跳
|------ LINKTEST_RSP ------>|
| |
|<----- SEPARATE -----------| 断链
| (连接关闭) |
注意 SELECT 之前,谁都不能发数据消息,只能发控制消息(SELECT/LINKTEST 这类)。这和 SECS-I 很像:串口上 ENQ 握手完才能发块;HSMS 上 SELECT 完才能发数据。只不过 HSMS 把"握手"从物理线路搬到了逻辑连接上。
四、心跳 Linktest 与断链 SEPARATE
网络跟串口不一样,串口线拔了你能立刻知道,网线"半断不断"(比如对端进程挂了但 TCP 没正常关闭)时,连接看起来还在,其实早死透了。HSMS 用两个机制对付这个:
- Linktest(心跳) :双方定期互相发 LINKTEST_REQ,对方回 LINKTEST_RSP,证明"我还活着"。周期由
TestTime控制,我在Libsecs的代码里就是收到 LINKTEST_REQ 直接回一个 LINKTEST_RSP,纯保活,不带业务数据。 - Separate(断链):哪一方想干净地结束,发个 SEPARATE 就行,对方收到立刻关连接,状态回 NOT_CONNECTED。比直接拔网线优雅,至少能让对端有机会做清理。
五、HSMS 的头,和 SECS-I 的头不一样在哪
这是我觉得最该讲清的一点,因为很多人第一次看会懵:同样是 10 字节消息头,HSMS 和 SECS-I 长不一样。
SECS-I 的块头(10 字节):
text
session_id(2) | stream+W(1) | func(1) | block_h+E(1) | block_l(1) | system(4)
HSMS 的消息头(也是 10 字节):
text
session_id(2) | stream+W(1) | func(1) | PType(1) | SType(1) | system(4)
看出差别没?第 5、6 字节,SECS-I 放的是 block 序号(带 E-bit 末块标志) ,因为串口要分块;HSMS 放的是 PType(协议类型,固定 0x00=SECS-II)+ SType(消息类型) 。
HSMS的SType可以枚举如下:
cpp
typedef enum {
DATA = 0x00, // 数据消息(真正的 SxFy)
SELECT_REQ = 0x01,
SELECT_RSP = 0x02,
DESELECT_REQ= 0x03,
DESELECT_RSP= 0x04,
LINKTEST_REQ= 0x05,
LINKTEST_RSP= 0x06,
REJECT = 0x07,
SEPARATE = 0x09,
}SType;
也就是说,HSMS 靠 SType 这一个字节,就把"这是业务数据还是控制指令"区分开了。这也解释了为什么 HSMS 不需要分块:TCP 是可靠字节流,它直接在消息最前面放 4 字节长度前缀来切分边界,一条消息多大都能一次发完,根本不用像串口那样剁成 254 字节的块。
对照 SECS-I:它没有那 4 字节长度前缀(串口靠 ENQ/ACK 和块边界定界),反而多了 block 序号和 E-bit。一个为"剁块"服务,一个为"定界"服务,分工不同。
六、超时参数:比标准更"急"
HSMS 定义了一组超时:
| 参数 | 含义 | 协议栈默认 |
|---|---|---|
| T3 | 等回复超时(一条消息发出去多久没回算失败) | 10s |
| T5 | 连接超时(建 TCP 连多久没成算失败) | 5s |
| T6 | 控制消息超时(SELECT/LINKTEST 等多久没回应) | 5s |
| T7 | 未选中超时(连上了但一直不 SELECT,多久后踢掉) | 10s |
| T8 | 网络空闲超时(多久没任何消息往来就认为死链) | 10s |
| TestTime | 心跳周期 | 5s |
HSMS 标准上 T3 推荐 45 秒,实际上一般来说 10 秒差不多了。
七、怎么去实现HSMS?
为了去实现HSMS ,我在Libsecs 里是这样组织的:
HsmsEquipment:设备对象,同样继承自通用的SecsEquipment。它在setParam时看 role 是 ACTIVE 还是 PASSIVE,当场 new 出HsmsEquipmentActive或HsmsEquipmentPassive------也就是"主动拨号"和"被动监听"两套实现分文件写,互不纠缠。handleMessage:所有控制消息的调度中枢。收到 SELECT/DESELECT/LINKTEST/SEPARATE,在这里用 switch 分派:该回什么的回什么,该切状态的切状态,该关连接的关连接。数据消息(SType=DATA)则往下交给 SECS-II 层去解析。serializeMessage:把一条SecsMessage编成"4 字节长度 + 10 字节头 + 数据体"的字节流丢进 TCP。HsmsParser:对端来的字节流,按 4 字节长度前缀把一条条消息切出来,再按 SType 还原成数据消息或控制消息。
这一套和上篇 SECS-I 的 Secs1Equipment / Secs1Protocol / Secs1Parser 三件套是对应的------只是底层从"串口 + ENQ/ACK 状态机"换成了"TCP socket + SELECT/LINKTEST 状态机"。上层的 SECS-II 消息体、事务管理完全不用动。这也侧面说明一个好的SEMI协议栈设计一定是:传输层可替换,业务层不动。
八、小结
HSMS(E37)这一层记住这几句:
- 它和 SECS-I 平级,都是传输层,只是脚下踩 TCP/IP 而非串口;
- 谁拨号可配(ACTIVE/PASSIVE),连上 TCP 还得 SELECT 才算 SELECTED 能通信;
- 用 Linktest 当心跳、Separate 干净断链,专治网络"假死";
- 消息头用 SType 区分数据/控制,靠 4 字节长度前缀定界,不分块------这是和 SECS-I 最直观的差异;
下一篇一起来讨论下 SECS-II(E5),也就是消息"里面到底装了什么"。前面这三层(SECS-I / HSMS / 事务)管的是"怎么把字节可靠送过去",从下篇开始才真正碰到数据------那 12 种数据类型(List、ASCII、各种整数浮点)、嵌套结构、还有 SxFy 这套编号到底怎么读。个人觉得 SECS-II 才是整个协议栈里最值得花篇幅的一块,下篇详细分析。
本人不是 SEMI 专家,上面这些也是当年啃 E37 文档一步一步进行理解的,如果哪里理解有偏差,评论区直接拍我。