简介:在半导体制造尤其是晶圆、封装测试等环节中,MES系统与设备之间常需按SECS II标准通信,数据报文格式是否符合客户规范往往成为联调难点。这套调试工具正面向此类场景,可模拟服务端或客户端,帮助程序开发、测试人员验证自行编制的报文是否满足客户要求,尤其适合上位机软件工程师与设备集成工程师使用。压缩包共4个文件,包含可执行程序、两个XML配置/样本文档以及txt使用说明,整体约90KB,轻量便携,可快速部署到联调环境。目前已有2848人学习下载。工具自带默认配置与程序样板文件,无需从零搭建即可上手;配套说明详细讲解使用方式,便于理解服务端/客户端的收发流程和参数配置,能显著缩短SECS II通信联调前的自测时间,对需要快速验证协议实现或排查通信异常的工程师非常实用。
1. SECS-II HSMS 调试工具:在设备和主机之间扮演“双方”的模拟器
做半导体设备上位机或者 AE 的人,大概率都经历过这种场景:设备端程序写好了,但现场的设备还没到;或者主机端(Host)联调时,对方工厂的设备在千里之外,你手上只有一个 IP 和端口号。这时候你最需要的不是文档,而是一个能在电脑上假装自己是设备、又能假装自己是主机的 HSMS 模拟器。所谓 SECS-II HSMS 调试工具,就是把 SECS/GEM 通信里的 Host 和 Equipment 两端都装进一个软件里,让你在没有真实硬件的情况下,把 S1F13、S6F11、S2F13 这些消息提前调通。
这套工具的核心价值有两个:一是它把 SECS-I 时代依赖串口的调试方式换成 TCP/IP,你只要把 IP、端口、设备 ID 填对,就能在局域网里模拟完整通信链路;二是它自带样板文件,S1F13 建连、S5F1 报警、S6F11 事件上报这些高频消息不用从零拼字节,直接改参数就能发。适合三类人:刚接触 SECS/GEM 的嵌入式工程师、需要写主机端通信模块的软件开发者、以及做设备验收时被对方主机折磨的现场调试人员。下面我按自己的使用习惯,从协议原理讲到实际踩坑,把这份资源拆开讲清楚。
2. 动手前先把协议对齐:HSMS 状态机与 SECS-II 消息编码
2.1 HSMS 在哪一层:从 SECS-I 到 TCP/IP 的迁移逻辑
SECS/GEM 协议族里,SECS-I 是走 RS-232 串口的物理传输层,波特率 9600 是常态,半双工、一收一发,调试时还要拿串口线两头接。而 HSMS(High-Speed SECS Message Services)是它的 TCP/IP 替代者,SEMI E37 标准里定义了它在 OSI 模型里的位置:传输层用 TCP,报文层用固定的 4 字节长度前缀加 10 字节消息头,再往上才是 SECS-II 的消息体。
理解这一点很关键,因为很多新手把 HSMS 和 SECS-II 当成同一个东西,实际它们是两回事。HSMS 只负责“怎么把一段字节可靠地送给对方”,SECS-II 负责“这段字节是什么意思”。模拟器的价值就在于,它同时实现了这两层:底层帮你管 socket、管 Select 握手、管 Linktest,上层帮你编码和解码 SECS-II 的 Data Item。你在界面上看到的“S6F11 已发送”,背后是它在 TCP 连接上先发了 Select.req,等对方回 Select.rsp 进入 Selected 状态,然后把 S6F11 的 body 按 SECS-II 格式码编码,最后加上长度前缀和 10 字节头发出。
我一般会建议初次使用的人先把 HSMS 的状态机画在纸上:一条 TCP 连接建立后,初始状态是 Not Selected,必须先发 Select.req,收到 Select.rsp 后进入 Selected,之后才能传数据消息(SType=0)。如果没进 Selected 就想发业务消息,对方的协议栈会直接断开连接,这是第一个高频翻车点,后面避坑章节会细说。
2.2 10 字节消息头与 SECS-II 格式码:看懂报文才能调出问题
HSMS 的数据帧格式很固定,每一帧都是“4 字节消息长度 + 消息本体”,消息本体里前 10 字节是消息头。这个 10 字节头结构一定要背下来,调试时看日志全靠它。我用 Python 的 struct 演示一下标准打包方式:
import struct def build_hsms_header(device_id, stream, function, trans_id, wait=False): # Session ID(2字节):设备ID写在低15位 session_id = device_id & 0x7FFF # Header Byte 1:最高位W位,低7位是Function编号 hbyte1 = (0x80 if wait else 0x00) | (function & 0x7F) # Header Byte 2:Stream编号,低7位有效 hbyte2 = stream & 0x7F # PType=0表示SECS-II,SType=0表示数据消息 header = struct.pack(">HBBBB I", session_id, hbyte1, hbyte2, 0, 0, trans_id) return header def build_frame(body): message = build_hsms_header(1, 6, 11, 0x0001, wait=False) + body return struct.pack(">I", len(message)) + message这里">HBBBB I"对应 2 字节 Session ID、4 个单字节字段、4 字节 Transaction ID,总共 10 字节。注意 PType=0 代表后面跟的是 SECS-II 消息,SType=0 代表这是数据消息而不是 Select.req 之类的控制消息。如果你在日志里看到 SType 不是 0 却当业务消息解析,那报文肯定对不上。
SECS-II 的消息体由 Data Item 组成,每个 Data Item 的第一个字节是格式码。格式码的高两位表示长度字节数,00 表示长度占 1 字节,01 表示占 2 字节;低六位是数据类型。常用的几个:0x01是 LIST、0x05是 BINARY、0x09是 BOOLEAN、0x0D是 U4、0x1D是 I4、0x31是 ASCII。举个例子,S6F11 事件报告的第一层一定是 LIST,里面包一个事件 ID(通常 U4)和一个值列表,编码是这样的:
def encode_list(*items): body = b"".join(items) return bytes([0x01]) + bytes([len(items)]) + body def encode_i4(value): return struct.pack(">Bi", 0x1D, value) def encode_ascii(text): data = text.encode("ascii") return bytes([0x31, len(data)]) + data注意encode_list里的长度字节目前只处理了小于 255 项的情况,如果 LIST 里超过 255 个元素,格式码就要用0x41并且长度写两字节。样板文件里常见的消息基本都在单字节长度范围内,所以这个简化版本够用,但你在自己拼报文的工具里要留个扩展位。
2.3 样板文件里的 SxFy 编号:为什么 S1F13/S6F11 总被当模板
SECS-II 的消息编号是 SxFy,Stream 表示消息大类,Function 表示具体功能。高频出现的就那么几个:S1F13 是建立通信请求(Establish Communications Request),对方回 S1F14 确认;S2F13 是变量数据读取请求(SVS Request),回复 S2F14 带一批数值;S5F1 是报警上报,回复 S5F2;S6F11 是事件报告,回复 S6F12;还有 S9F1 是“无法识别的消息”错误报告。
带样板文件的模拟器,等于把这些高频消息的 body 结构提前搭好了,你不需要去翻 SEMI E5 文档核对每个字节。比如 S1F13 的 body 在实际实现里很多设备是空的或者只带一个版本号列表,但协议栈要求它必须是一个合法 SECS-II 消息,样板文件直接给你填好一个空 LIST。这里有一个容易混淆的点:SxF0 是 Abort,所有 Stream 的 Function=0 都保留给中止消息,如果调试时收到 SxF0,说明对方协议栈认为你上一条消息无法处理。样板文件通常不会放 SxF0,因为它通常是运行时动态生成的,你要能认出它,而不是去模板里找。
3. 把模拟器跑起来:选边、配参数、导样板、发第一条消息
3.1 角色选择:Host 模式与 Equipment 模式分别模拟谁
这套模拟器打开之后第一步不是配 IP,而是选角色。Host 模式模拟工厂的主机系统,主动发起 TCP 连接,连上之后发 Select.req;Equipment 模式模拟设备端,监听一个端口,等主机来连。两个角色的报文格式完全一样,区别只在于谁主动建连、谁被动等待。
我自己的习惯是:开发设备端程序时,模拟器开成 Host 模式,让我的设备程序当服务端;反过来开发主机端程序时,模拟器开成 Equipment 模式。这样做的好处是,你的被测程序始终走真实的“被动接收”或“主动发送”路径,而不是靠模拟器帮你代答。如果你只是验证模拟器本身能不能跑通,那就开一个 Host 一个 Equipment,让它俩自己连。有一点要提醒:同一个模拟器实例通常只能选一个角色,两个角色要开两个实例,一个监听端口,一个作为客户端去连。别在界面上找“同时模拟双方”的开关,这个工具的设计逻辑是每个实例一条连接。
3.2 连接参数配置:IP、端口、设备 ID、Linktest 间隔
连接参数是翻车重灾区,尤其设备 ID 这一项。Host 模式下你要填目标 IP、端口(典型值是 5000 或 7777,SEMI E37 没有强制,但现场用的最多是 5000),以及本端设备 ID;Equipment 模式下只要填监听端口和设备 ID。设备 ID 在 HSMS 里占 Session ID 的低 15 位,取值一般是 0 到 32767,0 是默认的未注册设备。
Linktest 间隔也要顺手设置,标准建议值是 45 秒。它的作用是保活探测:如果 45 秒没有任何消息,主动方发一条 SType=9 的 Linktest.req,对端必须回 SType=10 的 Linktest.rsp。这个间隔设得太短(比如 5 秒)会频繁打断业务消息,太长(比如 10 分钟)又可能被中间防火墙判定空闲断开。现场联调时我一般先设 45 秒,等确认链路稳定再调长。
3.3 导入样板文件,拆解一条 S1F13 从点击到上线的过程
样板文件通常在模拟器的“消息模板”面板里,以树的形势列出 S1F13、S1F14、S5F1、S6F11 等。双击一条模板,会展开它的完整结构,包括 Stream、Function、W 位、消息体层级。以 S1F13 为例,它的标准流程是:你先在 Equipment 模式实例上点击“发送 S1F13”,模拟器自动完成 TCP 连接、Select 握手,然后发送 S1F13 数据消息;对端(你的 Host 程序)回 S1F14 后,模拟器把回执显示在日志里。
这一步最容易看错的是 W 位。S1F13 是要求回复的消息,所以 W 位必须为 1,即 Header Byte 1 的最高位是 1。如果你用自带模板,软件会正确处理;但如果你是自己新建消息,W 位 0/1 搞反,对端会认为这是一条不需要回复的消息,从而不给你回 S1F14,你就永远等不到建连确认。我一般会在导入模板后先看消息头的字节预览,确认 W 位状态再点击发送。
# 模拟器一般提供消息字节导出的功能 # 导出后的报文文件长这样:前缀 + 10字节头 + body # 一个典型 S1F13 帧(hex): # 00 00 00 0A <- 消息长度 10 字节(此时 body 为空) # 00 01 <- Session ID / Device ID = 1 # 8D <- W=1, Function=13 (0x0D) # 01 <- Stream=1 # 00 <- PType=0 # 00 <- SType=0,数据消息 # 00 00 00 01 <- Transaction ID = 1上面这个 10 字节消息头里,注意8D这个值:最高位是 1 表示 W 位,低七位 0x0D 就是 Function 13,Stream 是 1,所以这条就是 S1F13。你如果哪天在抓包工具里看到00 00 00 0A 00 01 8D 01 00 00 00 00 00 01,不用翻协议文档,按这个结构拆就能认出来。
3.4 日志窗口怎么看:从 TCP 帧到 SECS-II 消息的逐级核对
模拟器的日志窗口一般分两层:一层是 TCP 层,显示连接建立、断开、字节数统计;一层是 HSMS/SECS-II 层,显示已经解析好的消息编号。真正的调试高手只做一件事:先看 TCP 层有没有异常(连接被拒、半开连接、连接复用),再看 HSMS 层状态(是否 Selected、Linktest 是否正常),最后才看业务消息内容。
顺序不能反。很多朋友一上来就盯 SECS-II 的 body,结果发现 S6F11 里的数据项都对,但对方就是没反应,回头看 TCP 层才知道连接根本没进 Selected,业务消息压根发不出去。日志窗口还有个细节:大部分模拟器在收到无法解析的 SECS-II 消息时,会把它当原始字节显示而不是抛错误,这是故意设计的,为了让你能手工核对长度字段。如果看到日志里 Hex 区有一长串FF FF或者长度异常,十有八九是消息长度前缀字节序写反了,这在避坑章第一条会细讲。
4. 双向调试实战:用模拟器把 S2F13 和 S6F11 来回打通
4.1 Host 侧发起 S2F13:变量读取请求是怎么组织的
S2F13 是主机向设备请求变量数据的标准消息,常在设备上电初始化时用来同步状态。它的 body 是一个 LIST,里面放一堆要读取的变量 ID(VID),每个 VID 通常是 U4 类型。假设你要读设备的温度和腔体压力两个变量,VID 分别是 1001 和 1002,那么 S2F13 的 body 编码如下:
import struct def encode_u4(value): return struct.pack(">BI", 0x0D, value) vid_list = encode_list( encode_u4(1001), encode_u4(1002) ) body = encode_list(vid_list) print(body.hex()) # 输出示例:01 01 01 02 0d 00 00 03 e9 0d 00 00 03 ea这里最外层 LIST 只有一项,这一项又是一个 LIST,里面是两个 U4。之所以套两层,是因为 SECS-II 规范里 S2F13 的根节点定义就是 List<List >,很多设备的协议栈强制按这个层级解,少套一层就可能触发 S9F1。用模拟器发这条消息时,直接在模板库找 S2F13,把 VID 值改成 1001、1002,点击发送,然后等待 Equipment 模式的回包 S2F14。
S2F14 的 body 结构是 List<VID, Value> 的集合,每个 ID 对应一个值。调试时注意:如果回包里 VID 的顺序和请求不一致,很多设备协议栈是按请求顺序回,但模拟器并不保证,所以你的解析代码不要假设顺序一致,按 VID 匹配更稳妥。这点在真实设备上尤其重要,某厂商的设备就喜欢把回包里的 VID 排序后返回,和请求顺序完全不一样。
4.2 Equipment 侧上报 S6F11:事件报告的 body 该怎么拼
S6F11 是设备主动上报事件的常用消息,比如晶圆完成、腔体开门、报警恢复都会触发。它的 body 根节点是 LIST,第一项是事件 ID(U4),第二项是数据值列表(LIST)。模拟器自带的 S6F11 模板里事件 ID 一般预置成 1000,数据列表里放了几个占位变量。把你的事件 ID 替换上去,再按实际变量数量增减数据项:
def build_s6f11_body(event_id, values): # 第一层:事件 ID + 数据值列表 item_count = len(values) value_items = [encode_i4(v) for v in values] return encode_list( encode_u4(event_id), encode_list(*value_items) # 注意这里还有一层List )这段代码最容易写错的是把encode_list(*value_items)省略成encode_u4(values[0])。S6F11 规范要求第二项必须是一个数据值列表,哪怕只有一个值也要包一层 LIST,否则对端协议栈解析到第二项时类型对不上,会直接回 S6F12 带错误码,或者在更严格的设备上直接报 S9F1。模拟器里发完这条之后,Host 模式一侧应该回 S6F12,ack 字段一般是 0,表示接受。如果你在 Host 模式的回包里看到非 0 的 ack,说明对方认为你上报的事件 ID 未定义或者数据项数量不对,需要回设备端检查事件报告的配置。
4.3 Transaction ID 与回复匹配:一次请求对一次应答
Transaction ID 是 4 字节整数,用来关联请求和回复。规则一句话:请求方发出的数据消息带一个事务 ID,应答方回消息时必须带着完全相同的事务 ID。用模拟器做双向调试时,你会在日志里看到这样的成对记录:你发 S2F13 时事务 ID 假设是 5,对端回 S2F14 时事务 ID 也必须是 5,这个编号在你下一次发起新请求时递增或手工改动。
初学者最容易犯的错是打开两个模板同时发送,结果把事务 ID 搞混。模拟器通常有个“自动递增”选项,建议干掉它,改用固定值,一条一条地调试。举个例子:你先发 S1F13 用事务 ID 1,再发 S2F13 时如果还停留在事务 ID 1,第一次的 S1F14 回包管它叫 S1F13 的回执还是 S2F13 的回执,就看日志右下角的解析结果了。这个状态一旦混乱,你只能断开重连,因为协议栈对同一事务 ID 的第二次请求很多直接忽略。
事务 ID 还有一个坑是符号位。有些协议栈把 Transaction ID 当无符号整数处理,有些当有符号,如果你的值超过 0x7FFFFFFF,对端解析出来可能变成一个负数,日志里显示为-123456之类,查半天查不到原因。所以我建议调试阶段事务 ID 永远用小于 0x7FFFFFFF 的值,优先从 1 开始递增。
5. 避坑与常见问题:HSMS 调试里六个高频翻车点
5.1 Select 握手没完成就发数据,对方直接断开
现象:模拟器显示 TCP 已连接,你立刻发了一条 S1F13,结果对端端口无响应,几秒后日志显示连接被关闭。 原因:TCP 连接建立只代表传输层通了,HSMS 还要求先完成 Select 握手。对端协议栈在状态机还是 Not Selected 时,收到 SType=0 的数据消息直接判为非法,断开连接。 解决:把 Select.req/Select.rsp 的握手当成 TCP 连接的一部分看待,发送业务消息前先检查日志里状态是否为 Selected。模拟器菜单里的“自动握手”选项要保持开启,如果你是手工发 Select.req,注意它的事务 ID 用 0,且 SType 要填 1。
5.2 消息长度字段算错:多一万字节或丢一帧
现象:对端日志显示收到的消息长度是 65530 或者负值,消息体内容完全对不上。 原因:HSMS 帧前面的 4 字节长度是按网络字节序(大端)写的,如果按小端发送,长度全反了。另一个常见错误是把“消息本体的长度”算成了“包括长度前缀的总长度”,常见做法也是导致缓冲溢出。 解决:记住长度字段只包含消息头加 body,不含那 4 字节本身。写代码时用struct.pack(">I", len(header) + len(body)),别手算。验证时看模拟器日志里“接收字节数”和帧长度是否吻合,两者差 4 字节就是前缀算进去了。
5.3 Device ID 和 Session ID 混用,S9F1 回得莫名其妙
现象:发出去的消息对端回了一条 S9F1 Unrecognized Device ID,但设备 ID 明明在配置里填了。 原因:HSMS 头里的 Session ID 确实和 Device ID 相关,但不是一回事。Session ID 的低 15 位是设备 ID,但有的设备要求高位置 1 表示某个会话语义,一些协议栈对此很敏感。 解决:先用最简单配置,把设备 ID 填成 0 或 1,别用超过 32767 的“自定义编号”。同时检查模拟器界面上有没有单独的 Session ID 显示,如果它和 Device ID 不一致,以 Session ID 的实际字节为准。另一点:注意回包里 S9F1 自带一条 ASCII 描述,里面的 Stream/Function 编号指的是你发的那条消息,不是错误代码本身。
5.4 W 位语义搞反,把事件通知做成等待回复
现象:S6F11 事件上报之后对方一直没回 S6F12,或者设备端一直卡在等回复状态。 原因:W 位=1 时表示“本条消息需要回复”,W 位=0 表示“不需要回复”。如果把 S6F11 的 W 位设成 0,对端认为你不需要 ack,自然不会回 S6F12;反过来把 S6F11 设成 1,对端会认为你在等它的回复并开始超时计时。 解决:按消息类型而不是业务习惯设置。S6F11 的标准 W 位是 0,S1F13、S2F13 是 1。如果你不确定,去看样板文件里这条消息的 W 位默认值,然后保持一致。调试时最直观的特征:如果日志显示你发出去的帧头第一个字节小于 0x80,说明 W 位是 0,不需要回包;如果大于等于 0x80,说明需要回包。
5.5 Linktest.req 被当 SECS-II 数据消息解析
现象:模拟器日志里时不时莫名其妙出现一条 Stream/Function 都识别不了的消息,而且时间间隔固定。 原因:Linktest 消息的 SType 是 9 或 10,不是 0。如果你的解析代码把 SType 忽略,只按数据消息去解析它的 Header Byte 1 和 Byte 2,得到的 Stream/Function 是乱值。 解决:解析时先看 10 字节头里的 SType 字段,不是 0 就按控制消息处理。模拟器一般会正确显示“Linktest.req”文本而不是数字,如果你在日志里看到一条奇怪消息伴随 TCP 连接随后断开,优先怀疑对端没实现 Linktest 响应,去查对端的保活配置。这一步标准做法是:收到 Linktest.req 必须回 Linktest.rsp,否则对端会关闭连接。
5.6 样板文件占位符没替换,日志里全是乱码项
现象:模板里有几个字符串变量,你直接点了发送,对方回 S9F1 或者显示的数据和解码表对不上。 原因:样板文件里的值经常是占位符,比如把设备名写成了“AAAA”、把版本号写成“00.00”。这些数据按 ASCII 发出去,对端读到的就是真实字符串“AAAA”,如果你的设备名校验逻辑认不出来,会直接 reject。 解决:发送前养成检查模板变量的习惯。我一般会在导入模板后用“查找替换”把常见占位符批量换掉:AAAA/XXXX/00.00这种一律改成真实值。替换完点击“重新编码”按钮,看日志里 hex 区的 ASCII 部分是否已经变成你的字符串,再发送。
6. 把样板文件改成自己的 S6F11 事件模板:一套可复用的技法
6.1 三步改模板:改事件 ID、改数据项、替换格式码
样板文件只是起点,真实设备的事件各式各样,你要学会把它改成自己的。以 S6F11 为例,我固定走三步:第一步,把事件 ID 从模板默认值改成你设备事件表里定义好的编号,比如 3001 代表“Process Done”;第二步,按设备实际需要,把数据值列表里的变量改成对应的 VID 和类型,温度是 F4,压力是 F4,计时器是 U4,状态标志用 BOOLEAN;第三步,检查格式码,如果模板里某个数据项是 I4 而你的变量是 U4,一定要改格式码,否则对端按有符号数解析,负值会变成巨大正整数。
修改格式码的细节:SECS-II 里 I4 和 U4 只是格式码不同,I4 的格式码是0x1D,U4 是0x0D。如果你在模板界面上看到一个数据项类型显示“I4”而你确认设备端定义是“U4”,点开它的属性下拉框,把它改成“U4”再保存。这步不用改代码,但如果你在自研工具里拼消息,就得改 struct 的格式符,">B i"变成">B I"。改完以后保存为新的模板文件,命名时加上设备型号前缀,比如“某设备_ProcessDone_S6F11”,这样后面换了设备不至于找不到原始定义。
6.2 用模拟器做回归对比:同一个模板发两遍测试
模板改完之后,我会在模拟器里做一个低成本回归:把改好的 S6F11 模板连着发两遍,一遍给模拟器自带的 Host 解析器收,一遍抓包后用脚本解析。第一遍是验证模拟器本身认不认这条消息,第二遍是验证第三方解析器认不认。如果两遍结果一致,说明模板结构正确,可以拿到真实设备上用。
发两遍的目的很直白:排除模拟器“过分友好”的解析。有些模拟器在解析 SECS-II 时容忍了格式码不匹配,比如把 I4 当 U4 解,但真实设备的协议栈很严格,这种容错会掩盖模板问题。我踩过一次这样的翻车:模拟器里事件上报一切正常,拿到现场设备上对方直接拒绝,后来逐字节对比才发现是某个数据项格式码写错,模拟器给放过了,设备没放过。从那以后我每次改完模板都强制走一遍双端验证:先看回包 ack 是不是 0,再看抓包解析的字节与你预期是不是完全一致,最后才写提交记录。希望这个习惯能帮你在联调现场少熬几个夜。
本文还有配套的精品资源,点击获取