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>
13 KiB
14 — E40 + E94: Process jobs and Control jobs
← 13 E30 — GEM | Back to index | Next: 15 E87 — Carriers and load ports →
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.
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 viaS16F5 PRCMDstrings. 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.
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
("PJ Queued→SettingUp fires S16F9 PRJobAlert on the wire").
The PJ store
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).
Tests: 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 viaS16F27 CJCMDstrings. CJCMD ="CJSTART","CJPAUSE","CJRESUME","CJSTOP","CJABORT".AllJobsComplete— internal: every PJ in the CJ reachedProcessComplete.AbortComplete— internal: every PJ reached an aborted state.
Defined in
include/secsgem/gem/control_job_state.hpp;
transition table in
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 drives
the full S14F9 → S16F27 (CJSTART) → S14F11 sequence as one
conformance check.
The CJ store
include/secsgem/gem/store/control_jobs.hpp;
tests in 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:
- 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.
- Every transition fires S16F9. Hosts that subscribe to S16F9 don't need to poll with S16F7; they get push notification for every state change.
- CEIDs fire alongside FSM transitions.
ControlJobExecuting,ProcessStarted,ProcessCompleted,ControlJobCompletedare regular CEIDs (fromdata/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 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
AbortCompleteon 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_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.