简介:这份SEMI E5-1104标准文档是半导体设备通信SECS-II协议的核心英文原版,面向从事半导体制造设备与控制系统开发、维护的工程师,以及参与产线自动化集成项目的技术人员。它系统定义了智能设备与主机之间的消息交换规则,涵盖消息结构、流与函数、事务与对话协议、数据结构等内容,并逐一说明Stream 0至Stream 17的消息用途,涉及设备状态、控制与诊断、物料状态、异常处理、数据采集、工艺程序管理、晶圆映射等场景,同时给出T1至T4事务超时机制与错误恢复逻辑,便于开发者理解设备端与主机端的标准实现方式。资源包共1个PDF文件,约2.66MB,为高清英文原版,适合对照查阅条款与流函数定义。目前已有343人学习,可作为设备集成、协议对接与问题排查时的案头参考。
1. 半导体设备通信SECS协议文档:SEMI E5-1104到底在定义什么
如果你在半导体设备现场调过通信,大概率遇到过这种场景:设备端日志显示消息已发出,上位机却报“格式错误”,两边工程师对着各自的代码查了一整天,最后发现是某个数据项的格式码用错了——一个该用A(ASCII)的字段写成了B(Binary)。这类问题的根因,几乎都能追溯到同一份文档:SEMI E5-1104,也就是SECS-II的消息内容定义标准。
SECS-II全称Semiconductor Equipment Communication Standard II,是半导体设备与主机之间通信的消息层协议。它规定了消息的结构、数据项的编码方式、以及每条消息的语义。E5-1104是SEMI发布的原版标准文档,英文高清版,内容涵盖SECS-II消息的完整定义和应用规则。它解决的核心问题是:让不同厂商的设备和控制软件能用同一套“语言”对话。适合谁读?设备端固件工程师、EAP(Equipment Automation Program)开发者、CIM系统集成人员,以及需要对接SECS/GEM接口的任何从业者。
2. SECS-II消息结构拆解:从数据项到消息体的编码逻辑
2.1 数据项格式码:SECS-II的“字母表”
SECS-II的消息体由一个个数据项(Data Item)组成,每个数据项的第一个字节是格式码(Format Code),它决定了后续数据的类型和长度编码方式。E5-1104里定义的核心格式码如下表:
| 格式码 | 类型 | 含义 | 长度字节数 |
|---|---|---|---|
| 0o00 | List | 列表(嵌套结构) | 1-3 |
| 0o20 | Binary | 二进制 | 1-3 |
| 0o30 | Boolean | 布尔 | 1-3 |
| 0o40 | ASCII | 字符串 | 1-3 |
| 0o50 | JIS-8 | 日文编码 | 1-3 |
| 0o60 | I8 | 8字节有符号整数 | 1-3 |
| 0o70 | I1 | 1字节有符号整数 | 1-3 |
| 0o71 | I2 | 2字节有符号整数 | 1-3 |
| 0o72 | I4 | 4字节有符号整数 | 1-3 |
| 0o74 | F8 | 8字节浮点数 | 1-3 |
| 0o76 | F4 | 4字节浮点数 | 1-3 |
| 0o80 | U8 | 8字节无符号整数 | 1-3 |
| 0o90 | U1 | 1字节无符号整数 | 1-3 |
| 0o91 | U2 | 2字节无符号整数 | 1-3 |
| 0o92 | U4 | 4字节无符号整数 | 1-3 |
格式码的高6位表示类型,低2位表示长度字节数。比如0o41表示ASCII类型、1个长度字节;0o42表示ASCII类型、2个长度字节。这个设计让SECS-II在编码效率上做了优化——短字符串只占1字节长度描述,长字符串才用2或3字节。
实际编码时,一个ASCII字符串“Hello”的完整字节序列是:0x41 0x05 0x48 0x65 0x6C 0x6C 0x6F。第一个字节0x41是格式码,第二个字节0x05是长度,后面5个字节是内容。
2.2 消息头与消息体的组装规则
一条完整的SECS-II消息由消息头(10字节)和消息体组成。消息头包含:
- 设备ID(2字节,高位在前)
- 消息ID(W-Bit + Stream + Function,共3字节)
- 系统字节(1字节,含R-Bit和等待位)
- 保留字段(2字节)
- 消息体长度(2字节)
消息体的组装遵循严格的嵌套规则。List类型可以包含其他数据项,形成树状结构。E5-1104里对List的嵌套层数没有硬性上限,但实际实现中一般不超过8层,否则解析栈容易溢出。
下面是一个Python示例,演示如何按E5-1104的规则编码一条S1F1(Are You There)消息:
import struct def encode_ascii(s): """编码ASCII数据项,自动选择长度字节数""" data = s.encode('ascii') length = len(data) if length <= 255: return bytes([0x41, length]) + data elif length <= 65535: return bytes([0x42]) + struct.pack('>H', length) + data else: return bytes([0x43]) + struct.pack('>I', length)[1:] + data def encode_list(items): """编码List数据项,items是已编码的字节列表""" body = b''.join(items) length = len(body) if length <= 255: return bytes([0x01, length]) + body elif length <= 65535: return bytes([0x02]) + struct.pack('>H', length) + body else: return bytes([0x03]) + struct.pack('>I', length)[1:] + body def build_secs_message(stream, function, w_bit, body_items): """组装完整SECS-II消息""" # 消息头:设备ID(2) + 消息ID(3) + 系统字节(1) + 保留(2) + 体长度(2) device_id = 0x0000 msg_id = ((w_bit & 0x01) << 15) | ((stream & 0x7F) << 8) | (function & 0xFF) system_byte = 0x00 body = encode_list(body_items) header = struct.pack('>H', device_id) header += struct.pack('>I', msg_id)[1:] # 取低3字节 header += bytes([system_byte]) header += b'\x00\x00' header += struct.pack('>H', len(body)) return header + body # 构建S1F1,W-Bit=1,消息体为空List msg = build_secs_message(1, 1, 1, []) print(msg.hex())这段代码的关键点:msg_id的组装方式决定了W-Bit、Stream、Function三个字段的位布局。E5-1104规定消息ID占3字节,最高位是W-Bit,接着7位是Stream,低8位是Function。struct.pack('>I', msg_id)[1:]这个操作是取4字节大端整数的低3字节,等价于手动做位运算后拼接。
参数说明:w_bit为1表示需要回复,0表示不需要;stream范围0-127;function范围0-255。消息体长度字段是2字节无符号整数,所以单条SECS-II消息体最大65535字节。超过这个长度需要分片,但E5-1104本身不定义分片机制,那是上层协议的事。
2.3 常用消息的格式速查
E5-1104定义了数百条消息,但现场最常用的就那么几十条。下面列出高频消息的格式定义,方便快速对照:
| 消息 | 方向 | 消息体结构 | 典型用途 |
|---|---|---|---|
| S1F1 | 双向 | 空 | 心跳检测 |
| S1F2 | 双向 | L[2]: A, A | 心跳回复(MDLN, SOFTREV) |
| S1F3 | 主机→设备 | L[n]: U4... | 查询状态变量 |
| S1F4 | 设备→主机 | L[n]: L[2]: U4, A | 状态变量值 |
| S1F13 | 主机→设备 | L[2]: A, A | 建立通信 |
| S1F14 | 设备→主机 | L[2]: B, L[2]: A, A | 通信建立确认 |
| S2F33 | 主机→设备 | L[2]: U4, L[n]: L[2]: U4, L[m]: U4... | 定义报告 |
| S2F34 | 设备→主机 | L[1]: B | 定义报告确认 |
| S2F41 | 主机→设备 | L[2]: A, L[n]: L[2]: A, A | 远程命令 |
| S2F42 | 设备→主机 | L[1]: B | 命令确认 |
| S5F1 | 设备→主机 | L[3]: B, U4, A | 报警上报 |
| S6F11 | 设备→主机 | L[3]: U4, U4, L[n]: ... | 事件报告 |
| S7F19 | 主机→设备 | 空 | 查询工艺程序列表 |
| S7F20 | 设备→主机 | L[n]: A | 工艺程序列表 |
这张表建议打印出来贴在工位上。现场调试时,90%的通信问题都能通过对照这张表定位到具体字段。
3. 用Python实现SECS-II消息解析器:从字节流到结构化数据
3.1 解析器的核心逻辑与递归下降
解析SECS-II消息比编码要复杂,因为需要处理嵌套List和变长数据项。核心思路是递归下降:遇到List就递归解析其子项,遇到基本类型就按格式码读取对应字节数。
import struct class SecsItem: def __init__(self, fmt_code, value): self.fmt_code = fmt_code self.value = value def __repr__(self): return f"SecsItem(fmt=0o{self.fmt_code:o}, value={self.value!r})" def parse_item(data, offset): """解析单个数据项,返回(SecsItem, 新偏移量)""" if offset >= len(data): raise ValueError("数据不完整") fmt_byte = data[offset] fmt_code = fmt_byte >> 2 # 高6位是格式码 len_bytes = (fmt_byte & 0x03) # 低2位是长度字节数 if len_bytes == 0: raise ValueError(f"非法长度字节数: 0, 格式码=0o{fmt_code:o}") offset += 1 # 读取长度 if len_bytes == 1: length = data[offset] offset += 1 elif len_bytes == 2: length = struct.unpack('>H', data[offset:offset+2])[0] offset += 2 else: length = struct.unpack('>I', b'\x00' + data[offset:offset+3])[0] offset += 3 if fmt_code == 0o00: # List items = [] end = offset + length while offset < end: item, offset = parse_item(data, offset) items.append(item) return SecsItem(fmt_code, items), offset # 基本类型 raw = data[offset:offset+length] offset += length if fmt_code == 0o20: # Binary value = raw elif fmt_code == 0o30: # Boolean value = [b != 0 for b in raw] elif fmt_code == 0o40: # ASCII value = raw.decode('ascii') elif fmt_code == 0o70: # I1 value = list(struct.unpack(f'>{length}b', raw)) elif fmt_code == 0o71: # I2 value = list(struct.unpack(f'>{length//2}h', raw)) elif fmt_code == 0o72: # I4 value = list(struct.unpack(f'>{length//4}i', raw)) elif fmt_code == 0o90: # U1 value = list(raw) elif fmt_code == 0o91: # U2 value = list(struct.unpack(f'>{length//2}H', raw)) elif fmt_code == 0o92: # U4 value = list(struct.unpack(f'>{length//4}I', raw)) elif fmt_code == 0o74: # F8 value = list(struct.unpack(f'>{length//8}d', raw)) elif fmt_code == 0o76: # F4 value = list(struct.unpack(f'>{length//4}f', raw)) else: value = raw # 未知类型保留原始字节 return SecsItem(fmt_code, value), offset def parse_secs_message(data): """解析完整SECS-II消息""" if len(data) < 10: raise ValueError("消息头不完整") device_id = struct.unpack('>H', data[0:2])[0] msg_id = struct.unpack('>I', b'\x00' + data[2:5])[0] w_bit = (msg_id >> 15) & 0x01 stream = (msg_id >> 8) & 0x7F function = msg_id & 0xFF system_byte = data[5] body_length = struct.unpack('>H', data[8:10])[0] body_data = data[10:10+body_length] items = [] offset = 0 while offset < len(body_data): item, offset = parse_item(body_data, offset) items.append(item) return { 'device_id': device_id, 'w_bit': w_bit, 'stream': stream, 'function': function, 'system_byte': system_byte, 'body': items }这段解析器的关键设计:parse_item返回新偏移量而不是修改全局状态,这样递归调用时不会互相干扰。长度字节数的处理是容易翻车的地方——E5-1104规定长度字节数可以是1、2、3,但值为0是非法组合,因为格式码低2位为0意味着没有长度字段,这在标准里是不允许的。
参数说明:fmt_code是格式码的高6位,通过fmt_byte >> 2提取;len_bytes是低2位,决定后续读几个字节的长度。对于3字节长度,代码里用b'\x00' + data[offset:offset+3]补成4字节再解包,这是处理24位整数的常用技巧。
3.2 解析结果验证与常见格式错误
解析器写完后,需要用真实报文验证。一个典型的S1F2回复报文(十六进制)如下:
00 00 00 81 02 00 00 00 00 0E 01 0C 40 06 4D 4F 44 45 4C 31 40 04 31 2E 30 30逐段拆解:前10字节是消息头,00 00是设备ID,00 81 02是消息ID(W-Bit=0, Stream=1, Function=2),00是系统字节,00 00是保留字段,00 0E是消息体长度14字节。消息体从01 0C开始:01是List格式码(长度1字节),0C是长度12;接着40 06是ASCII格式码、长度6,内容MODEL1;然后40 04是ASCII、长度4,内容1.00。
用上面的解析器跑这段数据,应该得到:
{ 'device_id': 0, 'w_bit': 0, 'stream': 1, 'function': 2, 'system_byte': 0, 'body': [SecsItem(fmt=0o00, value=[ SecsItem(fmt=0o40, value='MODEL1'), SecsItem(fmt=0o40, value='1.00') ])] }常见格式错误有三种:一是长度字段与实际内容不匹配,比如声明长度6但只给了5个字节;二是List的嵌套长度计算错误,子项总长度与List声明的长度不一致;三是格式码与数据类型不匹配,比如用0o70(I1)去编码一个需要4字节的整数。这三种错误在解析时都会抛出异常或产生错误结果,排查时优先检查长度字段。
3.3 与设备联调时的日志抓取方法
实际联调时,光有解析器不够,还需要抓到原始字节流。常见做法是在通信层加一个透传日志钩子,把收发的原始字节按十六进制打印出来,同时用解析器转成可读格式。
import logging logger = logging.getLogger('secs_trace') def trace_message(direction, raw_data): """记录SECS消息的原始字节和解析结果""" hex_str = ' '.join(f'{b:02X}' for b in raw_data) logger.info(f"[{direction}] RAW: {hex_str}") try: parsed = parse_secs_message(raw_data) logger.info(f"[{direction}] PARSED: S{parsed['stream']}F{parsed['function']} " f"W={parsed['w_bit']} BODY={parsed['body']}") except Exception as e: logger.error(f"[{direction}] PARSE FAILED: {e}")这个钩子挂在socket的send和recv之后即可。注意日志里不要只打解析结果,原始十六进制才是排查问题的最终依据——解析器本身可能有bug,但原始字节不会骗人。
4. 避坑指南:SECS-II对接中最容易翻车的五个地方
4.1 长度字段的“差一字节”问题
现象:设备端发送的消息,上位机解析时报“数据不完整”或“长度超出预期”。抓包看原始字节,发现List声明的长度比实际子项总长度少1或多1。
原因:E5-1104规定List的长度字段是消息体的字节数,不包括格式码和长度字段本身。很多开发者第一次实现时会把格式码和长度字段也算进去,导致长度偏大。另一种情况是子项编码时多算了填充字节。
解决:编码完成后,先计算所有子项编码后的总字节数,再把这个数写入List的长度字段。建议在编码函数里加断言:assert len(body) == declared_length,上线前跑一遍全量消息类型。
4.2 ASCII与JIS-8的编码混淆
现象:包含日文或特殊字符的字段,在设备端显示正常,到上位机变成乱码。
原因:E5-1104定义了ASCII(0o40)和JIS-8(0o50)两种文本格式码。有些设备厂商在实现时把所有文本都按JIS-8编码,但格式码仍标0o40,导致上位机按ASCII解码时出错。
解决:对接前先确认设备端使用的文本编码。如果设备文档没有明确说明,发一条包含已知字符的测试消息,抓原始字节对比。ASCII的字符范围是0x00-0x7F,JIS-8会用到0x80以上的字节。发现格式码与内容不匹配时,要么改设备端编码,要么在上位机做兼容处理。
4.3 W-Bit与回复超时的配合
现象:主机发送S1F1后等待S1F2回复,但设备端没有回复,主机一直阻塞直到超时。
原因:W-Bit(Wait-Bit)设置为1表示需要回复,但设备端可能因为消息处理队列满、或者该消息被配置为不需要回复而丢弃。另一种情况是设备端回复了,但主机的T3定时器设置太短,回复还没到就超时了。
解决:E5-1104本身不定义超时时间,T3是SEMI E37(HSMS)里的概念,默认45秒。如果现场网络延迟大,适当调大T3。同时检查设备端的消息处理逻辑,确认W-Bit=1的消息一定会触发回复。调试阶段可以把T3临时调到120秒,排除超时因素后再逐步收紧。
4.4 List嵌套层数过深导致解析栈溢出
现象:解析某些复杂消息时程序崩溃,报“RecursionError”或栈溢出。
原因:SECS-II的List可以无限嵌套,但递归解析器的调用栈深度有限。某些设备在S6F11事件报告里嵌套了十几层List,Python默认递归限制是1000层,虽然理论上够用,但如果每层解析都占用较多栈帧,实际可能提前溢出。
解决:两种方案。一是把递归解析改成迭代加显式栈,彻底消除递归深度限制;二是提高Python的递归限制:sys.setrecursionlimit(5000)。推荐第一种,因为迭代版本更可控。另外在解析前先检查List嵌套深度,超过阈值(比如16层)直接报错,避免恶意或异常报文拖垮解析器。
4.5 消息体长度超过65535字节的处理
现象:发送大消息(比如完整的工艺程序列表)时,消息体长度字段溢出,接收端解析出错误的长度。
原因:E5-1104的消息头里,消息体长度字段是2字节无符号整数,最大65535。超过这个长度的消息体无法用单条SECS-II消息承载。
解决:E5-1104本身不定义分片机制,实际做法是把大消息拆成多条小消息,用上层协议(如GEM的工艺程序管理)的分段传输功能。如果设备端不支持分段,只能压缩消息体或减少单次传输的数据量。编码时加检查:if len(body) > 65535: raise ValueError("消息体超长,需要分片")。
5. 从E5-1104到GEM:消息定义如何支撑上层应用
5.1 E5-1104与E30/E37的关系
E5-1104定义的是消息内容,但消息怎么传输、连接怎么建立,是另外的标准。SEMI E37(HSMS)定义了基于TCP/IP的传输层,E30(GEM)定义了设备行为模型。三者的关系是:E37负责把字节流从A送到B,E5-1104负责定义字节流里每个字节的含义,E30负责定义设备在什么状态下该发什么消息。
实际开发中,通信库通常把这三层都封装好了。但排查问题时,需要清楚问题出在哪一层。比如连接建立失败,查E37;消息解析失败,查E5-1104;设备行为不符合预期(比如该上报的事件没上报),查E30。
5.2 用E5-1104定义自定义消息
标准消息不够用时,可以定义自定义消息。E5-1104允许在Stream 64-127范围内定义厂商私有消息。定义时需要注意:消息体的数据项格式必须符合E5-1104的编码规则,否则其他厂商的设备无法解析。
一个自定义消息的示例:S64F1,用于查询设备自定义状态。
# 自定义S64F1:查询自定义状态 # 消息体:L[1]: U4(状态ID) custom_query = build_secs_message(64, 1, 1, [ encode_list([encode_u4(1001)]) # 状态ID=1001 ]) # 自定义S64F2:回复自定义状态 # 消息体:L[2]: U4, A(状态ID, 状态值) custom_reply = build_secs_message(64, 2, 0, [ encode_list([ encode_u4(1001), encode_ascii("RUNNING") ]) ])自定义消息的关键是双方约定好消息体和语义。建议在项目初期就把自定义消息的格式写成文档,和E5-1104的格式定义放在一起,避免后期扯皮。
5.3 用最小系统验证消息定义
学E5-1104最有效的方法不是通读文档,而是搭一个最小验证系统:一个模拟设备端,一个模拟主机端,中间用socket连接,跑几条基本消息。
import socket import threading def mock_device(port): """模拟设备端:收到S1F1回复S1F2""" srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(('127.0.0.1', port)) srv.listen(1) conn, _ = srv.accept() while True: data = conn.recv(4096) if not data: break msg = parse_secs_message(data) if msg['stream'] == 1 and msg['function'] == 1: # 回复S1F2 reply = build_secs_message(1, 2, 0, [ encode_list([ encode_ascii("MOCK-EQ"), encode_ascii("1.0.0") ]) ]) conn.send(reply) # 启动模拟设备 t = threading.Thread(target=mock_device, args=(5000,), daemon=True) t.start() # 主机端发送S1F1 host = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host.connect(('127.0.0.1', 5000)) s1f1 = build_secs_message(1, 1, 1, []) host.send(s1f1) resp = host.recv(4096) print(parse_secs_message(resp))这个最小系统跑通后,再逐步加入S1F3/S1F4、S2F33/S2F34、S6F11等消息,每加一条就验证一次。这种增量验证的方式比一次性实现所有消息再联调要高效得多。
5.4 一个容易忽略的细节:消息ID的位序
最后说一个我踩过的坑。E5-1104里消息ID是3字节,但不同文档对这三个字节的位序描述不一致。有的文档写“Byte 0是高位”,有的写“Byte 2是高位”。实际抓包验证后确认:消息ID按大端序传输,第一个字节是最高字节。W-Bit在最高字节的最高位,Stream在最高字节的低7位,Function在第二个字节。
这个细节在实现编码器时如果搞反了,消息会发到错误的Stream/Function上,设备端要么不回复,要么回复错误的消息。验证方法很简单:发一条S1F1,抓包看第二个字节是不是0x81(W-Bit=1, Stream=1)。如果是0x01,说明位序搞反了。
我在第一次实现SECS-II编解码器时,因为没仔细核对位序,调试了整整两天。后来养成的习惯是:每实现一条新消息,先用已知正确的报文做单元测试,确认编码结果与预期字节完全一致后再接入通信层。这个习惯帮我省下了大量联调时间。希望帮到你。
本文还有配套的精品资源,点击获取