Wishbone · Module 23
Memory Controller Design
Wishbone Classic has no stall signal, so a controller stealing cycles for refresh is indistinguishable from a slow memory. Thirty per cent of the bus's waiting went to work it never requested.
Every slave in this curriculum so far has been reactive: it is asked, it answers, and between requests it does nothing. wb_shared_ram, reused from Chapter 16.4 through Module 22, is an array, a wait counter and an [ACK_O].
A memory controller is not like that. It has work of its own — refresh, scrubbing, calibration, training — that the bus never asked for and cannot see. And Wishbone Classic gives it exactly one way to say so.
There is no stall signal in Wishbone Classic. No
READY, noVALIDto withdraw, no back-pressure channel. A slave that needs time says so by withholding all three terminations, and that is the whole vocabulary.
1. Model Versus Controller
wb_shared_ram (a model) | wb_mem_ctrl (a controller) | |
|---|---|---|
| purpose | produce traffic for measurement | be the subject of the measurement |
| latency | a fixed WAITS counter | a state machine doing real work |
| agenda of its own | none | refresh, on its own timer |
| can it collide with a transfer? | no | yes, and that is the interesting case |
A model is exactly right for generating traffic and exactly wrong as a teaching example of a controller, because nothing inside it ever wants the bus for itself.
2. The Only Thing Classic Lets A Slave Say
The comment at the top of the module is the design rationale, and it is short because the protocol leaves so little room:
// ── WHAT THE BUS CAN AND CANNOT SEE ─────────────────────────────────────
// There is no stall signal in Wishbone Classic. A slave that needs time
// says so by withholding all three terminations, and RULE 3.60 obliges
// the master to hold the entire request still while it waits. So a
// refresh is INDISTINGUISHABLE FROM A SLOW MEMORY from the master's side -
// the wait states look identical.RULE 3.60 is what makes this survivable. Because the master must hold [ADR_O], [DAT_O()], [SEL_O()] and [WE_O] still for as long as [STB_O] stands unanswered, a slave can take an arbitrary number of clocks and come back to a request that has not moved. Without that rule, withholding the termination would be useless — the question would be gone by the time the slave was ready to answer it.
Compare what other protocols spend on the same problem, which Chapter 20.3 and Chapter 21.2 measured:
| protocol | how a slave says "not yet" | cost |
|---|---|---|
| Wishbone Classic | withhold [ACK_O] | nothing — no extra wire |
| AXI | de-assert READY on the relevant channel | a READY per channel, five channels |
| APB | assert PSLVERR-adjacent PREADY low | one wire, but only in the ACCESS phase |
Wishbone's answer costs zero signals, and the price is that the bus cannot distinguish why it is waiting. Which is the next section.
3. The Measurement: Refresh Is Invisible
Two controllers, identical but for REFRESH_EVERY. Same twelve accesses.
rig acks refreshes work clks stolen clks
refresh off 12 0 24 0
refresh on 12 6 24 10Both completed all twelve. Both were conformant. One took ten extra clocks doing something the bus never requested.
WHAT THE MASTER CAN SEE
work clocks (the access itself) 24
stolen clocks (the refresh) 10
-> the CONTROLLER can tell them apart and the BUS
cannot. That asymmetry is why a memory controller
exports counters.That asymmetry is the reason real memory controllers export performance counters. The information exists; the protocol has no field to carry it.
Chapter 22.2 decomposed transfer latency into ISSUE, PRESENT, ANSWER and RECOVER. Refresh clocks and access clocks both land in PRESENT, and nothing in that decomposition can separate them — which is a real limit of that method, stated here rather than glossed.
4. Three States, And Which One Refresh Is Allowed To Win
The controller has three states, and the entire design is the question of which state a refresh is allowed to interrupt.
localparam logic [1:0] M_IDLE = 2'd0,
M_WORK = 2'd1,
M_RFSH = 2'd2;In M_IDLE nothing is in flight, so a refresh takes priority over accepting a new command without argument:
// ── IDLE: free. Take a refresh if one is due, otherwise take a
// command. Refresh wins here because nothing is in flight. ──
M_IDLE: begin
if (refresh_due) begin
st_q <= M_RFSH;
cnt_q <= REFRESH_LEN[7:0];
rtimer_q <= 16'd0;
nrf_q <= nrf_q + 16'd1;
end else if (xfer) beginRefresh wins in IDLE and must lose in WORK. That single asymmetry is the correct design, and Section 5 shows what the other choice costs.
In M_RFSH, a presented request simply goes unanswered — and the controller counts the fact rather than hiding it:
// ── RFSH: the controller is busy with itself. Any presented
// request simply does not get answered, which is the only
// thing Classic lets a slave say. ──
default: begin
if (xfer) nsteal_q <= nsteal_q + 16'd1;There is a subtle trap in the parameters themselves, found while building this chapter's negative control. Set REFRESH_EVERY shorter than REFRESH_LEN + ACCESS_LAT and the controller returns from each refresh to find the next one already due.
One request, presented and held for 200 clocks — which is exactly what RULE 3.60 obliges a conformant master to do:
=== SIM K - a refresh schedule that never yields ===
REFRESH_EVERY REFRESH_LEN ACCESS_LAT acks refreshes viol
6 2 4 20 21 0
3 2 4 1 50 0
READ THE 1 CAREFULLY - IT IS NOT ZERO.
The one access that completed is the one that began
BEFORE the refresh timer first came due. After that
the controller returns from every refresh to find
the next already due, and never reaches M_WORK
again. It is starved, not dead.One access in two hundred clocks against twenty, and zero protocol violations.
The 1 is the dangerous part. A controller that answered nothing would be found in minutes. This one answers — once — so a smoke test asking "did the memory come back?" gets a yes, and throughput is down twentyfold with every check in this module still passing.
A refresh schedule is a real-time constraint disguised as a parameter, and nothing in the protocol, the elaborator or the conformance monitor will tell you that you have set it wrong.
The arithmetic of stolen time
For the measured rig — twelve accesses, ACCESS_LAT clocks each, refreshing six times:
| quantity | clocks | share |
|---|---|---|
| work (the accesses themselves) | 24 | 70% |
| stolen (refresh, with a request standing) | 10 | 30% |
| total the bus waited | 34 | 100% |
Thirty per cent of the bus's waiting was spent on something it never asked for. A designer profiling this system from the master's side sees a slave with an average of 2.8 wait states and no way at all to discover that 0.8 of them are refresh. The counters in Section 6 exist because that number is otherwise unobtainable.
4. The Collision, Which Is The Whole Chapter
A refresh becoming due while a transfer is in flight is where the design decision lives. The controller can finish the access and refresh afterwards, or abandon the access and refresh now.
RULE 3.50 decides it:
"SLAVE interfaces MUST be designed so that the
[ACK_O],[ERR_O], and[RTY_O]signals are asserted and negated in response to the assertion and negation of[STB_I]."
A request that is still presented is still owed an answer. So the correct controller records the collision and carries on:
// ── WORK: the access is in flight. A refresh that becomes due
// now is a COLLISION. The correct controller records it and
// waits; REFRESH_PREEMPTS abandons the transfer. ──
M_WORK: begin
if (refresh_due) ncol_q <= ncol_q + 16'd1; THE COLLISION
refreshes that became due mid-transfer: 16
-> the correct controller FINISHED the access and
refreshed afterwards. RULE 3.50 is why: the
terminations are "asserted and negated in
response to the assertion and negation of
[STB_I]", and a request still presented is still
owed an answer.Sixteen collisions across twelve transfers, every one of them handled by finishing first. The refresh timer keeps counting; the refresh happens late; nothing is lost.
5. The Defect, And What It Proves About Conformance
REFRESH_PREEMPTS does the other thing:
if (REFRESH_PREEMPTS && refresh_due) begin
// the defect: the master is still presenting, and will now
// wait forever, because nothing will ever answer it
st_q <= M_RFSH;
cnt_q <= REFRESH_LEN[7:0];Run it against a master that gives up when the correct controller answers, and the result is stark:
AND THE ROW THAT MATTERS MOST: mem ctrl, PREEMPTS
completed 0 of 12 transfers against the correct
controller's 12, and scored 0 protocol violations.
ZERO. 12 TRANSFERS WERE SILENTLY LOST AND THE BUS
STAYED LEGAL THE WHOLE TIME.Zero protocol violations. Twelve lost transfers. Every one of the five Classic rules is satisfied, and the conformance monitor reports the defective controller as clean.
Be precise about why, because the obvious explanation is wrong:
It is not a hang. The controller abandons the access, refreshes, returns, and starts the access over. Against a master that holds its request — which RULE 3.60 obliges it to do — the transfer would complete late, not never. Against this testbench's master, which withdrew
[STB_I]as soon as the correct controller answered, nothing was owed and nothing was reported, because RULE 3.50 ties the termination to the negation of[STB_I]as much as the assertion.
So the same defect is a latency bug against one master and a data-loss bug against another, and no rule in B3 distinguishes them.
| what B3 provides | what it does not |
|---|---|
| the termination must answer the request | any bound on how long that takes |
| the request must be held while unanswered | any timeout |
| one termination at a time | any way to report "I gave up" |
B3 specifies no timeout of any kind. Chapter 12.6 builds the master-side watchdog that this absence forces on you — and the reason it belongs to the master is that the slave has no signal with which to raise the alarm.
6. What The Controller Exports, And Why
Because the bus cannot see any of this, the controller says it out of band:
// observation - this is what makes the chapter measurable
output logic refreshing_o,
output logic [15:0] refreshes_o,
output logic [15:0] acks_o,
output logic [15:0] stolen_clocks_o, // wait clocks caused by refresh
output logic [15:0] work_clocks_o, // wait clocks caused by access
output logic [15:0] collisions_o // refreshes due mid-transferstolen_clocks_o and work_clocks_o are the same thing to the bus and different things to the designer. That distinction is the entire justification for the port list above, and it is the shape every real DRAM controller's performance-counter block takes.
An honest note on the counters themselves: they are simulation instrumentation. In silicon they would be readable registers — which is a register bank, which is Chapter 23.3, and wiring them up is left there rather than duplicated here.
7. Conformance Under Stress
Both controllers, monitored throughout:
CONFORMANCE
rig violations phases
refresh off 0 12
refresh on 0 12
-> both clean. Stealing cycles is not a violation;
it is the only vocabulary Classic gives a slave
for "not yet".Stealing ten clocks from the bus is not a protocol violation. That deserves emphasis because it is counter-intuitive: a slave that makes every access 80% slower is fully conformant, and a conformance suite will never say a word about it. Conformance and performance are orthogonal, and Chapter 18.3 made the same point from the other direction when Module 17's checker passed five broken interconnects.
8. The Datasheet
| item | value | authority |
|---|---|---|
| port size | 32-bit | RULE 2.15 |
| granularity | 8-bit via [SEL_I()] | RULE 2.15 |
| cycle types | SINGLE READ, SINGLE WRITE | RULE 2.15 |
[ERR_O] | never asserted | local policy |
[RTY_O] | never asserted | local policy — see below |
| access latency | ACCESS_LAT clocks | local policy |
| refresh | REFRESH_LEN clocks every REFRESH_EVERY | local policy |
| worst-case latency | ACCESS_LAT + REFRESH_LEN | derived, and B3 bounds nothing |
[RTY_O] deserves a word. A controller that is busy refreshing is precisely the situation [RTY_O] describes — "not now, ask again". This one does not use it, and the reason is a trade worth naming: [RTY_O] returns the bus to the master immediately, which is better for a shared bus with other traffic and worse for a single master that will simply retry and burn a full cycle doing it. Wait states keep the master parked; retry sends it away. Neither is more conformant. The datasheet has to say which you built.
9. What This Controller Is Not
Stated plainly, because "memory controller" invites assumptions:
// ── WHAT THIS MODULE DELIBERATELY DOES NOT DO ───────────────────────────
// no banking, no row/column addressing, no precharge, no CAS latency
// no read/write turnaround penalty
// no burst or CTI supportThose belong to a DRAM controller and to the DDR track. This module teaches one thing: that a slave with its own agenda still owes the bus an answer, and how Classic lets it say "not yet".
Next: Chapter 23.5 — Address Decoder RTL leaves the slave entirely. B3 never describes a decoder, but it does say whose job it is — and then declines to say what should happen to an address that belongs to nobody.
Continue learning
Related tutorials
- Related topic
Bus Transactions
A transaction is the unit of bus work: one beginning, one ending, and an interval in between during which the request must not move. Making waiting expressible is what lets a slow target share a bus with a fast one, and it is what turns an initiator from a wire into a state machine with real failure modes.
- Related topic
ACK_I
The only mandatory termination. What a slave promises by asserting it, how wait states work without a wait signal, and why RULE 3.55 requires a master to keep working when a slave holds it asserted.
- Related topic
Wait States
A slave throttles a read by withholding its acknowledge. The transfer does not disappear — and a read with a side effect must fire once for the whole wait, not once per cycle.
- Related topic
Why Wait States Exist
Targets answer at different speeds and Classic Wishbone gives a slave one way to say so. The same read at 0, 1, 3 and 7 wait states produces one transfer and one value every time.
Standards & specifications
- Governing standard
- Wishbone SoC Interconnection Architecture (OpenCores)(opens OpenCores in a new tab)
Defines the Wishbone signal set, the bus cycles built from it and the interface rules a portable IP core must follow. It deliberately leaves interconnect topology, address map and arbitration policy to the integrator, so those are system decisions rather than requirements of the specification.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Wishbone curriculum.
