ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SEMI E87-0301载具管理实战:状态机、事件上报与联调避坑

SEMI E87-0301载具管理实战:状态机、事件上报与联调避坑

简介:SEMI E87-0301 是半导体 GEM300 体系下的 Carrier Management System(CMS)临时规范,面向负责 300mm 晶圆厂自动化物料搬运的工艺集成工程师、设备工程师与 MES 厂商。标准以详细状态模型和场景定义了主机视图与生产设备在运载器传输协调、执行、完成阶段的行为要求,明确覆盖 AMHS 车辆与负载端口之间的传输、设备内部缓冲空间的运载器进出、设备与负载端口访问模式切换、运载器到负载端口关联、Carrier ID 验证及槽位映射验证等关键机制,为自动化与手动搬运提供统一通信准则。资源为单文件 PDF 原版标准,压缩包约 542KB,便于查阅、打印与归档。文档同时指出其作为 provisional 标准的限制,涉及与 SEMI E15.1 300mm 负载端口的衔接,以及集成设备配合 AMHS 时尚未完全定义的异常处理和错误恢复场景,有助于读者更准确理解标准适用范围和未来演进方向。目前已有 2130 人学习下载,适合需要深入掌握 CMS 运载器传输管理细节的半导体自动化从业者。

1. 为什么设备都跑通了,载具还在“乱跑”:SEMI E87-0301 到底管什么

某晶圆厂做自动化联调时,设备已经能自动加工,但载具在搬运轨道上不停报错:MES 显示载具在等待区,实际已经被机械手抓走;主控下发搬运指令,设备侧又因为“载具状态冲突”直接拒绝。整个团队查了三天,最后发现所有问题都指向同一个地方:设备侧和主控侧对“载具现在什么状态、能不能动”根本没有共同语言。这就是 SEMI E87-0301 要解决的事。E87-0301 是一套关于载具管理的通信规范,它统一了载具 ID 的表示、载具状态的迁移、位置上报和搬运请求的交互方式,让设备、搬运系统和工厂主控能用同一种语言描述“这个载具在哪、能不能用、正在做什么”。它不解决工艺问题,解决的是载具全生命周期管理的信息一致性问题。适合设备软件工程师、AMHS 集成工程师、MES 接口开发,以及所有被“状态对不上”折腾过的现场人员。

2. E87-0301 核心模型:载具状态机与消息交互

很多工程师第一次接触 E87-0301 时,容易把它当成一份“报文格式文档”,直接去找消息模板。实际上,真正撑起这套标准的是一张状态机和一套交互规则。不理解这两样东西,就算把报文抄得一字不差,联调时还是会乱。

2.1 载具生命周期状态机:把“乱跑”变成可预期

E87-0301 把载具从进入设备区域到离开设备区域的整个过程拆成了几个稳定状态。常见状态包括 AVAILABLE(空闲可用)、IN_USE(已被设备占用)、IN_PROCESS(内部晶圆正在加工)、HOLD(暂停)、NOT_AVAILABLE(不可用)。这些状态在设备侧和主控侧必须使用同一个名字、同一个含义。比如 IN_PROCESS 指的是“载具内的晶圆正在被设备加工”,此时任何搬运请求都应被拒绝,而不是定义为“设备正在忙”。

状态之间不是自由跳转。一个载具不能从 AVAILABLE 直接跳到 IN_PROCESS,因为中间必须经历“装载到端口”这个动作;同样,IN_PROCESS 结束后不能直接跳到 IN_USE,因为加工完必须先完成卸载确认。下面是一张我在设备软件里常用的基础迁移表:

当前状态触发事件下一状态允许条件
AVAILABLE载具装载到设备端口IN_USE端口已确认,载具 ID 已读取
IN_USE开始加工IN_PROCESS载具内晶圆批次已绑定
IN_PROCESS加工完成AVAILABLE卸载完成,端口已释放
IN_PROCESS工艺异常HOLD异常码已记录
HOLD人工恢复AVAILABLE恢复确认完成
任意状态设备下线维护NOT_AVAILABLE设备已锁定

这张表看起来简单,但现场大量翻车都出现在“跳状态”。我见过某设备的软件直接把载具从 IN_PROCESS 改成 AVAILABLE,理由是“加工完了”,但主控侧还没收到“卸载完成”事件,于是主控认为载具还在设备里,搬运系统自然不敢动它。标准里明确要求,状态迁移必须伴随对应的事件上报,而不是本地改一个变量就结束。

这里还有一个新手容易误解的点:本地状态和主控状态不必实时一样,但最终必须收敛到一致。E87-0301 给出的模型是“旧状态 → 新状态”的事件上报,主控收到后自己判断合法性。设备侧不用提前向主控查询“你现在的状态是什么”,只要按照真实发生的动作上报即可。反向来看,如果主控侧发现上报的迁移不合法,应拒绝并报警,而不是默默接受。

2.2 消息交互模型:事件上报、命令下发与事务确认

E87-0301 的交互模型非常接近“订阅/上报”:设备侧是状态源,主控侧是决策方。设备发现载具状态变化,主动上报一条事件消息,例如载具状态变化、载具到达端口、载具离开端口;主控需要搬运时,下发一条搬运请求或传送请求,设备侧执行后返回完成或错误。整个过程中,双方都需要维护同一份载具状态表,任一次上报漏了,后续就全对不上。

消息通常走 SECS/GEM 通道,例如用 HSMS 传输 SECS-II 数据消息。E87 本身更关心的是消息内容,而不是底层传输。消息体一般由消息头、载具 ID、旧状态、新状态、位置标识、事件时间戳组成。下面是我在设备端常用的一种字段结构,实际实现可以按设备平台调整:

字段类型说明
MESSAGE_ID字符串全局唯一消息 ID,用于去重和确认
CARRIER_ID字符串载具 ID,全局唯一
OLD_STATE枚举迁移前状态
NEW_STATE枚举迁移后状态
PORT_ID字符串端口或存储区标识
TIMESTAMPISO8601事件发生时间,建议统一 UTC

这里最常见的误解是:设备上报时必须携带当前主控侧的值。其实协议允许上报“旧状态 + 新状态”,主控自己判断是否合法。因此设备侧只要按真实发生的情况上报即可,不需要先查询主控的当前状态。

命令下发的核心是“事务确认”。主控发起的搬运请求不能只说一句“搬过去”,而必须要求设备侧在限定时间内先返回“已收到”,再返回“已完成”或“已拒绝”。这个确认机制是用来区分“请求丢了”和“请求失败”的。很多现场只做了完成回报,没做收到回报,结果超时后主控根本不知道设备是在执行还是已经忘了这条命令。

2.3 选型理由:为什么是 E87 而不是自己定一套 JSON

有人会问:我的设备只有一台,MES 也是自己写的,直接定一个“载具状态 + 位置”的 JSON 接口不行吗?短期看可以,但一旦要接 AMHS、接第二个设备、接第三家搬运系统,私有协议就会让每一条通道都要单独写转换层,而且状态定义很容易出现“同一状态两种含义”。

E87-0301 的价值在于把状态名、迁移规则、事件字段都统一了。你不需要一次性实现标准里所有消息,但至少状态图和事件字段建议按它来建模。另一个理由是它与相邻标准天然配套:E84 负责端口和载具的物理握手,E90 负责设备内搬运控制,E87 负责更上层的载具跟踪。使用同一套概念,联调时沟通成本会低很多。

此外,E87-0301 这个版本虽然发布较早,但至今仍然被大量设备商作为默认接口。很多设备控制软件已经把 E87 消息集成到标准 SECS/GEM 组件里,这意味着你选 E87,往往能直接复用成熟 SDK;选私有协议,几乎每个新设备都要从零写适配。我做项目时通常建议:即便你的系统只需要“载具在哪”这一个功能,也把状态机按 E87 建好,未来接 AMHS 时不用返工。

3. 按 E87-0301 落地最小载具管理:设备端状态机与主控端消息

这一章给出一个最小可用的落地路径。不需要一次性实现全部消息,先把状态上报和搬运请求跑通,后面的功能都是在这个骨架上加。整个落地路径分三步:确定 ID 读取方式与消息通道、实现状态机与上报骨架、处理命令时序。

3.1 第一步:确定载具 ID 读取方式、消息通道与读取时机

载具 ID 是 E87 的“主键”。设备在载具装载时通过条码扫描、RFID 读取或人工录入获得 ID,之后所有消息都必须携带这个 ID。这里的关键不是“读什么技术”,而是“什么时候读”。常见做法是:在载具被放置到设备端口并完成物理握手后,设备主动读取一次 ID,并把结果缓存在本地。后续所有上报都用这个缓存,避免在加工过程中反复读标签,也避免读到一半标签被遮挡。

读取失败时不能把载具放进流程。至少做三次重读,每次间隔 500ms;连续失败后把端口置为 HOLD,并上报 NOT_AVAILABLE。有些 RFID 读头在金属环境下不稳定,这时候要用条码扫码枪做人工兜底。我在现场遇到过读头对某个载具位置先天不识别的情况,最后是靠“RFID 为主、条码辅助”双通道解决的问题。

消息通道的选择上,老设备通常用 HSMS 连接主控;新设备如果已经有 E84 或 E90,消息通道可以直接复用同一套 HSMS 链路。我比较顺的方案是:把 E87 消息映射成 SECS-II 的 S6F11 事件上报和 S2F41 命令执行。这样主控侧只要有一个标准 GEM 连接,就能同时处理 E84 和 E87,不需要额外开端口。如果现场不让走 SECS,也可以用 XML over TCP,但要注意自己实现消息确认,工作量大不少。

3.2 第二步:实现状态迁移与事件上报的最小骨架

下面是一个极简的设备侧状态机骨架,用 Python 表达思路,实际项目中可以套到 C++ 或 C#。它不是标准参考实现,而是帮助你理解“状态迁移 + 事件上报”的最小代码结构:

CARRIER_STATES = { "AVAILABLE", "IN_USE", "IN_PROCESS", "HOLD", "NOT_AVAILABLE" } # 只允许标准迁移:键是 (旧状态, 新状态) ALLOWED_TRANSITIONS = { ("AVAILABLE", "IN_USE"), ("IN_USE", "IN_PROCESS"), ("IN_PROCESS", "AVAILABLE"), ("IN_PROCESS", "HOLD"), ("IN_USE", "HOLD"), ("HOLD", "AVAILABLE"), ("AVAILABLE", "NOT_AVAILABLE"), ("IN_USE", "NOT_AVAILABLE"), } def change_state(carrier_id, old_state, new_state, port_id): if old_state not in CARRIER_STATES or new_state not in CARRIER_STATES: raise ValueError(f"unknown state: {old_state} -> {new_state}") if (old_state, new_state) not in ALLOWED_TRANSITIONS: raise ValueError(f"disallowed transition: {old_state} -> {new_state}") # 上报前先持久化本地状态,避免上报失败后丢失 local_db.update(carrier_id, new_state) # 发送 E87 事件:载具状态变化 send_event( event_code="CARRIER_STATUS_CHANGE", carrier_id=carrier_id, old_state=old_state, new_state=new_state, port_id=port_id, timestamp=now_utc(), )

这段代码的逻辑:先检查状态和迁移合法性,再持久化本地状态,最后发送事件。注意顺序很重要——先落库再上报,这样即使网络报错了,本地也能查到实际状态,后续主控来查询时还能恢复。很多设备侧翻车都是先上报后落库,消息发出去了本地还是旧值,后续命令一来就乱。

这里有两个参数需要特别说明。port_id在 E87 消息里不是可选项,它决定载具在设备内的物理位置,必须由设备软件传入真实端口号。timestamp也要统一,建议都用 UTC,否则跨时区或跨系统联调时,主控按时间戳排序会把事件顺序搞反。另一个容易被忽略的点是:change_state()只负责状态迁移,不负责搬运动作。搬运动作应放在命令处理函数里,等物理动作完成后再调change_state()。

3.3 第三步:处理搬运请求的命令时序与去重

状态上报只是“说”,真正执行搬运还要接“听”。主控下发搬运请求后,设备侧需要按顺序执行:先检查载具当前状态是否允许搬运,再执行物理动作,最后上报状态变化。下面是一张用于联调的基本时序表:

步骤调用方动作成功响应
1主控下发搬运请求,携带载具 ID 和目标端口请求收到,返回处理中
2设备锁定载具,执行搬运动作动作完成
3设备上报载具状态变化:旧端口→新端口主控确认
4主控更新载具位置记录完成

实际联调时,我遇到过设备在第 2 步和第 3 步之间直接跳过上报,导致主控一直认为载具还在旧位置。所以我在代码里会在执行完物理动作后强制调用一次change_state(),哪怕状态值没有变化,也要补一条“位置变更”事件。这是最小系统里最容易漏的一环。

如果现场要求 XML 报文,可以把 E87 消息映射成下面的结构。这只是常见的映射方式,不建议当成标准定义,关键是字段语义一致:

<CarrierStatusChange> <MessageId>20241115-123456</MessageId> <CarrierId>FOUP-0001</CarrierId> <OldState>IN_USE</OldState> <NewState>IN_PROCESS</NewState> <PortId>P1</PortId> <Timestamp>2024-11-15T12:34:56Z</Timestamp> </CarrierStatusChange>

MessageId用于去重,PortId是设备内端口,Timestamp统一用 UTC。这里要注意:设备重发同一事件时必须带同一个MessageId,而不是重新生成一个。否则主控会认为是新事件,进而把它当成一次新的状态迁移处理,造成状态重复或回滚。

4. E87-0301 联调必调参数:Carrier ID、超时重传与并发优先级

这个章节是联调时最耗时间的部分。标准没有把所有数值写死,但现场必须提前约定,否则双方工程师会在“该等几秒”“该信谁的”这些问题上反复扯皮。

4.1 Carrier ID:长度、字符集、校验位与统一转换

Carrier ID 是整套系统的主键,最容易出问题。典型约定是:上限 32 个字符,使用 ASCII 可打印字符,不允许空格和中文;条码/RFID 读取时建议统一为十六进制字符串或原样字符串。注意同一工厂内,不同设备可能读出不同格式:有的读条码会带校验位,有的不带。解决方式是接入主控时做一次规则转换,并把转换规则写死到设备配置文件。

下面是我在项目里常写的字段约定表:

参数推荐值说明
最大长度32超长必须告警,不能静默截断
字符集ASCII 0x21-0x7E排除空格、控制字符
校验CRC16 或末尾校验位用于 RFID 读取误码检测
显示格式大写避免大小写不一致导致匹配失败

联调时建议在报文里打印每个字符的十六进制,很多“乱码”其实是 0x20 空格混进去了。还有一次,某个读头把标签里的十六进制字符串直接当成 ASCII 文本发送,结果主控收到的是一串乱码。后来约定:RFID 读出的原始数据一律转为大写十六进制再作为 Carrier ID,两边都按 hex 处理,问题才消失。

4.2 事件上报的超时与重传:确认、退避与去重窗口

E87 的事件上报在实际链路里可能丢包,所以需要一个确认机制。常见做法是:设备发送状态变化后,若在 N 秒内未收到主控确认,则重发;重发次数超过 M 次就报警并置本地 HOLD。不要无限重发,否则主控会收到重复消息,进一步造成状态冲突。

参数经验值适用场景
首次超时5s本地以太网
重发次数3 次一般 HSMS 链路
重试间隔2s / 5s / 10s递增退避
超时后动作载具置 HOLD避免带错误状态流动

如果你发现链路抖动比较多,超时值可以放宽到 15s,但重发次数不要增加,因为主控侧往往有 3 次去重窗口。重发时一定要带上原消息 ID,这样主控可以直接丢弃重复包。主控侧的确认消息也要包含原消息 ID,否则设备不知道确认的是哪一条。这个“确认必须带上下文”的规则,经常被两边各自写成“反正就是确认一下”,结果一丢包就乱套。

4.3 并发冲突时的优先级规则:谁先谁后必须写进规范

多台设备同时操作同一个载具时,E87 的“旧状态→新状态”模型能发现冲突,但不能自动解决。你需要提前约定优先级。我常用的规则是:

  1. 设备侧物理动作完成后上报的状态,优先于主控侧逻辑推测的状态。
  2. 主控下发搬运请求后,设备必须在 5 秒内返回“已收到”或“拒绝”,不能沉默。
  3. 人工干预(HOLD)优先级最高,任何自动流程遇到 HOLD 都必须停止并上报。

把这个规则写进接口规范,而不是写在代码注释里。因为现场联调时双方工程师往往各执一词,有了白纸黑字才能快速定位问题。

在并发场景下,我遇到过主控的搬运请求和设备的状态上报同时到达,设备先执行了搬运,再上报状态,但主控认为搬运还没开始就拒绝了请求。解决思路是把“命令确认”和“状态上报”串行化:设备收到命令后先发“已收到”,再执行,执行完发“完成”和“状态变化”;主控只有在收到“已收到”后才认为命令生效。这个约定必须在联调前定好,否则两边各写一套线程模型,很难排查。

5. E87-0301 实施避坑:5 个让生产停线的状态同步现场

这一章是血泪经验汇总。每一条都是现场真实遇到过的,现象、原因、解决对应写清楚,方便你复现时直接对照。这些问题单独看都不复杂,但它们叠加起来,足以让一条线停摆半天。

5.1 现象:载具被移走了,系统还标记 IN_PROCESS

有一次,机械手完成加工后把载具放回端口,但设备软件没有先上报“加工完成”,而是直接进入空闲状态。主控侧收到的还是旧的 IN_PROCESS,于是搬运系统一直等着,产能停了一个多小时。

原因:设备软件把“本地状态更新”和“上报主控”分成了两个动作,本地完成了但上报失败,失败后也没有重试。

解决:把状态持久化和上报封装成同一个事务。在代码里,change_state()必须先落库,再上报;上报失败时启动重试线程,同时在本地状态上打标记“待同步”。主控侧也可以加一个轮询接口,发现载具状态长时间不变就主动查询设备。关键教训:不能因为上报失败就假装没发生过状态变化。

5.2 现象:搬运请求被重复执行,载具来回搬动

主控因为超时重发了搬运请求,设备侧收到两次,结果载具被来回搬动了一次。幸好没有撞坏硬件,但晶圆批次被打乱,最后只能全批重查。

原因:设备侧命令处理没有做幂等。E87 消息里有消息 ID 或参考号,但设备侧没有保存已处理的 ID 列表。

解决:在命令接收入口处维护一个“最近已处理消息 ID”集合,长度保留 1000 条。收到重复消息 ID 时直接返回上一次的成功响应,不重复执行物理动作。注意集合本身要加锁,多线程下容易出并发问题。这个集合不需要永久保存,保留最近半小时的会话足够覆盖超时重发窗口。

5.3 现象:Carrier ID 读到乱码,字符集对不上

RFID 读取显示正常,但主控侧看到的是“0x0A”等字符,载具匹配失败。用串口抓包发现:设备软件把 RFID 标签的二进制数据直接当成字符串组装进报文,导致控制字符混入。

原因:标签厂商返回的数据本身是二进制,没有做 ASCII 安全转换。

解决:在设备侧把二进制数据统一转成十六进制字符串,再作为 Carrier ID 使用。同时约定主控侧也按十六进制解析。上线前在测试环境用全量标签样本跑一遍,把每个字符的 hex 值打印出来核对。尤其注意标签里可能带换行符 0x0A 和回车符 0x0D,这两个字符进了报文就是隐形炸弹。

5.4 现象:E84 握手和 E87 状态查询互相死锁

设备端口正在做 E84 载具装载握手,同时主控发来 E87 状态查询。设备软件是单线程处理消息,结果握手流程被卡住,状态查询超时,主控认为设备已经死了,又触发了系统级复位。

原因:E84 和 E87 共享同一消息解析线程,而 E84 握手要求在较短时间内完成响应,不能被长耗时操作阻塞。

解决:把消息解析做成多线程,E84 相关报文在优先线程处理,E87 的查询响应走普通线程。更保守的做法是,在没有彻底理清线程模型之前,设备侧先对 E84 消息采用中断级处理,E87 消息全部排队,避免互相阻塞。这里要特别小心:不要让 E87 的日志打印阻塞 E84 的响应,否则还是死锁。

5.5 现象:设备重启后双方状态不一致

设备断电重启后,本地数据库里的载具状态停留在重启前,但实际载具可能已经被人为移走。主控侧重新连接设备后,双方都认为自己的数据是对的,结果同一个载具出现两个版本。

原因:设备没有在启动时做状态同步,主控也没有主动查询设备的所有载具。

解决:在设备上线时增加一个“全量载具状态同步”流程。设备重启后,先扫描端口上的实际载具,再上报全部载具当前状态;主控收到后与自己的数据比对,不一致的置 HOLD 并报警。这个流程在 E87 实施里属于非常基础的环节,但很多团队都会忘。不要依赖“重启后设备会自己恢复”,一定要主动扫一遍物理端口的实际载具。

6. E87-0301 进阶技巧:用状态日志回溯替代“黑匣子”

联调到最后,你会发现最难的不是写代码,而是问题发生后怎么快速定位。E87 相关的状态不一致问题往往只在特定时序下出现,现场没有复现条件时,就只能靠日志回溯。这里分享一个我一直在用的习惯:把每个载具 ID 的状态迁移当作一条不可变记录,统一写到独立日志文件,并附带完整的消息上下文。

我通常会让设备端在每次change_state()时输出一行结构化日志,包含时间戳、载具 ID、旧状态、新状态、端口、消息 ID、是否上报成功。同时把主控侧的接收日志也按同一格式落盘。这样联调时只要把两份日志按载具 ID 排序,就能看出哪一边漏了消息、哪一边发了旧状态。前一阵遇到一个载具“凭空消失”的问题,最后就是靠日志发现:设备侧某次上报用了旧的 CARRIER_ID,主控侧把它当成另一个载具处理了,导致匹配混乱。

进阶一点,可以在设备端做个启动自检:把日志里的状态迁移按 E87 规则重新播放一遍,遇到非法跳转直接打印告警。这个自检不需要额外平台支持,写个小脚本就能跑。但它的价值很大,能让“状态冲突”在变成产线事故之前就被发现。我现在每完成一个 E87 项目,都会顺手做两件事:把双方日志格式统一,并写一个状态回放脚本。说实话,这个脚本救过我很多次,比在现场抓包省力得多。

如果你刚接触 E87-0301,先从状态机开始,再把超时、ID 编码和启动同步这三件套配好,就已经避开了大部分常见的坑。剩下的就是用日志把自己武装到牙齿,出了问题不靠猜,靠数据回放。希望这套经验能帮到正在与载具状态较劲的你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进