ND-120 main memory: how MEM_RAM_49 and the DRAM protocol work, and how to map it per board¶
Full path: Verilog/docs/nd120-dram-memory.md
Date: 8-JUL-2026. Measured numbers come from a Verilator run with -DDBG_MEM
(25,008 memory accesses captured, see Measured protocol).
1. Architecture and hierarchy¶
The ND-120's main memory is the original machine's 1M x 9 DRAM SIP modules (THM91020/THM91070), six of them, driven by a PAL-based DRAM controller. In Verilog:
ND3202D CPU board top (CPU-BOARD-3202/circuit/ND3202D.v)
└─ MEM_43 memory subsystem top (sheet 43)
├─ MEM_ADDR_44 address mux: row/column onto AA_9_0 (HIEN_n/LOEN_n)
├─ MEM_ADEC_45 address decode, bank select, RLRQ generation
├─ MEM_DATA_46 data path, parity generate/check (Am29833A), LBD<->DD
├─ MEM_ERROR_47 error handling (RDATA25)
├─ MEM_LBDIF_48 local-bus interface: grants, MWRITE50_n, RDATA strobe
├─ MEM_RAM_49 THE RAM SHEET - 3 banks x 2 chips (this is the
│ └─ 6x SIP1M9 swap point for per-board memory backends)
└─ MEM_RAMC_50 RAM control: PAL_44803A (arbitration/grants)
+ PAL_44902A (RAS/CAS state machine)
flowchart LR
CPU["CPU / bus masters<br/>(CGNT / BGNT / RGNT)"]
RAMC["MEM_RAMC_50<br/>PAL_44803A grants<br/>PAL_44902A RAS/CAS FSM"]
ADDR["MEM_ADDR_44<br/>row/col mux<br/>(HIEN_n / LOEN_n)"]
LBDIF["MEM_LBDIF_48<br/>MWRITE50_n, RDATA strobe"]
RAM["MEM_RAM_49<br/>3 banks x 18 bit<br/>6x SIP1M9"]
DATA["MEM_DATA_46<br/>parity gen/check<br/>LBD <-> DD"]
CPU --> RAMC
RAMC -->|"RAS, CAS"| RAM
RAMC -->|"HIEN_n / LOEN_n"| ADDR
ADDR -->|"AA_9_0"| RAM
LBDIF -->|"MWRITE50_n"| RAM
RAM -->|"DD_17_0_OUT"| DATA
DATA -->|"DD_17_0_IN"| RAM
LBDIF -->|"RDATA (sample strobe)"| DATA
classDef blue fill:#2196F3,stroke:#1565C0,color:#fff
classDef teal fill:#009688,stroke:#00695C,color:#fff
classDef green fill:#4CAF50,stroke:#2E7D32,color:#fff
classDef amber fill:#FFA726,stroke:#F57C00,color:#000
class CPU blue
class RAMC,ADDR,LBDIF teal
class RAM green
class DATA amber
Organization: 3 banks (BANK0/1/2), each bank = 2 SIP1M9 chips = 18 bits
per word (16 data + 2 parity). Per bank up to 1M words. MEM_RAM_49 ORs the
three banks' DD outputs together; unselected chips drive 0 (the FPGA-safe
"tri-state" convention used throughout this repo).
Interface of MEM_RAM_49 (the contract any board backend must honour):
| Signal | Dir | Meaning |
|---|---|---|
AA_9_0 |
in | Multiplexed address: row during RAS fall, column while CAS low |
BANK0/1/2 |
in | Bank select (already decoded, one-hot) |
RAS, CAS |
in | Active-high strobes from PAL_44902A (chips see them inverted/bank-gated) |
MWRITE50_n |
in | Write when low, stable across the whole access |
DD_17_0_IN |
in | Write data (16 data + 2 parity) |
DD_17_0_OUT |
out | Read data; must be 0 when not selected/reading (banks are OR-ed) |
CORR_n |
out | Parity of read data (chips' PRD outputs ANDed) |
2. Where the clocks come from¶
There is one clock. OSC at the memory subsystem is the same net as
sysclk/clk_cpu from ND120_TOP.v (clk1 = clk_cpu, "CLOCK_1/CLOCK_2
(OSC/bus) run at CPU speed"). Both control PALs are registered PALs clocked
on OSC (PAL16R8): every RAS/CAS/HIEN/LOEN edge happens on an OSC rising
edge. So although the interface looks like an async DRAM protocol, in this
implementation it is a fully synchronous, single-clock state machine -
which is exactly what makes it easy to retarget.
Current frequencies:
| Build | OSC = sysclk | Note |
|---|---|---|
| Verilator sim | BOARD_CLK_FREQ (100 MHz default) |
zero-delay; ratios are what matter |
| Basys3 | ~16.67 MHz (clk_cpu from MMCM) |
~39.06 MHz is the stated target after timing closure |
| Tang Nano 20K (planned) | 27 MHz from rPLL initially | raise later (see section 7) |
3. How RAS and CAS are generated¶
PAL_44803A (URAMA) arbitrates requests into grants: CGNT_n (CPU),
BGNT_n (bus), RGNT_n (refresh, from RLRQ_n). PAL_44902A (URAMC) is a
4-bit one-sequence state machine Q = {QD,QC,QB,QA} started by a grant; its
registered outputs sequence one access:
RASactive during internal states 1-6CASactive during states 4-8 (and a special "CAS ON REFRESH = 1,2,3,4,5" path when the grant is a refresh grantRGNT- i.e. CAS-before-RAS, the classic DRAM refresh cycle)HIEN_n(row address enable, states 0,1,2,8) /LOEN_n(column address enable, states 3-6) drive the row/column mux inMEM_ADDR_44- The PAL comment "NO WAIT STATE ON CPU TO MEMORY WRITE" is literal: the sequence has fixed length, there is no way for the memory to stall it. Any replacement backend must meet the fixed deadline.
The RDATA sample strobe (when the read data is consumed into the parity
checker/latches of MEM_DATA_46) is produced by a PAL in MEM_LBDIF_48 from
the grant/enable phases.
4. Measured protocol (ground truth from simulation)¶
Instrumentation is built into Shared/support/SIP1M9.v (-DDBG_MEM). Build:
cd Verilog/sim && make test_nd120 SIM_DEFINES="-DVERILATOR_SIM -DDBG_MEM".
From 25,008 captured accesses, every single access has the identical
signature (cycle numbers are OSC cycles, N = RAS falling edge):
| OSC cycle | RAS_n | CAS_n | AA carries | Notes |
|---|---|---|---|---|
| N | 0 | 1 | row | row must be captured at this edge; W_n already valid |
| N+1 | 0 | 1 | column | AA has switched to column |
| N+2 | 0 | 0 | column | CAS falls - access begins; write data DD_IN valid |
| N+3 | 0 | 0 | column | both-low window |
| N+4 | 0 | 0 | column | both-low window (last) |
| N+5 | 1 | 0 | (next) | RAS released, CAS tail |
| N+6 | 1 | 1 | - | idle |
Statistics: CAS falls exactly RAS_fall + 2 in 25,008 of 25,008 accesses.
RAS-to-RAS spacing: minimum 11 cycles (1,280 seen), mode 17 (18,004),
then 24/33/... No back-to-back accesses closer than 11 OSC cycles exist.
~10% of accesses are writes. W_n is stable from N through N+5.
Read-data deadline: the proven reference is the Basys3 BRAM path in
SIP1M9.v (ramSize=3): it performs a registered read on the first both-low
edge, so data is on DD_OUT from the start of N+3, registered and held
while CAS stays low; the RDATA strobe samples it "late in the cycle while
CAS is still low". A backend that has data valid and held by the start of
N+4 at the latest is on the safe side of the proven behaviour.
Refresh: the design has a complete refresh chain - DGA XRFN ->
REFRQ_n -> RLRQ_n -> RGNT_n -> PAL "CAS ON REFRESH" states - but zero
refresh cycles were observed in a ~2.8 ms sim window (an implemented XRFN
divider would have produced ~180). The sim DRAM model doesn't need refresh, so
nothing in the sim exercises it. Conclusion for real dynamic memory backends:
do not rely on the board logic to schedule refresh; generate it yourself.
5. The aligned backend family (implemented 8-JUL-2026)¶
All main-memory backends share the sheet-49 interface and are selected in
MEM_43.v by one define chain, checked in this order. A build that selects
none of them fails at elaboration (MEM_43.v:699-715); there is no silent
default. The define table with who sets what is docs/build-defines.md
section 1.
| Define | Module | Backend |
|---|---|---|
MAIN_RAM_SDRAM |
fpga/tang-nano-20k/sdram-bridge/MEM_RAM_49_SDRAM.v |
SDRAM through the sheet-49 bridge: Tang Nano 20K embedded 8 MB SDRAM (2 banks = 4 MB), and with ND_SDRAM_DQ16 the 16-bit modules of the MiSTer, MEGA65 R4-R6 and QMTECH |
MAIN_RAM_DDR2 |
fpga/nexys4ddr/ddr2/MEM_RAM_49_DDR2.v |
BRAM cache in front of external memory: Nexys 4 DDR (DDR2), MEGA65 R3 (HyperRAM behind the same seam) |
MAIN_RAM_BLOCKRAM |
CPU-BOARD-3202/circuit/MEM_RAM_49_BLOCKRAM.v |
One clean synchronous BRAM, parameterized size (Basys3 default 3 banks x 4K words; CMOD A7 etc. raise BANK_ADDR_BITS). Too small for SINTRAN on any board here |
VERILATOR_SIM |
CPU-BOARD-3202/circuit/MEM_RAM_49_SIM.v |
Zero-delay DRAM model, 3 banks x 1M = 6 MB; C++ preload via RAM.b0_lo/b0_lo_p/b0_hi/b0_hi_p |
MAIN_RAM_SIP1M9 |
CPU-BOARD-3202/circuit/MEM_RAM_49.v |
Original six SIP1M9 chips - schematic-faithful reference only; no build selects it |
Hardware-truth rules baked into the FPGA backends (from the 8-JUL Tang write debugging - three builds of evidence):
- Row: captured exactly once, at the RAS rising edge (never a level).
- Address registers upstream (MEM_ADDR_44): edge-capture on the grant
(AM29C821
USE_SYSCLK=2), never a level enable - LBD is address-then-data multiplexed. - Write data: the DD bus is driven BEFORE CAS and released around CAS-fall on silicon (mid-window samples read a dying/dead bus, even though zero-delay sim shows it valid forever). Capture on sysclk every edge until CAS is seen high - the final capture holds the settled pre-CAS value.
- Write executed once, at the first RAS&CAS window edge (last-write-wins re-writing captures the dead bus).
- Read: registered during the window, held while CAS is active, output and parity gated to 0/1 when unselected (banks OR together).
Pre-synth testbenches for all of this: CPU-BOARD-3202/circuit/sim/
(make test-memaddr test-memchain test-memchain-blockram test-memchain-sim)
plus the SDRAM bridge's own tb (fpga/tang-nano-20k/sdram-bridge/sim/).
Tang Nano 20K: embedded 8 MB SDRAM¶
See section 6.
6. Mapping the protocol onto the Tang Nano 20K SDRAM¶
The controller (validated on hardware in
../fpga/tang-nano-20k/sdram-test/)
is the nand2mario byte-based one: rd/wr pulse -> 5-cycle operation, read
data 4 cycles after rd, CL=2, auto-precharge, max 66.7 MHz, plus an explicit
refresh command (one per 15 us needed).
Structure: one sheet-49 body per board¶
The whole sheet-49 body is swapped per target; MEM_43.v picks it by
define (section 5). The SDRAM one is MEM_RAM_49_SDRAM.v: the protocol
bridge plus sdram18.v (derived from the nand2mario controller).
The bridge (MEM_RAM_49_SDRAM)¶
Clocking: run the SDRAM controller on a 2x OSC clock (54 MHz for a 27 MHz
OSC) from the same rPLL, plus the 180-degree clkoutp for the SDRAM chip.
Same-PLL integer-ratio clocks: the crossing is a fixed phase relationship,
not a true CDC.
Per the measured protocol (OSC cycles, 2x-clock cycles in parentheses):
- N: RAS fall - capture row from
AA,W_n, bank. - N+1: capture column from
AA(it is already there - one cycle before CAS falls, which buys an extra 2 fast cycles of latency budget). - N+1/N+2: issue
rdorwrto the controller (fast clock edge 2N+2..4). Address ={bank[1:0], row[9:0], col[9:0]}word index. For a write,DD_INis valid from N+2. - Read:
data_ready4 fast cycles later = by OSC N+4; register into an 18-bit holding register, driveDD_17_0_OUTwhile CAS is low (same register-and-hold shape as the proven Basys3 BRAM path). Deadline met. - Write: controller busy 5 fast cycles = 2.5 OSC; the minimum 11-cycle RAS-to-RAS spacing gives enormous headroom.
Refresh (we own it): 15 us timer on the fast clock. Issue refresh
immediately after an access completes (N+5): it finishes within 2.5 OSC
cycles, guaranteed before the earliest possible next access (N+11). Plus an
idle watchdog: if no access has happened for >1 us, refresh anytime; the
worst case - a refresh started on the same edge RAS falls - delays the read
issue by ~2.5 OSC and the data to ~N+5, which is why post-access refresh is
the primary mechanism and the watchdog only covers an idle CPU.
Word width: ND_SDRAM_PACK16 (since 11-JUL-2026)¶
The SDRAM stores only the 16 DATA bits, TWO ND words per 32-bit location
(one per location on a 16-bit module, ND_SDRAM_DQ16), with DQM
lane-masked single-access writes (no read-modify-write, protocol timing
untouched) and parity COMPUTED on the read path. The earlier "one 18-bit
word per 32-bit location" mapping is gone; the reason it was believed
necessary ("the self-test deliberately writes bad parity") was wrong - the
microcode self-test never touches memory parity. Evidence and the pinned
contract: docs/nd120-parity-analysis.md.
Capacity: 2 banks of 1M words = 4 MB. The populated pair is
BANK0 + BANK2; BANK1 is the one that reports absent, and the
ND-120's boot-time size probing handles missing banks (that is how the
machine was sold with less than max memory). The board decode PAL wires the
three 1M-word banks in PHYSICAL-ADDRESS order BANK0, BANK2, BANK1 - see
PAL/PAL_44445B.v:65-67, where PPN[21:20] decodes 00 -> BANK0 (words
0-1M), 01 -> BANK2 (1M-2M), 10 -> BANK1 (2M-3M). So the contiguous first 2M
words are BANK0+BANK2 and the absent bank sits at the TOP of the range.
(Corrected 24-AUG-2026; this file used to say BANK0+BANK1.)
BANK0+BANK2 occupy the LOWER half of the Tang chip (location bit 20 = 0)
and the upper 4 MB is the nd_storage region (nd-storage-design.md
section 5.2). The CPU/storage split is set at ND-row granularity by the
MEM_RAM_49_SDRAM parameter CPU_PART_ROWS (default 2048 = 4 MB). Two
bridge details from that work are commented in the code: DQM is restored
to 0 right after each masked write burst (sdram18.v:16-19, read DQM
latency is 2 cycles), and the absent-bank access B_TAIL hosts a refresh
slot (MEM_RAM_49_SDRAM.v).
7. Frequencies and how to adjust per board¶
Two independent knobs, both already established in this repo:
BOARD_CLK_FREQ- every derived count (UART baud divisors, DGA RTC tick, refresh interval) must be computed from it (this is the rule from the OPCOM console speed fix). The bridge's 15 us refresh count is2*BOARD_CLK_FREQ/1_000_000*15on the fast clock.- The PLL per board sets OSC and the memory fast clock:
| Board | OSC (CPU/bus/memory FSM) | Memory backend clock | Limits |
|---|---|---|---|
| Verilator | BOARD_CLK_FREQ (any) |
same (zero-delay model) | none |
| Basys3 | 16.67 MHz now, ~39.06 MHz target | = OSC (BRAM) | FPGA timing closure |
| Tang Nano 20K | 27 MHz (rPLL) first | 54 MHz (= 2x OSC, rPLL clkoutp for chip) |
controller params good to 66.7 MHz; LiteX proves the die at 48 MHz CL-2 |
| Other SDRAM boards | pick f | 2x f, 180-degree chip clock | keep 2x ratio and the deadline math of section 6 |
Raising the Tang above 27 MHz (TODO G4): 2x clock hits the 66.7 MHz controller ceiling at OSC = 33 MHz. Beyond that, either retune the controller's CAS/T_xx parameters for >66.7 MHz operation (the die itself is a 166 MHz part) or drop to a 1x-clock bridge, which no longer meets the N+4 deadline - so the realistic ceiling for this bridge design is OSC ~= 33 MHz, SDRAM ~= 66 MHz until the controller timing parameters are revisited.
8. References¶
CPU-BOARD-3202/circuit/MEM_RAM_49.v,MEM_RAMC_50.v,MEM_ADDR_44.v,MEM_LBDIF_48.v,MEM_DATA_46.v- the memory subsystem sheetsPAL/PAL_44902A.v(RAS/CAS state machine),PAL/PAL_44803A.v(grants)Shared/support/SIP1M9.v- the original chip model +DBG_MEMinstrumentation../fpga/tang-nano-20k/sdram-test/- hardware-validated SDRAM controller + board bring-up findingsVerilog/TODO.md- "Tang Nano 20K bring-up" section tracks this work