简介:基于 secsemulator 商业版 1.83.2 开发的半导体设备通讯调试工具,面向设备厂商、晶圆厂自动化工程师及通讯测试人员,重点解决设备端与上位机之间消息无法打通、报文格式异常等问题。工具支持 SECS-I、SECS-II、HSMS-SS 全套协议,能够读取 SML 格式的设备模型文件,快速在个人电脑上搭建虚拟设备环境,用于联机测试、通讯验证和故障定位,也可辅助理解 SEMI 标准中的设备对象与事件定义。压缩包共 15 个文件,总大小仅 421KB,主程序为可执行文件,三个动态链接库负责协议解析,内置两个 SML 示例、一个配置文件及七个真实运行日志,另有静态库供二次开发参考;整体解压即用,便于对照日志分析链路建立、消息重发、数据映射等关键环节。截至目前,这套工具已有 1300 人学习下载,适合刚接触 SECS/GEM 协议的新人入门前置学习,也适合现场调试人员快速模拟通讯。通过这套工具,可以直观观察消息如何封装、解析与应答,遇到通讯超时或断连时,还能借助日志中的主从消息顺序快速缩小排查范围;对设备端软件开发者而言,库文件也能作为编写通讯模块的参考,兼具学习与实用价值。
1. SECS调试工具++是什么:现场联调最缺的那块拼图
第一次做SECS/GEM联调的人,多半会卡在一个很尴尬的环节:设备侧把SECS服务打开,EAP环境还没就绪,手里只有一台能发十六进制报文的抓包工具。消息发出去石沉大海,就只能一条条猜,隔半小时重试一次,纯靠玄学推进度。SECS调试工具++这个概念,直白说就是用C++写一个既能当主机连设备、又能当设备挂EAP的调试器,把“消息能不能通、编码对不对、状态有没有搞对”这三件事展开成白纸黑字的日志。它适合做半导体封测设备SECS/GEM协议对接测机的工程师,也适合写C#/C++上位机又要跟EAP联调的人,同样适合做EAP系统现场实施、部署和日常运维的人。一句话,“++”不是C++语法里的自增,是给SECS这口冷灶添把火。
2. 从SECS到GEM:调试工具必须先搞清的协议骨架
2.1 SECS不是一个协议:传输层与消息层的分工
SECS/GEM这个叫法,底下其实是一组SEMI标准的组合。物理层是RS-232或TCP/IP,传输层是SECS-I(SEMI E4)或HSMS(SEMI E37),消息层是SECS-II(SEMI E5),再往上才是GEM设备模型。很多联调现场的问题,都出在把这几层混在一起看。调试工具的第一要务,就是替你把每一条报文的传输层和消息层拆开,方便判断故障到底在哪一层。
SECS-I走串口,同一时刻只能有一个方向在传输,属于半双工;HSMS走TCP/IP,全双工,收发互不干扰。这就是“SECS是单向还是双向”这类疑问的由来——方向性不是由消息层决定的,而是由传输方式决定的。现代封测设备基本都走HSMS,调试工具也按HSMS写最省事。做工具时我会明确一点:传输层只负责“把字节完整送到对方”,消息层负责“这几字节是什么意思”,两层之间一定要留清晰接口,否则出了问题根本说不清是网没通还是报文编错。
| 对比项 | SECS-I | HSMS |
|---|---|---|
| 物理载体 | RS-232C串口 | TCP/IP网口 |
| 收发模式 | 半双工,同一时刻单方向 | 全双工,可同时收发 |
| 典型速率 | 9600/19200bps | 以太网,远高于串口 |
| 消息头格式 | 依赖块头和校验 | 固定10字节HSMS Header |
| 适用场景 | 老设备、近距离 | 现代封测设备、EAP联调 |
调试工具不需要同时支持两种物理传输,绝大多数新项目都只要HSMS。串口的老设备另说,但那种项目现在基本是改造维护,不在“上手新调试工具”的范围内。
2.2 最小消息集:先把S1F1、S1F3、S6F11这三组摸透
协议栈再复杂,联调时要先跑通的消息就那么几组。S1F1/F2是“Are You There”握手,主机问设备在不在,设备回一句“我在”,这是验证通信链路最基础的消息。S1F3/F4是主机读设备变量(SVID)列表,用来确认设备能正确返回带Secs2编码的数据。S6F11/F12是设备主动上报事件给主机,EAP联调里最耗时的就是这类消息,设备端要按事件触发,主机端要应答。
| 消息号 | 方向 | 用途 | 调试价值 |
|---|---|---|---|
| S1F1/F2 | Host->Device | 通信握手 | 最快验证消息通路 |
| S1F3/F4 | Host->Device | 读取设备变量 | 验证Secs2编码与响应结构 |
| S6F11/F12 | Device->Host | 事件上报 | 验证设备主动消息与ACK链路 |
做工具的第一版我只建议做一件事:一键发S1F1,把返回的S1F2解码成可读内容。这个最小闭环跑通,HSMS连接、消息收发、Secs2解码三个核心模块就都有了,剩下的都是在这条骨干上加树枝。
2.3 GEM状态机:为什么下载的模拟器不够用
“secs/gem模拟器下载”在搜索里热度一直不低,但下载到的大多只能做到“收到什么答什么”。这类反射式模拟器可以用来测连接,没法用来测业务,因为它们没有状态机。GEM设备模型里定义了控制状态、通讯状态,以及不同状态允许收发哪些消息。很多现场“设备不回复”的故障,根源不是消息编错,是状态不对,消息合法但设备当前状态不允许处理它。
调试工具要把这个黑匣子打开,做法是每收发一条消息就把当前内部状态打出来。我去现场调机时会带着工具边跑边看:连接建立后处于什么状态,EAP上线触发哪条消息,设备报完事件后状态有没有按预期迁移。如果工具只替你对上消息字节,不帮你追踪状态,那它充其量是个高级点的抓包工具,把问题从“看不到”变成“看得见但还得自己串”,价值很有限。
3. 用C++搭一套可复现的SECS调试工具:消息层与连接层实现
3.1 双角色选型:模拟主机与模拟设备
选C++写SECS调试工具的常见理由是性能和数据表示能力。HSMS消息里大量出现二进制数据、定长整数和嵌套List,C++用结构体和vector就能直接映射,避免像脚本语言那样在“字节数组”和“对象”之间反复转换。另一个理由是调试工具经常要编译成DLL或静态库给上位机引用,C++在这种场景里最省事。
工具需要两种角色。平时联调用主机角色:主动连接设备,发S1F1/S1F3,看设备怎么回。做回归测试时用设备角色:自己作为SECS服务端挂着,让EAP主动连过来,验证EAP在“设备状态异常”时会不会按预期处理。我一般会把两侧共用同一套编解码和会话管理代码,只在上层收发方向不同,这样两边行为一致,不会出现“主机能通但设备角色没实现某个消息”的尴尬。
模块划分按这个思路做:
- 连接管理:负责HSMS的建立、心跳、断开和重连
- 接收循环:负责读TCP四字节长度头并拼出完整帧
- 编解码层:负责Secs2 Item与二进制互相转换
- 命令交互:负责从命令行输入SxFy并触发发送
- 日志层:负责把收发的帧按时间戳落地
三个核心模块都不长,重点是收包循环和编解码接口,下面逐个写。
3.2 接收循环:TCP半包粘包怎么处理
HSMS帧由两部分组成:最前面4字节是消息总长度(包含后面的10字节HSMS Header和可选的Secs2数据体),再往后就是实际内容。TCP本身是流式协议,一次recv可能只读到半条消息,也可能一次收到两条以上,接收循环必须自己处理拼接。我一般这样写:
// hsms_connector.cpp #include <cstdint> #include <vector> #include <sys/types.h> #include <sys/socket.h> #include <cerrno> constexpr size_t kHsmsHeaderSize = 10; // HSMS固定消息头长度 constexpr size_t kMaxFrameSize = 16 * 1024 * 1024; // 上限16MB static bool readExact(int fd, uint8_t* buf, size_t n) { size_t got = 0; while (got < n) { ssize_t r = recv(fd, buf + got, n - got, 0); if (r == 0) return false; // 对端关闭 if (r < 0) { if (errno == EINTR) continue; // 信号打断,重试 return false; } got += static_cast<size_t>(r); // 防止半包 } return true; } bool HsmsConnector::receiveFrame(std::vector<uint8_t>& out) { uint8_t lenBuf[4] = {0}; if (!readExact(sock_, lenBuf, 4)) { logError("读取消息长度失败"); return false; } // HSMS长度字段是4字节网络字节序,高位在前 uint32_t msgLen = (static_cast<uint32_t>(lenBuf[0]) << 24) | (static_cast<uint32_t>(lenBuf[1]) << 16) | (static_cast<uint32_t>(lenBuf[2]) << 8) | static_cast<uint32_t>(lenBuf[3]); if (msgLen < kHsmsHeaderSize || msgLen > kMaxFrameSize) { logError("帧长度越界: msgLen=%u", msgLen); return false; } out.resize(msgLen); if (!readExact(sock_, out.data(), msgLen)) { logError("读取消息体失败: msgLen=%u", msgLen); return false; } logByteBuffer(out.data(), out.size()); return true; }这段代码的关键在readExact函数。recv返回的字节数不保证等于请求长度,必须循环读到满n字节才算完成,否则一条被拆成两半的HSMS帧会被当成两条消息解析,后面的处理全乱。下限判断用10是因为合法的HSMS消息头固定10字节,长度字段本身就包含这10字节;上限16MB是兜底项,防止对端异常时无限分配内存,实际现场消息通常远小于这个值。
3.3 发送帧:设备ID、消息ID和10字节Header
发送比接收简单,但容易在Header字段顺序上翻车。HSMS的10字节Header里,前2字节是SessionID(设备上下文),中间2字节是Stream和Function,随后2字节是消息ID,后4字节保留置0。消息ID必须每次递增,主机才能把设备返回的响应和请求配对。发送函数我习惯这样写:
// hsms_connector.cpp bool HsmsConnector::sendFrame(uint16_t sessionId, uint8_t stream, uint8_t function, const std::vector<uint8_t>& data) { const uint32_t msgLen = kHsmsHeaderSize + static_cast<uint32_t>(data.size()); std::vector<uint8_t> frame; frame.reserve(4 + msgLen); // 4字节总长度,网络字节序 frame.push_back((msgLen >> 24) & 0xFF); frame.push_back((msgLen >> 16) & 0xFF); frame.push_back((msgLen >> 8) & 0xFF); frame.push_back((msgLen) & 0xFF); // 10字节HSMS Header frame.push_back((sessionId >> 8) & 0xFF); frame.push_back(sessionId & 0xFF); frame.push_back(stream); frame.push_back(function); frame.push_back((msgIdSeq_ >> 8) & 0xFF); // 高位在前 frame.push_back(msgIdSeq_ & 0xFF); msgIdSeq_++; // 递增,用于请求响应配对 frame.insert(frame.end(), 4, 0); // 保留字段 frame.insert(frame.end(), data.begin(), data.end()); return sendAll(sock_, frame.data(), frame.size()); }参数里sessionId对应设备端配置的设备ID,不同设备实例必须区分,不能全局写死。stream和function组合出对应的SxFy,例如S1F1就是stream=1、function=1。消息ID递增有个作用:现场拿到日志时,如果看到同一个消息ID出现两次,基本能断定是对端重发或自己忘递增了。另外,转发模式有个常见错误是把设备响应原样转发给EAP,但消息ID被设备端重新分配过,EAP侧会对应不上,联调时要留意。
3.4 Secs2编解码接口:把Item变成二进制
SECS-II消息体由Item嵌套组成。每个Item由格式字节、长度字段和实际数据构成,List类型可以嵌套其它Item。数据体编码不需要在工具里全手工写,但接口要干净,测试时才好加用例。我在工具里提供的是下面这种结构:
// secs2_item.h enum class Secs2Format { kList, // 嵌套容器 kAscii, // 定长/变长ASCII字符串 kBinary, // 原始二进制 kI1, kI2, kI4, kU1, kU2, kU4, kU8, kF4, kF8 }; struct Secs2Item { Secs2Format format = Secs2Format::kList; std::vector<Secs2Item> children; // kList时使用 std::vector<uint8_t> raw; // 非List时使用 }; class Secs2Encoder { public: std::vector<uint8_t> encode(const Secs2Item& root) const; Secs2Item decode(const uint8_t* data, size_t len) const; };编码的具体实现按SEMI E5规定:编完格式字节后写长度字段,长度字段自身占几字节由格式字节的低位决定,数据量大时自动扩展;List则递归处理子项。我在现场遇到最多的Secs2障是“想当然地把字符串按C风格读”,实际上ASCII Item在报文里是“长度+内容”的二进制块,不保证以0结尾。调试工具做decode时只认长度,这一点必须从接口设计上就杜绝。
3.5 命令行交互与日志输出
工具要让现场工程师拿起来就用,交互就一句话:输入S1F1回车,发出去,把响应解码后打印出来。命令解析不需要复杂,几十行就够。日志则比交互更重要,每一帧都要带方向、时间戳、SxFy和原始十六进制,方便事后对齐设备端和EAP端记录。
// main.cpp std::string line; while (std::getline(std::cin, line)) { if (line == "exit") break; if (line == "s1f1") { connector.sendFrame(sessionId, 1, 1, {}); logInfo("--> S1F1 sent"); continue; } // 其它消息走 stream,function 输入解析 }日志输出我会固定格式:[时间戳] [HOST->DEV] [S1F1] [消息ID=1] [hex=..]。时间戳带毫秒,消息ID放中间,原始hex放最后。不要怕日志看着枯燥,联调出问题时这份日志就是唯一的仲裁依据,字段越全,开撕越少。
4. 常见问题排查:SECS联调时最容易翻车的5处细节
4.1 第一条消息就超时:T3计时器与Device ID
现象:TCP连接明明建立了,S1F1发出去设备一点反应都没有,几十秒后工具报超时并把连接断开。原因通常有两个:一是Header里的Device ID和设备配置不一致,设备认为消息不是发给自己的,静默丢弃;二是T3计时器设置太短,消息还在路上就被当成超时。解决:先抓包看设备端在超时前有没有回任何字节,没有任何字节先查Device ID;T3按标准建议先给45秒,不要为了“快速感知失败”设成5秒,那只会让握手期疯狂超时。老设备如果是串口SECS-I,还要先确认半双工方向切换逻辑,这种场景别按全双工猜。
4.2 长度字段算错:设备收到后直接断开
现象:收发工具在A设备上跑得好好的,换到B设备上第一帧发出去就被对方FIN。原因:HSMS长度字段统计的是“10字节Header+数据体”,很多人写成只算数据体,或者落了4字节长度字段本身。设备按错误长度解析,解析不到完整Header就直接断开连接。解决:长度计算统一用10 + data.size(),并且转成4字节网络字节序。改完之后用抓包软件确认一下帧里的长度值和工具打出来的日志一致,这一步能省掉大半“换台设备就翻车”的排查时间。
4.3 字符串乱码:定长与变长的坑
现象:S1F3读回来的设备名或者软件版本,打印出来是一串乱码,或者前面正常后面多了几个无效字符。原因:设备返回的ASCII Item是“长度头+内容”的二进制块,长度字段告诉你有多少字节有效,后续字节属于其它Item,不该被打印。工具如果按C字符串读到\0再截断,或者干脆忽略长度字段直接整段打印,就必然乱。解决:编解码层严格按长度字段截取内容,打印前再校验一下长度是否合理。联调时多看一眼原始hex,能直接看出是设备多发了字节还是工具读错了长度字段。
4.4 跨语言调用翻车:C#调用C++的AccessViolation
现象:把调试工具封装成DLL给C#上位机引用,一调用就弹AccessViolation C0000005。原因:导出函数没有加extern "C"导致符号被C++名字修饰改名,或者调用约定不一致,C#那边用默认的stdcall,C++这边却是cdecl,栈乱了就崩溃。解决:导出接口统一写extern "C"并明确__stdcall;跨语言传结构体时用#pragma pack(1)对齐,或者干脆只传JSON字符串,避免两边内存布局不一致。这条路走通以后,调试工具就能变成上位机的一个内嵌模块,联调时不用来回切窗口。
4.5 部署到国产环境连不上:防火墙与回环测试
现象:工具在本地Windows或者开发机上调试一切正常,部署到麒麟V10这类国产系统上,同网段设备就是连不上。原因:这类系统默认安全策略把非回环端口挡了,HSMS端口没有放通。解决:先在目标机本地跑回环测试,确认程序本身没问题,再放通指定TCP端口做最小范围验证;服务端和客户端都用高位端口,避免跟默认服务冲突。字节序在x86架构下不用担心,但换到飞腾等其它架构时,要把长度字段和2字节/4字节整数的字节序重新过一遍。
5. 把工具用回生产:回放验证与一个趁手习惯
5.1 消息流转文件:先把联调过程存下来
联调不是调完就结束。设备固件升级、换机、参数变更都可能让原本正常的SECS链路突然出问题,这时候最缺的是一份“当时正常时”的原始记录。工具要把每一步收发落在流转文件里,字段如下:
| 字段 | 示例 | 说明 |
|---|---|---|
| 时间戳 | 2025-05-12 10:24:01.123 | 精确到毫秒 |
| 方向 | HOST->DEV | 标识谁发的 |
| 消息号 | S1F1 | 方便人眼检索 |
| 消息ID | 5 | 用于请求响应配对 |
| 原始hex | C100000001 | 与抓包逐字节对比 |
5.2 回放模式:设备升级后的第一道回归测试
回放是这套工具最值钱的功能。把流转文件按原时序重新发给设备,再将设备的每一次响应和文件里当时记录的响应逐条对比。升级固件后如果响应链一致,说明协议行为没变;如果某条S6F11结构变了,马上就能锁是在哪个版本引入的。我一般把回放当成固件升级后的第一道关卡,比直接挂EAP联调省力得多,因为EAP同时带着一堆业务逻辑,响应异常时很难区分是设备变了还是EAP状态乱了。给工具加Lua脚本接口也是常见做法,把固定场景写成脚本,每次升级后跑一遍,相当于把人工点按回归变成自动化验证。
这个留档习惯帮我在多次换机项目里避开了大坑:新设备上线前先跑一遍旧设备留下的流转文件,三分钟就能发现新设备把某个SVID类型从U4改成了I4,不用等产线联调时靠现场报错来定位。调试工具做得顺手之后,它就不再只是调试工具,而是让设备行为变得可对比、可追溯的一张底牌。希望这条经验对你也有用。
本文还有配套的精品资源,点击获取