Handoff: the ACCP <-> ND-5000 link seam, for the station split¶
Full path: E:\Dev\Ronny\NDInsight\SINTRAN\ND5000\HANDOFF-ACCP-LINK-SEAM-CONTRACT-2026-08-04.md
Date: 2026-08-04
To: the LLM splitting OctobusND5000Station so only its ACCP part moves to the real firmware
From: the control-store link side
Why you are getting this: the link contract changed twice today, and one of the changes
invalidates a shape the station split would naturally assume.
FILE OWNERSHIP - agreed 2026-08-04, to stop two agents colliding¶
We already collided once today: a file read mid-write by the other agent was diagnosed as "my edits were reverted". They had not been. So we split by FILE, not by goal.
| Owner | Files (RetroCore, full paths under E:\Dev\Repos\Ronny\RetroCore\) |
|---|---|
| You | Emulated.HW\ND\CPU\NDBUS\OctobusND5000Station.cs, Emulated.HW\ND\CPU\NDBUS\NDBusOctobus.cs, Emulated.Machines\ND\ND100\ND100Machine.ND5000.cs, Emulated.Machines\ND\ND100\ND100Memory.cs |
| Me | Nuget\HackerCorpLabs.Emulation.Machines.Accp\src\Devices\Nd5000ControlStoreLink.cs, ...\src\Devices\Nd5000LinkWindow.cs, ...\src\AccpMachine.cs |
If you need a change on my side, ask rather than edit - and I will do the same.
THE ONE THING THAT WILL BITE THE SPLIT¶
The station models the ACCP as a COMMAND layer: one command in, one reply out. That is right for
the octobus command library (OMD 3) and it is how CMSYSPAR, RTEST, ALIVE, LPARP/VPARP
behave. It is NOT how the control-store layer works, and a seam carved on the command shape will
not pass the control-store traffic through.
The real firmware drives the ND-5000 through a register/shift protocol, not commands:
| Address | Role |
|---|---|
0x220000 |
clock/command port - BOTH a parallel command register AND a shift clock, decided by the value |
0x330000 |
gate latch (bit 1 or bit 2 - see below) |
0x440000 / 0x550000 |
the 32-bit data pair; 0x550000 is also the microword staging port |
0x660000 |
status; bit 0 = "control-store operation OK" |
A single control-store access is: shift 8 x 16 bits through 0x550000 with 8 clock pairs each, then
one command word. There is no request/reply framing anywhere in it.
THE TWO OPERATIONS - do not conflate them (this was a real defect, fixed 2026-08-04)¶
Carved from octo.bin at 0x773A:
7746 jsr 0x7776 ; shift the 128-bit microword OUT
774C move.w #0x2018,(0x220000) ; the operation
7754 unlk / rts
Compare 0x741E:
741E <word parameter in D0>
742E jsr 0x76E6 ; address phase
7434 bset #2,(0x001144EE) ; -> 0x330000, GATE ON
7446 move.w #0x0018,(0x00220000) ; the control-store command
744E btst #0,(0x00660000) ; STATUS CHECK
7484 bclr #2,(0x001144EE) ; gate off
7496 jsr 0x775A ; read-back verify
0x0018 (CommandPerform) |
0x2018 (CommandOperation) |
|
|---|---|---|
| Gate set by the routine | YES (bit 2) | no |
| Address phase | YES | no |
| Status checked | YES (0x660000 bit 0) |
no |
| Target | control store, at an address | the MIR register |
| Boot uses it | 4 times | 284 times |
An operation with no address cannot write addressed storage. 0x2018 loads the microinstruction
register - which is what the card's own MIR test and Start/stop microprogram test drive.
How this stayed hidden, and the lesson worth carrying into the split: both commands were routed to the same commit, so every
0x2018wrote control-store address 0, 284 times per boot. Nothing failed. The read-back verify still matched, because it read back the same slot that had just been overwritten. A test assertingMicrowordsWritten >= 200during boot PASSED - it was encoding the defect. The only external symptom was a wrong CLAIM ("booting the card loads 281 microwords"). A round trip that agrees with itself proves nothing about what the far end is.
MEASURED FACTS THE SPLIT MUST NOT RE-DERIVE¶
- The gate is bit 1 OR bit 2 of the
0x330000latch. Boot uses bit 1 (0x764Epath) exclusively; bit 2 (0x741E) is the console command. Recognising only bit 2 gave 1.6M clock pairs and ZERO microwords. - ~~The address is the NINTH gated word, after the eight halves - not a pre-gate write.~~ REPLACED 2026-08-04 - see "THE ADDRESS PHASE, CARVED" below. The firmware does write the address ninth, but it is the address PHASE doing it, ended by an explicit command. The old reading only worked because the folded latch held the gate open across the whole shift.
- ~~The completed word must be LATCHED - the card closes and reopens the gate between the shift and the perform.~~ REFUTED 2026-08-04 - an artifact of the fold.
-
~~Multiple shift+operate cycles happen inside ONE gate window - staging must reset after each.~~ REFUTED 2026-08-04 - an artifact of the fold.
Audited by counting which buffer each commit actually took its words from, over a real boot: latch / staging / ring = 0 / 0 / 8. Neither the latched-word path nor the gated staging buffer is used at all any more; every microword is shifted out ungated and committed from the ring. Both claims were compensating for the gate spuriously closing when
0x330001traffic was being read as a gate change.The gate therefore does not capture the shift. What it does select is still open - the carve's phrase "BUFFERED CI-bit groups" may still be the answer, but nothing here has shown it. The latch and staging code paths in
Commitare now dead for the boot path and are candidates for removal once the console path has been audited the same way.This is the concrete cost of the fold: two "MEASURED" facts that were really workarounds. 5.
0x440000must be HELD and echoed (a0x71F832-bit round trip), not ignored. 6.0x2018words can arrive GATED or UNGATED. The boot's0x764Epath holds the gate open across the whole sequence; the console path shifts ungated. Read only one and you get zero. [Learned by being wrong: a first attempt read only the ungated buffer and a real boot returnedMirLoads = 0.] 7. Word order:word[0]= bits 127-112, confirmed three ways.
WHAT THE LINK OFFERS YOU¶
Nd5000ControlStoreLink (my side) exposes, all earned rather than stubbed:
MirHi/MirLo/MirLoaded/MirLoads- the microinstruction register.MicrowordsWritten- addressed control-store writes ONLY. Do not use it as a proxy for activity; that is the mistake above.ClockPairs,GateOpens,PerformCommands- diagnostics. A run with many clock pairs and no gate opens means a path is unrecognised; that is how the bit-1 gate was found.Trace/TraceEnabled/TraceLimit- operation log, clocks excluded.HighBits(viaIAccpStatusBitSource) - contributes0x660000bit 0.
The ND-5000 side is the IControlStoreSink interface (WordCount, WriteWord, TryReadWord).
It exists so the ACCP package never depends on the CPU package - please keep that direction. A real
ControlStore satisfying it is proven by Nd5000RealControlStoreTests.
WHAT IS STILL OPEN ON MY SIDE¶
Start/stop microprogram test abc failed at CSA: 00FFH. The card now reports a VALUE where it
used to report only a failure, and 0x00FF has the shape of an unmodelled register reading all-ones.
The MIR is loaded correctly; what is missing is CSA (the control-store address register) and,
presumably, anything that would make the microengine actually step. That is the remaining half of
"run it", and it is mine.
Do not model CSA on your side - if we both do it we will disagree, and the card will believe whichever answers first.
First carve of the start/stop test - what it actually drives [V, ROM read 2026-08-04]¶
The failing test body is ROM 0xBE22-0xC05A (bounded by its own two loop branches). Its calls:
| Target | Called from | What it is |
|---|---|---|
0x773E |
0xBE78 |
the MIR load (jsr 0x7776 + 0x2018) - so this test drives the path fixed in c1e73a711 |
0x775A |
0xBE9E |
the read-back verify (0x2010 + 0x77B6) |
0x474C |
0xBF28, 0xBFA6 |
hex-print helper, WORD (masks 0xFFFF, tag 0x30) |
0x478C |
(same family) | hex-print helper, BYTE (masks 0xFF, tag 0x30) |
0x1A0A, 0x1BF6 |
several | console print helpers |
Loop structure: an inner count to 7 (0xC050) inside an outer count to 0x7F (0xC05E), so
8 x 128 iterations. The failure message descriptor is at 0x11B00 (" failed at CSA:"), printed from
0xC0B6.
CORRECTION, same day, before anyone acted on it. The first version of this section said
0x474C/0x478Cwere a hardware service call and therefore that00FFHwas "the signature of an unanswered read, NOT a wrong value - do not go looking for an off-by-one". Both halves were wrong, and the advice was actively misleading. The setup at0xBF00settles it:bf0a lea (0x00013044).l,A1 ; format-descriptor table bf10 move.w (A1,...),(0x16,A0) ; descriptor into the call frame bf1e lea (0x001144F0).l,A2 ; the 128-bit microword buffer bf24 move.w (A2,D0.l),D0 ; the VALUE, read out of that buffer bf28 jsr 0x474C ; print itThey are hex-print helpers -
0x474Cword,0x478Cbyte, both tagged0x30. So00FFHis a VALUE the test read back, and an off-by-one or a wrong read-back word is exactly the kind of thing that could produce it. How the error happened: the wrapper's shape (stash two words, tag with a byte, branch to a dispatcher) reads like a device call, and I concluded from the shape without looking at what the CALLER passed in. One dump of the call site refuted it.
How the test actually works (0xBE6C onward):
be7a jsr 0x773E ; MIR load - the 0x2018 path
be80..be9e ; CLEAR the 0x001144F0 buffer, words 0..0x0E
bea0 jsr 0x775A ; verify = shift the microword back IN
beba lea (0x001144F0).l,A0
bec0 move.w (A0,D0.l),D2 ; what came back
bec4 cmp.w (0x20,A6,D1.l),D2 ; against the expected array in the frame
bec8 beq ... ; mismatch falls through to the failure print
It zeroes the buffer between write and read-back on purpose, so a read-back that returns nothing
cannot be mistaken for a match. That is the same anti-vacuous discipline used in
ScanAccpBitDispatchTests, and it means the value printed is genuinely what our link supplied.
Where the CSA value comes from - and why chasing it statically is a dead end [V, ROM read 2026-08-04]¶
The CSA failure print is a fragment of a chained print routine, not a subroutine. Nothing in the
ROM does jsr 0xC0AE (searched all 131072 bytes); each fragment ends 4ED5 jmp (A5), so A5 is a
continuation register and the fragments are strung together by whoever set it.
c0ae 2f0e / 2c56 / 2d4f 0008 ; PLANC prologue, re-frame to caller
c0b6 45ee 0028 lea (0x28,A6),A2 ; install the arg block at frame+0x28
c0bc 41f9 00011B00 lea 0x11B00,A0 ; " failed at CSA:"
c0c8 22d8 x3 move.l (A0)+,(A1)+ ; 12-byte descriptor into the arg block
c0ce jsr 0x1A0A ; console print
c0d6 3039 001131FC move.w (0x001131FC).l,D0 ; <-- THE NUMBER
c0e8 3179 001131FC 0014 ; ... into arg slot 0x14
c0f0 43f9 00013044 lea 0x13044,A1 ; format-descriptor table (same one as 0xBF0A)
c0f6 3171 0800 0016 ; descriptor into arg slot 0x16
c100 jsr 0x474C ; hex print, WORD
MEASURED, and it refuted the reading above [V, emulator run 2026-08-04]¶
Nd5000CsaFailureTraceTests watches 0x001131FC through a real 120-million-instruction boot.
The cell changes exactly FOUR times, all inside the RAM walk-test, and never holds 0x00FF:
#0 @ 44,793 PC 0x000BE6 0x0000 -> 0x0011
#1 @ 44,798 PC 0x000BF6 0x0011 -> 0xFFEE
#2 @ 124,294 PC 0x000C50 0xFFEE -> 0x0000
#3 @ 150,143 PC 0x000DB6 0x0000 -> 0x0010
It settles at 0x0010 before the selftests even start and is never touched again, while the console
still prints failed at CSA: 00FFH. So 0x001131FC is not the printed value.
Re-reading the fragment with that in hand, the operand roles are the other way round:
0x001131FC(=0x0010) is a format/width selector. It is what gets+0x20-ed, doubled and used to index the table at0x13044- and0x13044is not a descriptor table at all, it sits inside a string blob ("d not store$Max address "), so the index reaches a descriptor further in.- The printed number is the frame local at
(0x20,A6), loaded by302E 0020at0xC0FCimmediately beforejsr 0x474C.
This is the third wrong answer about this one message, and all three failed the same way: reading a routine's shape and picking the operand that looked like the payload, instead of measuring which one carried it. The neighbouring
0x001131F8/0x001131FAare the CPU-identity word and its valid flag, so0x001131FCis an ordinary variable in that status block - the "180 references = shared print scratch" reading was wrong too.
00FFH is NOT our absent-device fill [V, emulator run 2026-08-04]¶
Worth ruling out explicitly, because the same boot prints Result : 0000FFFFH for the neighbouring
A,MARG D,AIB test and that has the shape of an all-ones fill. Re-run with
AbsentDeviceDataValue and StubReadValue both changed to 0x5A:
CSA line with fill 0x5A: failed at CSA: 00FFH
Unchanged. 0x00FF is a value the firmware computed, not a byte this emulator handed it. The
"unanswered read" theory has now been floated twice and is measured dead - do not raise it a third
time.
SOLVED: 00FFH is a HARD-CODED CONSTANT, and the real test is elsewhere [V, measured + ROM 2026-08-04]¶
Trapping 0xC0FC gives exactly one hit: D0 = 0x000000FF, A6 = 0x11008C, and the console prints
failed at CSA: 00FFH. So the printer decode is confirmed end to end, and the slot is the fixed
address 0x001100AC. Watching that names the producer - PC 0x00CDA6, the last write before the
print (0x0011 -> 0x00FF).
And 0xCDA6 is this:
cd78 4eb9 0000775A jsr 0x775A ; the read-back verify (shift the microword IN)
cd7e 700c moveq #12,D0
cd80 41f9 001144F0 lea 0x001144F0,A0 ; the 128-bit microword buffer
cd86 0c70 0100 0800 cmpi.w #0x0100,(0x00,A0,D0.l) ; buffer word[6] must be 0x0100
cd8c 6700 00a4 beq 0xCE32 ; ... and that is the PASS path
cd90 2256 movea.l (A6),A1 ; failure: build the print argument block
cd92 45f9 001144F0 lea 0x001144F0,A2
cd98 234a 0014 move.l A2,(0x14,A1) ; point it at the buffer
cd9c 42a9 0018 clr.l (0x18,A1)
cda0 7207 moveq #7,D1
cda2 2341 001c move.l D1,(0x1C,A1) ; 8 words to dump
cda6 337c 00ff 0020 move.w #0x00FF,(0x20,A6) ; <-- the "CSA" value, an IMMEDIATE
cdac 6100 f300 bsr 0xC0AE ; the failure printer
0x00FF is a literal in the EPROM. It is a canned marker stored just before the failure print,
not a control-store address, not a read-back word, and not a measurement of anything. There is no
off-by-one to hunt and never was - the number carries no information at all.
The actual pass condition is one line earlier: after jsr 0x775A shifts the microword back in,
word[6] of the buffer at 0x001144F0 must read 0x0100. Nothing else is compared. (Word[6] is
the field the load test already noted as incrementing from record to record in the EPROM's own
microcode blob.)
That is the concrete, checkable thing the link now has to satisfy, and it is what "run it" reduces
to on this test. The three calls immediately before the verify - 0x7A66, 0x78B2, 0x7A84 - are
the start/stop microprogram operations that are supposed to make word[6] come back as 0x0100;
carving those is the next step.
WHY word[6] never comes back as 0x0100 - three gaps in the link [V, ROM read 2026-08-04]¶
The three calls before the verify carve cleanly. 0x7A66 is START, 0x7A84 is STOP (they set and
clear the same flag at 0x001143AC), and 0x78B2 is the known 0x660000 bit-4 spin. The real work
is in 0x78CA (start) and 0x795A (stop):
78ca 4e56 ffe8 link A6,#-0x18
78d2 3d40 0014 move.w D0,(0x14,A6) ; the start ADDRESS parameter (0 from this test)
78d6 6100 fe0e bsr 0x76E6 ; address phase - same one the 0x0018 path uses
78da 1039 001144EF move.b (0x001144EF).l,D0 ; shadow of a SECOND latch byte
78e2 0881 0001 bclr #1,D1
78e6 13c1 00330001 move.b D1,(0x00330001).l ; ... which lives at 0x330001
78ec 1439 001144EE move.b (0x001144EE).l,D2 ; shadow of the KNOWN latch
78f2 08c2 0002 bset #2,D2
78f6 13c2 00330000 move.b D2,(0x00330000).l ; gate on, as usual
78fc 33fc 0017 0022 0000 move.w #0x0017,(0x00220000) ; COMMAND 0x0017
7904 33fc 0015 0022 0000 move.w #0x0015,(0x00220000) ; COMMAND 0x0015
790c 13f9 001144EE 00330000 ; gate off from the shadow
7916 0000 005c ori.b #0x5C,D0 ; ... and 0x330001 gets bits 2,3,4,6 set
791a/7920 ; shadow and latch 0x330001 updated
7926 0c79 5400 001131F8 cmpi.w #0x5400,(identity) ; an ND-5400 / ND-5500 only tail follows
Three things the link does not do:
- Commands
0x0017and0x0015are unknown to it.Nd5000ControlStoreLink.WriteCommandrecognises0x0018,0x2018,0x2010and0x2011and nothing else. These two are the start-microprogram pair; the stop path at0x795Aissues its own. No microprogram ever runs, so nothing can update word[6]. This is the direct cause of the CSA failure.
THE NINTH-GATED-WORD ADDRESS MODEL IS ENTANGLED WITH A BUG [V, measured 2026-08-04] - READ THIS¶
Splitting the two latch bytes exposed something bigger, and it undermines fact 2 in the MEASURED
list above. Both gate bits provably live in 0x330000:
7434 08f9 0002 001144EE bset #2,(0x001144EE) ; console 0x741E - gate bit 2
765a 08f9 0001 001144EE bset #1,(0x001144EE) ; boot 0x764E - gate bit 1
13f9 001144EE 00330000 ; BOTH store that shadow to 0x330000
So gating on 0x330000 alone is the correct model - and it fails. The typed
LOAD-CONTROL-STORE then stages nothing at all. Measured over a boot plus one typed command:
| Byte | Writes | Settles at |
|---|---|---|
0x330000 |
208 | 0x00 |
0x330001 |
42,297 | 0x52 (bit 1 SET; flips 0x53/0x13, bit 6 is the AOB strobe) |
The odd byte holds bit 1 set almost permanently, so folding the two bytes together held the gate open across the entire shift sequence - by accident. Every word therefore looked "gated" and the address looked like a ninth gated word. Split the registers correctly and those words shift out UNGATED and never reach the staging buffer.
The ROM agrees that the model is wrong: the address phase is its own routine (0x76E6) called
before the gate opens, in both the console path and the start path. That is not "the ninth gated
word".
RESOLVED the same day - the fold is GONE. Gating is now
0x330000only, which is what the ROM says. What made it possible was carving the address phase; see below.
THE ADDRESS PHASE, CARVED - and it replaces the ninth-word model [V, ROM + measured 2026-08-04]¶
0x76E6 is called by EVERY control-store path (console 0x741E, boot 0x764E, start 0x78CA)
before the gate opens:
76ee 3d40 0014 move.w D0,(0x14,A6) ; the address parameter
76f2 207c 00220000 movea.l #0x00220000,A0
76f8 303c 0010 move.w #0x0010,D0 ; loop count = 16
7704 33ee 0014 00550000 move.w (0x14,A6),(0x00550000).l ; the address, written ONCE
770c 3084 / 3085 move.w D4,(A0) / move.w D5,(A0) ; 16 clock pairs
7714 30bc 3010 move.w #0x3010,(A0) ; LATCH IT AS THE ADDRESS
7718 1439 001144EF ... bclr #1 -> (0x00330001) ; bracket the strobe
7728 30bc 0015 move.w #0x0015,(A0) ; the strobe
772c 13f9 ... restore 0x330001 from its shadow
Three corrections fall out:
0x3010is the address-latch command. The word most recently written to0x550000is the control-store address. Explicit, so it no longer depends on the gate being open at the right moment - which is exactly what let the fold be removed.0x0015is a generic STROBE, not a microprogram run. It is issued here, by every access. My first pass named itCommandMicroprogramRunbecause it follows0x0017at0x7904; a typedLOAD-CONTROL-STOREthen logged two "MICRO-RUN" entries with no0x0017anywhere, which refuted it. RenamedCommandStrobe.- The shift ring needs NINE slots, not eight. The address goes through the same
0x550000port right after the eight halves, so with eight slots it evicted the first half and the microword committed one word out of step - measured as33445566...F0010100stored where11223344...DDEEF001was expected.
Result, measured: the typed command now stores 112233445566778899AABBCCDDEEF001 at 0x0100,
and a boot performs 8 addressed control-store writes where the folded model saw 4. MIR loads
unchanged at 284.
0x330001is a SECOND latch byte and the link folds it into the first.Nd5000LinkWindow.WriteBytecalls_link.WriteLatch(value)for theLatchrole without passing the address, so writes to0x330000and0x330001land on one register. The start/stop sequence drives0x330001bits 0, 1, 2 and 3 and ORs in0x5C, entirely independently of the gate bits on0x330000.This is the same defect shape as the
0x2018/0x0018conflation - two distinct registers routed to one handler, invisible because the paths that exercised it only ever used one of them. Worth fixing on its own merits even before the start/stop work.- The address phase runs, so a microprogram start has a control-store address (0 in this test).
Whatever models the start must take it from the same staging the
0x0018path uses.
Also noted, not a gap: the tail at 0x7926 only executes for identity 0x5400 / 0x5500
(ND-5400 / ND-5500). The default configuration reports ND-5800, so it is skipped - but a test that
sets a different identity word will take a different path through both start and stop.
Method note worth keeping. Three hand-decodes of this message produced three wrong answers. Two instruments - a word watch and a PC trap, both in
AccpMachineand both off by default - settled it in about twenty seconds of run time each. When a value cannot be traced statically, stop reading and measure.
The generic mismatch path at 0xBECC (descriptor 0x11B1C) remains a different site; the two were
conflated once already.
STATE OF RECORD¶
- Commit
c1e73a711- the0x2018/0x0018split. ACCP suite 135/135, 0 failed. - Commit
6dc2ad5fb- the AFLAG seam (AflagAtrapBit = 5; FATAL is a trap-word payload). - Commit
55aa3edce- the control-store link itself. - Companion docs:
ACCP-EMULATION-STATUS-AND-HANDOFF.md(headline section),ANSWERS-ACCP-CPU-SEAM-CONTRACT-2026-08-04.md,ACCP-COMPLETE-REFERENCE.mdsections 2.4l-2.4n.