e84: per-port FSM via E84PortStore

E84 (Parallel I/O) is fundamentally per-load-port: each port has its
own ten-wire handshake with the AMHS.  Earlier revisions modeled it
as a single equipment-wide FSM; this commit refactors to a per-port
store, so multi-LP tools can run independent handshakes in parallel.

Public API change in EquipmentDataModel:
  E84StateMachine e84;   -> removed
  E84PortStore    e84_ports;  // create(port_id), get(port_id), ...

Convenience pass-throughs: E84PortStore::on_signal_change auto-creates
the port on first use (ergonomic for demos); applications should call
create() explicitly with their full port set.

The two existing callsites (test_gem300_scenario, test_e87_wire_scenarios)
are updated.  The multi-LP test now demonstrates the actual win:
interleaved LP1 load + LP2 unload handshakes that reach their
respective Ready states without sequencing, and an ES on LP1 that
does NOT affect LP2 — exactly the failure mode the previous design
couldn't catch.

Five new dedicated tests in test_e84_ports.cpp for the store itself.

COMPLIANCE.md §4i updated: row now reflects per-port design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:50:18 +02:00
parent 2f0a4ba339
commit 77197b9c1e
7 changed files with 224 additions and 44 deletions
+17 -17
View File
@@ -48,15 +48,15 @@ TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") {
// ---- 2. E84 load handshake at LP1 --------------------------------------
// AMHS asserts CS_0, then VALID; equipment asserts L_REQ; AMHS asserts
// BUSY, then drops BUSY + asserts COMPT.
m.e84.on_signal_change(E84Signal::CS_0, true);
m.e84.on_signal_change(E84Signal::VALID, true);
m.e84.on_signal_change(E84Signal::L_REQ, true);
CHECK(m.e84.state() == E84State::LoadReady);
m.e84_ports.on_signal_change(1,E84Signal::CS_0, true);
m.e84_ports.on_signal_change(1,E84Signal::VALID, true);
m.e84_ports.on_signal_change(1,E84Signal::L_REQ, true);
CHECK(m.e84_ports.get(1)->state() == E84State::LoadReady);
m.e84.on_signal_change(E84Signal::BUSY, true);
m.e84.on_signal_change(E84Signal::BUSY, false);
m.e84.on_signal_change(E84Signal::COMPT, true);
CHECK(m.e84.state() == E84State::Complete);
m.e84_ports.on_signal_change(1,E84Signal::BUSY, true);
m.e84_ports.on_signal_change(1,E84Signal::BUSY, false);
m.e84_ports.on_signal_change(1,E84Signal::COMPT, true);
CHECK(m.e84_ports.get(1)->state() == E84State::Complete);
// Load port transitions on the equipment side.
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartLoading);
@@ -153,15 +153,15 @@ TEST_CASE("GEM300 scenario: carrier arrival -> processing -> departure") {
}
// ---- 12. Carrier unloads via E84 -------------------------------------
m.e84.reset();
m.e84.on_signal_change(E84Signal::CS_0, true);
m.e84.on_signal_change(E84Signal::VALID, true);
m.e84.on_signal_change(E84Signal::U_REQ, true);
CHECK(m.e84.state() == E84State::UnloadReady);
m.e84.on_signal_change(E84Signal::BUSY, true);
m.e84.on_signal_change(E84Signal::BUSY, false);
m.e84.on_signal_change(E84Signal::COMPT, true);
CHECK(m.e84.state() == E84State::Complete);
m.e84_ports.reset(1);
m.e84_ports.on_signal_change(1,E84Signal::CS_0, true);
m.e84_ports.on_signal_change(1,E84Signal::VALID, true);
m.e84_ports.on_signal_change(1,E84Signal::U_REQ, true);
CHECK(m.e84_ports.get(1)->state() == E84State::UnloadReady);
m.e84_ports.on_signal_change(1,E84Signal::BUSY, true);
m.e84_ports.on_signal_change(1,E84Signal::BUSY, false);
m.e84_ports.on_signal_change(1,E84Signal::COMPT, true);
CHECK(m.e84_ports.get(1)->state() == E84State::Complete);
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::StartUnloading);
m.load_ports.fire_transfer_event(1, LoadPortTransferEvent::CompleteUnloading);