858ca22975
Three more chapters of Part 2: 11 — E37 HSMS. 4-byte length prefix + 10-byte header (R-bit + session id + W-bit + stream + function + PType + SType + system_bytes), the 9 SType control messages, the NOT-SELECTED → SELECTED state machine, T3/T5/T6/T7/T8 with what each one bounds, the auto-S9 paths (S9F1/F3/F5/F7/F9/F11), HSMS-SS vs HSMS-GS, the asio single-threaded contract. 12 — E4 SECS-I. Half-duplex line turnaround (ENQ/EOT/ACK/NAK), the 10-byte block header bit-packing (R-bit / W-bit / E-bit / system bytes), the 244-byte block cap and multi-block split/assemble, the event-driven IO-free FSM with its Action / Event variants, T1/T2/T3/T4 with semantics + defaults, master/slave contention. Notes the deferred asio serial_port adapter; explains why this chapter matters even for HSMS-only readers. 13 — E30 GEM. Disambiguates the three state machines (HSMS transport vs GEM communication vs GEM control), walks the comm-state FSM (DISABLED → WAIT-CRA → COMMUNICATING with T_CRA / T_DELAY) and the control-state FSM (5 states + the YAML transition table). Lists every Fundamental and Additional capability with its messages, code locations, and store assignments. One worked Event-Notification scenario tracing seven on-wire steps to their EquipmentDataModel internals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
260 lines
9.7 KiB
Markdown
260 lines
9.7 KiB
Markdown
# 12 — E4: SECS-I — the serial origin
|
||
|
||
← [11 E37 — HSMS transport](11_e37_hsms.md) | [Back to index](00_index.md) | Next: [13 E30 — GEM](13_e30_gem.md) →
|
||
|
||
HSMS (chapter 11) is what every modern 300 mm tool runs. But SECS-I
|
||
— published 1980, the *original* SECS transport — is still on the
|
||
wire. Older 200 mm fabs, smaller specialty tools (e.g. inspection
|
||
microscopes, simple metrology), some legacy lithography steppers,
|
||
even some new equipment shipping into mixed-fleet fabs all speak
|
||
SECS-I over RS-232 or RS-422.
|
||
|
||
This chapter is short, because the protocol is small. By the end:
|
||
|
||
- You'll understand half-duplex line turnaround.
|
||
- You'll know the ENQ / EOT / ACK / NAK handshake.
|
||
- You'll be able to read the 10-byte block header.
|
||
- You'll know exactly what's implemented here, what isn't, and
|
||
why.
|
||
|
||
---
|
||
|
||
## What SECS-I actually is
|
||
|
||
A **half-duplex block protocol** for RS-232 / RS-422 serial links.
|
||
Half-duplex means: at any moment, exactly one side is allowed to be
|
||
transmitting. Switching direction requires an explicit handshake.
|
||
|
||
The handshake uses four single-byte control codes:
|
||
|
||
| Byte | Mnemonic | Meaning |
|
||
|------|----------|----------------------|
|
||
| 0x05 | `ENQ` | I want to send |
|
||
| 0x04 | `EOT` | Go ahead, send |
|
||
| 0x06 | `ACK` | Block received OK |
|
||
| 0x15 | `NAK` | Block bad, retry |
|
||
|
||
A successful transmission looks like:
|
||
|
||
```
|
||
sender receiver
|
||
────── ────────
|
||
ENQ ──────────────► "I want to send"
|
||
◄────────── EOT "go ahead"
|
||
<block bytes> ────► "here you go"
|
||
◄────────── ACK "got it"
|
||
```
|
||
|
||
Compare to HSMS: HSMS gets all this for free from TCP. TCP is
|
||
full-duplex, segments are framed by the operating system, and the
|
||
ACK semantics are at the byte level not the message level. SECS-I
|
||
predates that affordance — it was designed for a UART straight on
|
||
the equipment's serial port.
|
||
|
||
---
|
||
|
||
## The 10-byte block header
|
||
|
||
A block carries the same logical information as an HSMS data
|
||
message — session, stream, function, W-bit, system bytes — packed
|
||
slightly differently:
|
||
|
||
```
|
||
byte 0 1 2 3 4 5 6 7 8 9
|
||
┌──────┬──────────┬──────────┬────────┬───────────┐
|
||
│R+sid │ W+stream │ function │E+block#│ sys bytes │
|
||
└──────┴──────────┴──────────┴────────┴───────────┘
|
||
u16 bit7 W func id bit15 E u32
|
||
bit15 R bits6-0 (byte 3) bits14-0 (BE)
|
||
bits14-0 stream block #
|
||
device id
|
||
```
|
||
|
||
Three bit-packings, all in [`Header::encode/decode`](../include/secsgem/secsi/header.hpp):
|
||
|
||
- **Byte 0–1**: R-bit (bit 15) + 15-bit device ID. `R=1` means
|
||
"host → equipment", `R=0` means "equipment → host".
|
||
- **Byte 2**: W-bit (bit 7) + 7-bit stream. Same W-bit semantics as
|
||
HSMS.
|
||
- **Byte 4–5**: E-bit (bit 15) + 15-bit block number. `E=1` marks
|
||
the *last* block of a multi-block message. Block numbers are
|
||
1-based.
|
||
- **Byte 6–9**: 32-bit `system_bytes` correlation token.
|
||
|
||
Defined in [`include/secsgem/secsi/header.hpp`](../include/secsgem/secsi/header.hpp).
|
||
|
||
---
|
||
|
||
## Multi-block messages
|
||
|
||
A single SECS-I block carries at most **244 bytes** of body:
|
||
|
||
```cpp
|
||
// include/secsgem/secsi/block.hpp
|
||
inline constexpr std::size_t kMaxBlockBody = 244;
|
||
```
|
||
|
||
Why 244? The framing uses a one-byte length field that counts
|
||
10 (header) + body, with byte values 10–254 valid (255 reserved).
|
||
254 − 10 = 244.
|
||
|
||
A SECS-II body larger than 244 bytes is **split into multiple
|
||
blocks**, each with the same header except for the incrementing
|
||
block number and the E-bit (set only on the last block).
|
||
[`secsi::split_message`](../include/secsgem/secsi/block.hpp) does
|
||
the split; [`secsi::assemble_message`](../include/secsgem/secsi/block.hpp)
|
||
recombines them.
|
||
|
||
Each block has a 2-byte checksum after the body — sum of every
|
||
byte in the header + body modulo `2^16`, big-endian.
|
||
|
||
Multi-block round-trip is verified by [`tests/test_secsi.cpp`](../tests/test_secsi.cpp)
|
||
(15 cases) and [`tests/test_secsi_tcp.cpp`](../tests/test_secsi_tcp.cpp)
|
||
(3 cases — an end-to-end split / send / reassemble over the test
|
||
TCP transport).
|
||
|
||
> **Why HSMS doesn't need this.** HSMS frames have a 4-byte length
|
||
> prefix, so a single frame can carry up to 4 GiB. Multi-block is
|
||
> a SECS-I concept that simply doesn't apply on TCP.
|
||
|
||
---
|
||
|
||
## The line-turnaround FSM
|
||
|
||
The interesting part of SECS-I — and the part that bites every
|
||
implementer — is the **half-duplex** state machine. Both sides
|
||
might want to send at the same time. Both might `ENQ`
|
||
simultaneously. Both must agree on who yields.
|
||
|
||
E4 §7.1.4: **the master holds, the slave yields**. By convention
|
||
the host is master and the equipment is slave, but this is
|
||
configurable. In code:
|
||
|
||
```cpp
|
||
// include/secsgem/secsi/protocol.hpp
|
||
enum class Role { Master, Slave };
|
||
```
|
||
|
||
The FSM is event-driven and IO-free. It takes:
|
||
|
||
- **`EventByte`** — one received byte.
|
||
- **`EventSend`** — the application wants to send a block.
|
||
- **`EventTimeout`** — a previously-armed timer fired.
|
||
|
||
And produces a sequence of actions:
|
||
|
||
- **`ActionTransmit`** — push these bytes onto the wire.
|
||
- **`ActionStartTimer`** / **`ActionCancelTimer`** — arm or
|
||
cancel one of T1/T2/T3/T4.
|
||
- **`ActionDeliverBlock`** — pass this received block up to the
|
||
application.
|
||
- **`ActionRaiseError`** — fatal: retries exhausted, line
|
||
protocol violated, etc.
|
||
|
||
State names from [`Protocol::State`](../include/secsgem/secsi/protocol.hpp):
|
||
|
||
```
|
||
Idle ──ENQ─► SendEnqSent ──EOT─► SendBlock ──bytes─► WaitAck ──ACK─► Idle
|
||
└─NAK─► retry (RTY budget)
|
||
|
||
Idle ──ENQ(rx)─► RecvEnq ──EOT(tx)─► RecvBlock ──bytes(rx)─► RecvAck ──ACK(tx)─► Idle
|
||
└─bad checksum─► NAK(tx) → RecvBlock (retry)
|
||
```
|
||
|
||
Tests in [`tests/test_secsi.cpp`](../tests/test_secsi.cpp) and
|
||
[`tests/test_secsi_timers.cpp`](../tests/test_secsi_timers.cpp)
|
||
walk every transition.
|
||
|
||
---
|
||
|
||
## The four SECS-I T-timers
|
||
|
||
Distinct from HSMS T-timers despite the name overlap:
|
||
|
||
| Name | Default | Bounds |
|
||
|------|---------|-------------------------------------------------|
|
||
| T1 | 500 ms | Inter-character — gap between bytes in one block |
|
||
| T2 | 10 s | Protocol — waiting for EOT after our ENQ, or vice versa |
|
||
| T3 | 45 s | Reply — primary (W=1) waiting for the reply block |
|
||
| T4 | 45 s | Inter-block — gap between blocks of a multi-block message |
|
||
|
||
Defaults in
|
||
[`secsi::Timers`](../include/secsgem/secsi/protocol.hpp):
|
||
|
||
```cpp
|
||
struct Timers {
|
||
std::chrono::milliseconds t1{500};
|
||
std::chrono::milliseconds t2{10000};
|
||
std::chrono::milliseconds t3{45000};
|
||
std::chrono::milliseconds t4{45000};
|
||
uint8_t rty = 3;
|
||
};
|
||
```
|
||
|
||
Each timer is armed by the FSM via `ActionStartTimer`, cancelled
|
||
by `ActionCancelTimer`, and fired by the wrapping host's wall
|
||
clock. The FSM itself has no wall clock — it only sees
|
||
`EventTimeout` when the host tells it the timer fired.
|
||
|
||
Tested independently in
|
||
[`tests/test_secsi_timers.cpp`](../tests/test_secsi_timers.cpp)
|
||
(9 cases — every armed-and-cancelled scenario, every expiry).
|
||
|
||
---
|
||
|
||
## What's implemented here vs. what isn't
|
||
|
||
Per [docs/COMPLIANCE.md](COMPLIANCE.md) §1a:
|
||
|
||
| Item | Status |
|
||
|--------------------------------------------|--------|
|
||
| 10-byte block header bit-packing/unpacking | ✅ |
|
||
| Length-prefixed block + 2-byte checksum | ✅ |
|
||
| Multi-block split / assemble (E-bit, block#) | ✅ |
|
||
| ENQ/EOT/ACK/NAK half-duplex handshake | ✅ |
|
||
| RTY retry budget | ✅ |
|
||
| T1/T2/T3/T4 timer hooks (event-driven) | ✅ |
|
||
| Master/slave contention resolution | ✅ |
|
||
| TCP tunnel for testing | ✅ |
|
||
| **Serial-port driver (asio `serial_port`)** | **⬜ deferred** |
|
||
|
||
The FSM is complete and tested end-to-end **over a TCP transport**:
|
||
[`secsi::TcpTransport`](../include/secsgem/secsi/tcp_transport.hpp)
|
||
wraps the FSM behind an asio TCP socket. This is enough for
|
||
testing and for the docker-compose interop flows, but it's not a
|
||
real serial port.
|
||
|
||
The remaining piece — a serial driver that pumps bytes between
|
||
the FSM and an `asio::serial_port` — has not been written. Most
|
||
modern GEM equipment runs HSMS; the deferral is documented in
|
||
the README "Deferred follow-ups" section. Mirror `TcpTransport`
|
||
to add it.
|
||
|
||
---
|
||
|
||
## Why this matters even if you only run HSMS
|
||
|
||
Two reasons to read this chapter even if you'll never touch serial:
|
||
|
||
1. **The line-turnaround FSM informs the GEM communication state
|
||
machine.** E30 §6.5 reuses the establish-comms pattern that
|
||
originated in SECS-I — T_CRA / T_DELAY echo T3 / T2. See
|
||
chapter [13](13_e30_gem.md).
|
||
2. **Block-level error recovery is a useful mental model.** Even
|
||
on HSMS, the per-message correlation by `system_bytes` and the
|
||
T3 reply timer are direct descendants of SECS-I's block-level
|
||
tracking. Understanding one helps you read the other.
|
||
|
||
---
|
||
|
||
## Where to go next
|
||
|
||
Now you know both transports. Chapter [13](13_e30_gem.md) lifts up
|
||
one level: **E30 — GEM behaviour**. This is where the protocol
|
||
stops being plumbing and starts encoding *what equipment is
|
||
supposed to do*: communication state, control state, the GEM
|
||
Fundamental + Additional capabilities, scenarios for every
|
||
typical interaction.
|
||
|
||
Next: [→ 13 E30 — GEM behaviour](13_e30_gem.md)
|