ND-120 memory-parity semantics: what the self-test and runtime actually require¶
Full path: Verilog/docs/nd120-parity-analysis.md
Date: 11-JUL-2026 (section 6 corrected 28-SEP-2026). Written for the
18-bit -> 16-bit SDRAM packing work (ND_SDRAM_PACK16, done 11-JUL-2026,
commits d26fd66..2c44efb). Every claim below carries a microcode reference; the
semantics are pinned by testbench (see section 6), not by this prose.
1. Verdict¶
The ND-120/ND-110 CPU self-test does NOT test memory parity. It is a pure CPU-core test (ALU, shifts, register file, STS, loop counter, SWAP, PIC/PID). It never writes bad parity, never reads stored parity bits back, and never waits for a parity-error flag or trap. Of the three cases in the work order section 2 - (a) stored-parity readback, (b) error-flag-on-readback, (c) datapath-only exercise - none applies: the self-test does not exercise memory parity at all.
Runtime software (SINTRAN) consumes parity only through the error
machinery: internal interrupt level 14 with an IIC code, plus the
PES (Parity Error Status) and PEA (Parity Error Address) internal
registers read via TRA. No code path reads stored parity bits as data.
Consequence: implementation shape (a) COMPUTED PARITY from the work order is sufficient. Store 16 data bits, two ND words per 32-bit SDRAM location; generate parity on the read path so the AM29833A checkers always see consistent parity. No stored-bad-parity shim, no side-band BRAM window, no side-band SDRAM region is required.
2. Ground truth used¶
- Clean ROM-validated ND-110 source (same labels as ND-120; the
authoritative semantic decode):
$ND_REPOS/ND110Compile/ND110Compile/uCode/ND-110-RASK.uc, self-test at lines 4859-5199. - L-EPROM decode (bit-exact):
the nd120uc repository,
source/nd-120-delilah.uc, self-test at octal o2053-o2156. (Labels/comments in this range are OCR-polluted - trust the bits, not the labels.) - Address-annotated listing:
Code/Microcode/ND-120-DELILAH-L.LISTING.txtlines 5290 (self-test start), 5518 (o2123), 5676 (o2156 STERR). - Token semantics:
nd120uc/scripts/nd120_tokens.json(IDBS,PEA= "PEA-REGISTER -> IDB",IDBS,PES= "PES-REGISTER -> IDB"; no token forces bad parity or inhibits the parity generators).
3. The self-test, subtest by subtest¶
Entry SELFT: at octal o2053 (RASK.uc:4862; listing line 5290). Eight
subtests; each preloads R2 with a fixed error code before running:
| # | RASK.uc line | Tests | Error code (R2, octal) |
|---|---|---|---|
| 1 | 4867 | ALU: 0-1+1=0 | 400 |
| 2 | 4887 | ALU add-loop: 0+1*5=5 | 1000 |
| 3 | 4917 | shift double left | 1400/2000 |
| 4 | 4966 | -1 -> R5, 0 -> Q | 2400/3000 |
| 5 | 4998 | 170(AARG) -> SWAP = 74000 | 3400 |
| 6 | 5023 | loop counter LC, shift-right-double via GPR | 4000/4400 |
| 7 | 5072 | STS + register file (COMM,EWRF) | 5000/5400/6000 |
| 8 | 5123 | PIC/PID register | 6400 |
No PARITY subtest. No MOR/PEA/PES/ECC write anywhere in the
program. A repo-wide microcode search for parity manipulation returned only
UART even-parity comments (o1724-o1761, o4090-o4096) - serial-line parity,
unrelated to memory.
TEST 6 is the loop the Verilog boots into (o2116-o2123; the boot-golden-spec "Phase 4 self-test loop"). It is a shift/loop-counter test, not a parity test.
4. Where the folklore came from¶
Two artifacts:
- The
IDBS,PEAword at o2123. Decoded L-EPROM word:ALUD,NONE ALUF,A+Q LCOUNT IDBS,PEA T,HOLD T,NEXT(delilah.uc o2123; RASK.uc:5064-5066, whose comment literally reads% LOOP BACK).ALUD,NONEdiscards the ALU result; the word's real job isLCOUNT(decrement the loop counter) and fall-through for TEST 6. The IDB-source select is a don't-care - the PEA read is never consumed, never compared, never checked. This is loop plumbing, not a parity test. - A stale RTL claim.
Verilog/TODO.md(lines ~142-153) and the work order describeAM29833A.vas stubbed (ERR_n=1,PAR_OUT=0). That is no longer true:Verilog/Shared/support/AM29833A.vimplements real parity (PAR_OUT = ~(^R) on transmit, 9-bit receive-side check register driving ERR_n; reviewed 22-MAR-2025). The stub is history; the semantics below were derived from the actual module.
Also corrected: the note "self-test STERR increment = o1134" (nd120-fpga
skill / older notes) is wrong for the L EPROM. o1134 is unrelated
firmware-loop code (FWLO3, IDBS,SWAP). STERR: is at o2156
(delilah.uc o2156; RASK.uc:5193; listing line 5676) and it is not a
counter: it is the error display/halt routine - a failing subtest jumps
to STERR/STER1/STER2, which display error number R2/R2+1/R2+2 and then
% LOOP ON ERROR (RASK.uc:5197). A failing CPU parks displaying the
failing subtest's code.
Implication for the 7/14 self-test baseline: none of the 8 subtests touches memory parity, so the parity path (stubbed or real) cannot be the cause of any of the 7 failures. The hoped-for "parity fix improves the baseline" side effect in the work order will not materialize via the self-test; the real suspects remain the ones in TODO.md's microcode-fidelity section (JMP0-3 vectored jump, IDB OR-bus merging).
5. Runtime (SINTRAN) parity surfacing¶
TRA PES- handlerTAPESreadsIDBS,PES(RASK.uc:8864; L-EPROM o3673, delilah.uc line 10944). A second pathAPES1at o0514 (delilah.uc line 2235) doesCOMM,SIOC, commented "TRA PES ... NEED TO CLEAR DOWN PARERR INTERRUPT FROM THE ON-BOARD MEMORY".TRA PEA- handlerTAPEAreadsIDBS,PEA(RASK.uc:8868; L-EPROM o3675, delilah.uc line 10954).TRA IIC-AIIC1(RASK.uc:2059; delilah.uc o0500) returns the internal interrupt code used on the level-14 dispatch to distinguish memory/parity errors from other internal faults.
All of these read purpose-built error registers, not parity bits as
data. Note also that in the current RTL the parity-error path is
switch-disabled: MEM_DATA_46.v ties the PAL_45008B SWDIS_n input to
GND ("SW4 - Parity disable ... HERE: Disabled!").
6. The pinned contract (what the testbench encodes)¶
For the packed 16-bit storage mode (ND_SDRAM_PACK16):
- Data round-trip: all 16 data bits (DD[16:9], DD[7:0]) of every ND word are stored and read back exactly; two adjacent ND words (even/odd) sharing one 32-bit SDRAM location are fully independent - writing one never disturbs the other (DQM lane-masked single-access writes, no read-modify-write).
- Computed parity on read: DD[8] and DD[17] are regenerated as odd
parity (DD[8] = ~^DD[7:0], DD[17] = ~^DD[16:9]), so
CORR_n= 1 and the AM29833A receive-side check always passes for stored data. - Bad-parity writes are absorbed: a write with deliberately wrong DD[8]/DD[17] stores the 16 data bits; readback returns the data with CORRECT (computed) parity. This is a deliberate, documented semantic change from stored-parity 18-bit mode, licensed by sections 3-5: no self-test or runtime path observes it.
- Legacy mode untouched: without
ND_SDRAM_PACK16the 18-bit one-word-per-location behavior (including stored-bad-parity round-trip) is bit-identical to before; Verilator/Basys3 builds are unaffected entirely (the define only exists inside the Tang SDRAM backend).
Testbench: Verilog/fpga/tang-nano-20k/sdram-bridge/sim/ - the 18-bit tb
(test) plus the packed-mode tb (test-pack16), both registered in
Verilog/tests/run_all_tests.sh and emitting TB_RESULT: PASS.
6b. POLICY (Ronny, 3-AUG-2026): no FPGA target ever STORES parity¶
Section 6's contract was written for the Tang SDRAM backend only. It is now the rule for every sheet-49 memory backend, by decision, not by build flag: FPGA block RAM is never spent on parity bits. One bit per word costs a whole RAMB18 per chip (the smallest block the tools allocate) to hold data nothing reads back.
Every backend therefore drops DD[8] / DD[17] on write and regenerates them on
read as odd parity of the byte returned - PAR = ~^data, the Am29833A
convention:
| Backend | Target | Before 3-AUG-2026 | Now |
|---|---|---|---|
MEM_RAM_49_SDRAM (ND_SDRAM_PACK16) |
Tang | regenerated | unchanged |
SIP1M9 ramSize=3 |
Basys3 | returned constant 0 | regenerated |
SIP1M9 ramSize=2 |
Verilator | stored | regenerated |
MEM_RAM_49_BLOCKRAM |
FPGA variant | stored, 18-bit array | regenerated, 16-bit array |
MEM_RAM_49_SIM |
Verilator | regenerated only under ND_SDRAM_PACK16 |
regenerated always |
The old Basys3 behaviour was not merely "not stored" - constant 0 is the
WRONG parity for every byte of even population, i.e. 128 of 256 values
(measured, see the tb below). It survived only because MEM_43.v masked
LPERR_n at the time (fixed later in 148594d, see below). The sim backends were changed too, deliberately: a backend that
stores parity in Verilator while the FPGA regenerates it is the sim-vs-silicon
split that the golden reference exists to prevent.
RAM_PARITY_STORAGE is gone. There is no way to switch storage back on, which
is the point.
Gates:
Shared/support/sim :: test-am29833a-parity- drives the chip that actually checks parity over all 256 byte values: regenerated parity must never fault, the inverted bit must always fault, and constant 0 faults exactly 128/256. This is what makes unmaskingLPERR_na safe future step.CPU-BOARD-3202/circuit/sim :: test-memchain{,-blockram,-sim}- the same chain testbench against three backends. Its parity sweep writes 16 patterns with deliberately wrong parity and requires correct regenerated parity back, so storing, zeroing or inverting all fail. Teeth-proven 3-AUG-2026: a build with Q9 forced back to 0 fails 35 checks.
Two reasons parity was never CHECKED:
MEM_43.vforcedLPERR_ninactive with an OR with 1. Fixed in 148594d ("sheet 43 LPERR~ was forced inactive by an OR with 1");MEM_43.v:270is nowassign LPERR_n = s_lperr_n;.- Still open:
AM29833A.vevaluates its parity check only when!ReceiveModewhileMEM_DATA_46wires the memory bus to T and LBD to R - so a memory READ is receive mode and the board's check does not evaluate there at all. SeeVerilog/TODO.md.
6c. The forced-parity-error probe path (TRR ECCR), 11-AUG-2026¶
From the retired HANDOFF-mpm5-parity-eccprobe.md (git 202c606).
- Links 1-3 of the probe path exist and are correct: TRR ECCR (IOX 100115)
is decoded in
CGA_MAC_APOS_CALCA.v:99and reaches the memory sheets; PAL_45008B latches TST/DISB and drives both AM29833A output enables on a test write;AM29833A.vforced-error mode inverts the parity bit (fixed 30-JUL). - Link 4 is the break: every backend regenerates parity on read, so the
injected error is healed. Two downstream slips were fixed: 6d95b09
(MEM_DATA_46 gated the AM29833A ERR pins on OET_n, not on the drawing,
sheet 46 E2-F3) and 148594d (MEM_43
LPERR_n = s_lperr_n | 1). - Verilator only:
MEM_RAM_49_SIM.vstores a per-byte-lane "deliberately corrupted" flag (b0_lo_bad .. b2_hi_bad), set only by a forced-error write and used to invert the computed parity on read; it does not self-clear (nd100x consumes its latch on read - if TPE MEMORY hangs at PIL 14 after the parity-detection subtest, try consuming it). Whether CONFIGURATION then shows "Local" in Verilator was never measured. - Not done: (1) the FPGA path still regenerates parity, so silicon reports
Mpm 5 (a flag there costs SDRAM and reverses 451b05b - the owner's call);
(2)
ND3202D.vs_ibperr_n = 1'b1(ND-bus parity, left alone); (3) PGS has no lock (reference: errors lock PGS, TRA PGS unlocks; CGA_IDBCTL_PGSREG has none and EPGSN never reaches it); (4) VEX is commented "Violation exception" inCGA_DCD.v:34but "Vector EXecute" inCGA_MAC.v:45- one is wrong, not settled; (5)CGA.v:620ORs the IDBCTL output into FIDBO where the drawings do not.
7. References¶
- Work order: the 11-JUL-2026 PACK16 commits (d26fd66..2c44efb) in git history
- Bridge design:
Verilog/docs/nd120-dram-memory.mdsection 6 - Storage consumer of the freed half:
Verilog/docs/nd-storage-design.md - Parity transceiver RTL:
Verilog/Shared/support/AM29833A.v; instantiationVerilog/CPU-BOARD-3202/circuit/MEM_DATA_46.v