ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用C++实现SECS调试工具:HSMS报文与SECS-II解析实战

用C++实现SECS调试工具:HSMS报文与SECS-II解析实战 简介面向半导体设备通讯测试与自动化集成工程师的SECS/GEM调试工具基于secsemulator商业版1.83.2封装聚焦解决设备联调前的协议验证、消息排查与产线对接问题。工具完整支持SECS-I、SECS-II及HSMS-SS通讯协议可直接加载SML档案模拟或解析SEMI标准消息流适用于半导体前道/后道设备、上位机开发、MES集成以及产线自动化系统的功能测试场景对初学与中级工程师均能提供实操参考。压缩包共15个文件总体积421KB核心包含可执行主程序、三组动态链接库、SML示例档案、XML配置文件、日志及库文件体积小巧、结构清晰便于工程师快速部署和按需查阅。目前已有1300人学习使用。借助包内自带的SML样例与多份运行日志使用者可对照消息格式逐条复盘SECS通讯过程理解HSMS会话建立、SECS-II数据项编码及异常日志定位方法从而在设备联调前完成通讯仿真缩短现场调试周期为设备通讯验收提供直接依据。1. SECS调试工具到底在调什么从一次现场联调说起去年在封测厂做 EAP 系统现场实施最耗时的不是写上位机逻辑而是跟测机对接时排查“报文对不上”的玄学问题设备端说发了 S1F13可 EAP 端等半天收不到抓包看 TCP 连接是通的数据也到了可解析出来的 Stream/Function 全是乱的。后来发现根子是 SECS 调试工具没选对——通用网络调试工具只能看 TCP 流SECS/GEM 协议是带有 10 字节头、SECS-II 嵌套格式和字节序规则的机器语言拿十六进制裸数据做人工对照效率极低。SECS 调试工具就是干这个的一端连设备、一端连主机或两端都模拟把 HSMS 报文和 SECS-II 消息内容可视化出来同时支持主动发消息、回消息和脚本化交互。用 C 重写这类工具核心诉求就一条在产线那种长时间、高速率、多连接的环境下能稳定地收包、解包、组包、回包而不是三天两头内存泄漏或收包线程卡死。这篇文章我按自己做过的一版工具拆开讲协议骨架怎么立、C 代码怎么组织、字节序和粘包这些坑在哪最后一章给验证手法。新手照着能跑通最小版本熟手可以绕过我踩过的那些血泪坑。2. 先立住协议骨架HSMS 报文怎么拼、SECS-II 消息怎么编码2.1 HSMS 的 10 字节头Message Length 和四个关键字段SECS/GEM 在 TCP 之上的传输层叫 HSMSHigh-Speed SECS Message Services它不是简单的“一条消息一个 TCP 包”而是自己在 TCP 流上划帧。一帧 HSMS 消息由两部分组成先是 4 字节的 Message Length表示紧跟其后的报文总字节数含 10 字节报文头本身然后是报文体。报文体前 10 字节是固定头结构如下表偏移长度字段说明04Message Length大端数值 10 Data 区字节数42Session ID设备 ID通常为 0大端61Header Byte 9流控相关常为 071Header Byte 10消息类型标识见下81PType0x00 表示 SECS-II91SMType0x00 表示 SECS-I 消息104System Bytes事务 ID大端Header Byte 10 的取值决定消息类型0x0000 是 Select Request / 0x0001 是 Select Response0x0002 / 0x0003 是 Deselect 的请求和响应0x0005 / 0x0006 是 Linktest 请求和响应0x0007 是 Data Message真正承载 SECS-II 内容0x0009 / 0x000A 是 Reject 请求和响应。工具里最常打交道的只有 0x0007其余是连接管理和心跳。这里有个新手必踩的坑Message Length 是包括这 10 字节头在内的总长。比如一帧数据消息10 字节头 SECS-II 编码区共 35 字节Length 就写 35。很多调试工具拿通用 TCP 调试器看会把 Length 当成“数据区长度”于是每帧少算 10 字节后续解析全部错位。解决方式工具内部只管按 Length 收完整帧再解析不要把 Length 的语义改掉。2.2 SECS-II 的格式字节与嵌套结构L 和 A 是最常用的两类SECS-II 消息体是递归嵌套的 item 树每个 item 的编码规则是一个格式字节Format Byte高位 6 位表示数据类型低位 2 位表示“长度字段占几个字节”。常见格式字节值类型说明0x01List长度字节数按低 2 位0x01 表示 1 字节长度即子项数 ≤ 2550x05List4 字节长度子项数可超过 65535但一般不用0x21Binary存放原始字节0x25Binary4 字节长度0x40字符床1 字节长度注意长度 字节数不是字符数0x41ASCII 字符床1 字节长度与 0x40 的区别历史遗留0x61JIS-8 字符床日系设备常见0x71Unicode 字符床中文设备消息可能用低 2 位值为 0 表示长度占 1 字节1 表示 2 字节2 表示 3 字节3 表示 4 字节。比如 0x41 的低 2 位是 1所以后面跟 1 字节长度再跟 ASCII 内容0x01 低 2 位是 1也是 1 字节长度表示 List 的子项个数。量的类型如 U40x49、I80x65、F80x89同理数据本身按大端排列。一帧完整的 Data Message 解析流程是先按 2.1 的 10 字节头拿到 System Bytes 和 Session ID然后对 Data 区从第 0 字节开始按格式字节递归解成树。写工具时我建议直接做成树形结构节点存类型和字节内容UI 上展示成缩进列表这样看 S2F41 这类带复杂嵌套参数的现场消息一眼就能找到第几个子项是 Recipe 名。2.3 心跳与连接状态机Linktest 不是可选项很多第一次写 SECS 调试工具的人把 Select 做完就以为完事了结果设备跑十分钟就主动断连——十有八九是没回 Linktest。HSMS 的 T7 定时器默认 4 秒不同设备可能不同常见 410 秒TCP 连接空闲超过 T7设备就会发 Linktest Request 来确认主机还活着主机必须在 T6默认 45 秒常见 5 秒内回 Linktest Response否则设备判定连接失效。工具里要处理的最小状态机Select 未完成收到 Data Message 直接 Reject回复 0x000A Reject/Req 并带原因码。Select 完成进 Normal 状态可以收发 Data Message。收到 Linktest Request0x0005立即回 0x0006 Linktest ResponseSystem Bytes 原样返回。收到 Deselect Request回 Deselect Response然后按需关闭连接。这个状态机建议单独写一个类不要和 UI 逻辑混在一起。否则现场联调时设备一断连你都不知道是状态没切对还是网络真的断了。3. 用 C 写最小可用的 SECS 调试工具核心模块与代码骨架3.1 工程结构和网络层不做界面也能先跑通收发C 写 SECS 工具我见过两种路线一种用 Qt 做带界面和按钮的工具另一种做无界面的命令行/服务型调试器配合脚本驱动。前者适合人肉点来点去后者适合 EAP 现场实施时挂在后台做协议代理。我建议先做后者核心代码不依赖 UI命令行动起来再接 Qt 或 Web 界面都容易。目录结构secs_debug_tool/ include/secs/hsms.h -- HSMS 帧头定义与编解码 include/secs/secs2.h -- SECS-II item 编码与解析 include/secs/session.h -- 连接与 Select 状态机 src/hsms.cpp src/secs2.cpp src/session.cpp src/tool_main.cpp -- 命令行入口网络层用原生 BSD socket 在 Linux 上测Windows 下换 Winsock 接口差异不大但要注意两点一是接收端必须用非阻塞或带超时的 recv不能死等二是收发最好分线程避免收到大消息时 UI/主流程被拖住。核心循环代码如下// tool_main.cpp -- 简化版收包循环完整版见 session.cpp #include sys/socket.h #include netinet/in.h #include unistd.h #include cstdint #include cstring #include vector // 从 socket 精确读 n 字节返回实际读到的字节数 bool read_full(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; // 连接断开或出错 got (size_t)r; } return true; } // 收一帧 HSMS 消息先读4字节长度再读完整帧 // 返回帧字节含10字节头或空表示失败 std::vectoruint8_t read_hsms_frame(int fd) { uint8_t len_buf[4]; if (!read_full(fd, len_buf, 4)) return {}; uint32_t frame_len (len_buf[0] 24) | (len_buf[1] 16) | (len_buf[2] 8) | (len_buf[3]); if (frame_len 10 || frame_len 64 * 1024) return {}; // 防异常 std::vectoruint8_t frame(frame_len); if (!read_full(fd, frame.data(), frame_len)) return {}; return frame; }逻辑说明read_full 循环调用 recv 直到读够指定字节数这解决了 TCP 粘包/拆包的核心问题——底层 recv 返回的字节数不保证等于业务帧长度必须自己拼装。read_hsms_frame 先读 4 字节长度再按长度读整帧frame_len 小于 10 说明连 HSMS 头都不完整直接丢弃并返回空调用方负责断连或跳过。参数说明frame_len 上限 64KB 是经验值。SECS-II 在 300mm 半导体设备上很少超过 16KBRecipe 列表也就几 KB设 64KB 是给托盘数据等极端情况留余量如果现场发现 Trace Data 上报能刷到几百 KB那就得把这个上限调大或改成动态增长否则解析不出来是小事内存消耗不可控才是大事。3.2 SECS-II 编码器用模板把 item 拼出字节流编码器的核心是递归拼字节。每个 item 由格式字节、长度字段、内容三部分组成。下面这段是把一个字符串和一个 U4 数值拼成 List 的示例// secs2.cpp -- 最小编码器片段 #include vector #include cstring #include string #include cstdint // 编码一个 ASCII 字符串 item返回完整 item 字节 std::vectoruint8_t enc_ascii(const std::string s) { std::vectoruint8_t out; out.push_back(0x41); // ASCII, 长度占1字节 out.push_back((uint8_t)s.size()); out.insert(out.end(), s.begin(), s.end()); return out; } // 编码一个 U4 数值 item大端 std::vectoruint8_t enc_u4(uint32_t v) { std::vectoruint8_t out; out.push_back(0x49); // U4, 长度占1字节(4字节数据写死在格式字节低2位1) out.push_back(0x04); // 数据长度4 out.push_back((v 24) 0xFF); out.push_back((v 16) 0xFF); out.push_back((v 8) 0xFF); out.push_back( v 0xFF); return out; } // 编码一个 Listitems 为已拼好的子项字节 std::vectoruint8_t enc_list(const std::vectorstd::vectoruint8_t items) { std::vectoruint8_t out; out.push_back(0x01); // List, 长度占1字节 out.push_back((uint8_t)items.size()); for (const auto it : items) out.insert(out.end(), it.begin(), it.end()); return out; }逻辑说明enc_ascii 输出 0x41 格式字节后跟 1 字节长度长度是字节数不是字符数。写中文字符串时特别容易错——假设你用 UTF-8 存“测试”两个字size() 是 6 而不是 2如果误把字符串长度当字节数后面的内容全得错位。enc_list 的 0x01 表示子项数不超过 255封装常见报文足够如果现场要拼超过 255 个 item 的大 List得换 0x032 字节长度甚至 0x044 字节长度但产线几乎遇不到。参数说明格式字节的写法 0x49、0x41、0x01 都是“类型 长度字段字节数”拼在一起的新手常踩的坑是把 U4 的长度字段写 0 或写 8。0x49 的低 2 位是 1表示后面跟 1 字节长度这个长度指数据区字节数U4 就是 4不是“还有 4 字节长度字段”。这个语义搞混编码出来的消息被设备端解析成乱码是常事。3.3 最小 Select 流程把连接立起来真正对设备发起通信前主机必须发 Select Request设备回复 Select Response 后才能传数据。下面是最小实现// session.cpp -- 极小 Select 流程 #include secs/hsms.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include cstring // 拼 HSMS 帧Session ID 默认 0Header Byte100x0000(Select Req) std::vectoruint8_t build_select_req(uint32_t system_bytes) { uint8_t hdr[10] {0}; hdr[0] 0x00; hdr[1] 0x00; hdr[2] 0x00; hdr[3] 0x0A; // Length10 hdr[4] 0x00; hdr[5] 0x00; // Session ID hdr[7] 0x00; // Header Byte10 Select Req hdr[8] 0x00; // PType hdr[9] 0x00; // SMType hdr[10] (system_bytes 24) 0xFF; hdr[11] (system_bytes 16) 0xFF; hdr[12] (system_bytes 8) 0xFF; hdr[13] (system_bytes) 0xFF; return std::vectoruint8_t(hdr, hdr 14); // 4字节Length 10字节头 } int main() { int fd socket(AF_INET, SOCK_STREAM, 0); sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(5000); // 设备 HSMS 端口 inet_pton(AF_INET, 192.168.1.20, addr.sin_addr); if (connect(fd, (sockaddr*)addr, sizeof(addr)) ! 0) return 1; auto req build_select_req(0x00000001); send(fd, req.data(), req.size(), 0); auto rsp read_hsms_frame(fd); // 收完后解析: rsp[7]0x00 表示 Select Req, rsp[7]0x01 表示 Select Rsp if (rsp.size() 10 rsp[7] 0x01 rsp[14] 0x00) printf(Select OK\n); else printf(Select Failed\n); close(fd); return 0; }逻辑说明build_select_req 拼的是 Select Request14 字节 4 字节长度 10 字节头后面没有 Data 区所以 Length 写 10。main 里连上设备端口后直接发等读回一帧 Select Responsersp[7] 判断消息类型rsp[14] 是 Select Response 的状态字节0 表示成功1 表示已连接非 0 表示失败。参数说明Session ID 默认 0。不过现场有几种设备会把它设成非 0 或带上标志位比如某些台系设备把 bit15 拉高表示“是设备发的”或“是主机发的”解析时如果发现 Select 老失败、抓包报文里 Session ID 又不是 0就要按设备手册的“Session ID 语义”重新处理不能死认 0。System Bytes 是事务 ID每笔事务要递增不能两笔消息用同一个数否则设备端回消息时你都没法分辨对应的是哪一笔请求。4. SECS 调试工具避坑实录版本、断包、字节序与双端对齐4.1 现象明明发了 S1F13设备端就是回 S1F14 超时原因解码时把 SECS-II 消息里的 Stream/Function 字段当成 ASCII 处理了。S1F13 的消息体通常是 List第一个子项是设备 IDS1F13 典型上报内容是 MHEADER但实际常见为 L,{...}有些工具把它按 Text 显示成人眼可读的 “13” 就完事但设备端的 GEM 栈会把第一个 List 的整形字段切出来当 S/F 用。如果编码器把设备 ID 的类型写错或者把整型发成了 ASCII设备端解析失败自然不回。解决所有 SECS-II 消息的第一个 List 至少要保留标准结构。发 S1F13 请求时Stream 和 Function 由帧头“之外”的什么表示严格说 S/F 在 SECS-I 报文头里但 HSMS 数据消息里 SECS-II 层的第一个 List 的第一个 item 必须是设备 ID一般是 U4 或 A 类型看 SECS-II 标准。咱们调试工具拼消息时固定按“L, { U4:0 /* 设备ID */ , ... }”来拼千万别把设备 ID 漏了否则会有设备报“Message Unsupported”或直接不响应。4.2 现象抓包能看到完整帧但工具解一半就乱原因TCP 流上出现粘包和拆包而工具代码只做了一次 recv没能把帧拼完整。前面 read_hsms_frame 已经处理了这个但很多从 Python 脚本转过来写 C 的人容易在“先 recv 长度、再 recv 数据”漏掉循环读取的边界。解决没有捷径必须按“读 4 字节长度 → 循环读满 frame_len”的步骤严格落地。同时给读循环加超时比如 poll 等待 3 秒防止设备只发了半个包就把工具卡在 recv 里连 UI 都拖死。这是现场联调排第一的大坑远远比业务逻辑更容易炸。4.3 现象和台系/日系设备对不上同一份报文换台设备就错原因SECS-II 的 ASCII 与字符床类型不统一。有些老设备发的“字符串”格式字节是 0x40字符床不是 0x41而调试工具只认 0x41就把整段 message 解析成乱码有些设备发的 List 长度字节是 0x032 字节长度工具只处理 0x011 字节长度遇到超过 255 个元素的 List 就翻车。这叫“SECS/GEM 协议栈实现差异”每个厂家的 GEM 都有细微出入。解决解析 SECS-II 时格式字节统一按低 2 位算长度字段字节数不要写死“长度都是 1 字节”。0x40 和 0x41 都按 ASCII 内容处理反正调试工具只是给人看0x05/0x06 这类 4 字节长度的 List 也要支持。工具里做个“宽松模式”解析失败时不直接报错把原始字节以 hex 形式展示出来方便人工排查格式字节。这是我认为调试工具最重要的能力——不是把每串字节都解释对而是解释失败时还能把现场数据完整留给你看。4.4 现象设备连上了但过一会就断线原因多半是心跳没处理。设备在 T7 超时后发 Linktest Request工具没有在收包循环里识别 0x0005 并回复 0x0006于是设备判定主机不可用主动断开。解决收包循环里加一个判断解析出 HSMS 头后若 Header Byte10 为 0x0005立即拼一个 Linktest Response内容Length10, Session ID 原样复制, Header Byte100x0006, System Bytes 原样复制发回去。这是最容易被忽视却是保命的逻辑。另外工具的收包线程要和发送分离如果发送操作阻塞心跳回复也会被拖住。4.5 现象用 C 写工具跑几天后内存越来越大、CPU 占用升高原因典型就是 vector 在循环里反复扩容、线程局部变量堆积、以及日志打印把 SECS-II 大消息当成字符串硬转。C 写这类长时间运行的调试工具最忌讳的是在收包热路径里做低效拷贝和频繁分配。虽然不像 C# 那样有 GC但 vector 每帧都 push_back 成千上万次也会让分配器吃不消。解决热路径上复用发/收 buffervector 用 reserve 预留容量日志打印先判级别再格式化别把每一帧都转成 hex 字串存内存。顺手把编译选项打开 AddressSanitizer 跑一天一夜能抓到大多数内存问题。产线上工具挂三天最后发现是日志文件没轮转那才是真憋屈。5. 验证你的调试工具可信用抓包回放和异常注入自测5.1 双向模拟自测工具连自己谁都骗不了工具做出来后别急着连真机。先让工具同时起“设备模拟端”和“主机调试端”两个角色设备模拟监听 5000 端口主机调试端去连 5000。主机发 S1F13设备模拟回 S1F14两边窗口同时打印自己解析出的树形内容。如果两边的 Stream/Function、System Bytes 对得上说明编解码一致。自测用例可以这样铺S1F13/S1F14 带设备名和软硬件版本覆盖 ASCII 类型S2F41 带完整 RCMD 参数列表覆盖嵌套 ListS6F11 带一个几十 KB 的 Data 块覆盖大帧拆分最后再测一个超过 255 个子项的 List覆盖 2 字节长度。这些场景在真机上一旦失败排错成本极高自测阶段全过现场就放心很多。5.2 异常注入把“半个包”和“错字节”故意塞给工具只测正常帧是不够的要故意制造网络层异常来验证工具韧性。常见做法是用脚本往工具端口发几种坏数据发半个 HSMS 头不结束发长度声明 100 字节、实际只发 50 字节把某个 SECS-II 格式字节改成 0x99非法类型把 U4 数据改成 3 字节。工具的表现应当是半包等超时后报错但进程不崩非法格式字节能在 UI 显示“未知类型 0x99”而不是死循环长度不符能打印“期望 XX 字节实际 XX 字节”。这些测试能用 Linux 下的 bash /dev/tcp 写脚本做也能拿 Python 脚本快速造包。关键在于让工具天天跑跑到你觉得它“不会死了”才能上产线。5.3 与 Wireshark 同源对比双端抓包校验 System Bytes真正的终极验证是让工具和 Wireshark 同时抓同一个连接把工具解析出来的帧和 Wireshark 的 HSMS 解码结果逐帧对比。Wireshark 支持 HSMS 协议的解析需要 TLS 和一定的配置但原始 TCP 上的 HSMS 能直接解码。我见过不少“工具显示正常但设备不理睬”的情况最后抓包才发现工具发的 System Bytes 有一帧和之前重复了。这种问题工具自测时不容易暴露因为设备模拟端一般不校验 System Bytes 累积但真机 GEM 栈会。所以自测时加一个严格模式System Bytes 从 1 递增重包直接报错。我现在每到一个现场第一件事不是打开别人给的“成熟工具”而是先用自己的调试工具连设备的 HSMS 端口发一条 Select再看设备回的 Select Response 状态码。状态码为 0 才继续往下做为 1 确认连接已存在为其他值直接查设备手册对应含义。这个习惯帮我避开了至少五次“以为是自己代码问题、其实是端口被占用/连接数满”的翻车现场。调试工具做得再花哨能在三分钟内在现场确认“连接层是否健康”才是真本事。希望帮到你。本文还有配套的精品资源点击获取
返回列表