docs: chapters 14–19 — GEM 300 standards (Part 2 complete)
Six more chapters finishing Part 2. Together with chapters 10–13 they document every SEMI standard this codebase implements. 14 — E40 + E94: process jobs (8-state lifecycle, S16F11/F5/F7/F9 on the wire) and control jobs (CJ wraps PJs with batch policy, S14F9/S16F27 messages). Worked cascade showing how CJSTART propagates through the PJ FSM and triggers S6F11 CEIDs at each transition. 15 — E87 carriers: three orthogonal sub-machines (CarrierID, SlotMap, CarrierAccess) per carrier and three more (Transfer, Reservation, Association) per load port. S3F17 CarrierAction strings + CAACK codes, S3F19 SlotMap verify, the 5-state slot encoding, multi-port concurrency. 16 — E90 + E157: substrate tracking via three orthogonal axes (STS / SPS / SubstrateIDStatus) and module process tracking (NotExecuting / GeneralExecuting / StepExecuting / StepCompleted). End-to-end PVD example showing E40 + E157 + E90 transitions cascading into CEIDs. 17 — E116 + E120 + E39: equipment performance time-buckets across six states, common equipment model object hierarchy, S14F1/F3 GetAttr/SetAttr as the uniform wire access for any object type across multiple standards. 18 — E84 parallel I/O: ten signal lines, the 9-state handshake FSM, the three TA1/TA2/TA3 timing-critical timers, why a physical handshake gets modeled in software (testability, timer enforcement, CEID emission, multi-port concurrency), the pure-FSM + asio-adapter split. 19 — E42 + E148 + S5F9–F18: formatted recipes (S7F23/F25 typed PPBODY), time synchronization with 16-char + 14-char accepted on set, exception recovery as a persistent multi-step host-supervised FSM (Posted → Recovering → Cleared with abort/retry). Revisits the auto-S9 family and contrasts S9 (transport) vs S5F9 (application). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,296 @@
|
|||||||
|
# 14 — E40 + E94: Process jobs and Control jobs
|
||||||
|
|
||||||
|
← [13 E30 — GEM](13_e30_gem.md) | [Back to index](00_index.md) | Next: [15 E87 — Carriers and load ports](15_e87_carriers.md) →
|
||||||
|
|
||||||
|
A modern fab tool doesn't just "process wafers" — it executes
|
||||||
|
**jobs** with explicit lifecycles that the MES can submit,
|
||||||
|
monitor, pause, abort, and audit. Two SEMI standards govern this:
|
||||||
|
|
||||||
|
- **E40** (1999) — Process Jobs. A PJ describes one *recipe run*
|
||||||
|
on a defined set of material. "Run RECIPE-Cu-A on this list of
|
||||||
|
25 wafers."
|
||||||
|
- **E94** (2001) — Control Jobs. A CJ wraps a *batch* of PJs with
|
||||||
|
a processing policy. "Run these 4 PJs in order; abort the rest
|
||||||
|
if any one fails."
|
||||||
|
|
||||||
|
In production the host almost always creates a CJ wrapping its
|
||||||
|
PJs and uses CJ commands (CJSTART, CJPAUSE) to drive the batch.
|
||||||
|
Per-PJ commands (PJSTART) exist but are less common.
|
||||||
|
|
||||||
|
This chapter walks both lifecycles, the messages that drive them,
|
||||||
|
the FSMs in code, and how they cascade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E40 — Process Jobs
|
||||||
|
|
||||||
|
### The PJ lifecycle
|
||||||
|
|
||||||
|
Eight states. Values match the **PRJOBSTATE byte** that S16F9
|
||||||
|
carries on the wire (E40-0705 §10.3.2):
|
||||||
|
|
||||||
|
| Value | State | Meaning |
|
||||||
|
|-------|--------------------|--------------------------------------------------------|
|
||||||
|
| 0 | `Queued` | Created; awaiting selection by a CJ or by S16F5 SELECT.|
|
||||||
|
| 1 | `SettingUp` | Equipment loading the recipe + verifying material. |
|
||||||
|
| 2 | `WaitingForStart` | Ready; awaiting PJSTART (or auto-start if configured). |
|
||||||
|
| 3 | `Processing` | Recipe running. |
|
||||||
|
| 4 | `ProcessComplete` | Recipe finished; awaiting host dequeue. |
|
||||||
|
| 5 | `Paused` | Mid-process pause; resumable. |
|
||||||
|
| 6 | `Stopping` | Graceful abort in progress. |
|
||||||
|
| 7 | `Aborting` | Forceful abort in progress. |
|
||||||
|
| 255 | `NoState` | Sentinel: "doesn't exist yet / freshly deleted." |
|
||||||
|
|
||||||
|
```
|
||||||
|
Created ───► Queued ──Select──► SettingUp ──SetupComplete──► WaitingForStart
|
||||||
|
│
|
||||||
|
PJSTART ──────────┤
|
||||||
|
▼
|
||||||
|
Processing
|
||||||
|
╱ │ ╲
|
||||||
|
PJPAUSE PJSTOP PJABORT
|
||||||
|
╱ │ ╲
|
||||||
|
Paused Stopping Aborting
|
||||||
|
│ │ │
|
||||||
|
PJRESUME ▼ ▼
|
||||||
|
╲ ProcessComplete AbortComplete
|
||||||
|
╲ │
|
||||||
|
╲ ▼
|
||||||
|
back to Processing
|
||||||
|
```
|
||||||
|
|
||||||
|
Defined in
|
||||||
|
[`include/secsgem/gem/process_job_state.hpp`](../include/secsgem/gem/process_job_state.hpp).
|
||||||
|
|
||||||
|
Drivers of the FSM (`ProcessJobEvent`):
|
||||||
|
|
||||||
|
- **`Created`** — synthetic observer signal when the store first
|
||||||
|
records a PJ. Doesn't appear in the transition table.
|
||||||
|
- **`Select`** — `Queued → SettingUp`. Fires when a CJ promotes
|
||||||
|
this PJ for processing, or when S16F5 SELECT arrives.
|
||||||
|
- **`SetupComplete`** — equipment-internal (recipe loaded, material
|
||||||
|
verified).
|
||||||
|
- **`Start` / `Pause` / `Resume` / `Stop` / `Abort`** — host-driven
|
||||||
|
via `S16F5 PRCMD` strings. PRCMD = `"PJSTART"`, `"PJPAUSE"`,
|
||||||
|
`"PJRESUME"`, `"PJSTOP"`, `"PJABORT"`.
|
||||||
|
- **`ProcessComplete` / `AbortComplete`** — equipment-internal,
|
||||||
|
fire when the recipe runner or abort controller finishes.
|
||||||
|
|
||||||
|
The transition table is loaded from
|
||||||
|
[`data/process_job_state.yaml`](../data/process_job_state.yaml).
|
||||||
|
Same spec-as-data pattern as E30 control state (chapter 13).
|
||||||
|
|
||||||
|
### The E40 messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|---------|-----------|-----------------------------------------------------------|
|
||||||
|
| S16F11 | H → E | PRJobCreate. Body carries PRJobID, MF, recipe spec, material list, PRProcessStart flag. |
|
||||||
|
| S16F12 | E → H | PRJobAck. PRJobAck byte: 0 = accepted, non-zero = errored. |
|
||||||
|
| S16F13 | H → E | PRJobDequeue. Host clears the PJ from equipment storage after observing ProcessComplete. |
|
||||||
|
| S16F14 | E → H | PRJobDequeueAck. |
|
||||||
|
| S16F5 | H → E | PRJobCommand. Body carries PRJobID + PRCMD string. |
|
||||||
|
| S16F6 | E → H | PRJobCommandAck. |
|
||||||
|
| S16F7 | H → E | PRJobMonitor. Host pulls current state for a PJ. |
|
||||||
|
| S16F8 | E → H | PRJobMonitorAck. Body carries PRJOBSTATE byte. |
|
||||||
|
| S16F9 | E → H | PRJobAlert. Equipment-initiated state-change notification. |
|
||||||
|
|
||||||
|
`S16F9` is the *interesting* one: it's a W=0 unsolicited message
|
||||||
|
that fires on every state transition (configurable per-PJ via
|
||||||
|
`alert_enabled`). The body carries the new PRJOBSTATE so the
|
||||||
|
host can update its tracking without polling.
|
||||||
|
|
||||||
|
Tested on the wire by
|
||||||
|
[`tests/test_wire_ceid_emission.cpp`](../tests/test_wire_ceid_emission.cpp)
|
||||||
|
("PJ Queued→SettingUp fires S16F9 PRJobAlert on the wire").
|
||||||
|
|
||||||
|
### The PJ store
|
||||||
|
|
||||||
|
[`include/secsgem/gem/store/process_jobs.hpp`](../include/secsgem/gem/store/process_jobs.hpp)
|
||||||
|
houses one entry per PJ — id, MF, recipe spec, current state,
|
||||||
|
material list, alert_enabled bit. Persistent: a per-record file
|
||||||
|
journal lets the store survive equipment restarts (chapter
|
||||||
|
[36](36_persistence_validation_metrics.md)).
|
||||||
|
|
||||||
|
Tests: [`tests/test_process_jobs.cpp`](../tests/test_process_jobs.cpp)
|
||||||
|
(21 cases — every transition, every wire message round-trip,
|
||||||
|
persistence).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E94 — Control Jobs
|
||||||
|
|
||||||
|
### The CJ lifecycle
|
||||||
|
|
||||||
|
Eight states, similar shape to PJ but distinct values (E94 doesn't
|
||||||
|
pin a wire byte for state; this project picks its own encoding):
|
||||||
|
|
||||||
|
| Value | State | Meaning |
|
||||||
|
|-------|--------------------|------------------------------------------------------|
|
||||||
|
| 0 | `Queued` | Created; not yet promoted. |
|
||||||
|
| 1 | `Selected` | CJ has selected one of its PJs (the PJ is now `SettingUp`). |
|
||||||
|
| 2 | `WaitingForStart` | All material ready; awaiting CJSTART. |
|
||||||
|
| 3 | `Executing` | At least one PJ in flight. |
|
||||||
|
| 4 | `Paused` | Mid-execution pause. |
|
||||||
|
| 5 | `Completed` | All PJs done; awaiting deletion. |
|
||||||
|
| 6 | `Stopping` | Graceful abort in progress. |
|
||||||
|
| 7 | `Aborting` | Forceful abort in progress. |
|
||||||
|
| 255 | `NoState` | Sentinel. |
|
||||||
|
|
||||||
|
Drivers:
|
||||||
|
|
||||||
|
- **`Select`** — Queued → Selected (CJ promotes its first PJ).
|
||||||
|
- **`SetupComplete`** — Selected → WaitingForStart (PJ reached
|
||||||
|
WaitingForStart).
|
||||||
|
- **`Start` / `Pause` / `Resume` / `Stop` / `Abort`** — host-driven
|
||||||
|
via `S16F27 CJCMD` strings. CJCMD = `"CJSTART"`, `"CJPAUSE"`,
|
||||||
|
`"CJRESUME"`, `"CJSTOP"`, `"CJABORT"`.
|
||||||
|
- **`AllJobsComplete`** — internal: every PJ in the CJ reached
|
||||||
|
`ProcessComplete`.
|
||||||
|
- **`AbortComplete`** — internal: every PJ reached an aborted
|
||||||
|
state.
|
||||||
|
|
||||||
|
Defined in
|
||||||
|
[`include/secsgem/gem/control_job_state.hpp`](../include/secsgem/gem/control_job_state.hpp);
|
||||||
|
transition table in
|
||||||
|
[`data/control_job_state.yaml`](../data/control_job_state.yaml).
|
||||||
|
|
||||||
|
### The E94 messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|--------|-----------|---------------------------------------------------------------|
|
||||||
|
| S14F9 | H → E | CreateControlJob. Body carries CJobID + ordered PRJobID list.|
|
||||||
|
| S14F10 | E → H | OBJACK reply. |
|
||||||
|
| S14F11 | H → E | DeleteControlJob. |
|
||||||
|
| S14F12 | E → H | OBJACK reply. |
|
||||||
|
| S16F27 | H → E | CJCommand. Body carries CJobID + CJCMD string. |
|
||||||
|
| S16F28 | E → H | HCACK reply. |
|
||||||
|
|
||||||
|
The wire test in
|
||||||
|
[`apps/secs_conformance.cpp`](../apps/secs_conformance.cpp) drives
|
||||||
|
the full S14F9 → S16F27 (CJSTART) → S14F11 sequence as one
|
||||||
|
conformance check.
|
||||||
|
|
||||||
|
### The CJ store
|
||||||
|
|
||||||
|
[`include/secsgem/gem/store/control_jobs.hpp`](../include/secsgem/gem/store/control_jobs.hpp);
|
||||||
|
tests in [`tests/test_control_jobs.cpp`](../tests/test_control_jobs.cpp)
|
||||||
|
(9 cases). Also persistent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How a PJ and its CJ cascade
|
||||||
|
|
||||||
|
The interesting part isn't either FSM in isolation — it's how they
|
||||||
|
**cascade** during a typical batch run.
|
||||||
|
|
||||||
|
```
|
||||||
|
t=0 host S16F11 PRJobCreate (PJ-1)
|
||||||
|
→ PJ-1 enters Queued
|
||||||
|
→ S16F9 PRJobAlert (PJ-1 NoState → Queued, if alerts enabled)
|
||||||
|
|
||||||
|
t=1 host S16F11 PRJobCreate (PJ-2) (similar)
|
||||||
|
|
||||||
|
t=2 host S14F9 CreateControlJob (CJ-1, [PJ-1, PJ-2])
|
||||||
|
→ CJ-1 enters Queued
|
||||||
|
→ equipment internally fires Select on PJ-1 (first in CJ list)
|
||||||
|
→ PJ-1 enters SettingUp + S16F9 alert
|
||||||
|
→ CJ-1 enters Selected
|
||||||
|
|
||||||
|
t=3 equipment recipe runner: PJ-1 SetupComplete
|
||||||
|
→ PJ-1 enters WaitingForStart + S16F9 alert
|
||||||
|
→ CJ-1 enters WaitingForStart
|
||||||
|
|
||||||
|
t=4 host S16F27 CJSTART (CJ-1)
|
||||||
|
→ CJ-1 enters Executing
|
||||||
|
→ CEID = ControlJobExecuting fires → S6F11
|
||||||
|
→ equipment fires Start on PJ-1
|
||||||
|
→ PJ-1 enters Processing + S16F9 alert
|
||||||
|
→ CEID = ProcessStarted fires → S6F11
|
||||||
|
|
||||||
|
t=N equipment: PJ-1 recipe done
|
||||||
|
→ PJ-1 enters ProcessComplete + S16F9 alert
|
||||||
|
→ CEID = ProcessCompleted fires → S6F11
|
||||||
|
→ equipment fires Select on PJ-2
|
||||||
|
→ PJ-2 enters SettingUp ...
|
||||||
|
... (same dance for PJ-2)
|
||||||
|
|
||||||
|
t=M all PJs done
|
||||||
|
→ CJ-1 fires AllJobsComplete
|
||||||
|
→ CJ-1 enters Completed
|
||||||
|
→ CEID = ControlJobCompleted fires → S6F11
|
||||||
|
|
||||||
|
t=M+1 host S16F13 PRJobDequeue (PJ-1)
|
||||||
|
host S16F13 PRJobDequeue (PJ-2)
|
||||||
|
host S14F11 DeleteControlJob (CJ-1)
|
||||||
|
→ all three records removed
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice three things:
|
||||||
|
|
||||||
|
1. **CJ events drive PJ events.** CJSTART makes the CJ go to
|
||||||
|
Executing, which causes the equipment to fire Start on the
|
||||||
|
first PJ — the host doesn't send PJSTART explicitly.
|
||||||
|
2. **Every transition fires S16F9.** Hosts that subscribe to
|
||||||
|
S16F9 don't need to poll with S16F7; they get push
|
||||||
|
notification for every state change.
|
||||||
|
3. **CEIDs fire alongside FSM transitions.** `ControlJobExecuting`,
|
||||||
|
`ProcessStarted`, `ProcessCompleted`, `ControlJobCompleted` are
|
||||||
|
regular CEIDs (from `data/equipment.yaml`) that fire when the
|
||||||
|
FSMs transition. They drive S6F11 events bundled with whatever
|
||||||
|
reports the host has linked.
|
||||||
|
|
||||||
|
End-to-end demonstration:
|
||||||
|
[`tests/test_live_gem300.cpp`](../tests/test_live_gem300.cpp) drives
|
||||||
|
the full cascade over a real loopback HSMS connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why CJs exist as a separate layer
|
||||||
|
|
||||||
|
Could the host just submit PJs one at a time and orchestrate the
|
||||||
|
batch itself? Yes — but:
|
||||||
|
|
||||||
|
- **Ordering / dependencies** belong on the equipment side. The
|
||||||
|
CJ FSM ensures PJ-2 only starts after PJ-1 finishes, even if
|
||||||
|
the host network drops between them.
|
||||||
|
- **Abort semantics** are sharper. CJSTOP applies to *every PJ in
|
||||||
|
the CJ*, deterministically. Aborting a PJ at a time leaves
|
||||||
|
race windows.
|
||||||
|
- **Reporting** is unified. A CJ-level CEID summarises "this
|
||||||
|
batch is done"; without CJs the host has to track every PJ
|
||||||
|
completion individually.
|
||||||
|
|
||||||
|
E94 is the layer that makes "run this batch of recipes safely" a
|
||||||
|
single command instead of an orchestration script.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge cases worth knowing
|
||||||
|
|
||||||
|
- **PJ in Paused state when CJ goes Stopping.** The PJ has to
|
||||||
|
resume first (so the recipe runner can reach a safe stopping
|
||||||
|
point), then stop. The transition tables handle this.
|
||||||
|
- **Partial cancel.** Host can S16F5 PJABORT on a single PJ
|
||||||
|
inside a running CJ. The CJ continues with the remaining PJs.
|
||||||
|
- **CJ delete while PJs are still queued.** E94 §6: deleting a
|
||||||
|
CJ that owns Queued PJs cancels them — the equipment fires
|
||||||
|
`AbortComplete` on each.
|
||||||
|
- **PJ status byte on the wire is a raw `uint8_t`.** This is why
|
||||||
|
the enum values match the spec exactly — the encoder just
|
||||||
|
casts to byte. Don't reorder the enum.
|
||||||
|
|
||||||
|
Every edge case has a test:
|
||||||
|
[`tests/test_process_jobs.cpp`](../tests/test_process_jobs.cpp) +
|
||||||
|
[`tests/test_control_jobs.cpp`](../tests/test_control_jobs.cpp) +
|
||||||
|
the CEID emission and live-scenario tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
You now know how a *job* is created, sequenced, executed, and torn
|
||||||
|
down. But before any of that can happen, the **carrier** holding
|
||||||
|
the material has to arrive at the tool — and the equipment has to
|
||||||
|
know it.
|
||||||
|
|
||||||
|
Next: [→ 15 E87 — Carriers and load ports](15_e87_carriers.md)
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
# 15 — E87: Carriers and load ports
|
||||||
|
|
||||||
|
← [14 E40 + E94 — Process and control jobs](14_e40_e94_jobs.md) | [Back to index](00_index.md) | Next: [16 E90 + E157 — Substrate and module tracking](16_e90_e157_substrates_modules.md) →
|
||||||
|
|
||||||
|
Before a wafer can be processed it has to physically arrive at the
|
||||||
|
tool, dock with the load port, expose its slot contents, get
|
||||||
|
verified, and be authorised by the host. That's six distinct
|
||||||
|
state transitions, each one tracked by SEMI **E87 — Carrier
|
||||||
|
Management** (2000).
|
||||||
|
|
||||||
|
This chapter covers:
|
||||||
|
|
||||||
|
- The carrier (FOUP) and the load port — what each *is* and what
|
||||||
|
state they each track.
|
||||||
|
- The three carrier state sub-machines: ID, Slot Map, Access.
|
||||||
|
- The three load-port state sub-machines: Transfer, Reservation,
|
||||||
|
Association.
|
||||||
|
- The E87 message catalog (S3F*).
|
||||||
|
- Where each piece is in code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vocabulary
|
||||||
|
|
||||||
|
- **Carrier** — a physical container holding wafers. In a 300 mm
|
||||||
|
fab almost always a **FOUP** (Front-Opening Universal Pod, 25
|
||||||
|
slots). In smaller fabs sometimes a SMIF pod or a cassette.
|
||||||
|
- **Load port** — the equipment-side dock where carriers physically
|
||||||
|
attach. A typical PVD tool has 2 load ports (1 input, 1 output);
|
||||||
|
a lithography stepper might have 4.
|
||||||
|
- **Slot map** — the equipment's reading of which of the carrier's
|
||||||
|
N slots contain wafers, expressed as N bytes. Slot states are
|
||||||
|
Empty, CorrectlyOccupied, DoubleSlotted, CrossSlotted, NotRead.
|
||||||
|
- **Carrier ID** — bar-coded string on the FOUP (e.g. "C-31415").
|
||||||
|
Read by the equipment's bar-code reader on docking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The three carrier state sub-machines
|
||||||
|
|
||||||
|
E87 doesn't track "carrier state" as one variable — it tracks
|
||||||
|
three orthogonal aspects:
|
||||||
|
|
||||||
|
### 1. ID Status
|
||||||
|
|
||||||
|
How sure are we who this carrier is?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/carrier_state.hpp:23
|
||||||
|
enum class CarrierIDStatus : uint8_t {
|
||||||
|
NotConfirmed = 0,
|
||||||
|
WaitingForHost = 1,
|
||||||
|
Confirmed = 2,
|
||||||
|
IDVerificationFailed = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Bar-code reader fires `ID_READ_OK` → NotConfirmed → Confirmed.
|
||||||
|
If the reader fails or returns gibberish, `ID_READ_FAIL` →
|
||||||
|
NotConfirmed → IDVerificationFailed. Some hosts insist on
|
||||||
|
verifying the ID themselves (look it up against their LMS); they
|
||||||
|
hold the carrier in WaitingForHost until they reply with
|
||||||
|
`HOST_ID_CONFIRMED`.
|
||||||
|
|
||||||
|
### 2. Slot Map Status
|
||||||
|
|
||||||
|
Have we read the carrier's contents?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/carrier_state.hpp:45
|
||||||
|
enum class SlotMapStatus : uint8_t {
|
||||||
|
NotRead = 0,
|
||||||
|
WaitingForHost = 1,
|
||||||
|
Read = 2,
|
||||||
|
SlotMapVerificationFailed = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The mapper (an optical sensor reading wafer positions) runs after
|
||||||
|
ID confirmation. Result: a 25-byte vector (one byte per slot).
|
||||||
|
Hosts can validate the map against expectation (S3F19 Slot Map
|
||||||
|
Verify); a mismatch flips to `SlotMapVerificationFailed`.
|
||||||
|
|
||||||
|
### 3. Access Status
|
||||||
|
|
||||||
|
Is the carrier authorised for processing right now?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/carrier_state.hpp:64
|
||||||
|
enum class CarrierAccessStatus : uint8_t {
|
||||||
|
NotAccessed = 0,
|
||||||
|
InAccess = 1,
|
||||||
|
CarrierComplete = 2,
|
||||||
|
CarrierStopped = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`InAccess` means the equipment is currently using slots from this
|
||||||
|
carrier. `CarrierComplete` means done; awaiting unloading.
|
||||||
|
|
||||||
|
All three sub-machines progress **independently**. A carrier can
|
||||||
|
be `Confirmed` (ID) + `Read` (Map) + `NotAccessed` (Access) — and
|
||||||
|
that's the typical state after docking but before the host
|
||||||
|
authorises processing.
|
||||||
|
|
||||||
|
`CarrierStateMachine` in
|
||||||
|
[`include/secsgem/gem/carrier_state.hpp`](../include/secsgem/gem/carrier_state.hpp)
|
||||||
|
composes the three; tests in
|
||||||
|
[`tests/test_carrier_state.cpp`](../tests/test_carrier_state.cpp)
|
||||||
|
(11 cases) and
|
||||||
|
[`tests/test_e87_wire_scenarios.cpp`](../tests/test_e87_wire_scenarios.cpp)
|
||||||
|
(4 wire scenarios).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The three load-port state sub-machines
|
||||||
|
|
||||||
|
The load port has its own three sub-machines:
|
||||||
|
|
||||||
|
### 1. Transfer State
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/load_port_state.hpp:19
|
||||||
|
enum class LoadPortTransferState : uint8_t {
|
||||||
|
OutOfService = 0,
|
||||||
|
ReadyToLoad = 1,
|
||||||
|
ReadyToUnload = 2,
|
||||||
|
InService = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Physical readiness — is the port mechanically ready to dock a new
|
||||||
|
FOUP, release the current one, or busy?
|
||||||
|
|
||||||
|
### 2. Reservation Status
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class LoadPortReservationStatus : uint8_t {
|
||||||
|
NotReserved = 0,
|
||||||
|
Reserved = 1,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Has the host pre-reserved this port for an inbound carrier?
|
||||||
|
Reservation is how the host tells the AMHS "send the carrier to
|
||||||
|
this specific port."
|
||||||
|
|
||||||
|
### 3. Association Status
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class LoadPortAssociationStatus : uint8_t {
|
||||||
|
NotAssociated = 0,
|
||||||
|
Associated = 1,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Is there a known Carrier object linked to this port? Becomes
|
||||||
|
`Associated` when a carrier docks and the ID is read.
|
||||||
|
|
||||||
|
`LoadPortStateMachine` in
|
||||||
|
[`include/secsgem/gem/load_port_state.hpp`](../include/secsgem/gem/load_port_state.hpp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-port + multi-carrier
|
||||||
|
|
||||||
|
A real fab tool runs **multiple load ports in parallel**. A
|
||||||
|
4-port cluster tool can have:
|
||||||
|
|
||||||
|
- Port 1: carrier A in `Associated` + `InAccess` (processing)
|
||||||
|
- Port 2: carrier B in `Associated` + `CarrierComplete` (done,
|
||||||
|
awaiting unload)
|
||||||
|
- Port 3: AMHS robot docking carrier C (`InService`)
|
||||||
|
- Port 4: `OutOfService` (mechanical fault)
|
||||||
|
|
||||||
|
The codebase models this as a **per-port store**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/store/carriers.hpp
|
||||||
|
class CarrierStore {
|
||||||
|
// one CarrierStateMachine per Carrier ID
|
||||||
|
// one LoadPortStateMachine per PortID
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests for the parallel scenario:
|
||||||
|
[`tests/test_e87_wire_scenarios.cpp`](../tests/test_e87_wire_scenarios.cpp)
|
||||||
|
(4 multi-port scenarios — independence between ports asserted).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The E87 messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|-------|-----------|--------------------------------------------------|
|
||||||
|
| S3F17 | H → E | CarrierAction. Body: CARRIERACTION + CARRIERID. |
|
||||||
|
| S3F18 | E → H | CAACK reply. |
|
||||||
|
| S3F19 | H → E | Slot Map Verify. Body: CARRIERID + expected slot states. |
|
||||||
|
| S3F20 | E → H | SMACK reply. |
|
||||||
|
| S3F25 | H → E | Carrier Transfer. Move a carrier between ports. |
|
||||||
|
| S3F26 | E → H | CAACK reply. |
|
||||||
|
| S3F27 | H → E | Cancel Carrier. Pre-arrival cancellation. |
|
||||||
|
| S3F28 | E → H | CAACK reply. |
|
||||||
|
|
||||||
|
### CARRIERACTION strings (S3F17 body)
|
||||||
|
|
||||||
|
The dominant E87 messages are S3F17 carrier-action commands.
|
||||||
|
CARRIERACTION is an ASCII string from a fixed E87 set:
|
||||||
|
|
||||||
|
| String | Meaning |
|
||||||
|
|-------------------------|------------------------------------------------------|
|
||||||
|
| `ProceedWithCarrier` | Authorise processing. Triggers Access → InAccess. |
|
||||||
|
| `CancelCarrier` | Refuse the carrier; equipment doesn't process it. |
|
||||||
|
| `CancelCarrierAtPort` | Same but specifies port. |
|
||||||
|
| `BypassCarrier` | Process nothing from this carrier (audit slot map only). |
|
||||||
|
| `CarrierOut` | Mark `CarrierComplete`; AMHS will retrieve. |
|
||||||
|
| `CarrierReCID` | Re-read the carrier ID (e.g. bar code was iffy). |
|
||||||
|
|
||||||
|
CAACK reply codes (S3F18, 1 byte):
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|--------------------------------------------------|
|
||||||
|
| 0 | Acknowledged. |
|
||||||
|
| 1 | Invalid command. |
|
||||||
|
| 2 | Cannot perform now. |
|
||||||
|
| 3 | Invalid carrier ID. |
|
||||||
|
| 4 | Invalid port ID. |
|
||||||
|
| 5 | Carrier ID unknown. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A typical carrier flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. AMHS docks FOUP at port 1.
|
||||||
|
2. Equipment fires CarrierArrived event (CEID per equipment.yaml)
|
||||||
|
→ S6F11 (host gets pinged).
|
||||||
|
3. Equipment reads bar code → CarrierIDStatus: NotConfirmed → Confirmed.
|
||||||
|
4. Equipment runs slot mapper → SlotMapStatus: NotRead → Read.
|
||||||
|
5. (Optional) Host sends S3F19 SlotMapVerify with expected contents
|
||||||
|
→ equipment compares → SMACK = 0 (match) or 1 (mismatch).
|
||||||
|
6. Host sends S3F17 CARRIERACTION = "ProceedWithCarrier"
|
||||||
|
→ CarrierAccessStatus: NotAccessed → InAccess.
|
||||||
|
7. Processing happens. Substrate state changes are tracked by E90
|
||||||
|
(chapter 16).
|
||||||
|
8. All substrates done. Equipment fires CarrierComplete event.
|
||||||
|
9. Host sends S3F17 CARRIERACTION = "CarrierOut"
|
||||||
|
→ CarrierAccessStatus → CarrierComplete.
|
||||||
|
10. AMHS retrieves FOUP from port 1. LoadPortAssociation goes back
|
||||||
|
to NotAssociated; carrier object can be deleted.
|
||||||
|
```
|
||||||
|
|
||||||
|
The slot-map-mismatch path in step 5 is tested by
|
||||||
|
[`tests/test_e87_wire_scenarios.cpp`](../tests/test_e87_wire_scenarios.cpp);
|
||||||
|
the happy path by
|
||||||
|
[`tests/test_carriers.cpp`](../tests/test_carriers.cpp) (6 cases).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slot maps in detail
|
||||||
|
|
||||||
|
A slot map is a **byte-vector** with one byte per carrier slot.
|
||||||
|
For a 25-slot FOUP, that's 25 bytes. Byte values:
|
||||||
|
|
||||||
|
| Value | State | Meaning |
|
||||||
|
|-------|--------------------|------------------------------------------------|
|
||||||
|
| 0 | `Empty` | No wafer detected. |
|
||||||
|
| 1 | `CorrectlyOccupied`| One wafer in the correct vertical position. |
|
||||||
|
| 2 | `DoubleSlotted` | Two wafers in one slot — physical fault. |
|
||||||
|
| 3 | `CrossSlotted` | Wafer at an angle / wrong height. |
|
||||||
|
| 4 | `NotRead` | Sensor couldn't read this slot. |
|
||||||
|
|
||||||
|
S3F19 (Slot Map Verify) carries the host's *expected* slot map.
|
||||||
|
The equipment compares against its read map; if they match,
|
||||||
|
SMACK = 0. Mismatches imply someone interfered with the carrier
|
||||||
|
(or the mapper is broken).
|
||||||
|
|
||||||
|
The map is part of the carrier store and persists across restarts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
Like all GEM-300 stores, the carrier store is **persistent**. A
|
||||||
|
file per active carrier + a file per port lets the equipment
|
||||||
|
recover its full E87 state after a restart — including in-progress
|
||||||
|
carriers stranded mid-Access by a power loss.
|
||||||
|
|
||||||
|
Tested by
|
||||||
|
[`tests/test_carrier_persistence.cpp`](../tests/test_carrier_persistence.cpp)
|
||||||
|
(6 cases — write, restart, replay, corrupted-file drop, removal).
|
||||||
|
|
||||||
|
Per-store journal pattern is the same across E40, E87, E90, E94,
|
||||||
|
E116; chapter [36](36_persistence_validation_metrics.md) walks the
|
||||||
|
mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
You now know how a carrier arrives, gets authorised, and leaves.
|
||||||
|
But every wafer *inside* the carrier needs its own tracking — and
|
||||||
|
when wafers move into a process module, their state has to follow
|
||||||
|
them. That's **E90 and E157**.
|
||||||
|
|
||||||
|
Next: [→ 16 E90 + E157 — Substrate and module tracking](16_e90_e157_substrates_modules.md)
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# 16 — E90 + E157: Substrate and module tracking
|
||||||
|
|
||||||
|
← [15 E87 — Carriers and load ports](15_e87_carriers.md) | [Back to index](00_index.md) | Next: [17 E116 + E120 + E39 — Performance, CEM, objects](17_e116_e120_e39_objects.md) →
|
||||||
|
|
||||||
|
E87 (chapter 15) tracks the **container**. E90 and E157 track
|
||||||
|
what's *inside* the container — every individual wafer (substrate)
|
||||||
|
and every process module the wafer passes through.
|
||||||
|
|
||||||
|
This chapter is shorter than the others in Part 2 because the
|
||||||
|
ideas overlap E87 (the same three-orthogonal-axes pattern repeats)
|
||||||
|
and E40 (state events drive S6F11 CEIDs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E90 — Substrate tracking
|
||||||
|
|
||||||
|
### What it tracks
|
||||||
|
|
||||||
|
One state-bearing record **per wafer**, identified by a substrate
|
||||||
|
ID (an ASCII string, often the laser-etched serial number).
|
||||||
|
|
||||||
|
E90 has **three orthogonal axes** — the same pattern as E87's
|
||||||
|
carrier (chapter 15 §3):
|
||||||
|
|
||||||
|
#### 1. Substrate State (STS) — location
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/substrate_state.hpp:26
|
||||||
|
enum class SubstrateState : uint8_t {
|
||||||
|
AtSource = 0, // in its origin carrier slot
|
||||||
|
AtWork = 1, // in-process at a module
|
||||||
|
AtDestination = 2, // delivered to final location
|
||||||
|
NoState = 255,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Events: `Acquire` (Source→Work), `Release` (Work→Destination),
|
||||||
|
`Return` (Work→Source, for unprocessed return).
|
||||||
|
|
||||||
|
#### 2. Substrate Processing State (SPS) — lifecycle
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class SubstrateProcessingState : uint8_t {
|
||||||
|
NeedsProcessing = 0,
|
||||||
|
InProcess = 1,
|
||||||
|
Processed = 2,
|
||||||
|
Aborted = 3,
|
||||||
|
Stopped = 4,
|
||||||
|
Rejected = 5,
|
||||||
|
Lost = 6,
|
||||||
|
Skipped = 7,
|
||||||
|
NoState = 255,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Events: `StartProcessing`, `EndProcessing`, `Abort`, `Stop`,
|
||||||
|
`Reject`, `ReportLost`, `Skip`.
|
||||||
|
|
||||||
|
#### 3. Substrate ID Status — identity confidence
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class SubstrateIDStatus : uint8_t {
|
||||||
|
NotConfirmed = 0,
|
||||||
|
WaitingForHost = 1,
|
||||||
|
Confirmed = 2,
|
||||||
|
Mismatched = 3,
|
||||||
|
NoState = 255,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Mirrors the `CarrierIDStatus` pattern from E87 — same problem
|
||||||
|
(equipment reads ID, host may need to verify), same shape of
|
||||||
|
solution.
|
||||||
|
|
||||||
|
### Why three axes?
|
||||||
|
|
||||||
|
The same reason E87 has three: these aspects evolve **at different
|
||||||
|
times** and **for different reasons**.
|
||||||
|
|
||||||
|
- A wafer can be `AtWork` + `NeedsProcessing` (just arrived,
|
||||||
|
recipe hasn't started).
|
||||||
|
- A wafer can be `AtWork` + `InProcess` (recipe running).
|
||||||
|
- A wafer can be `AtSource` + `Processed` + `Confirmed` (back in
|
||||||
|
its carrier slot after processing — typical end state).
|
||||||
|
|
||||||
|
Putting all three in one enum would multiply to ~30 valid
|
||||||
|
combinations. Three independent FSMs with ~3 events each is much
|
||||||
|
cleaner.
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
State machines:
|
||||||
|
[`include/secsgem/gem/substrate_state.hpp`](../include/secsgem/gem/substrate_state.hpp)
|
||||||
|
defines `SubstrateStateMachine`, which composes the three.
|
||||||
|
|
||||||
|
Store:
|
||||||
|
[`include/secsgem/gem/store/substrates.hpp`](../include/secsgem/gem/store/substrates.hpp)
|
||||||
|
holds one record per substrate ID, with a Location string the
|
||||||
|
application updates as the wafer moves.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
[`tests/test_substrates.cpp`](../tests/test_substrates.cpp) (14
|
||||||
|
cases — every axis, every event); persistence in
|
||||||
|
[`tests/test_substrate_persistence.cpp`](../tests/test_substrate_persistence.cpp)
|
||||||
|
(7 cases).
|
||||||
|
|
||||||
|
CEID-on-wire emission ("Substrate StartProcessing fires the
|
||||||
|
configured SubstrateInProcess CEID") is verified by
|
||||||
|
[`tests/test_wire_ceid_emission.cpp`](../tests/test_wire_ceid_emission.cpp).
|
||||||
|
|
||||||
|
### Wire interaction
|
||||||
|
|
||||||
|
E90 doesn't define its own S/F messages — substrate state changes
|
||||||
|
fire as **CEIDs** that the host has subscribed to via the standard
|
||||||
|
E30 §6.6 Dynamic Event Report Configuration (chapter 13). So:
|
||||||
|
|
||||||
|
- Equipment fires `Acquire` event on substrate `W-2026-06-09-A47`.
|
||||||
|
- `SubstrateStateMachine` transitions Source → Work.
|
||||||
|
- The state-change handler looks up the configured CEID for
|
||||||
|
"SubstrateInProcess" (from `data/equipment.yaml`).
|
||||||
|
- That CEID fires → `compose_reports_for(ceid)` → `S6F11`.
|
||||||
|
|
||||||
|
Host gets one S6F11 per wafer transition. In a 25-wafer FOUP
|
||||||
|
that's 25–50 events per processing pass. Persistent reports +
|
||||||
|
spool (chapter 13 Additionals) handle the burst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E157 — Module Process Tracking
|
||||||
|
|
||||||
|
### What it tracks
|
||||||
|
|
||||||
|
One state-bearing record **per process module**. A cluster tool
|
||||||
|
has multiple modules (Chamber A, Chamber B, Pre-clean, …); each
|
||||||
|
runs its own recipe step in parallel or sequence. E157 lets the
|
||||||
|
host see *which module is in which step of which recipe right
|
||||||
|
now*.
|
||||||
|
|
||||||
|
### The states
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/module_state.hpp:20
|
||||||
|
enum class ModuleState : uint8_t {
|
||||||
|
NotExecuting = 0,
|
||||||
|
GeneralExecuting = 1, // setup, pre-process, post-process
|
||||||
|
StepExecuting = 2, // actively running a recipe step
|
||||||
|
StepCompleted = 3,
|
||||||
|
NoState = 255,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Events: `StartGeneral`, `StartStep`, `CompleteStep`, `Reset`,
|
||||||
|
`Abort`.
|
||||||
|
|
||||||
|
Notice this is a much simpler FSM than E90 — one axis only. That's
|
||||||
|
because modules are more deterministic than substrates: a module
|
||||||
|
is either running a step or it isn't; substrates can be in many
|
||||||
|
overlapping conditions.
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
[`include/secsgem/gem/module_state.hpp`](../include/secsgem/gem/module_state.hpp)
|
||||||
|
defines `ModuleStateMachine`.
|
||||||
|
|
||||||
|
Store:
|
||||||
|
[`include/secsgem/gem/store/modules.hpp`](../include/secsgem/gem/store/modules.hpp).
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
[`tests/test_modules.cpp`](../tests/test_modules.cpp) (5 cases).
|
||||||
|
|
||||||
|
### How E157 plays with E40 and E90
|
||||||
|
|
||||||
|
Concrete example. A PVD tool with three modules (Chamber A, B,
|
||||||
|
C); host submits PJ for wafer W-1, recipe says "process at
|
||||||
|
Chamber B for 90 seconds":
|
||||||
|
|
||||||
|
```
|
||||||
|
1. PJ-1 transitions Queued → SettingUp → WaitingForStart → Processing.
|
||||||
|
(E40 FSM, chapter 14)
|
||||||
|
2. Equipment fires ModuleEvent::StartGeneral on Chamber B.
|
||||||
|
ModuleState: NotExecuting → GeneralExecuting.
|
||||||
|
(E157 FSM)
|
||||||
|
3. Equipment fires SubstrateEvent::Acquire on W-1.
|
||||||
|
Substrate STS: AtSource → AtWork.
|
||||||
|
(E90 FSM)
|
||||||
|
4. Recipe step begins. ModuleEvent::StartStep on Chamber B.
|
||||||
|
ModuleState: GeneralExecuting → StepExecuting.
|
||||||
|
Substrate SPS: NeedsProcessing → InProcess.
|
||||||
|
5. ...90 seconds pass...
|
||||||
|
6. Recipe step ends. ModuleEvent::CompleteStep on Chamber B.
|
||||||
|
ModuleState: StepExecuting → StepCompleted.
|
||||||
|
Substrate SPS: InProcess → Processed.
|
||||||
|
7. Substrate released. SubstrateEvent::Release on W-1.
|
||||||
|
Substrate STS: AtWork → AtDestination.
|
||||||
|
8. PJ-1: ProcessComplete.
|
||||||
|
```
|
||||||
|
|
||||||
|
Each of the eight transitions fires a CEID, which fires an S6F11
|
||||||
|
event report. The host sees the **complete trace** of where every
|
||||||
|
wafer was at every moment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
You now know how every component of in-flight material is
|
||||||
|
tracked. The next chapter covers the three smaller GEM 300
|
||||||
|
standards that round out the suite: equipment performance time
|
||||||
|
tracking, the common equipment model, and generic object
|
||||||
|
services.
|
||||||
|
|
||||||
|
Next: [→ 17 E116 + E120 + E39 — Performance, CEM, objects](17_e116_e120_e39_objects.md)
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# 17 — E116 + E120 + E39: Performance, CEM, objects
|
||||||
|
|
||||||
|
← [16 E90 + E157 — Substrate and module tracking](16_e90_e157_substrates_modules.md) | [Back to index](00_index.md) | Next: [18 E84 — Parallel I/O handoff](18_e84_parallel_io.md) →
|
||||||
|
|
||||||
|
Three smaller GEM 300 standards in one chapter. Each is narrow in
|
||||||
|
scope but load-bearing for production fab operations.
|
||||||
|
|
||||||
|
- **E116** — Equipment Performance Tracking. Time-buckets per
|
||||||
|
equipment state for OEE / utilisation reporting.
|
||||||
|
- **E120** — Common Equipment Model. A generic typed object
|
||||||
|
hierarchy the host can query.
|
||||||
|
- **E39** — Object Services. CRUD-style messages (`S14F*`) that
|
||||||
|
operate over E120 (and other) object types.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E116 — Equipment Performance Tracking
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
In a fab, **equipment utilisation** is a primary KPI. Tools cost
|
||||||
|
$10–100M; idle minutes are visible on quarterly P&L statements.
|
||||||
|
E116 standardises how equipment reports *how much time it spent
|
||||||
|
in each state* so MES dashboards can compute OEE (Overall Equipment
|
||||||
|
Effectiveness) without per-vendor logic.
|
||||||
|
|
||||||
|
### The states
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/ept_state.hpp:22
|
||||||
|
enum class EptState : uint8_t {
|
||||||
|
NonScheduledTime = 0, // not in the schedule (weekend, planned down)
|
||||||
|
UnscheduledDowntime = 1, // in schedule, but broken (alarm, fault)
|
||||||
|
ScheduledDowntime = 2, // in schedule, planned maintenance
|
||||||
|
Engineering = 3, // running engineering / qualification work
|
||||||
|
Standby = 4, // ready, awaiting material
|
||||||
|
Productive = 5, // actively processing
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
These are the SEMI E116 §6.2 standard states. Per-state events:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class EptEvent {
|
||||||
|
Begin_NonScheduledTime,
|
||||||
|
Begin_UnscheduledDowntime,
|
||||||
|
Begin_ScheduledDowntime,
|
||||||
|
Begin_Engineering,
|
||||||
|
Begin_Standby,
|
||||||
|
Begin_Productive,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### What the FSM records
|
||||||
|
|
||||||
|
[`EptStateMachine`](../include/secsgem/gem/ept_state.hpp) is a
|
||||||
|
"what kind of time is this" classifier rather than a strict
|
||||||
|
lifecycle. Any state can transition to any other. What it
|
||||||
|
tracks: **how long was the equipment in each state**.
|
||||||
|
|
||||||
|
The store accumulates time-buckets:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class EptStore {
|
||||||
|
// For each EptState, accumulated wall-clock duration.
|
||||||
|
std::array<std::chrono::seconds, 6> bucket_;
|
||||||
|
|
||||||
|
// Current state + when it was entered (so the dwell so far is
|
||||||
|
// counted as part of the current bucket on read).
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
A host querying "how much Productive time today?" gets the bucket
|
||||||
|
value for `Productive`, plus the dwell of the current state if
|
||||||
|
that state is Productive.
|
||||||
|
|
||||||
|
### Wire
|
||||||
|
|
||||||
|
E116 doesn't define its own S/F messages. Like E90, state changes
|
||||||
|
fire as **CEIDs** the host has subscribed to.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
[`tests/test_ept.cpp`](../tests/test_ept.cpp) (7 cases — initial
|
||||||
|
state, transitions, bucket accumulation including current dwell,
|
||||||
|
reset, same-state no-op).
|
||||||
|
|
||||||
|
### When EPT transitions happen
|
||||||
|
|
||||||
|
EPT classification is *application logic*. The library doesn't
|
||||||
|
decide that processing a PJ = Productive — the EAP does, by
|
||||||
|
explicitly calling `EptStateMachine::on_event(Begin_Productive)`
|
||||||
|
when a PJ starts. Typical wiring:
|
||||||
|
|
||||||
|
```
|
||||||
|
PJ Processing → EPT Productive
|
||||||
|
PJ Paused → EPT Standby (or Engineering, depending on cause)
|
||||||
|
Alarm category 2 (equipment safety) → EPT UnscheduledDowntime
|
||||||
|
Maintenance recipe running → EPT ScheduledDowntime
|
||||||
|
```
|
||||||
|
|
||||||
|
The [`examples/pvd_tool/main.cpp`](../examples/pvd_tool/main.cpp) §5
|
||||||
|
shows one concrete wiring; chapter
|
||||||
|
[41](41_integration_hardware_mes_production.md) discusses the
|
||||||
|
production patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E120 — Common Equipment Model
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
E120 defines a **generic typed object hierarchy** the equipment can
|
||||||
|
expose to the host. The motivation: every E30/GEM 300 standard
|
||||||
|
defines its own object type (Carrier, Substrate, ProcessJob,
|
||||||
|
ControlJob, Alarm, …), each with its own attributes. E120 says
|
||||||
|
"all of these are *objects* with the same hierarchical structure;
|
||||||
|
let's standardise how the host queries them."
|
||||||
|
|
||||||
|
### The object types
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/store/cem_objects.hpp:27
|
||||||
|
enum class CemObjectType : uint8_t {
|
||||||
|
Equipment = 1,
|
||||||
|
IOProcessor = 2,
|
||||||
|
IODevice = 3,
|
||||||
|
SubsystemController = 4,
|
||||||
|
Subsystem = 5,
|
||||||
|
Module = 6,
|
||||||
|
// ... more
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Each object has:
|
||||||
|
|
||||||
|
- A unique `OBJID` (ASCII string).
|
||||||
|
- A type from the enum above.
|
||||||
|
- A `parent_objid` (or empty for root — Equipment).
|
||||||
|
- A typed attribute bag.
|
||||||
|
|
||||||
|
That builds the hierarchy:
|
||||||
|
|
||||||
|
```
|
||||||
|
Equipment "PVD-1"
|
||||||
|
├── IOProcessor "IOP-1"
|
||||||
|
│ ├── IODevice "Sensor-Pressure-A"
|
||||||
|
│ └── IODevice "Sensor-Temp-A"
|
||||||
|
└── SubsystemController "SubC-1"
|
||||||
|
├── Subsystem "Vacuum"
|
||||||
|
│ └── Module "Pump-1"
|
||||||
|
└── Subsystem "Gas-Manifold"
|
||||||
|
```
|
||||||
|
|
||||||
|
The host can walk this tree, read attributes, and update its own
|
||||||
|
asset model.
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
Store:
|
||||||
|
[`include/secsgem/gem/store/cem_objects.hpp`](../include/secsgem/gem/store/cem_objects.hpp).
|
||||||
|
Tests:
|
||||||
|
[`tests/test_cem_objects.cpp`](../tests/test_cem_objects.cpp) (3
|
||||||
|
cases — create, lookup, child enumeration).
|
||||||
|
|
||||||
|
### Wire
|
||||||
|
|
||||||
|
E120 itself doesn't define messages — it defines the *data model*.
|
||||||
|
The wire access is **E39 Object Services**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E39 — Object Services
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
E39 generalises "get attribute of an object" and "set attribute of
|
||||||
|
an object" into one message family — `S14F*` — that works across
|
||||||
|
**any object type** (E120 hierarchy, E40 process jobs, E94 control
|
||||||
|
jobs, E87 carriers, …).
|
||||||
|
|
||||||
|
### The messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|-------|-----------|----------------------------------------------------------|
|
||||||
|
| S14F1 | H → E | GetAttr. Body: object type + OBJID + attribute name list. |
|
||||||
|
| S14F2 | E → H | GetAttr reply. Body: attribute values + OBJACK byte. |
|
||||||
|
| S14F3 | H → E | SetAttr. Body: object type + OBJID + name/value pairs. |
|
||||||
|
| S14F4 | E → H | SetAttr reply. |
|
||||||
|
|
||||||
|
OBJACK = 0 means accepted; non-zero means error.
|
||||||
|
|
||||||
|
E39 is the **uniform API** for object introspection — same shape
|
||||||
|
of message whether the host is reading a Carrier attribute, an
|
||||||
|
Alarm attribute, or a Process Module attribute.
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
Handlers live in
|
||||||
|
[`include/secsgem/gem/host_command_registry.hpp`](../include/secsgem/gem/host_command_registry.hpp)
|
||||||
|
and the generated message catalog.
|
||||||
|
|
||||||
|
Tests are bundled into
|
||||||
|
[`tests/test_cem_objects.cpp`](../tests/test_cem_objects.cpp) and
|
||||||
|
[`tests/test_messages.cpp`](../tests/test_messages.cpp) — the
|
||||||
|
`S14F1`/`S14F2` round-trip is exercised against multiple object
|
||||||
|
types.
|
||||||
|
|
||||||
|
### Why E39 exists separately from E120
|
||||||
|
|
||||||
|
The split is the SEMI typical-shape: one standard defines the
|
||||||
|
*data model*, a separate standard defines the *wire access*. This
|
||||||
|
way E39 can extend to objects defined in other standards (E40 PJs,
|
||||||
|
E94 CJs, E87 carriers) without E120 having to know about them.
|
||||||
|
|
||||||
|
In code, each object store registers itself with a generic
|
||||||
|
attribute-resolver; `S14F1` handlers look up the right resolver by
|
||||||
|
object type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
You now know how the equipment reports *time* (E116), *structure*
|
||||||
|
(E120), and *attribute access* (E39). The next chapter is the
|
||||||
|
last GEM 300 standard with its own state machine — the **parallel
|
||||||
|
I/O handshake** that physically hands carriers between robot and
|
||||||
|
load port.
|
||||||
|
|
||||||
|
Next: [→ 18 E84 — Parallel I/O handoff](18_e84_parallel_io.md)
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
# 18 — E84: Parallel I/O handoff
|
||||||
|
|
||||||
|
← [17 E116 + E120 + E39 — Performance, CEM, objects](17_e116_e120_e39_objects.md) | [Back to index](00_index.md) | Next: [19 E42 + E148 + S9 — Misc](19_e42_e148_s9_misc.md) →
|
||||||
|
|
||||||
|
E84 is unusual in the GEM 300 suite: it's the only standard that's
|
||||||
|
**not SECS at all**. Not a wire format, not a message catalog —
|
||||||
|
ten *physical wires* between the AMHS robot and the load port,
|
||||||
|
asserted at CMOS voltage levels with strict timing.
|
||||||
|
|
||||||
|
Why? Because dropping a $20 000 FOUP is catastrophic, and you
|
||||||
|
can't afford to coordinate the kinematics over TCP — too much
|
||||||
|
latency, too many failure modes. The handshake has to be
|
||||||
|
deterministic in hardware.
|
||||||
|
|
||||||
|
This chapter:
|
||||||
|
|
||||||
|
- The ten signal lines and what each one means.
|
||||||
|
- The handshake state machine.
|
||||||
|
- The three timing-critical timers (TA1, TA2, TA3).
|
||||||
|
- How the codebase models a physical-layer handshake as software
|
||||||
|
(and why it does).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The ten signals
|
||||||
|
|
||||||
|
Each signal is one **single-bit boolean** asserted on a physical
|
||||||
|
wire. Four go from the equipment to the AMHS; six go from the
|
||||||
|
AMHS to the equipment:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/e84_state.hpp:28
|
||||||
|
enum class E84Signal : uint8_t {
|
||||||
|
CS_0 = 0, // AMHS -> equip: carrier stage select 0
|
||||||
|
CS_1 = 1, // AMHS -> equip: carrier stage select 1
|
||||||
|
VALID = 2, // AMHS -> equip: handshake start
|
||||||
|
TR_REQ = 3, // AMHS -> equip: transfer request
|
||||||
|
BUSY = 4, // AMHS -> equip: transfer in progress
|
||||||
|
COMPT = 5, // AMHS -> equip: transfer complete
|
||||||
|
L_REQ = 6, // equip -> AMHS: load request (port ready to receive)
|
||||||
|
U_REQ = 7, // equip -> AMHS: unload request (port ready to release)
|
||||||
|
READY = 8, // equip -> AMHS: ready
|
||||||
|
ES = 9, // either: emergency stop
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CS_0 + CS_1**: two bits encoding which port the AMHS is
|
||||||
|
addressing (CS = Carrier Select). Tools with up to 4 ports can
|
||||||
|
be indexed by two bits.
|
||||||
|
- **VALID**: the AMHS asserts this when CS bits are stable —
|
||||||
|
"you can read me now."
|
||||||
|
- **TR_REQ**: AMHS is requesting a transfer.
|
||||||
|
- **BUSY**: AMHS is actively moving the carrier. Goes high when
|
||||||
|
the robot starts lowering / lifting.
|
||||||
|
- **COMPT**: AMHS has finished the kinematic operation.
|
||||||
|
- **L_REQ**: equipment is ready to *receive* a carrier.
|
||||||
|
- **U_REQ**: equipment is ready to *release* a carrier.
|
||||||
|
- **READY**: equipment kinematic interlocks are satisfied.
|
||||||
|
- **ES**: Emergency Stop. Either side can assert. If asserted,
|
||||||
|
every state machine on both sides goes to a safe state.
|
||||||
|
|
||||||
|
Defined in
|
||||||
|
[`include/secsgem/gem/e84_state.hpp`](../include/secsgem/gem/e84_state.hpp).
|
||||||
|
Stored in `E84SignalSet` as a 10-bit bitmap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The handshake state machine
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// include/secsgem/gem/e84_state.hpp:63
|
||||||
|
enum class E84State : uint8_t {
|
||||||
|
Idle = 0, // no signals
|
||||||
|
CarrierPresent = 1, // CS asserted; no VALID yet
|
||||||
|
ValidAsserted = 2, // CS + VALID; equipment hasn't ack'd
|
||||||
|
LoadReady = 3, // VALID + L_REQ; port ready to receive
|
||||||
|
UnloadReady = 4, // VALID + U_REQ; port ready to release
|
||||||
|
Transferring = 5, // BUSY asserted; transfer happening
|
||||||
|
Complete = 6, // COMPT asserted; AMHS done
|
||||||
|
EmergencyStop = 7, // ES asserted
|
||||||
|
HandoffFault = 8, // a timer expired
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The happy path for an **inbound load**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Idle ─CS asserted─► CarrierPresent ─VALID asserted─► ValidAsserted
|
||||||
|
│
|
||||||
|
(equipment
|
||||||
|
decides: yes,
|
||||||
|
I can take it)
|
||||||
|
│
|
||||||
|
L_REQ asserted
|
||||||
|
▼
|
||||||
|
LoadReady
|
||||||
|
│
|
||||||
|
TR_REQ asserted
|
||||||
|
BUSY asserted
|
||||||
|
▼
|
||||||
|
Transferring
|
||||||
|
│
|
||||||
|
(robot lowers
|
||||||
|
carrier onto
|
||||||
|
port; takes a
|
||||||
|
few seconds)
|
||||||
|
│
|
||||||
|
BUSY de-asserted
|
||||||
|
COMPT asserted
|
||||||
|
▼
|
||||||
|
Complete
|
||||||
|
│
|
||||||
|
(signals all
|
||||||
|
drop back; CS
|
||||||
|
de-asserted)
|
||||||
|
▼
|
||||||
|
Idle
|
||||||
|
```
|
||||||
|
|
||||||
|
An **outbound unload** follows the same pattern but uses `U_REQ`
|
||||||
|
instead of `L_REQ`, and ends with the carrier moving *off* the
|
||||||
|
port.
|
||||||
|
|
||||||
|
The FSM is **event-driven**: every transition is triggered by one
|
||||||
|
signal change, not by a clock tick. `E84StateMachine::on_signal_change()`
|
||||||
|
re-evaluates the bitmap and emits a state transition if one is due.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
[`tests/test_e84.cpp`](../tests/test_e84.cpp) (6 cases — every
|
||||||
|
happy-path transition);
|
||||||
|
[`tests/test_e84_ports.cpp`](../tests/test_e84_ports.cpp) (5 cases
|
||||||
|
— per-port store).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The three TA timers
|
||||||
|
|
||||||
|
These are why E84 matters more than "the AMHS lifts the carrier."
|
||||||
|
Without timer enforcement, a stuck signal could leave the
|
||||||
|
mechanical handoff frozen mid-motion — the robot holding the
|
||||||
|
carrier, neither side noticing the other has gone quiet.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct E84Timeouts {
|
||||||
|
std::chrono::milliseconds ta1{0};
|
||||||
|
std::chrono::milliseconds ta2{0};
|
||||||
|
std::chrono::milliseconds ta3{0};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
(Spec defaults are 2 s / 2 s / 60 s; tool builders tune per
|
||||||
|
port.)
|
||||||
|
|
||||||
|
### TA1
|
||||||
|
|
||||||
|
Armed: on entering `ValidAsserted` (AMHS asserted VALID).
|
||||||
|
Cancelled: on entering `LoadReady` or `UnloadReady` (equipment
|
||||||
|
asserted L_REQ or U_REQ).
|
||||||
|
|
||||||
|
Bounds: **how long may the equipment take to respond to VALID?**
|
||||||
|
If TA1 expires the AMHS doesn't know whether the equipment is busy,
|
||||||
|
broken, or asleep — fault.
|
||||||
|
|
||||||
|
### TA2
|
||||||
|
|
||||||
|
Armed: on entering `LoadReady` or `UnloadReady`.
|
||||||
|
Cancelled: on entering `Transferring` (AMHS asserted BUSY).
|
||||||
|
|
||||||
|
Bounds: **how long may the AMHS take to start moving once the
|
||||||
|
port is ready?** Prevents the equipment holding its port idle
|
||||||
|
forever waiting for an AMHS that's stuck.
|
||||||
|
|
||||||
|
### TA3
|
||||||
|
|
||||||
|
Armed: on entering `Transferring`.
|
||||||
|
Cancelled: on entering `Complete`.
|
||||||
|
|
||||||
|
Bounds: **how long may the actual transfer take?** If the robot
|
||||||
|
freezes mid-motion, TA3 catches it.
|
||||||
|
|
||||||
|
### What happens on timeout
|
||||||
|
|
||||||
|
The FSM transitions to `HandoffFault` with the relevant
|
||||||
|
`E84Fault` reason:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class E84Fault : uint8_t {
|
||||||
|
None = 0,
|
||||||
|
TA1Expired = 1,
|
||||||
|
TA2Expired = 2,
|
||||||
|
TA3Expired = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The equipment fires an alarm (configurable ALID per port), the
|
||||||
|
EAP brings up the operator panel, and someone has to physically
|
||||||
|
inspect.
|
||||||
|
|
||||||
|
Tested by
|
||||||
|
[`tests/test_e84_timers.cpp`](../tests/test_e84_timers.cpp) (12
|
||||||
|
cases — every timer armed/cancelled/expired path).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why model a physical handshake in software
|
||||||
|
|
||||||
|
The wires are real. The signals are CMOS-level on opto-isolated
|
||||||
|
24 V lines. But the software needs to:
|
||||||
|
|
||||||
|
1. **Test the protocol logic without a real load port.** Spinning
|
||||||
|
up actual hardware for unit tests is impossible.
|
||||||
|
2. **Drive the timer enforcement.** Even if the wires are
|
||||||
|
physical, the timers TA1/TA2/TA3 are wall-clock and need a
|
||||||
|
software clock to track.
|
||||||
|
3. **Emit CEIDs alongside transitions.** When the port goes
|
||||||
|
`Transferring`, the equipment also wants to fire `CarrierIn`
|
||||||
|
over SECS-II — the same way E40/E87/E90 transitions do.
|
||||||
|
4. **Model multi-port concurrency.** A 4-port tool has four
|
||||||
|
independent E84 FSMs running in parallel; they have to be
|
||||||
|
modeled distinctly.
|
||||||
|
|
||||||
|
The codebase ships **two implementations**:
|
||||||
|
|
||||||
|
### Pure FSM (testable)
|
||||||
|
|
||||||
|
[`E84StateMachine`](../include/secsgem/gem/e84_state.hpp) is the
|
||||||
|
IO-free FSM. Inputs: signal change events. Outputs: state
|
||||||
|
transitions + timer arm/cancel requests. No wall clock.
|
||||||
|
|
||||||
|
This is what tests drive — they feed signal events in, expect
|
||||||
|
transitions out, and synthetically expire timers.
|
||||||
|
|
||||||
|
### asio adapter (production)
|
||||||
|
|
||||||
|
[`E84AsioTimers`](../include/secsgem/gem/e84_asio_timers.hpp)
|
||||||
|
wraps the FSM with real `asio::steady_timer`s. When the FSM
|
||||||
|
requests `arm(TA1, 2s)`, the adapter schedules a wall-clock timer;
|
||||||
|
when 2 s pass and nothing's cancelled it, the adapter feeds the
|
||||||
|
expiry event back into the FSM.
|
||||||
|
|
||||||
|
This is what runs in production — connected to a GPIO driver
|
||||||
|
that pulses the actual wires.
|
||||||
|
|
||||||
|
Tested by
|
||||||
|
[`tests/test_e84_asio_timers.cpp`](../tests/test_e84_asio_timers.cpp)
|
||||||
|
(4 cases — every timer fires on real wall clock).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How E84 connects to the rest of GEM
|
||||||
|
|
||||||
|
E84 itself only manages the physical handoff. Once a carrier is
|
||||||
|
docked, *SECS messages* take over:
|
||||||
|
|
||||||
|
1. E84 reaches `Complete` → equipment fires CEID `CarrierArrived`
|
||||||
|
(configured in `data/equipment.yaml`).
|
||||||
|
2. CEID `CarrierArrived` fires → S6F11 (host informed).
|
||||||
|
3. Host sees S6F11 → looks up carrier ID → optionally sends
|
||||||
|
S3F19 SlotMapVerify (E87).
|
||||||
|
4. Host sends S3F17 `ProceedWithCarrier` → CarrierAccess goes
|
||||||
|
InAccess (E87).
|
||||||
|
5. Processing happens (E40 + E90 + E157).
|
||||||
|
6. All wafers done → equipment fires CEID `CarrierComplete`.
|
||||||
|
7. Host sends S3F17 `CarrierOut`.
|
||||||
|
8. AMHS comes back; E84 runs in reverse to unload.
|
||||||
|
|
||||||
|
E84 is the **bookend** at both ends of the carrier flow. Without
|
||||||
|
it, the carrier never docks and never undocks; without the SECS
|
||||||
|
messages after step 1, nothing knows the carrier arrived.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
You now know every state-machine-bearing standard in the GEM 300
|
||||||
|
suite. One more chapter wraps up the remaining narrow ones —
|
||||||
|
formatted process programs, distributed time sync, and the
|
||||||
|
exception recovery streams.
|
||||||
|
|
||||||
|
Next: [→ 19 E42 + E148 + S9 — Misc](19_e42_e148_s9_misc.md)
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# 19 — E42 + E148 + S9 + exception recovery
|
||||||
|
|
||||||
|
← [18 E84 — Parallel I/O handoff](18_e84_parallel_io.md) | [Back to index](00_index.md) | Next: [30 Repository tour](30_repository_tour.md) →
|
||||||
|
|
||||||
|
Three remaining standards-shaped concerns to round out Part 2:
|
||||||
|
|
||||||
|
- **E42** — Formatted (enhanced) Process Programs.
|
||||||
|
- **E148** — Time synchronization.
|
||||||
|
- **S5F9–S5F18** — Exception recovery (E5 §13 + GEM Additional).
|
||||||
|
|
||||||
|
Each is narrow enough that a half-chapter would do. Together they
|
||||||
|
round out the GEM 300 picture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E42 — Formatted Process Programs
|
||||||
|
|
||||||
|
### What it is
|
||||||
|
|
||||||
|
E30's Process Program Management (chapter 13) covers **unformatted**
|
||||||
|
recipes: the PPBODY is opaque bytes that only the equipment knows
|
||||||
|
how to parse. E42 adds **formatted** PPs — the recipe has a typed
|
||||||
|
SECS-II structure the host can introspect.
|
||||||
|
|
||||||
|
In practice, formatted recipes are a fab-internal standard rather
|
||||||
|
than a SEMI-defined schema. E42 just gives the wire shape; the
|
||||||
|
fab agrees what the structure means.
|
||||||
|
|
||||||
|
### The messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|-------|-----------|---------------------------------------------------------------|
|
||||||
|
| S7F23 | H → E | Formatted PP Send. Body: PPID + typed SECS-II body. |
|
||||||
|
| S7F24 | E → H | ACKC7 reply. |
|
||||||
|
| S7F25 | H → E | Formatted PP Request. Body: PPID. |
|
||||||
|
| S7F26 | E → H | Formatted PP Send (back). Body: PPID + typed body. |
|
||||||
|
|
||||||
|
Compare to unformatted S7F3 / S7F5: same direction pattern, just a
|
||||||
|
typed body instead of opaque bytes.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
[`RecipeStore`](../include/secsgem/gem/store/recipes.hpp) carries
|
||||||
|
**both** views per recipe: an unformatted PPBODY (opaque bytes)
|
||||||
|
and an optional formatted body (a `secs2::Item` tree). S7F3 sends
|
||||||
|
the unformatted; S7F23 sends the formatted; both are stored side
|
||||||
|
by side and the host can request either via S7F5 (unformatted) or
|
||||||
|
S7F25 (formatted).
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
[`tests/test_e42_formatted_pp.cpp`](../tests/test_e42_formatted_pp.cpp)
|
||||||
|
(6 cases — send formatted, request back, round-trip integrity,
|
||||||
|
ACKC7 error paths).
|
||||||
|
|
||||||
|
### Why both?
|
||||||
|
|
||||||
|
Some MES — and some equipment — only speak unformatted PPs.
|
||||||
|
Coexistence lets a vendor ship one EAP that handles both.
|
||||||
|
COMPLIANCE §4j has the audit detail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E148 — Time synchronization
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
In a multi-tool fab, the host and the equipment need a **common
|
||||||
|
notion of time** for timestamp correlation. If tool A logs an
|
||||||
|
alarm at 14:32:01 and tool B logs a related alarm at 14:31:58, did
|
||||||
|
B precede A or did the clocks drift?
|
||||||
|
|
||||||
|
E148 defines the wire mechanism for keeping equipment clocks in
|
||||||
|
sync with a host-authoritative time source — typically NTP behind
|
||||||
|
the scenes — and lets equipment **report clock quality** so the
|
||||||
|
host knows how much to trust the timestamps coming off the tool.
|
||||||
|
|
||||||
|
### The messages
|
||||||
|
|
||||||
|
E148 doesn't add new streams; it specialises the existing E30
|
||||||
|
clock messages:
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|-------|-----------|----------------------------------------------------------|
|
||||||
|
| S2F17 | H → E | Read clock. |
|
||||||
|
| S2F18 | E → H | Reply with current time string. |
|
||||||
|
| S2F31 | H → E | Set clock to specified time string. |
|
||||||
|
| S2F32 | E → H | TIACK reply. |
|
||||||
|
|
||||||
|
The time string is **16 ASCII chars `YYYYMMDDhhmmsscc`** (E148
|
||||||
|
extended form, including hundredths). 14-char `YYYYMMDDhhmmss`
|
||||||
|
(without hundredths) is the older E30 form; the codebase **accepts
|
||||||
|
both on set** but emits 16 chars on read by default. See
|
||||||
|
[`docs/COMPLIANCE.md`](COMPLIANCE.md) §4g.
|
||||||
|
|
||||||
|
### Clock store
|
||||||
|
|
||||||
|
[`include/secsgem/gem/store/clock.hpp`](../include/secsgem/gem/store/clock.hpp)
|
||||||
|
holds the wall-clock plus a **drift / quality** indicator:
|
||||||
|
|
||||||
|
- `Drift_ms`: cumulative drift since last set.
|
||||||
|
- `Quality`: enum from {Authoritative, GoodNTP, FreeRunning,
|
||||||
|
Unreliable}.
|
||||||
|
|
||||||
|
A host can read both via E120/E39 attribute access (chapter 17) or
|
||||||
|
via DVID exposures (the EAP wires them).
|
||||||
|
|
||||||
|
### Why this matters
|
||||||
|
|
||||||
|
Without clock sync, **alarm root-cause analysis is impossible**.
|
||||||
|
SPC charts get the X-axis wrong. Yield correlations across tools
|
||||||
|
fall apart. Most modern fabs run NTP on every tool's
|
||||||
|
control-plane host; E148 is the mechanism for *reporting* clock
|
||||||
|
state, not for synchronising it (that's NTP's job).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exception recovery — S5F9–S5F18
|
||||||
|
|
||||||
|
### What it adds beyond base alarms
|
||||||
|
|
||||||
|
E5 §13 + E30 Alarm Management (covered in chapter 13) handles
|
||||||
|
alarms as **set/clear** events: an alarm goes active, the
|
||||||
|
equipment fires S5F1; later it clears, equipment fires another
|
||||||
|
S5F1. Simple.
|
||||||
|
|
||||||
|
But some alarms aren't simple to clear. A vacuum leak that
|
||||||
|
required a chamber vent + manual seal replacement can't just
|
||||||
|
"go away" — the equipment has to run a recovery procedure with
|
||||||
|
the host's involvement. **S5F9–S5F18** is the **exception
|
||||||
|
recovery** family that handles that.
|
||||||
|
|
||||||
|
### The exception lifecycle
|
||||||
|
|
||||||
|
Defined in
|
||||||
|
[`include/secsgem/gem/exception_state.hpp`](../include/secsgem/gem/exception_state.hpp):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class ExceptionState : uint8_t {
|
||||||
|
Posted = 0, // S5F9 sent; awaiting host action
|
||||||
|
Recovering = 1, // S5F13 accepted; recovery in progress
|
||||||
|
RecoverFailed = 2, // S5F15 reported failure; retry possible
|
||||||
|
Cleared = 3, // resolved; terminal
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Events: `Created` (NoState → Posted), `Recover` (host's S5F13),
|
||||||
|
`RecoveryComplete`, `RecoveryFailed`, `RecoveryAbort` (host's
|
||||||
|
S5F17), `Clear`.
|
||||||
|
|
||||||
|
### The messages
|
||||||
|
|
||||||
|
| S/F | Direction | Purpose |
|
||||||
|
|-------|-----------|--------------------------------------------------------------|
|
||||||
|
| S5F9 | E → H | Exception Post. Equipment-initiated. Body: EXID + EXTYPE + EXMESSAGE + recovery-method list. |
|
||||||
|
| S5F10 | H → E | Ack. |
|
||||||
|
| S5F11 | E → H | Exception Clear. Equipment-initiated when condition resolves. |
|
||||||
|
| S5F12 | H → E | Ack. |
|
||||||
|
| S5F13 | H → E | Exception Recover. Body: EXID + which recovery method to attempt. |
|
||||||
|
| S5F14 | E → H | Recovery progress. |
|
||||||
|
| S5F15 | E → H | Recovery Complete (or Failed). |
|
||||||
|
| S5F16 | H → E | Ack. |
|
||||||
|
| S5F17 | H → E | Exception Recover Abort. Cancel an in-progress recovery. |
|
||||||
|
| S5F18 | E → H | Recovery Aborted reply. |
|
||||||
|
|
||||||
|
The flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Vacuum leak detected. EAP calls exceptions.post(EXID=42, recovery=["vent","seal","pump-down"]).
|
||||||
|
→ ExceptionState: NoState → Posted.
|
||||||
|
→ S5F9 fires.
|
||||||
|
2. Host sees S5F9 → operator decides to attempt recovery → host sends S5F13(EXID=42, method="vent").
|
||||||
|
→ ExceptionState: Posted → Recovering.
|
||||||
|
3. Recovery in progress. EAP fires S5F14 periodically with progress.
|
||||||
|
4. EAP completes the venting step. Fires recover_complete event.
|
||||||
|
→ ExceptionState: Recovering → Cleared.
|
||||||
|
→ S5F15 fires.
|
||||||
|
5. Host acknowledges (S5F16). EAP fires S5F11 to confirm the
|
||||||
|
underlying condition is gone.
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, the abort path:
|
||||||
|
|
||||||
|
```
|
||||||
|
3'. Operator decides recovery isn't working → host sends S5F17.
|
||||||
|
4'. ExceptionState: Recovering → Posted.
|
||||||
|
5'. EAP can be re-instructed via another S5F13 with a different
|
||||||
|
method, or the condition can clear autonomously (Clear event
|
||||||
|
→ Cleared state).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code
|
||||||
|
|
||||||
|
State machine:
|
||||||
|
[`include/secsgem/gem/exception_state.hpp`](../include/secsgem/gem/exception_state.hpp).
|
||||||
|
Store:
|
||||||
|
[`include/secsgem/gem/store/exceptions.hpp`](../include/secsgem/gem/store/exceptions.hpp)
|
||||||
|
— persistent, so an exception in flight survives a power cycle.
|
||||||
|
Tests:
|
||||||
|
[`tests/test_exceptions.cpp`](../tests/test_exceptions.cpp) (11
|
||||||
|
cases) + persistence in
|
||||||
|
[`tests/test_exception_persistence.cpp`](../tests/test_exception_persistence.cpp)
|
||||||
|
(5 cases).
|
||||||
|
|
||||||
|
### Why this is its own family
|
||||||
|
|
||||||
|
Two reasons:
|
||||||
|
|
||||||
|
1. **State persistence.** Alarms come and go in seconds; exception
|
||||||
|
recovery can span hours and a few power cycles. The store
|
||||||
|
journal lets the equipment remember "we were halfway through
|
||||||
|
recovery method 2 of EXID=42" across restarts.
|
||||||
|
2. **Multi-step coordination.** Each step (`S5F13` → `S5F14` →
|
||||||
|
`S5F15`) is a host-supervised transaction. Base alarms can't
|
||||||
|
express "host, here are three recovery options, pick one."
|
||||||
|
|
||||||
|
Exception recovery is an Additional GEM capability — not every MES
|
||||||
|
asks for it — but the codebase implements it because it's
|
||||||
|
upstream-absent in `secsgem-py` (see [docs/COMPLIANCE.md](COMPLIANCE.md)
|
||||||
|
§4k) and because the persistent state machine is a nice example of
|
||||||
|
the spec-as-data pattern applied to a less-trivial FSM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The auto-S9 family (revisited)
|
||||||
|
|
||||||
|
We covered the S9 wire-error replies in chapter 11. Worth
|
||||||
|
re-listing here because S9 is part of the **error/exception layer**
|
||||||
|
even though it's transport-level rather than application-level:
|
||||||
|
|
||||||
|
| Function | Trigger |
|
||||||
|
|----------|----------------------------------------------------------|
|
||||||
|
| S9F1 | Unrecognized Device ID |
|
||||||
|
| S9F3 | Unrecognized Stream |
|
||||||
|
| S9F5 | Unrecognized Function |
|
||||||
|
| S9F7 | Illegal Data (body failed to decode) |
|
||||||
|
| S9F9 | Transaction Timer Timeout (T3 expired) |
|
||||||
|
| S9F11 | Data Too Long (body exceeded configured cap) |
|
||||||
|
| S9F13 | Conversation Timer Timeout (equipment-internal) |
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
[`hsms::Connection::emit_s9`](../include/secsgem/hsms/connection.hpp)
|
||||||
|
called from the connection's framing and routing paths. Tested
|
||||||
|
across [`tests/test_hsms_s9.cpp`](../tests/test_hsms_s9.cpp) and
|
||||||
|
[`tests/test_s9_fallback.cpp`](../tests/test_s9_fallback.cpp).
|
||||||
|
|
||||||
|
Difference from S5F9: S9 is **transport-level** (the bytes
|
||||||
|
themselves were wrong); S5F9 is **application-level** (the
|
||||||
|
equipment can't continue normal operation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## End of Part 2
|
||||||
|
|
||||||
|
You now know every SECS/GEM and GEM 300 standard that this
|
||||||
|
codebase implements. Twelve standards across nine chapters, each
|
||||||
|
one mapped to its state machine, its messages, its store, and the
|
||||||
|
tests that hold it down.
|
||||||
|
|
||||||
|
Part 3 starts. We turn from "what the spec says" to "how this
|
||||||
|
codebase implements it" — repository tour, codegen, the data model
|
||||||
|
structure, transport internals, state-machine composition,
|
||||||
|
persistence mechanics.
|
||||||
|
|
||||||
|
Next: [→ 30 Repository tour](30_repository_tour.md)
|
||||||
Reference in New Issue
Block a user