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)