Wishbone · Module 2
SoC Communication
Six chapters built the pieces; this one assembles them into a working fabric and traces three real accesses through it. The result works, and reading the nine unwritten rules a third party would need is what makes the case for a published protocol concrete rather than theoretical.
Module 2 has built six pieces separately: roles and ownership, decode, the datapath, control, the transaction, and arbitration. This chapter puts them in one module and runs real accesses through it.
How do address, data, control, transactions, decoding and ownership combine into a working SoC communication fabric?
And then the question the whole of Module 1 and Module 2 has been walking toward: looking at the finished thing, what is still missing?
1. The System
2. RTL — The Fabric
// ─────────────────────────────────────────────────────────────────────────
// soc_fabric — the Module 2 interconnect, assembled.
//
// PURPOSE. Do the four fabric jobs and nothing else:
// 1. arbitrate between two initiators, holding the grant for a whole
// transaction (Chapter 2.6);
// 2. decode the granted address to a one-hot target select and a local
// offset (Chapter 2.2);
// 3. route the request forward to exactly one target (Chapter 2.3);
// 4. route read data and completion back to the owning initiator
// (Chapters 2.3 and 2.6).
//
// This is an EDUCATIONAL fabric, not a production interconnect. Section 5
// is an explicit list of what it does not do.
//
// Generic educational interface — NOT Wishbone signal names. Wishbone's own
// signals and their rules are Module 4's subject.
// ─────────────────────────────────────────────────────────────────────────
module soc_fabric #(
parameter int unsigned AW = 32,
parameter int unsigned DW = 32,
parameter int unsigned NT = 5 // 4 real targets + default
) (
input logic clk,
input logic rst_n,
// ── Initiator 0 (CPU) ────────────────────────────────────────────────
input logic i0_valid,
input logic i0_write,
input logic [AW-1:0] i0_addr,
input logic [DW-1:0] i0_wdata,
input logic [3:0] i0_byte_en,
output logic i0_ready,
output logic [DW-1:0] i0_rdata,
output logic i0_error,
// ── Initiator 1 (DMA) ────────────────────────────────────────────────
input logic i1_valid,
input logic i1_write,
input logic [AW-1:0] i1_addr,
input logic [DW-1:0] i1_wdata,
input logic [3:0] i1_byte_en,
output logic i1_ready,
output logic [DW-1:0] i1_rdata,
output logic i1_error,
// ── Target side, flattened: index 0=SRAM 1=GPIO 2=UART 3=Timer 4=dflt
output logic [NT-1:0] t_sel,
output logic t_valid,
output logic t_write,
output logic [AW-1:0] t_offset,
output logic [DW-1:0] t_wdata,
output logic [3:0] t_byte_en,
input logic [NT-1:0] t_ready,
input logic [NT*DW-1:0] t_rdata_flat,
input logic [NT-1:0] t_error
);
// ── 1. ARBITRATION ───────────────────────────────────────────────────
logic [1:0] grant;
logic locked, xfer_done;
fixed_arbiter u_arb (
.clk (clk),
.rst_n (rst_n),
.req ({i1_valid, i0_valid}),
.xfer_done (xfer_done),
.grant (grant),
.locked (locked)
);
// ── 2. FORWARD ROUTING — the granted initiator's request ─────────────
logic g_valid, g_write;
logic [AW-1:0] g_addr;
logic [DW-1:0] g_wdata;
logic [3:0] g_byte_en;
always_comb begin
unique case (grant)
2'b01: begin
g_valid = i0_valid; g_write = i0_write; g_addr = i0_addr;
g_wdata = i0_wdata; g_byte_en = i0_byte_en;
end
2'b10: begin
g_valid = i1_valid; g_write = i1_write; g_addr = i1_addr;
g_wdata = i1_wdata; g_byte_en = i1_byte_en;
end
default: begin
// No grant: the target port must be QUIET. Without this branch
// t_valid becomes a latch and targets see stale requests.
g_valid = 1'b0; g_write = 1'b0; g_addr = '0;
g_wdata = '0; g_byte_en = 4'h0;
end
endcase
end
// ── 3. DECODE — only meaningful while something is granted ───────────
logic [4:0] sel_raw;
logic [AW-1:0] offset_raw;
logic unmapped_raw;
bus_decoder #(.AW(AW), .NTARGET(4)) u_dec (
.addr (g_addr),
.target_sel (sel_raw),
.offset (offset_raw),
.unmapped (unmapped_raw)
);
// The select is gated by `g_valid`. The decoder is always computing
// something — it is combinational — but a target must only be selected
// when a real request exists. This is Chapter 2.1's sel-and-valid rule
// enforced in the fabric rather than left to each target.
assign t_sel = {NT{g_valid}} & sel_raw;
assign t_valid = g_valid;
assign t_write = g_write;
assign t_offset = offset_raw;
assign t_wdata = g_wdata;
assign t_byte_en = g_byte_en;
// ── 4. RESPONSE — collect from the selected target ───────────────────
logic [DW-1:0] r_rdata;
logic r_ready, r_error;
function automatic logic [DW-1:0] rdata_of(input int unsigned i);
return t_rdata_flat[i*DW +: DW];
endfunction
// AND-OR reduction. Correct ONLY because t_sel is one-hot, which the
// decoder's elaboration check and assertion P1 guarantee.
always_comb begin
r_rdata = '0;
r_ready = 1'b0;
r_error = 1'b0;
for (int unsigned i = 0; i < NT; i++) begin
r_rdata |= {DW{t_sel[i]}} & rdata_of(i);
r_ready |= t_sel[i] & t_ready[i];
r_error |= t_sel[i] & t_error[i];
end
end
// ── 5. RETURN ROUTING — to the OWNER only ────────────────────────────
// Read data fans out harmlessly; the COMPLETION is what is gated, so a
// non-owning initiator is never told a transfer finished.
assign i0_rdata = r_rdata;
assign i1_rdata = r_rdata;
assign i0_ready = (grant == 2'b01) & r_ready;
assign i1_ready = (grant == 2'b10) & r_ready;
assign i0_error = (grant == 2'b01) & r_error;
assign i1_error = (grant == 2'b10) & r_error;
// The acceptance edge of whatever is currently granted — the single term
// that releases the arbiter's lock.
assign xfer_done = t_valid & r_ready;
endmoduleReading this module
Purpose. Do the four jobs, in order, once.
The ordering matters and is worth stating. Arbitrate first, then decode the granted address. Decoding before arbitrating would mean decoding an address that may not be the one that proceeds — wasted logic, and a select that changes when the grant does.
Combinational behaviour. Everything except the arbiter's two registers is combinational: forward multiplexer, decode, select gating, response reduction, return gating. That is one long path — initiator register, through the mux, through the decoder, through the target, back through the response reduction, into the initiator's capture register — and Section 5 is honest about it.
Sequential behaviour. Only grant_q and locked_q, inside the arbiter. The fabric itself holds no transaction state, because Chapter 2.5's txn_ctrl holds it on the initiator side.
The two gating decisions that carry most of the correctness:
t_sel = {NT{g_valid}} & sel_raw. The decoder always computes a selection; gating it withg_validmeans no target is selected unless a real request exists. Doing it here rather than in each target means a target that forgets Chapter 2.1's rule is still safe.i0_ready/i1_readygated by grant. The response is private. Ungating this is the corruption from Chapter 2.6 §5.
How it could fail. Remove the default branch and t_valid latches. Decode before arbitrating and the select moves with the grant. Ungate t_sel and targets act on stale addresses. Derive xfer_done from an initiator's signals and the wrong lock is released.
3. Three Accesses, Traced
Access 1 — the CPU writes a GPIO output register.
| Step | What happens | Chapter |
|---|---|---|
| 1 | txn_ctrl latches addr=0x4000_0004, write=1, byte_en=0x1; asserts valid | 2.5 |
| 2 | Arbiter sees req=01, grants initiator 0, takes the lock | 2.6 |
| 3 | Forward mux routes the CPU's request onto g_* | 2.3 |
| 4 | Decoder: 0x40000 matches GPIO → t_sel=00010, t_offset=0x004 | 2.2 |
| 5 | GPIO sees sel & valid, byte_en[0] set → writes lane 0 of its output register | 2.1, 2.3 |
| 6 | GPIO asserts ready; the reduction returns it; grant routes it to initiator 0 | 2.4 |
| 7 | xfer_done releases the lock; txn_ctrl returns to idle and pulses done | 2.5, 2.6 |
Every step is a chapter. That is the point of the trace, not the GPIO.
Access 2 — the CPU reads the UART status register. Identical through step 4, with t_sel=00100 and t_offset=0x004. Then the return path does the work: the UART drives rdata in the cycle it asserts ready; the AND-OR reduction selects it because only the UART's select bit is set; the grant gate delivers the completion to initiator 0 and not to the DMA; txn_ctrl captures rdata on that same edge, because that is the only cycle it is valid.
The failure lurking in step 5 of access 2 is worth naming: if any other target drove non-zero rdata while unselected, the reduction would OR it in. Nothing would report an error. That is why Chapter 2.1's P3 puts the obligation on every target, not only on the multiplexer.
Access 3 — the DMA and the CPU both want SRAM.
Both assert valid in the same cycle. The arbiter's priority encoder picks initiator 0 and locks. The DMA's request is not routed — the forward multiplexer does not select it — so the SRAM never sees it, and the DMA's i1_ready stays low because the grant gate holds it there. The DMA's txn_ctrl sits in S_OUTSTANDING holding its request stable, exactly as it would against a slow target: from the DMA's point of view, arbitration is indistinguishable from latency.
When the CPU's access completes, xfer_done releases the lock, the arbiter grants the DMA, and its request — unchanged throughout — is finally routed.
4. Verification — The Module 2 Checklist
The properties from every chapter, as one list, with what each protects.
| # | Property | Protects against | Chapter |
|---|---|---|---|
| 1 | $onehot(t_sel) when granted | two targets answering, or none | 2.2 |
| 2 | `t_ready[i] | -> t_sel[i]` | a target completing a transfer it was not given |
| 3 | `!t_sel[i] | -> (t_rdata[i] == 0)` | an unselected target corrupting the reduction |
| 4 | `error | -> ready` | an errored access hanging the initiator |
| 5 | request stable while valid && !ready | a transaction changing identity in flight | 2.5 |
| 6 | valid not withdrawn before acceptance | a target completing into nothing | 2.5 |
| 7 | $onehot0(grant) | two initiators owning one port | 2.6 |
| 8 | grant stable while locked && !xfer_done | response delivered to the wrong initiator | 2.6 |
| 9 | !(i0_ready && i1_ready) | both initiators believing one transfer was theirs | 2.6 |
| 10 | `grant == 0 | -> !t_valid` | a request reaching a target with no owner |
Two observations about the list as a whole.
Every property is a safety property — each says something bad never happens and is violated by a single cycle. Not one expresses starvation, a timeout, or eventually. Those are liveness properties, they need a bound to become checkable, and choosing the bound requires knowing a real deadline. Chapter 2.6 §8 is that argument.
Every property is written against this module's contract. Rename a signal, change whether a request may be withdrawn, allow two transactions in flight, and most of them need rewriting. That is the observation Section 6 turns into the module's conclusion.
5. What This Fabric Does Not Do
Being explicit is what separates a teaching model from something someone might ship.
One transaction at a time, system-wide. The arbiter locks for the whole transaction, so while the CPU waits on a slow UART, the DMA cannot use the SRAM — even though the two targets are independent. A real fabric decodes first and arbitrates per target, so independent accesses proceed concurrently. This single limitation is the largest gap between this module and a production interconnect.
No pipelining. One outstanding transaction per initiator, so the bus is idle through every wait cycle.
One long combinational path. Initiator register → forward mux → decoder → target → response reduction → grant gate → initiator capture. On any real device this is the critical path, and the fixes — registering the response, hierarchical decode — both cost a cycle and are only affordable because completion is signalled.
No timeout. A target that never completes holds the arbiter's lock forever, so one broken target stops the entire system rather than one initiator.
Fixed priority. Starvation by construction, per Chapter 2.6 §6.
No protection, no security, no quality of service, no ordering rules across targets. All real, all deliberately absent.
6. What Is Still Missing — and It Is Not RTL
The fabric works. Two initiators, five targets, correct decode, private responses, held ownership, verified properties. If you built it, it would run.
And it is still not something another engineer could connect a block to.
Everything a block must obey to work here exists only as prose in these seven chapters and as assertions in a testbench:
- that
selandvalidtogether qualify an access, and neither alone does; - that the request must not move while
validis high andreadyis low; - that
validmay not be withdrawn before acceptance; - that read data is valid in exactly the cycle
readyis high, and not held; - that an unselected target must drive zero read data;
- that
erroraccompaniesreadyrather than replacing it; - that a side effect fires on the acceptance edge, not on
valid; - that reset is asynchronously asserted and active low;
- that
readymay be combinational fromvalid, and what that forbids.
Nine rules. Every one is load-bearing, and not one of them is written anywhere a third party could read.
Hand this fabric to somebody with a UART core and they cannot connect it. They can read the RTL and infer the rules — Chapter 1.4 §2 is exactly this — but an inference is not a contract. It is unverifiable, it is invalidated silently by the next revision, and it must be re-derived by every engineer who touches the system.
That is the gap, stated precisely: this module has produced a working interconnect and no interface specification. The RTL is the easy half.
| What Module 2 built | What is still missing |
|---|---|
| A fabric that routes correctly | The rules a block must obey to use it |
| Signals that work | Signal names anyone else would recognise |
| A transaction that completes | A written statement of when it may not |
| Properties in a testbench | Properties anybody else can bind |
| One system | Anything reusable in a second system |
Chapter 1.3 argued this abstractly. Module 2 has now made it concrete: nine specific rules, each derived from a failure, each currently unwritten.
7. Common Mistakes
"The fabric should handle peripheral quirks."
Wrong mental model: the interconnect is the place to fix awkward targets.
Concrete failure: latency compensation or register semantics migrate into the fabric, which now knows about specific peripherals and cannot be reused or reasoned about.
Observable evidence: a fabric with per-target special cases, where adding a peripheral means editing the interconnect.
Correct model: four jobs — arbitrate, decode, route forward, route back. Anything else belongs in a target or an initiator.
"Decode first, then arbitrate."
Wrong mental model: decoding is independent of who is asking.
Concrete failure: the decoder resolves an address that may not proceed, and the select changes whenever the grant does — so a target can be momentarily selected for a transaction that never runs.
Observable evidence: targets seeing brief selects with no matching access.
Correct model: arbitrate first, decode the granted address. Unless you decode per target and arbitrate per target, which is what a real fabric does to get concurrency — and that is a different, larger design.
"It works, so the interface is defined."
Wrong mental model: working RTL is a specification.
Concrete failure: a second engineer connects a block by reading the code, infers one of the nine rules differently, and produces a system that works until a target inserts a wait state.
Observable evidence: integration failures that appear only when timing changes.
Correct model: the RTL is one implementation of an unwritten contract. Section 6 is the list of what is unwritten.
"One transaction at a time is a simplification we can remove later."
Wrong mental model: concurrency is an optimisation bolted on afterwards.
Concrete failure: adding pipelining requires response-to-request matching, ordering rules, and buffering — none of which this fabric's structure anticipates. It is a rewrite.
Observable evidence: a performance requirement that cannot be met without redesigning the interconnect.
Correct model: how many transactions may be in flight is an early architectural decision that shapes everything, which is why real specifications state it explicitly.
8. Interview Reasoning
Initiator side. The store produces an address, a direction, write data and byte enables. The transaction controller latches all of it and asserts a qualifier, holding everything stable — and it drives the bus from the latched copy, so stability is structural rather than a rule the core must obey.
Arbitration. If another initiator is also asking, one is granted and the grant is held for the whole transaction. From the loser's point of view this is indistinguishable from a slow target.
Decode. The granted address is compared against the region table, producing a one-hot select and a local offset. The target never sees the full address — that is what lets it be instantiated twice.
Forward routing. The request reaches exactly one target, with the select gated by the qualifier so no target acts on a cycle with no request.
At the target. Selected and qualified, direction is write, byte enables say which lanes participate, so only those lanes change. The target asserts completion.
Return routing. The response reduction picks the selected target's completion; the grant gate delivers it to the owning initiator only. The controller sees acceptance, captures read data if it was a read, releases the arbiter's lock, and pulses done.
The detail that shows real understanding: every state change and every side effect in the system hangs off one term — the acceptance edge where qualifier and completion are both high. Not on the qualifier alone, which is the bug that appears the first time a target waits.
9. Understanding Check
10. What's Next
Module 2 is complete, and its argument is one line.
Roles fix ownership of every signal. Addresses become selections and local offsets through a decode that must be exhaustive and mutually exclusive. Data fans out one way and multiplexes back the other, with byte lanes deciding what participates. Control is what makes payload interpretable, and each control signal answers a specific way an unqualified bus fails. A transaction is an interval with rules, and making waiting expressible is what lets fast and slow targets share a bus. Shared resources need arbitration, and the rule that matters is that ownership cannot move in flight. Assembled, all of it works.
And it is still not connectable by anyone who did not write it, because the nine rules in Section 6 live in prose.
What does a published interconnect specification actually contain — what does it fix, what does it deliberately leave open, and what is the mental model that makes the rest of it read as obvious rather than arbitrary?
Module 3 — Wishbone Architecture Overview answers that: the complete Wishbone system, the master and slave interfaces, the interconnect between them, and the transaction lifecycle — at architecture level, before any signal. Module 4 then owns the signals themselves, and Module 5 the handshake. Bold rather than linked is this track's convention for chapters that have not shipped yet. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
Need for Standardized Interconnects
An address map answers where a register lives. It says nothing about which wires carry the request, when they are valid, how the target reports completion, or what happens on an error. Three peripherals with three private interfaces produce three adapters, three verification efforts and three ways to be wrong — which is the argument for standardising the interface rather than the map.
- Related topic
CPU to Peripheral Communication
A CPU reaches hardware outside itself by reading and writing addressed locations, and a peripheral is hardware it cannot execute. Everything a driver does has to be expressed as a read or a write of a location the peripheral answers for — and once more than a couple of peripherals exist, wiring each one to the core separately stops scaling. That is the problem an on-chip bus is the answer to.
- Related topic
Data Flow
One Wishbone access, followed through every block in both directions: what the master drives, where the address changes form, which signals are broadcast and which are decoded, and how read data and termination find their way back to exactly one requester.
- Related topic
Why Wishbone Was Created
Six chapters of engineering pressure produce a specific set of requirements: a fixed interface, a signalled completion, a synchronous reference, an interconnect the integrator still owns, and a licence a volunteer project can adopt without a legal review. Wishbone is what those requirements look like written down — including the things it deliberately refuses to decide.
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.
