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:
2026-06-09 20:14:42 +02:00
parent 858ca22975
commit 40df3067a4
6 changed files with 1587 additions and 0 deletions
+296
View File
@@ -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)
+306
View File
@@ -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)
+212
View File
@@ -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 2550 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)
+229
View File
@@ -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
$10100M; 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)
+280
View File
@@ -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)
+264
View File
@@ -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.
- **S5F9S5F18** — 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 — S5F9S5F18
### 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. **S5F9S5F18** 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)