[SECS/GEM研究] (五) SECS-II 是什么
SECS/GEM 协议栈开发笔记 · 第 5 篇
基于代码库:Libsecs(一个 C++ 实现的 SEMI 协议栈)
说在前面:由于工作的原因,本人早些年开发了一套SEMI标准的协议栈用于公司的SECS标准实现,为了防止经验遗忘,特写下本专辑以免后续忘记。
前面几篇聊了SECS-I/HSMS,分别来自标准SEMI-E4和SEMI-E37,这俩标准其实都是规定如何把消息可靠的送给通讯的对方。那么消息是什么?之前一直用"SxFy"指代真正的业务消息,却没解释"SxFy"到底是啥、括号里那串东西又是什么结构。这一篇就来讨论下消息内部,看看 SECS-II,SECS-II其实就是SECS协议的消息体,标准来自SEMI-E5。
SECS-I / HSMS管的数据是"怎么运",SECS-II 管的是"货长啥样"。我个人觉得这一层才是整个协议栈里最该花篇幅的——因为设备跟工厂所有"对话内容",最终都落在这套数据类型上。
一、SECS-II 是"内容层"
一句话定位:SECS-I 和 HSMS 把消息当不透明的字节流搬来搬去,SECS-II 则定义了这些字节流的语法——什么类型、几个元素、怎么嵌套。
它不规定"你该发什么业务消息"(那是以后的 GEM 层干的),它只规定"数据的最小单元长什么样"。这个最小单元在 SECS-II 里叫Data Item(数据项)。这是SECS协议中的最小单元。
二、12 种数据类型(Item)
SECS-II 把数据归成十来种基础类型,一般来说其中常用的有 12 种。我把它列成一张表,format byte 那列就是它在线上真正的"身份证号"(E5 标准定的):
| 类型 | 含义 | format byte | 例子 |
|---|---|---|---|
| L | List(容器,可嵌套) | 0x00 | <L ...> |
| B | Binary(字节串) | 0x20 | <B 0x01 0x02> |
| BOOLEAN | 布尔 | 0x24 | <BOOLEAN 1> |
| A | ASCII(字符串) | 0x40 | <A "OK"> |
| I1/I2/I4/I8 | 有符号整数(1/2/4/8 字节) | 0x64/0x68/0x70/0x60 | <I4 -100> |
| U1/U2/U4/U8 | 无符号整数(1/2/4/8 字节) | 0xA4/0xA8/0xB0/0xA0 | <U2 500> |
| F4/F8 | 浮点(4/8 字节) | 0x90/0x80 | <F8 3.14159> |
(标准里其实呢还有个 JIS8,那是一个日文旧字符集,现在基本没人用了,我在我的协议栈中也没去实现,有兴趣的可以去阅读SEMI-E5标准文献)
细心的人这里能看出规律:I 系列 format 高字节是 0x6x,U 是 0xAx,F 是 0x8x/0x9x,A/B/Boolean 落在 0x0x~0x4x 档。我想这不是巧合——E5 把"类型 + 每个元素占几字节"编码进了一个字节。具体哪几位代表啥,我建议直接翻 E5 原文档,至于你要说为什么这样定义,那我也不知道,这是之前的半导体专家标准组织决定的。
很多人第一次看 S1F1 / S6F11 这种编号,以为是哪家设备厂的内部代号,其实完全是 SEMI 标准拍板的,跟厂家无关。
三、一个 Item 具体是怎么个结构?
光看类型名不够,得知道它在网线上到底几个字节。一个 Item 的线格式就三段:
[ format byte ] [ length 字段 ] [ 数据体 ]重点在 length 字段——它是可变长的,这是 SECS-II 一个很聪明的设计:
- format byte 的低 2 位表示"长度字段本身占几个字节"(1、2 或 3 字节);
- 长度 ≤ 255 用 1 字节长度就够了;
- 超过 255,自动升到 2 字节(最多 65535);
- 再大就 3 字节。
长度这里其实比较搞混,我在我的协议栈里面写L::toByteArray()时会先算长度,>0xff 就把长度字节数加 1,>0xffff 再加 1,最后把FORMAT_CODE | 长度字节数写进 format byte,再按字节数吐出长度,最后递归写子项。如下(伪代码思路):
intnoOfLengthBytes=1;if(len>0xffff)noOfLengthBytes++;// 升到 3 字节if(len>0xff)noOfLengthBytes++;// 升到 2 字节//1. 写 format byte//2. 写 length 字段(1/2/3 字节,按 len 大小)//3. 递归写子项字节这种设计带来很大的好处:一条消息无论多长,接收端看一眼 format byte 的低 2 位,就知道后面要读几个长度字节、数据有多长,完全自描述,不用提前约定上限。
四、List 嵌套:一条消息就是一棵树
12 种类型里,只有L(List)是容器,它可以装任意 Item,包括别的 L。这就让 SECS-II 能表达任意复杂的层级结构——按照数据结构来讲,一条消息本质是一棵Item 树。
我在代码里面实现L这个类,值类型就是std::vector<ItemPtr>(Item 的智能指针数组),所以L能add()任意子项,子项还能是另一个L。所以可以无限制的嵌套。最终行成一颗可以有任意节点的Item树,这颗树还可以用16进制完整描述。
举个真实结构的例子,一台设备上报"当前状态",可能是这样一棵树:
L (根) ├── A "RUNNING" ← 状态字符串 ├── U1 1 ← 某开关量 ├── F4 98.6 ← 温度 └── L (子列表) ├── U2 500 ← 产量 └── U2 3 ← 报警数这就是 SECS-II 表达复杂数据的全部秘密:用 L 把基础类型一层层包起来。没有 JSON 那种花括号,全靠 List 的层层嵌套。一开始不习惯,看多了 SML(第六节)就顺了。
五、SxFy 编号到底怎么读
回到开头那个"SxFy"。S = Stream(流),F = Function(功能)。它俩合起来唯一标识"这是条什么消息"。
Stream(S)—— 主题分类。SEMI 给每个数字段分配了大致主题,常用的:
| Stream | 主题 |
|---|---|
| S1 | 设备状态、在线识别、通信建立(S1F1/S1F2、S1F13/S1F14) |
| S2 | 远程命令、时间、设备信息 |
| S5 | 报警(Alarm) |
| S6 | 事件与报告(Collection Event / Report) |
| S7 | 配方(Process Recipe) |
| S9 | 通信异常 / 错误消息 |
| S10 | 终端服务(Terminal) |
| S11/S12 | 设备控制(Control) |
Function(F)—— 具体操作,而它的奇偶性有个标准规定的规矩,这个标准是强制性的,必须遵循:
- 奇数 Function = primary(主动发起 / 询问)
- 偶数 Function = reply(应答)
比如 S1F1 是设备问"你在吗"(primary),host 回 S1F2 “在,我是 host”(reply);S5F1 设备报"报警来了",host 回 S5F2 “收到”。
这个的好处也显而易见,看编号就能猜出谁问谁答。再回想第三篇讲的W-bit(消息头 stream 字节最高位):置 1 表示"我这条要你回",正好跟"奇数=询问"呼应——primary 消息一般带 W-bit,reply 不带。
六、SML:让人能读的文本表示
字节流人眼看不懂,调试会疯。SECS-II 配套了一个文本表示法叫SML(SECS Message Language),把那棵树写成带尖括号的嵌套文本。上节那棵树的 SML 长这样:
<L[4] <A "RUNNING"> <U1 1> <F4 98.6> <L[2] <U2 500> <U2 3> > ><L[4]表示这是个含 4 个子项的 List,<A "RUNNING">是 ASCII 串,<U1 1>是无符号 1 字节整数。一眼就能看出结构,排错时比抓包里的十六进制友好多了。
七、小结
SECS-II(E5)这一层记住这几句:
- 它是"内容层",定义数据的最小单元 Item 和消息结构,不关心怎么运;
- 12 种基础类型(L/A/B/Boolean/I1-8/U1-8/F4/F8),每种一个 format byte 身份证;
- Item 线格式 = format byte(低 2 位标长度字段几字节)+ 可变长长度 + 数据,完全自描述;
- 靠 L 嵌套表达任意复杂结构,一条消息就是一棵 Item 树;
- SxFy 里 S 是主题、F 是操作,F 奇数是询问、偶数是应答,跟 W-bit 呼应;
- SML 是消息的文本表示,libsecs 支持 Item 树与 SML 双向转换。
下一篇聊 SML 更细的用法,以及"多块(multi-block)拆分"那点事——前面说过 HSMS 一条消息多大都能一次发完,但 SECS-II 自己还有一套大消息拆成多块传输的机制,跟 SECS-I 那种 254 字节硬砍不是一回事,下篇捋清楚。
本人不是 SEMI 专家,上面这些也是当年啃 E5 文档一点点试出来的,哪里理解有偏差,评论区直接拍我。