Skip to content

HyperRAM for Beginners

sy2002 edited this page Jul 11, 2026 · 2 revisions

You already know how to use Block RAM: you present an address, and one clock later the data is simply there — no protocol, no waiting, no maintenance. This article takes that knowledge and walks you, from first principles, to confidently using the MEGA65's 8 MB of external HyperRAM through the MiSTer2MEGA65 (M2M) framework. It assumes you can write VHDL and use a BRAM, and nothing else about memory controllers.

It is a concepts article — a little textbook you can read cover to cover. It teaches you why HyperRAM works the way it does and what each M2M facility is for, so that the step-by-step porting instructions make sense. Those instructions — the exact ports you wire, the C_HMAP_* constants, the XDC — live in The Ultimate MiSTer2MEGA65 Porting Guide (Phase 6 §2.6, steps S77–S78; and the reference sections §1.4.3, §3.I.8, §3.F), and this article points you there at each step. For the clock-domain background it leans on Clocking and Clock Domain Crossing.

If you take three sentences away, take these. HyperRAM is huge but it lives off-chip, so it is slow-to-first-word and you talk to it through a handshake bus, not a plain address port. It is shared with the video scaler and the Shell CPU, so you must tolerate being told "wait". And you hide its latency the way every computer hides slow memory — with FIFOs and caches. Master those three ideas and the rest is detail. The best news, which Parts 4 and 5 unpack: the framework already owns the genuinely hard parts — the controller and its timing (Part 4), and the pins plus a fair arbiter (Part 5). You are handed one clean bus and a toolbox.


Part 1 — Why HyperRAM, and what it is

1.1 The wall you eventually hit: BRAM runs out

BRAM is wonderful and scarce. The MEGA65's Artix-7 (xc7a200t) has 365 blocks of 36 kbit each — about 1.6 MB total, and the framework already spends some of it. The Porting Guide's rule of thumb (§1.4.2) is blunt: if the "fast" RAM + ROM your core needs stays under about 1.4 MB, porting is straightforward; above that, you need a plan.

Plenty of real cores blow past 1.4 MB:

  • A Commodore REU (RAM Expansion Unit) is 512 KB and the mechanism scales to 16 MB.
  • A C64 cartridge image (Ocean, EasyFlash, Magic Desk…) can be 1–2 MB.
  • An Amiga floppy (ADF) is ~880 KB, and the Amiga's own Chip+Slow RAM already nearly fills BRAM — the Amiga port sits at 363.5 / 365 blocks (Porting Guide §1.4.4).
  • The video scaler needs whole framebuffers.

None of that fits in BRAM. It has to go off-chip, and on the MEGA65 the framework's off-chip memory is HyperRAM. (The board also has SDRAM on R4/R5/R6, but the framework does not use it yet — see §5.9. Today, HyperRAM is the large memory.)

1.2 The memory zoo, anchored to the one you know

Before HyperRAM specifically, place it among its relatives. Everything here is measured against BRAM, the memory you already understand.

Property BRAM (on-chip) HyperRAM Async SRAM SDRAM / DDR
Where it lives inside the FPGA external chip external external
Cell type SRAM (static) DRAM (needs refresh) SRAM DRAM
MEGA65 capacity ~1.6 MB total (≈13 Mbit) 8 MB present on R4+, unused
Pins used 0 (internal) 11 signals 40–50 40–60
Time to first word 1 clock, fixed ~7 clocks (up to ~11), variable ~1 access, no protocol many, command-based
Refresh none self-refresh, hidden none host-managed
Bandwidth very high, many ports ~200 MB/s, one port moderate very high
Handshake none yes (Avalon) none command-based
Effort to use trivial moderate — but the framework does it low very high

A few words in that table are new if you have only used BRAM:

  • DRAM (Dynamic RAM) stores each bit as a tiny charge on a capacitor. Capacitors leak, so every cell must be rechargedrefreshed — periodically or it forgets. DRAM is far denser and cheaper per bit than SRAM, which is why bulk memory is always DRAM. BRAM is SRAM (bits held in latches): no refresh, faster, but big and expensive per bit.
  • Pseudo-Static RAM (PSRAM) is DRAM that refreshes itself internally, so from the outside it behaves like static RAM you never have to maintain. HyperRAM is a PSRAM.
  • DDR (Double Data Rate) — moving data on both clock edges, so a bus transfers twice per clock. It is what lets HyperRAM's eight data wires carry a 16-bit word every cycle; it is unpacked in §1.3.

1.3 What HyperRAM actually is

HyperRAM is an ordinary DRAM array hidden behind a very small serial interface called HyperBus. It self-refreshes, so it looks maintenance-free; it uses few pins, so it is cheap to wire; and it pays for both with latency.

The exact chip on the MEGA65 is an ISSI 64-Mbit (8 MB) part running at 100 MHz on 3.0 V (IS66WVH8M8BLL on R3; the electrically-identical revision-D IS66WVH8M8DBLL on R4, R5 and R6 — the difference matters only in §4.5). At 100 MHz it moves about 200 MB/s in bursts.

Why serialize the interface at all? Pins. A parallel SRAM or SDRAM needs a full address bus plus a data bus plus control — 40 to 60 FPGA pins and a lot of board routing. HyperBus time-multiplexes command, address, and data onto eight data wires and clocks them at double data rate, so the entire 8 MB chip needs only:

11 bus signals: a clock CK, a chip-select CS#, a strobe RWDS, and eight data lines DQ[7:0]. (Plus a reset and power.)

Double Data Rate (DDR) means one byte is transferred on the clock's rising edge and another on its falling edge — two bytes per clock, so DQ[7:0] carries one 16-bit word per 100 MHz cycle. That is the native unit of HyperRAM: a 16-bit word, and the whole thing is word-addressed (address 1 is the second word — byte offset 2 — not byte 1). Hold on to that; it is the single most common source of bugs.

The price of eight narrow wires is that you cannot just "read address X". You must send a 48-bit command+address serially, wait while the DRAM opens the addressed row, and only then does data stream out. That wait is latency, and it is the concept a BRAM user has never met. Part 2 makes it concrete.

1.4 The three ideas that organize everything

Every complication in the rest of this article is one of three consequences of "big, external, DRAM":

  1. It is far away and slow, so you talk to it through a handshake bus. That bus is Avalon Memory-Mapped (Part 3). Unlike BRAM, the memory can say "not yet" and can return your data an unpredictable number of cycles later.
  2. It is shared. The video scaler, your core, and the Shell CPU all use the one chip, so a fair arbiter stands between them (Part 5). You must tolerate waiting your turn.
  3. You hide its latency the way CPUs always have — with FIFOs and caches (Part 6). A small, fast on-chip buffer serves the common case; the slow chip is touched only when it must be.

And wrapping all three: the framework already owns the controller, the pins, the clocks, the constraints, and the arbiter. Your job shrinks to driving one Avalon bus and, if you want speed, dropping in a FIFO and a cache. Let us build up to that.


Part 2 — HyperBus: how the chip actually talks

You will never write HyperBus timing yourself — the controller does (Part 4). But five minutes understanding it explains why the bus you do drive looks the way it does, and why "just read a word" costs what it costs. If you would rather see the real waveforms, MJoergen's HyperRAM documentation renders them beautifully; this section is the plain-English version.

2.1 The wires

Signal Width Role
CK 1 The clock. Crucially it is not free-running — the FPGA only toggles it during a transaction and lets it sit idle otherwise.
CS# 1 Chip select, active-low. Pulling it low starts a transaction; releasing it high ends one. Think of it as the envelope around everything else.
DQ[7:0] 8 The one 8-bit bus that carries command, address, and data — at different times, double-data-rate.
RWDS 1 Read/Write Data Strobe — the tricky bidirectional signal with three different jobs (§2.4).
RESET# 1 Hardware reset to the chip.

2.2 The shape of every transaction

Every HyperBus transaction has the same skeleton:

         |Command/Address|  latency  |   data burst  |
CS#  ----____________________________________________----   Chip Select: low = one transaction
CK   ____--__--__--__--__--__--__--__--__--__--__--______   clock runs only during the transaction
DQ   ....<  48-bit CA   >............<D0><D1><D2><D3>....   command/address, wait, then the data

(In every timing diagram in this article _ is logic low, - is logic high, and <value> is a value carried on a bus. The clock idles between transactions — it is not free-running.)

  1. Command/Address (CA). The first thing on the wire is a 48-bit / 6-byte header sent over three DDR clocks, MSB first. It encodes: read or write; whether you want the memory array or the chip's small register space; the burst type; and the word address. (In Avalon terms below, this is built directly from your read/write/address — you never assemble it by hand.)
  2. Latency — the wait. After the CA the clock keeps ticking but no data moves for a fixed number of cycles. This is §2.3, and it is the whole reason a single read is expensive.
  3. Data burst. Then words stream, one per clock, until CS# goes high. On a read the chip drives DQ; on a write you drive DQ.

2.3 Latency: the thing BRAM never had

Why isn't a read instant? Because the array is DRAM. To read any word the chip must first open the addressed row — sense a whole row of capacitors into a buffer — and that takes a fixed wall-clock time, the initial access time, about 40 ns on this part. At 100 MHz, 40 ns is 4 clock cycles. So after sending the address you must clock roughly four idle cycles before the first word appears. That count is programmable (the framework sets it to 4; the chip's power-on default is 6).

Now the twist that makes latency variable. Because it is DRAM, the chip has to slip its self-refresh in somewhere (§2.5). If a refresh comes due at the exact moment you start a transaction, the chip must finish that refresh before it can open your row — so it needs extra time. It announces which case applies by driving RWDS during the CA phase:

  • RWDS low during CA → 1× latency (normal, ~4 clocks here).
  • RWDS high during CA → 2× latency (a refresh is colliding; wait ~8 clocks so it can finish first).

This is variable latency: sometimes fast, sometimes twice as slow, decided per transaction by the chip. (The chip also offers a fixed latency mode that always takes the worst case for predictable timing; the framework chooses variable for a better average — one config write, §4.5.) The practical upshot for you: never assume a fixed number of cycles for a HyperRAM access. The handshake bus in Part 3 exists precisely so you don't have to.

2.4 RWDS: the one signal with three jobs

RWDS is confusing because it does three unrelated things at three phases of a transaction, and it even changes direction mid-transaction. Pin it to the timeline once and it stops being mysterious:

Phase Who drives it What it means
During the CA the chip → FPGA latency flag: high = 2×, low = 1× (§2.3).
During a write FPGA → chip byte mask: for each byte, high = don't write it, low = write it. This is how you write a single byte of a 16-bit word.
During a read the chip → FPGA data strobe: a clock the chip sends alongside the read data.

That third job is the deep one. It is a source-synchronous clock: the sender (the chip) ships a strobe next to the data so the receiver can capture it reliably despite the delay of the PCB traces. But it is non-free-running — it toggles only while read data flows, then stops. Capturing data from a clock that starts and stops, whose edges land exactly where the data changes, is genuinely hard; it is the whole subject of §4.4, and it is where the controller earns its keep.

2.5 Refresh: the DRAM tax

Because the cells leak, every row must be recharged periodically or it forgets. HyperRAM does this itself — you never issue a refresh command — which is what "pseudo-static" means. Two facts leak out of the chip and into your design, so know them:

  • Refresh steals a little time, sometimes from you. Refreshes are spread out ("distributed" — one row at a time across the whole interval, rather than one big stall). But when one lands on top of your transaction, that is exactly the 2× latency of §2.3. So "why is this read sometimes twice as slow?" and "how does the chip stay refreshed?" are the same mechanism.
  • A single transaction may not hold CS# low forever. The chip can only refresh between transactions, so it caps how long one transaction may last (a few microseconds). A client that wants to move a lot of data must therefore split it into reasonable bursts rather than one endless one. The 8-bit burstcount (Part 3) keeps you comfortably under the cap.

2.6 You do not write any of this

Every wire, every timing number, every refresh subtlety above is handled for you by the HyperRAM controller — a block M2M ships and instantiates in the framework. What the controller hands up to you is a clean, standard bus. That bus is next.


Part 3 — Avalon-MM: the bus the controller hands you

The controller converts messy HyperBus into a friendly request/response bus called Avalon Memory-Mapped (Avalon-MM). This is the interface you actually drive, so learn it well. The fastest way in is by contrast with the BRAM you know.

3.1 From BRAM to Avalon in one sentence

Avalon-MM is BRAM, but the memory is far away and slow: the same put-address / get-data idea, except the slave can say "not yet" (waitrequest), and the data can come back an unpredictable number of cycles later, announced by its own strobe (readdatavalid).

BRAM gives you a promise — data next cycle, always, never stalls. Avalon gives you a handshake — data eventually, when both sides agree. Everything else follows from that.

3.2 The signals

Here is the whole set, at the widths the HyperRAM interface uses. "Master" is the side that issues requests (your core); "slave" is the side that answers (the framework). The 2021 Avalon spec renames these to host and agent; the M2M source still says master/slave — treat them as synonyms.

Signal Master drives? Width Meaning
address out 32 Word address of the first word. Bit 31 selects register space (§3.6).
read out 1 Assert to request a read.
write out 1 Assert to request a write.
writedata out 16 The word to write.
byteenable out 2 Which byte(s) of writedata to store ("11" both, "01" low, "10" high).
burstcount out 8 How many consecutive words this one request covers (1…255).
readdata in 16 A returned word.
readdatavalid in 1 Strobe: "readdata is valid this cycle."
waitrequest in 1 Back-pressure: "I'm busy — hold your request steady."

3.3 waitrequest: the slave saying "not yet"

waitrequest is the heart of sharing a bus. The rule for you, the master, is simple and absolute:

Assert your request (read/write + address + …) and hold every one of those signals frozen until you see waitrequest = 0 on a rising edge. That edge is the moment your request is accepted.

If you change the address while waitrequest is high, you have silently issued a different transaction than you meant. Every well-written M2M master obeys this with the same idiom — the single most reused line in the whole HyperRAM stack:

if m_avm_waitrequest_i = '0' then   -- request was accepted this cycle
   m_avm_write_o <= '0';            -- so drop it (address/data are now don't-care)
   m_avm_read_o  <= '0';
end if;

Because read/write are only cleared when waitrequest is 0, they automatically stay asserted through every stall cycle — the "hold steady" rule is satisfied for free. A single read that the slave stalls for three cycles, then returns the word two cycles after acceptance:

                c1   c2   c3   c4   c5   c6   c7
read     (m->s) --------------------_______________  request, held high through the stall
address  (m->s) <        A         >...............  held valid until accepted
wait     (s->m) ---------------____________________  high = "not yet"; drops at c4 = ACCEPT
rdvalid  (s->m) _________________________-----_____  one pulse when the word is ready (c6)
readdata (s->m) .........................< D >.....  the returned word

(In these diagrams wait is waitrequest and rdvalid is readdatavalid.) Note the two independent gaps: the stall (c1–c3, until your request is accepted at c4) and the access latency (c4→c6, until the data returns). With BRAM both are zero and fixed; with HyperRAM both are variable. One more caution: when you are idle, waitrequest is meaningless — do not poll it to ask "is the memory free?"

3.4 readdatavalid: data arrives later, on its own strobe

With BRAM, "asked at cycle N ⟹ data at N+1." With Avalon, acceptance and delivery are two separate events. You accept the request when waitrequest drops; you receive the data later, whenever the slave raises readdatavalid. This decoupling is why you must watch readdatavalid rather than count cycles: the data lands an unpredictable number of cycles after acceptance. (The M2M controller finishes one transaction before it accepts the next — it holds waitrequest high until the read completes — so your reads never overlap; but the latency still varies, which is exactly why the strobe exists.) It also carries a guarantee you can lean on:

For a read of burstcount = N, you get exactly N readdatavalid strobes, delivering your words in the order you asked for them. No tags, no bookkeeping — just count the strobes; the N-th strobe is the N-th word.

The one BRAM habit you must unlearn: never latch readdata on a fixed delay. Always gate on readdatavalid.

3.5 Bursts: one request, many words

A single-word access is almost all overhead — the 6-byte CA and the 4-to-8-clock latency dwarf the one word you get. A burst pays that overhead once and then streams many words back-to-back. That is the lever for HyperRAM throughput. You supply address and burstcount on the first beat only (a beat is one clock's worth of transfer on the bus); the slave infers the rest of the addresses. A four-word read burst:

                c1   c2   c3   c4   c5   c6   c7   c8
read     (m->s) -----___________________________________  one request, accepted at c1
address  (m->s) < A >...................................  first beat only
burstcnt (m->s) < 4 >...................................  first beat only
wait     (s->m) ________________________________________  no stall this time
rdvalid  (s->m) ____________________--------------------  high for 4 cycles = 4 words, in order
readdata (s->m) ....................< D0>< D1>< D2>< D3>  words arrive after the latency gap (c2-c4)

A write burst is the mirror image with one asymmetry: you must feed writedata on N successive accepted cycles, and byteenable may differ per word. You may pause mid-burst by deasserting write (which stalls, not ends, the burst), and the slave may pause you with waitrequest. One more consequence to file away for Part 5: a burst locks the arbiter for its whole length, so a long, greedy burst starves the other users — split large transfers into shorter bursts so you do not hog your turn on the shared bus.

3.6 Two traps worth a box of their own

Trap 1 — word addressing. address + 1 steps by one 16-bit word = 2 bytes, not one byte. Half-remembering "Avalon is byte-addressed" (the spec's default for some interfaces) will corrupt every address here. A byte-wide device writes one byte of a word by setting byteenable to "01" or "10", not by changing the address.

Trap 2 — bit 31 is not an address. address(31) = 1 selects the chip's tiny register space (ID and configuration registers), not a high memory address. For normal RAM, address(31) must stay 0. A stray top bit turns a memory read into a register read.

3.7 Rules for a well-behaved master (checklist)

  1. Assert exactly one of read/write (plus address, burstcount, and for writes writedata/byteenable).
  2. Hold every request signal steady while waitrequest = 1.
  3. The accept is the rising edge where waitrequest = 0; only then advance or drop the request (use the deassert-on-accept idiom of §3.3).
  4. For reads, wait for readdatavalid, count exactly burstcount strobes, trust the order.
  5. For write bursts, deliver burstcount words on successive accepted cycles.
  6. Only the first beat's address/burstcount matter.
  7. address is a word address; keep address(31) = 0 for RAM.
  8. Don't read waitrequest while idle.
  9. Cross into the HyperRAM clock domain before you reach the bus — that is Part 5–6.

Part 4 — The controller: MJoergen's HyperRAM PHY

The bridge between the messy HyperBus of Part 2 and the clean Avalon of Part 3 is MJoergen's open-source HyperRAM controller (MIT-licensed; the same code lives standalone at github.com/MJoergen/HyperRAM and verbatim-ish inside M2M at M2M/vhdl/controllers/hyperram/). A controller like this is often called a PHY (physical-layer block — the logic that drives the chip's real pins with correct electrical timing). You do not instantiate it — the framework does — but "learn everything about it" is a worthy goal, and its receive path is one of the most instructive pieces of hardware in the whole project. This part is the guided tour.

First-time readers eager to start wiring can skip ahead to Part 5 (how you actually get the bus) and come back here for depth later — nothing in Parts 5–10 depends on the controller's internals.

(A small honesty note: the copy M2M ships is a slightly older snapshot than today's GitHub repo, which has since added extra diagnostic status outputs — underrun, timeout, and FIFO-overflow flags — and an extra clock buffer on the receive path. The protocol behaviour and the ideas below are identical; only fine details of the receive timing differ. Read the M2M files as the source of truth for what your core builds.)

4.1 One entity, two interfaces

The wrapper hyperram.vhd is the only entity anyone instantiates. It has exactly two faces:

  • Client side — Avalon-MM (Part 3): the friendly read/write/address/readdata/… bus.
  • Device side — HyperBus (Part 2): CK, CS#, and the bidirectional RWDS/DQ. Because those are bidirectional, the controller exposes each as three signals — an input, an output, and an output-enable — and lets the top level build the actual tri-state driver (more in §4.6). There is a single generic worth knowing, G_ERRATA_ISSI_D_FIX, discussed in §4.5.

4.2 Six modules and the "sandwich"

Internally the controller is six small modules with a clean division of labour: three form the protocol brain (pure, portable RTL) and three form the physical hands (full of Xilinx I/O primitives).

flowchart LR
  C["Your core (Avalon master)"] --> E["hyperram_errata (fix 1-word writes)"]
  E --> CF["hyperram_config (boot + set latency)"]
  CF --> CT["hyperram_ctrl (HyperBus state machine, 100 MHz)"]
  CT --> TX["hyperram_tx (drives CK, DQ, RWDS out)"]
  RX["hyperram_rx (samples DQ in, contains async FIFO)"] --> CT
  TX --> CHIP["HyperRAM chip"]
  CHIP --> RX
Loading

The elegant part is that errata and config are built as Avalon "sandwiches" — each has an Avalon slave face toward the client and an Avalon master face toward the next stage, so it sits transparently in series on the bus and can be spliced out without touching the state machine. errata is removed at compile time by a generic; config makes itself transparent at runtime once boot is done (it becomes a pure pass-through). This is a lovely pattern to steal for your own designs: a feature you can insert or remove by wiring, not by surgery.

The heart is hyperram_ctrl, a single state machine running entirely at 100 MHz on rising edges. It walks the transaction skeleton of §2.2: on a request it pulls CS# low, shifts the 48-bit CA out over DQ two bytes per clock, counts the latency (sampling RWDS to choose 1× or 2×), then streams the data burst, and finally observes the mandatory recovery gap before the next transaction. It turns your burstcount into its loop counters and your byteenable into the write mask on RWDS. It never touches a pin directly — it emits DDR-encoded control that tx and rx turn into real toggling wires.

4.3 Three clocks, and the I/O primitives behind them

A memory PHY needs things a BRAM user has never met, because BRAM never leaves the die. Here they are, minimally:

  • ODDR / IDDR — Output/Input Double-Data-Rate registers that live inside the FPGA's I/O pad. An ODDR emits two bits per clock (one per edge); an IDDR captures two bits per clock. This is the hardware that makes DDR possible.
  • IDELAY (IDELAYE2) — a programmable delay line on an input pin: a chain of tiny "taps" (~0.15 ns each) you dial to shift an incoming signal by a precise sub-nanosecond amount. Used to move a strobe into the middle of the data window before sampling.
  • IDELAYCTRL — a calibration helper the IDELAY needs; it trims the tap sizes against temperature and voltage drift using a stable 200 MHz reference.

That is why the controller wants three phase-locked clocks, all from one MMCM (Xilinx's on-chip PLL-style clock generator — it multiplies, divides, and phase-shifts an input clock) so they stay coherent:

                 ┌──────── one MMCM ────────┐
 100 MHz board ─▶│ 100 MHz,  0°  → clk       │─▶ state machine + DQ/RWDS output registers
                 │ 100 MHz, 90°  → clk_del   │─▶ the CK output register (write timing, below)
                 │ 200 MHz,  0°  → refclk    │─▶ IDELAYCTRL (read timing, §4.4)
                 └──────────────────────────┘

Why the 90° clock (the write side). When you write, the chip wants the data on DQ centered on the CK edges — the edge in the middle of the data window, for maximum setup and hold margin. DQ changes on the main clock's edges; if CK also changed on the same edges, the chip would sample right where the data is transitioning. Driving CK from a clock delayed a quarter period (90° = 2.5 ns at 100 MHz) slides each CK edge into the center of the data eye. That 2.5 ns is the recurring magic number of the whole PHY.

4.4 The receive path: capturing data from a clock that stops

This is the marquee. On a read, the chip sends data on DQ and toggles RWDS as a strobe, with the data edge-aligned to RWDS — meaning an RWDS edge lands exactly where DQ is changing, the worst possible instant to sample. And RWDS stops after the last word. Two problems, solved in steps:

 hr_rwds_in ─▶[ IDELAY  +2.5ns ]─▶ delayed RWDS ─┬─▶ clocks the IDDRs
                     ▲                            └─▶ write clock of a small async FIFO
               IDELAYCTRL (200 MHz)
 hr_dq_in[7:0] ─────────────────────▶[ 8× IDDR ]──▶ 16-bit word ──▶[ async FIFO ]──▶ ctrl (100 MHz)
  1. Delay RWDS by ~90° with an IDELAY, so its edge now sits in the center of the data eye instead of on the data transition.
  2. Sample DQ on that delayed strobe with eight IDDRs — one clean 16-bit word per RWDS cycle.
  3. Cross into the 100 MHz domain with a tiny asynchronous FIFO whose write side is the delayed RWDS and whose read side is the controller's steady clock. (It uses gray-code pointers — a counter encoding where only one bit changes per step — so the pointer can cross clock domains without ever being sampled mid-change as a garbage value. This is a general-purpose CDC trick worth knowing.)
  4. The "extra word" trick. Here is the delightful part. Because RWDS stops after the last data word, the FIFO's write side is one clock pulse short of pushing that final word across. The fix: the controller deliberately clocks one extra word out of the chip — sends one more CK pulse than the burst needs — so the real last word gets strobed through, and then it throws away the very first FIFO word (which is now bogus). Reading one word too many and discarding the first is not a bug; it is the cure for a strobe that quits early.

4.5 Boot and the errata fix

Two of the small modules do jobs BRAM never needs:

  • hyperram_config waits out the chip's 150 µs power-up and then writes one configuration register to choose the operating parameters: 4-clock latency, variable latency mode (§2.3), and strong output drive. That single register write is literally why this module exists. Takeaway: an external RAM has personality registers you must program at boot — something BRAM never has.
  • hyperram_errata works around a real silicon bug: on ISSI revision-D dies (the part fitted on R4, R5, and R6; only R3 uses the non-D die) a lone single-word write in variable-latency mode is silently dropped. The fix, done entirely inside the controller and invisibly to you, is to turn every single-word write into a two-word write whose second word is fully masked (byteenable off) — a legal burst that commits your word and touches nothing else. It is on by default (G_ERRATA_ISSI_D_FIX => true) and harmless on the R3 part, which is why the same framework bitstream is correct on every board. (If you ever watch single writes on a scope and see them come out as doubles — that is this, working as intended.)

4.6 Constraints and tri-state: the framework's job, not yours

A memory PHY only works if the toolchain is told the truth about its timing: push the output registers into the I/O pads (IOB TRUE), forbid a global clock buffer on the hand-delayed RWDS (CLOCK_BUFFER_TYPE NONE, or the carefully-dialed 2.5 ns is swamped), and bound the receive datapath (set_max_delay -datapath_only). The standalone controller's PORTING.md asks you to add all of this — but in M2M every line already lives in the framework's XDC. Likewise the tri-state buffers: the standalone version tells you to write hr_dq_io <= hr_dq_out when oe else 'Z'; in your top level; the framework already does this in framework.vhd. So a core author writes zero constraints and zero tri-state logic for HyperRAM. If you copy the PORTING.md snippets into your core, you will multiply-drive the pins — don't. This is a large part of what "the framework owns the hard parts" means in practice.


Part 5 — How M2M gives HyperRAM to your core

Now the payoff. The framework owns the controller, the clocks, the pins, and the constraints, and hands your core exactly one thing: a single Avalon master port called hr_core_*, in the 100 MHz HyperRAM clock domain, sharing an 8 MB chip with two other tenants. This part is that contract in full.

5.1 The whole picture

flowchart TB
  subgraph clks["clk_m2m: one PLL, three clocks"]
    C1["hr_clk 100 MHz"]
    C2["hr_clk_del 100 MHz at 90 deg"]
    C3["hr_delay_refclk 200 MHz"]
  end
  DIG["ascal video scaler (index 2)"] --> ARB["avm_arbit_general (round-robin, 3 to 1)"]
  CORE["YOUR CORE via hr_core (index 1)"] --> ARB
  QN["QNICE Shell via qnice2hyperram (index 0)"] --> ARB
  ARB --> HRC["hyperram controller (Part 4)"]
  HRC --> PINS["tri-state buffers in framework.vhd"]
  PINS --> CHIP["HyperRAM 8 MB chip"]
  C1 --> HRC
  C2 --> HRC
  C3 --> HRC
Loading

5.2 The hr_core_* contract

Your core is the Avalon master; the framework is the slave. So on your mega65.vhd entity these are outputs (_o), and the same signals are inputs on the framework side. The template declares them (CORE/vhdl/mega65.vhd:66-82) and, because the demo core does not use HyperRAM, ties the outputs to zero (mega65.vhd:261-266) — that is your starting point to replace:

Port (your side) Dir Width Meaning
hr_clk_i in 1 the 100 MHz HyperRAM clock
hr_rst_i in 1 HyperRAM reset (coupled per §5.8)
hr_core_write_o out 1 request a write
hr_core_read_o out 1 request a read
hr_core_address_o out 32 word address (bit 31 = register space)
hr_core_writedata_o out 16 data to write
hr_core_byteenable_o out 2 per-byte write mask
hr_core_burstcount_o out 8 words in this burst
hr_core_readdata_i in 16 read data returned
hr_core_readdatavalid_i in 1 one pulse per valid read word
hr_core_waitrequest_i in 1 slave busy — stall
hr_high_i / hr_low_i in 1 flicker-free hints (§5.7)

It is exactly the Avalon bus of Part 3. Everything you learned there applies verbatim.

5.3 You are one of three tenants

Between hr_core_* and the controller sits avm_arbit_general, merging three masters onto the one chip:

  • index [2] — the ascal video scaler (the digital-HDMI framebuffer), which reads and writes whole frames continuously during display;
  • index [1] — your core (hr_core_*);
  • index [0] — QNICE, the Shell CPU (§5.6).

Two things to internalize. First, the indices are wiring order, not priority — the arbiter is round-robin, taking turns fairly (built from a tree of two-input arbiters). Second, and practically: because the video scaler is hammering the bus during active display, your core will see hr_core_waitrequest_i asserted for long stretches. This is not a fault; it is the cost of sharing. It is the reason Part 3 insisted you be fully Avalon-compliant and latency-agnostic. Design your client to tolerate waiting, always.

5.4 You are in the 100 MHz domain — so you must cross into it

hr_core_* lives in hr_clk (100 MHz). Your core logic almost certainly runs on a different clock. Wiring a signal straight across two unrelated clocks is a clock-domain-crossing bug that passes in simulation and fails rarely on hardware. So the rule (see Clocking and Clock Domain Crossing for the why): cross into hr_clk with an asynchronous FIFO before you reach hr_core_*. The framework gives you exactly that FIFO — avm_fifo — in Part 6, and every reference core uses it.

5.5 Where in the 8 MB things live: C_HMAP_*

The 8 MB is partitioned by hand, in globals.vhd, in units of one 4k-word window (8 KB) — so the whole chip runs from window x"0000" at the bottom to x"0400" at the top (= 8 MB). The one partition line every core must respect (Porting Guide S77) is easiest to state as two boundaries:

   x"0000"                        x"0200"                           x"0400"
   |  reserved for the framework  |        your core's region       |
   |  (ascal video framebuffers)  |    (REU, cartridges, buffers)   |
   C_HMAP_M2M                     C_HMAP_CORE                       C_HMAP_END

The framework owns the first 4 MB — everything from C_HMAP_M2M = x"0000" up to (but not including) x"0200". Your core owns the second 4 MB — from C_HMAP_CORE = x"0200" up to C_HMAP_END = x"0400" (the 8 MB top). So the rule is concrete: put your data at or above x"0200", and never below it — the video scaler lives beneath and will overwrite anything you place there.

A window constant becomes a byte/word base address by appending twelve zero bits (× 4096 words). Inside its 4 MB a core sub-divides as it likes: the C64 core, for example, starts its cartridge pool at x"0200", adds a disk-image staging area, then its REU region higher up, and keeps the very top window as a burst guard. Design your own map the same way, and leave a little slack between areas. (In today's globals.vhd the framework boundary is the constant C_HMAP_M2M and the top is C_HMAP_SIZE = x"0400"; C_HMAP_CORE is simply a clear name for "where your region begins".)

5.6 How the Shell loads data into HyperRAM

QNICE — the Shell CPU that runs the on-screen menu and reads the SD card — is arbiter tenant [0]. It reaches HyperRAM through qnice2hyperram.vhd, a small bridge that turns one QNICE bus cycle into one single-beat Avalon transaction (QNICE is slow and pokes one word at a time; there is no burst benefit). This is the path the Shell uses to stream a file from the SD card into HyperRAM — a cartridge image, a ROM, a disk image — before your core serves it. You will see this used to great effect in the case studies (Parts 8–9). For the QNICE device-bus mechanics in general, see Devices.

5.7 Flicker-free: hr_high_i / hr_low_i

These two inputs are a hint from the video pipeline, not part of the memory interface: they tell a core with a dynamically adjustable pixel clock whether it is running slightly too fast or too slow relative to the fixed HDMI output, so it can nudge itself and avoid tearing. (They are derived by watching how far the scaler's read and write pointers have drifted apart in the HyperRAM framebuffer.) The template ignores them; the C64 core uses them to nudge between PAL timing modes. For the full story see the video-pipeline material; for HyperRAM purposes, just know the two ports exist and where they come from.

5.8 One reset rule: don't decouple it

The framework deliberately ties the HyperRAM reset to the core reset (clk_m2m.vhd). The reason is an Avalon-correctness invariant: a read burst is a stateful conversation, and the arbiter also remembers who owns the bus mid-burst. If you reset one end (the core, or the scaler) without resetting the controller, one side thinks a burst is still in flight and the other has forgotten it — the bus deadlocks. Coupling the resets guarantees both ends restart clean together. Do not "fix" this by decoupling it.

5.9 A note on the boards

From your core's point of view, HyperRAM is identical on R3, R4, R5, and R6 — same hr_core_* contract, same clocks, same arbiter. The only chip difference (the revision-D die on R4/R5/R6) is absorbed by the errata fix of §4.5. And although SDRAM is present on R4/R5/R6, the framework ties it off and does not use it yet — do not tell your users they can use it today. HyperRAM is the framework's one large external memory.


Part 6 — The Avalon toolbox

The framework ships a small library of composable Avalon-MM blocks in M2M/vhdl/memory/. They all speak the one protocol of Part 3, so each is a pipe fitting you splice into the path between your logic and hr_core_*: cross a clock, hide latency, merge clients, convert widths. Here is the toolbox, and then the standard way to assemble it.

6.1 The blocks

Block What it does Key generic Clocks Synth/Sim
avm_fifo Async CDC of a whole Avalon transaction between two clocks — your core → hr_clk G_WR_DEPTH/G_RD_DEPTH, G_DATA_SIZE (>8) two Synth
avm_cache Read cache + sequential prefetch: turns single-word reads into bursts G_CACHE_SIZE one Synth
avm_arbit Fair round-robin 2→1 arbiter G_PREFER_SWAP one Synth
avm_arbit_general 3-or-4→1 arbiter (tree of avm_arbit) G_NUM_SLAVES one Synth
avm_decrease Width down-converter (e.g. 128-bit client → 16-bit HyperRAM) data sizes one Synth
avm_increase Width up-converter (narrow client → wider memory) data sizes one Synth (unused in-tree)
avm_pause Injects wait cycles — a test throttle G_REQ_PAUSE/G_RESP_PAUSE one Synth (test only)
avm_memory / avm_rom / avm_memory_pause Behavioral RAM/ROM models one SIM ONLY

Two you will always use, and the placement rules that make them work:

avm_fifo — the clock-domain crossing. This is the block that gets you from your core's clock into hr_clk. It has a full Avalon slave face on your clock (s_clk_i) and a full master face on the HyperRAM clock (m_clk_i), and internally packs a whole request — address, data, byte-enables, the read/write bit — into one FIFO word carried across by a proven Xilinx async FIFO. (It requires G_DATA_SIZE > 8, which is fine for HyperRAM's 16 bits.) Any HyperRAM client that lives on a different clock — which is essentially all of them — goes through one of these.

avm_cache — hiding latency with prefetch. A tiny read cache with one cache line of G_CACHE_SIZE words (8 in both reference cores). On a read miss it issues one G_CACHE_SIZE- word burst to fill the line; subsequent single-word reads inside that line are hits served in one cycle with no HyperRAM traffic. As you consume the first half of the line, it prefetches the next half in the background, so a long sequential read never stalls. Writes pass through and also patch the cached line (write-through), keeping the cache coherent — which matters enormously for read-write memory (Part 9).

The load-bearing placement rule (Porting Guide S78): put avm_cache on the core side, before the CDC FIFO. Its whole value is answering a hit in one fast cycle; if it sat past the FIFO, every hit would have to cross the clock boundary and pay the very latency it exists to hide. And keep the path from the FIFO to hr_core_* clean, protocol-compliant Avalon — the FIFO's registered output, at most through the stock avm_arbit_general — with no ad-hoc combinational glue of your own spliced onto it, because the framework arbiter expects a well-behaved master.

The arbiters let your core merge several of its own HyperRAM clients into the single hr_core_* port. This gives you nested arbitration: your avm_arbit_general reduces your clients to one master, which is then itself just one of the framework's three tenants (§5.3). The C64 core does exactly this — three internal clients merged before hr_core_*.

The width converters adapt a client whose word is wider (or narrower) than 16 bits. The framework itself uses avm_decrease on the video path: the ascal scaler runs a 128-bit Avalon bus, and avm_decrease serializes it down to the 16-bit HyperRAM (ratio 8).

The sim-only models (avm_memory, avm_rom, avm_memory_pause) stand in for the real chip in a testbench so you can simulate your HyperRAM client with no controller. Their giveaway is architecture simulation (versus synthesis); they use full-address-space arrays and file I/O and must never enter a bitstream. Verifying a HyperRAM client this way — before Vivado, per the emulator-testbed discipline in The Ultimate MiSTer2MEGA65 Porting Guide §5 — is exactly how you catch protocol bugs early.

6.2 The canonical stack

Assemble it like this — the recipe every real core follows:

YOUR CORE (your clock)                        │  hr_clk (100 MHz)
                                              │
protocol adapter ─▶ avm_cache ─▶ avm_fifo ────┼──▶ [your avm_arbit_general, if >1 client] ─▶ hr_core_*
(native → Avalon)   (hide         (CDC:        │       (merges your clients into one port)      │
                     latency)      your clk →   │                                                ▼
                                   hr_clk)      │                             framework arbiter ─▶ controller ─▶ chip

Three things this makes visible, and which you should be able to explain: (a) the cache sits before the CDC; (b) only avm_fifo crosses the clock boundary; (c) arbiters nest — you arbitrate your own clients down to one port, and the framework arbitrates that port against the scaler and QNICE. You write only the protocol adapter (native memory port → Avalon) and the wiring; avm_cache, avm_fifo, and the arbiters are stock. The exact steps are Porting Guide §2.6 (S77–S78) and §3.I.8. Now watch three real cores do it.


Part 7 — Case study 1: the C64 REU (bulk expansion RAM)

The cleanest first example. The C64 core's optional REU (RAM Expansion Unit — a cartridge that adds expansion RAM driven by DMA: Direct Memory Access, where the unit copies blocks of memory autonomously instead of the CPU issuing every access; this core emulates the 512 KB "1750") is a textbook match for HyperRAM: it is big, latency-tolerant (the original had its own timing; nothing needs a byte "this cycle"), and sequential (its DMA controller walks consecutive addresses). Big, slow-OK, and bursty — exactly what HyperRAM is good at, and exactly what BRAM should not be spent on.

The data path is the canonical stack of §6.2 made concrete (C64MEGA65/CORE/vhdl/):

CORE (main_clk ~31.5 MHz)                          │  hr_clk (100 MHz)
                                                   │
reu (MiSTer) ─▶ reu_mapper ─▶ avm_cache ─▶ main2hr_avm_fifo ─┼─▶ hr_reu ─┐
 byte-wide      (native →      (8-word       (CDC:            │           ├─▶ avm_arbit_general (3→1)
 512 KB DMA      Avalon +       line cache)   main → hr)      │  hr_crt ──┤   0=REU 1=CRT 2=MOUNT
                base addr)                                    │  hr_mnt ──┘        │ = hr_core_*
                                                             │                     ▼   framework arbiter ─▶ chip

Following it stage by stage teaches the whole pattern:

  1. reu_mapper.vhd adapts the REU's native byte-wide RAM port to Avalon. This is where the two traps of §3.6 get handled in real code. Since HyperRAM is word-addressed, it drops the low address bit and uses byteenable to pick the byte lane ("01" for even bytes, "10" for odd) — the canonical "byte-wide device on a 16-bit memory" adapter. It adds the region base (G_BASE_ADDRESS => X"0" & C_HMAP_REU & X"000") so the REU lands in its slice of HyperRAM. (It also contains a clever, REU-specific "pre-fetch on address change" hack to keep the emulated DMA cycle-exact — interesting, but not something you copy into an unrelated core.)
  2. avm_cache (G_CACHE_SIZE => 8), on the core clock, turns the mapper's stream of single-word reads into occasional 8-word bursts. This is the single reason the REU is usable over a shared, latency-variable HyperRAM: pay the full latency once per eight words, not once per word.
  3. avm_fifo does the actual crossing from main_clk into hr_clk.
  4. The core's own avm_arbit_general (3 masters: REU, cartridge, disk-mount) merges the REU with the core's other HyperRAM users and drives hr_core_*.

Two design choices are worth lifting straight into your own core. The cache is before the FIFO (§6.1): a hit is answered in one core-clock cycle and never touches the crossing. Between the FIFO and hr_core_* there is only the FIFO's registered output and the stock arbiter — no hand-rolled combinational glue: the framework arbiter samples a well-behaved Avalon master, and inserting your own combinational reshaping there risks both breaking 100 MHz timing and violating the protocol. What to copy versus adapt: copy the shape (adapter → cache → fifo → arbiter) and the G_CACHE_SIZE = 8 starting point; adapt the byteenable/address math to your device's width and the C_HMAP_* window to your map. (The Porting Guide describes this recipe in S78; note its line numbers have drifted as the C64 code grew, but the recipe is unchanged.)


Part 8 — Case study 2: caching a cartridge (the advanced one)

The REU tolerates latency. A cartridge does not — and that forces a real cache. This is the showcase, because it is a genuine, hand-built CPU-style cache in front of a distant, shared memory, and because it uses a different caching strategy than the REU's avm_cache. Seeing both teaches you how to pick.

8.1 The problem: bank-switching demands fast random access

A C64 cartridge appears in the CPU's address space through two small 8 KB windows — ROML at address $8000 and ROMH at $A000. But the physical cartridge — Ocean, Magic Desk, EasyFlash — can be 1–2 MB, organized as many banks, and a register write (to $DE00 or similar) selects which bank is visible right now. This is bank switching, and the running program jumps between banks expecting each to answer at ROM speed.

So: the whole image is far too big for BRAM → it lives in HyperRAM. But HyperRAM is too slow and too shared for the CPU to fetch cartridge ROM directly every cycle. The saving grace is locality: bank switches are infrequent and clustered — a program hammers a few banks repeatedly (code in one, data in another) before moving on. That is textbook temporal and spatial locality, and locality is exactly what a cache exploits. Keep the whole image in HyperRAM; keep the currently hot banks in BRAM.

8.2 Loading: QNICE streams the file in, hardware parses it in place

Before anything is served, the cartridge must get into HyperRAM. This is a beautiful example of the §5.6 path and the CSR mechanism:

flowchart LR
  SD["SD card .crt file"] -->|"byte by byte"| Q["QNICE Shell firmware"]
  Q -->|"stream via qnice2hyperram"| HR["HyperRAM: CRT pool"]
  Q -.->|"start / poll status via CSR"| CSR["sw_cartridge_csr (qnice_csr)"]
  HR -->|"Avalon reads"| P["crt_parser (walks CRT + CHIP headers)"]
  P -->|"bank number to HyperRAM address"| C["crt_cacher"]
Loading

The Shell reads the .crt file off the SD card and streams it byte-by-byte into the HyperRAM CRT pool (via qnice2hyperram, packing two bytes per word). It then uses a small Control-and-Status-Register (CSR) mailbox — the framework's qnice_csr helper — to say "file loaded, size N, start parsing", and to poll for "done" or "error, here is the message". Meanwhile a hardware module, crt_parser, walks the file in place inside HyperRAM as its own Avalon master, decoding the CRT header and every CHIP packet, and building a table: bank number → the HyperRAM address where that bank's data lives. The same register bank is described once in VHDL and once in the Shell's assembly — a nice concrete illustration of the QNICE device contract from Devices.

8.3 Serving: a hand-built bank cache

Here is the twist that makes this a real case study: the serve-path cache is not avm_cache. It is a bespoke, fully-associative bank cache written by hand in crt_cacher.vhd, and its design is worth studying because it is a textbook CPU cache built for one specific access pattern:

  • Cache line = one 8 KB bank. Tag = the bank number. There are 8 line-slots per side (ROML and ROMH), held in dual-port BRAM — the fast, private working set.
  • Fully associative: on a bank switch the module searches all 8 tags in parallel.
    • Hit (the bank is already in a BRAM slot): it simply re-points the C64's read address at that slot. HyperRAM is never touched; the CPU never stalls. This is the common case.
    • Miss: it allocates a slot (round-robin / FIFO eviction — a simple wrapping victim pointer, not LRU), updates the tag, and bursts the 8 KB bank from HyperRAM into that slot (in 256-byte bursts, prefetching the next before the current drains).
  • The miss stall is a CPU freeze. While a bank is being filled, the module asserts a "wait", which is crossed into the core clock and used to hold the 6502 in a DMA bus-hold for the tens of microseconds it takes to pull 8 KB from shared HyperRAM, then releases it. Because bank switches are discrete and rare, this pause is imperceptible — which is precisely why the cache pattern works.

From the C64's point of view, cartridge ROM is just a fast BRAM read; the entire HyperRAM-and-cache machinery is invisible. And note the depth of sharing: a single cache-miss burst travels through the cartridge cache's arbiters, the core's own arbiter, and the framework's arbiter before it reaches the chip — four levels. The design keeps misses rare (caching) rather than trying to make any single access fast, because under contention no single access can be made reliably fast.

8.4 Two caching strategies, and how to choose

You have now seen the framework's two answers to "hide HyperRAM latency", and the contrast is the real lesson:

avm_cache (REU, Part 7) crt_cacher (this part)
Kind stock framework block bespoke, hand-written
Structure one line, streaming prefetch fully-associative, 8 slots, tags
Best for sequential streaming (DMA walking addresses) random access with locality (bank switching)
On a slow access prefetch hides it; client rarely stalls freeze the consumer during the miss fill
Storage small on-chip line dual-port BRAM slots

Choose avm_cache when your client reads mostly sequentially and can tolerate an occasional stall — it is a few lines of instantiation. Build something like crt_cacher when access is random but local and you can stall the consumer on a miss. The reusable recipe for the latter: pick your unit of locality (here, a bank) as the cache line and BRAM slot size; keep a tag array and a victim pointer; on access, search tags — hit re-points BRAM, miss allocates a slot and bursts the unit from HyperRAM; stall the consumer during the miss; and put the BRAM outside the cache FSM so a dual-port BRAM cleanly bridges the HyperRAM clock (fill side) and your core clock (read side). This same shape fits a large banked ROM, an arcade tile ROM, or a disk/CD sector buffer (line = one sector, tag = sector number).


Part 9 — Case study 3: the Amiga floppy (read-write, with write-back)

The two C64 examples read HyperRAM (the REU also writes, but its contents are pure volatile scratch — there is nothing to save). The Amiga core (AExp) is the leading edge: a read-write device whose changes must persist back to the SD card. That makes it the richest HyperRAM lesson in the project, and it is built almost entirely from the stock toolbox you have already met.

9.1 The problem: a writable disk that must survive power-off

An Amiga floppy (ADF) is ~880 KB — far too big for BRAM, especially since the Amiga's own RAM already nearly fills it (§1.1). So the disk image lives in HyperRAM. But a floppy is read-write: the running machine writes to it, so (1) the image in HyperRAM must be writable in real time, and (2) changes must be written back to the .adf file on SD so they survive a power cycle. Every prior core used HyperRAM either as volatile scratch (REU) or as a read-only blob (cartridge); this is the first to do a persistent, read-write store. The mental model:

Amiga floppy logic  ⇄  HyperRAM ADF image  ── a track was written ──▶  QNICE firmware  ──▶  SD .adf file
    (real-time)          (the "surface")         (dirty flag)            (background flush)   (persistence)
                    ▲ write-through cache keeps reads coherent

HyperRAM is "the magnetic surface"; the SD file is the archival copy; the FPGA logic reads and writes the surface in real time, and the QNICE firmware lazily reconciles the two.

9.2 The HyperRAM side: mostly stock, with one crucial detail

The run-time data path is the familiar stack: the floppy engine is an Avalon master issuing single-word reads and writes → avm_cache (8-word line) → avm_fifo (CDC from the ~28 MHz core clock into hr_clk) → the core's own avm_arbit (merging the run-time engine with the load-time mount path) → hr_core_*. Loading the ADF in uses the same qnice2hyperram + qnice_csr path as the cartridge. Nothing new in the plumbing — which is itself the headline: a full read-write storage device was built on the stock M2M HyperRAM stack.

The one subtle correctness point is caching a read-write image: a read cache in front of writable memory would return stale data after a write, unless the cache is write-through. avm_cache is — it updates its cached line on a write hit as well as forwarding the write to HyperRAM — so a read right after a write sees the new value. This is easy to overlook and is the single most important reason avm_cache is the right block here.

9.3 The new part: write-back to SD

Reading never needed the core to talk back to QNICE; writing does — the firmware must learn "track T changed" so it can save it. AExp adds a lightweight channel for exactly this:

  • A dirty bitmap. When the engine commits a written track to HyperRAM, it sets that track's bit in a small Write-Back Control register block (its own CSR window on the mount device). Firmware clears the bit when it has saved the track (a write-1-to-clear register, arranged so a fresh hardware "set" always wins over a concurrent firmware "clear" — no lost updates).
  • Anti-thrashing. Exactly like M2M's virtual-drive system, a timer (default 2 seconds) delays the flush until writes go quiet, so the SD card is not hammered mid-operation.
  • A background flush in firmware. The Shell periodically checks the bitmap and, for each dirty track, seeks the retained file handle and writes just that track's 5632 bytes back to SD — per-track, not the whole 880 KB image, so a save feels instant.

That last piece needs a place to run, and it exposes the one framework change this feature required: a new mandatory callback, HANDLE_CORE_IO, invoked from the Shell's HANDLE_IO loop (marked with an M2M-UPSTREAM tag in AExp's copy — it is a candidate for the framework's next version). Because HANDLE_IO runs not just in the main loop but inside every blocking wait (the menu, the file browser, help screens), the background flush keeps running even while the user sits in the on-screen menu. The MEGA65's drive LED glows yellow while any track is still unsaved and green when clean — the honest "don't power off yet" signal that no write-back cache can avoid.

9.4 The contrast that ties the three studies together

Cartridge (Part 8) REU (Part 7) Amiga floppy (Part 9)
HyperRAM access read-only after load read + write read + write
Source of truth SD file (loaded once) none — volatile scratch SD .adf file
Persisted back to SD? never modified no (lost on reset) yes — dirty tracks flushed
Cache bespoke bank cache streaming avm_cache write-through avm_cache
Core → QNICE signalling none none new dirty-bitmap + HANDLE_CORE_IO

Read the table by capability, not by the order we met the studies — we sequenced those by caching sophistication (stock streaming avm_cache → bespoke bank cache → write-through avm_cache), which is a different axis. By capability, HyperRAM goes from read-only (cartridge), to volatile read-write (REU), to persistent read-write (Amiga) — each step adding exactly one idea: a load path, then writes, then a write-back discipline. And all three draw from the same small toolbox — avm_fifo and an arbiter throughout; avm_cache where a streaming cache fits (REU, Amiga) or a hand-built one where it does not (cartridge); and qnice2hyperram (with qnice_csr) wherever the Shell must load a file (cartridge, Amiga).


Part 10 — A recipe for your own core

You now have the whole picture. Here is how to apply it.

10.1 Does this memory even belong in HyperRAM?

Decide per memory, before wiring anything:

  • Small and needs an answer every cycle? (CPU RAM, character ROM, a line buffer.) → BRAM. HyperRAM's variable, shared latency cannot meet a hard per-cycle deadline.
  • Big, and latency-tolerant or cacheable? (expansion RAM, cartridge/ROM images, disk/track buffers, large tables.) → HyperRAM.
  • Big and needs fast random access? → HyperRAM plus a cache in BRAM (Part 8), stalling the consumer on a miss.
  • Read-write and must survive power-off? → HyperRAM with a write-through cache and a write-back-to-SD discipline (Part 9).

Total your "fast" RAM+ROM against the ~1.4 MB BRAM budget first (Porting Guide §1.4.2); whatever does not fit, and can tolerate it, goes to HyperRAM.

10.2 The standard wiring

  1. Write a protocol adapter that turns your device's native memory port into an Avalon-MM master (word address, byteenable for sub-word writes, single-word requests). Place its data in your HyperRAM region via a C_HMAP_* base (§5.5).
  2. Put avm_cache right after it, on your core's clock, if access is sequential.
  3. Cross into hr_clk with avm_fifo.
  4. If you have more than one HyperRAM client, merge them with an avm_arbit_general.
  5. Drive hr_core_* from that — from the FIFO's registered output or the stock arbiter, with no hand-rolled combinational logic of your own spliced onto the bus in between.
  6. If the data comes from a file, let the Shell stream it in via qnice2hyperram (+ qnice_csr if it must be parsed).
  7. Simulate the client against avm_memory_pause before you synthesize.

10.3 The trap list (keep this by your keyboard)

  • Word-addressed, not byte-addressed. address + 1 = +2 bytes; use byteenable for a byte.
  • address(31) = 1 selects register space, not RAM. Keep it 0.
  • Never latch readdata on a fixed delay — gate on readdatavalid; count burstcount strobes.
  • Hold your request steady while waitrequest = 1 (deassert-on-accept idiom).
  • You share the bus — expect long waitrequest under video load; design latency-tolerant.
  • Cross into hr_clk with avm_fifo; never wire straight across clock domains.
  • Cache before the CDC; no hand-rolled combinational glue just before hr_core_* (the FIFO's registered output or the stock arbiter is what belongs there).
  • A read cache over read-write memory must be write-through, or reads go stale.
  • Keep your data at or above x"0200" (C_HMAP_CORE) — the framework owns the first 4 MB below it, and the scaler will overwrite anything you place there.
  • Don't write HyperRAM tri-state or constraints — the framework owns them.
  • Don't decouple the HyperRAM reset from the core reset.

Get those right and the 8 MB is yours.


Where to go next

  • The actual wiring steps: The Ultimate MiSTer2MEGA65 Porting Guide§2.6 Phase 6 (S77–S78) for the hr_core_* master and the avm_fifo+avm_cache recipe, §3.I.8 for the Avalon reference, §1.4.2–1.4.4 for the BRAM budget and the Amiga saturation case, and §3.F for CDC and the XDC constraints.
  • The clock-domain half of the job: Clocking and Clock Domain Crossing — what the framework crosses for you, and how to cross the rest safely.
  • The QNICE device bus you use to load data into HyperRAM: Devices.
  • The controller itself, in depth: MJoergen's HyperRAM documentation and source — the receive-path timing diagrams, the porting notes, and the full Avalon and HyperBus references.

Clone this wiki locally