简介:SEMI E5-1104(SECS-II)是2004年发布并沿用至今的半导体设备通信核心标准文档,面向从事半导体制造设备与控制系统开发、维护的工程师,以及参与产线自动化集成项目的管理人员。它定义了智能设备与主机之间消息交换的详细规则,与E4(SECS-I)完全兼容,涵盖消息结构、流与函数、事务与对话协议、数据结构等内容,并细化Stream 0至Stream 17共18个流的用途,涉及设备状态、控制与诊断、物料状态、异常处理、数据收集、工艺程序管理、晶圆映射等场景,同时明确T1-T4超时机制与错误恢复逻辑。资源包为1个PDF文件,约2.66MB,高清英文原版,便于检索与打印研读。目前已有343人学习。读者可借此掌握SECS-II消息定义机制与实现要点,理解设备端与主机端的标准落地方式,为设备集成、通信调试与协议扩展提供权威依据。
1. 从一次设备联调卡壳说起:SECS-II 到底管什么
产线上新到一台设备,主机端按老模板发S1F1问在线状态,设备回了S1F2,但后面S1F3请求状态数据时,设备直接超时。抓包一看,设备把S1F3的回复拆成了两个 block,而主机端只等单 block,T3 超时后直接判失败。这种问题翻遍设备手册也找不到答案,最后是在 SEMI E5-1104 这份 SECS-II 消息内容定义里找到的:哪些消息必须单 block 发、哪些允许拆多 block、T3/T4 超时怎么算,标准里写得明明白白。
这份 SEMI E5-1104 是 2004 年 11 月发布的 SECS-II 标准原版高清英文文档,定义了智能设备与主机之间消息交换的完整规则。它和 SECS-I(SEMI E4)完全兼容,同时允许适配其他消息传输协议。文档覆盖 18 个 Stream(Stream 0 到 Stream 17)的消息用途、数据结构、事务协议、超时机制和计量单位。适合做半导体设备通信开发、主机端 EAP 系统集成、产线自动化联调的工程师,尤其是需要从协议层定位通信异常的人。如果你只调过现成的 SECS/GEM 库,没翻过原始标准,这份文档能补上很多“库帮你兜了但你没意识到”的底层逻辑。
2. 消息结构拆解:Item、List 与自描述数据格式
2.1 为什么 SECS-II 用自描述格式而不是固定结构
SECS-II 的消息体不是固定长度的二进制帧,而是由 Item 和 List 嵌套组成的自描述结构。每个 Item 自带格式码,告诉接收方“我是什么类型、多长”。这样设计的好处是:设备和主机不需要提前约定每个消息的精确字节布局,只要按标准解析格式码就能还原数据。代价是解析器必须严格按格式码分支处理,遇到未知格式码不能猜,只能报错。
标准第 9 章定义了 Item 的格式码体系,常见的有:
| 格式码 | 类型 | 长度字节数 | 说明 |
|---|---|---|---|
| 0o20 | Binary | 1 | 无符号二进制 |
| 0o40 | Boolean | 1 | 布尔值 |
| 0o60 | ASCII | 1 | 字符串 |
| 0o70 | JIS-8 | 1 | 日文编码字符 |
| 0o44 | I8 | 1 | 8 字节有符号整数 |
| 0o54 | F8 | 1 | 8 字节浮点数 |
| 0o00 | List | 1 | 嵌套列表 |
格式码的高 6 位标识类型,低 2 位标识长度字节数。比如0o21表示 2 字节长度的 Binary,0o61表示 2 字节长度的 ASCII。List 的格式码固定为0o00,后面跟一个长度字段表示包含多少个元素。
2.2 用 Python 构造一个 S1F3 请求消息
下面这段代码演示如何按 SECS-II 规则手工拼一个S1F3(请求设备状态数据)的消息体。实际项目中你可能会用现成的库,但理解手工拼装过程对排查“为什么设备不认这条消息”很关键。
import struct def encode_item(format_code, value): """按 SECS-II 规则编码单个 Item""" if format_code == 0o00: # List # List 的长度字段表示元素个数 body = b''.join(value) return bytes([format_code]) + encode_length(len(value)) + body elif format_code == 0o60: # ASCII data = value.encode('ascii') return bytes([format_code]) + encode_length(len(data)) + data elif format_code == 0o20: # Binary return bytes([format_code]) + encode_length(len(value)) + value else: raise ValueError(f"暂不支持的格式码: {oct(format_code)}") def encode_length(n): """长度字段:1 字节时直接写,2 字节时高字节在前""" if n < 256: return bytes([n]) else: return struct.pack('>H', n) # 构造 S1F3 消息体:一个 List,包含一个 Binary 类型的 SVID 列表 # 假设请求 SVID 1 和 SVID 3 的状态数据 svid_list = encode_item(0o20, bytes([0x01, 0x03])) body = encode_item(0o00, [svid_list]) print(body.hex()) # 输出: 010201020103这段代码的输出010201020103可以逐段拆开看:01是 List 格式码0o00加长度 1(表示 List 里有 1 个元素),02是 Binary 格式码0o20加长度 2(表示后面跟 2 字节数据),0103就是两个 SVID 的值。设备端解析时先读格式码判断类型,再读长度字段确定数据边界,逐层还原。
参数说明:format_code必须用八进制写,因为标准里格式码是按八进制定义的;encode_length里 256 是分界点,小于 256 用 1 字节长度,大于等于 256 用 2 字节大端序。实际设备通信中,单 block 消息最大 244 字节,超过这个长度必须走多 block 传输,这是 SECS-I 兼容性要求。
2.3 List 嵌套的边界:别把设备解析器撑爆
List 可以无限嵌套,但设备端解析器通常有递归深度限制。我见过某设备固件在解析超过 5 层嵌套的 List 时直接复位通信栈。标准没有规定最大嵌套层数,但实际实现中建议控制在 3 层以内。如果你在主机端构造的消息被设备静默丢弃,先检查 List 嵌套层数,再检查总长度是否超过 244 字节。
3. Stream 与 Function 的分配逻辑:从 S1F1 到 S18F0
3.1 Stream 编号不是随便定的
标准第 7 章和第 10 章把消息按活动类别分成 18 个 Stream,每个 Stream 下用 Function 编号区分具体消息。奇数 Function 是主消息(Primary),偶数 Function 是对应回复(Secondary)。比如S1F1是主机问设备“你在吗”,S1F2是设备回“我在”。S1F3是主机请求状态数据,S1F4是设备回状态数据。
Stream 的分配有明确意图:
| Stream | 用途 | 典型 Function |
|---|---|---|
| S0 | 事务中止和错误 | S0F0 中止事务 |
| S1 | 设备状态 | S1F1 在线查询、S1F3 状态请求 |
| S2 | 设备控制与诊断 | S2F13 设备常量请求 |
| S3 | 物料状态 | S3F17 载具状态 |
| S4 | 物料控制 | S4F1 开始物料传输 |
| S5 | 异常处理 | S5F1 报警上报 |
| S6 | 数据收集 | S6F11 事件报告 |
| S7 | 工艺程序管理 | S7F1 程序上传请求 |
| S8 | 控制程序传输 | S8F1 控制程序上传 |
| S9 | 系统错误 | S9F1 无法识别的设备 ID |
| S10 | 终端服务 | S10F1 终端显示 |
| S12 | 晶圆映射 | S12F1 晶圆映射请求 |
| S13 | 数据集传输 | S13F1 数据集传输 |
| S14 | 对象服务 | S14F1 对象创建 |
| S15 | 配方管理 | S15F1 配方上传请求 |
| S16 | 工艺管理 | S16F1 工艺步骤协调 |
| S17 | 设备控制与诊断 | S17F1 设备诊断 |
| S18 | 子系统控制与数据 | S18F1 子系统数据请求 |
Stream 11 在 2004 版中已删除,Stream 0 的 Function 0 专门用于中止事务。标准明确说“设备只需实现它支持的功能子集”,所以不是每台设备都要支持全部 18 个 Stream。实际选型时,先看设备手册声明支持哪些 Stream,再对照标准确认每个 Function 的消息体格式。
3.2 用 S1F3 和 S1F4 走一遍完整事务
假设主机要查设备当前的温度和压力两个状态变量。SVID 1 对应温度,SVID 3 对应压力。
主机发S1F3:
# 主机端构造 S1F3 请求 svid_list = encode_item(0o20, bytes([0x01, 0x03])) body = encode_item(0o00, [svid_list]) # 消息头:Stream=1, Function=3, W-bit=1(要求回复) # 实际发送时消息头由传输层封装,这里只关注消息体 print(f"S1F3 body: {body.hex()}")设备回S1F4:
# 设备端构造 S1F4 回复 # 假设温度=25.5,压力=1013.2 temp = encode_item(0o54, struct.pack('>d', 25.5)) # F8 浮点数 pressure = encode_item(0o54, struct.pack('>d', 1013.2)) reply_body = encode_item(0o00, [temp, pressure]) print(f"S1F4 body: {reply_body.hex()}")这里有个容易翻车的点:S1F4 回复的数据顺序必须和 S1F3 请求的 SVID 顺序一致。标准没有强制规定设备必须按请求顺序回复,但大多数设备实现是按顺序的。如果主机端按固定顺序解析,遇到不按顺序回复的设备就会把温度和压力读反。稳妥做法是在主机端按 SVID 值匹配,而不是按位置匹配。
3.3 W-bit 和事务超时的配合
消息头里有一个 W-bit(Wait-bit),表示发送方是否要求回复。S1F1的 W-bit 通常为 1,因为主机需要知道设备是否在线。S1F2的 W-bit 为 0,因为回复消息不需要再回复。如果主机发了 W-bit=1 的消息但设备没回,T3 超时后主机会报事务失败。标准第 6.5 节定义了 T3 超时:从发出主消息到收到回复第一个 block 的最大等待时间。T4 超时是多 block 消息的 block 间隔超时。
实际配置中,T3 通常设 45 秒,T4 设 45 秒。但有些老设备固件把 T3 硬编码成 10 秒,主机端如果设 45 秒,设备早就超时重发了。联调时先确认双方 T3/T4 配置一致,再排查其他问题。
4. 避坑排查:联调中最容易翻车的五个点
4.1 设备回了 S9F1 但主机没处理
现象:主机发S1F1后收到S9F1,事务中断。原因:S9F1表示“无法识别的设备 ID”,通常是消息头里的 Device ID 和设备实际 ID 不匹配。解决:检查主机端配置的 Device ID 是否和设备手册一致,注意 Device ID 范围是 0 到 32767,有些设备默认 ID 是 0,有些是 1。
4.2 多 block 消息的 block 顺序错乱
现象:设备回S6F11事件报告时数据错位。原因:多 block 消息的每个 block 有独立序号,接收方必须按序号重组。如果中间某个 block 丢失,T4 超时后整个消息作废。解决:抓包确认每个 block 的序号是否连续,检查网络层是否有丢包。SECS-I 的 block 序号是 1 字节,范围 0 到 255,超过 255 后回绕。
4.3 ASCII 字符串里的不可见字符
现象:设备回S1F2的 MDLN 字段看起来是空的,但长度不为零。原因:设备填了空格或\0,主机端打印时不可见。解决:解析 ASCII Item 时先按长度字段截取原始字节,再判断内容是否全为空白字符。标准没有规定字符串必须去除尾部空格,所以主机端要自己处理。
4.4 List 长度字段和实际元素数不一致
现象:设备解析主机发的S2F13时报格式错误。原因:List 的长度字段写的是元素个数,但实际拼进去的元素数量对不上。比如长度字段写 3,但只拼了 2 个 Item。解决:构造 List 时先收集所有子 Item 的编码结果,再根据列表长度写长度字段。不要先写长度再拼内容,容易数错。
4.5 T3 超时后设备才回复
现象:主机报 T3 超时,但抓包显示设备在超时后 1 秒发了回复。原因:设备处理慢,或者网络延迟。解决:适当增大 T3 值,但不要超过设备端的 T3 配置。如果设备端 T3 是 10 秒,主机端设 45 秒,设备会在 10 秒时重发主消息,导致主机收到重复回复。联调时双方 T3 必须一致。
5. 从标准到代码:用 S5F1 报警消息验证实现
5.1 S5F1 的消息体结构
S5F1 是设备主动上报报警的消息。标准第 10.9 节定义了它的消息体:一个 List,包含三个元素——报警代码(Binary,1 字节)、报警类型(Binary,1 字节)、报警描述(ASCII)。报警类型 0 表示报警清除,1 表示报警发生。
def build_s5f1(alarm_code, alarm_type, alarm_text): """构造 S5F1 报警消息体""" code_item = encode_item(0o20, bytes([alarm_code])) type_item = encode_item(0o20, bytes([alarm_type])) text_item = encode_item(0o60, alarm_text) return encode_item(0o00, [code_item, type_item, text_item]) # 报警代码 100,类型 1(发生),描述 "Temperature high" msg = build_s5f1(100, 1, "Temperature high") print(msg.hex())输出示例:0103016401010f54656d70657261747572652068696768。拆开看:01List 长度 3,03是第一个元素01 64(Binary 长度 1,值 100),01 01是第二个元素(Binary 长度 1,值 1),0f是第三个元素(ASCII 长度 15),后面 15 字节是Temperature high的 ASCII 编码。
5.2 用报警消息验证主机端解析器
主机端收到 S5F1 后,解析器要能正确还原三个字段。验证方法:构造一条已知内容的 S5F1,用主机端解析器解析,对比输出是否和输入一致。如果解析器把报警代码读成了报警类型,说明 List 元素顺序处理有误。
def parse_s5f1(body): """解析 S5F1 消息体,返回 (alarm_code, alarm_type, alarm_text)""" # 跳过 List 格式码和长度字段 assert body[0] == 0o00, "不是 List 格式" list_len = body[1] pos = 2 items = [] for _ in range(list_len): fmt = body[pos] length = body[pos + 1] data = body[pos + 2: pos + 2 + length] items.append((fmt, data)) pos += 2 + length alarm_code = items[0][1][0] alarm_type = items[1][1][0] alarm_text = items[2][1].decode('ascii') return alarm_code, alarm_type, alarm_text # 用上面构造的消息验证 body = bytes.fromhex("0103016401010f54656d70657261747572652068696768") print(parse_s5f1(body)) # 输出: (100, 1, 'Temperature high')这个解析器只处理了单层 List 和三种格式码,实际项目中需要递归处理嵌套 List 和更多格式码。但用它验证 S5F1 这种简单消息足够了。如果解析结果和构造输入不一致,先检查长度字段的字节序,再检查 ASCII 解码是否有多余空白。
5.3 一个习惯:每次改完解析器都跑一遍回归
我自己的做法是维护一组“标准消息样本”,覆盖 S1F2、S1F4、S5F1、S6F11 这几种最常用的消息。每次改完解析器代码,先跑这组样本,确认输出和预期一致,再上设备联调。这样能把大部分格式解析问题挡在联调之前。从那以后我每次改完 SECS-II 解析逻辑,都强制走一遍样本回归,省下了不少在产线上抓包的时间。希望帮到你。
本文还有配套的精品资源,点击获取