Skip to content
VLSI Mentor

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

A complete small system on chip. Two initiators, a CPU and a DMA engine, present requests to a shared fabric. Inside the fabric, an arbiter grants one initiator at a time and holds that grant for the whole of a transaction; a decoder turns the granted address into a one-hot target select and a local offset; a request multiplexer routes the granted initiator's request to the selected target; and a response path multiplexes read data and completion back from the selected target and then routes it to whichever initiator holds the grant. Five targets hang off the fabric: SRAM, GPIO, UART, a timer, and a default target that answers every address no other target owns.CPUinitiator 0DMA engineinitiator 1Arbiter + decodegrant, then selectRequest / responseforward mux, return demuxSRAM0x2000_0000, 64 KiBGPIO0x4000_0000, 4 KiBUART0x4000_1000, 4 KiBTimer0x4000_2000, 4 KiBDefault targeteverything unmapped12
Figure 1 — the complete Module 2 system: two initiators, one fabric, five targets.

2. RTL — The Fabric

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// 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;
endmodule

Reading 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 with g_valid means 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_ready gated 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.

StepWhat happensChapter
1txn_ctrl latches addr=0x4000_0004, write=1, byte_en=0x1; asserts valid2.5
2Arbiter sees req=01, grants initiator 0, takes the lock2.6
3Forward mux routes the CPU's request onto g_*2.3
4Decoder: 0x40000 matches GPIO → t_sel=00010, t_offset=0x0042.2
5GPIO sees sel & valid, byte_en[0] set → writes lane 0 of its output register2.1, 2.3
6GPIO asserts ready; the reduction returns it; grant routes it to initiator 02.4
7xfer_done releases the lock; txn_ctrl returns to idle and pulses done2.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.

#PropertyProtects againstChapter
1$onehot(t_sel) when grantedtwo targets answering, or none2.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
5request stable while valid && !readya transaction changing identity in flight2.5
6valid not withdrawn before acceptancea target completing into nothing2.5
7$onehot0(grant)two initiators owning one port2.6
8grant stable while locked && !xfer_doneresponse delivered to the wrong initiator2.6
9!(i0_ready && i1_ready)both initiators believing one transfer was theirs2.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 sel and valid together qualify an access, and neither alone does;
  • that the request must not move while valid is high and ready is low;
  • that valid may not be withdrawn before acceptance;
  • that read data is valid in exactly the cycle ready is high, and not held;
  • that an unselected target must drive zero read data;
  • that error accompanies ready rather than replacing it;
  • that a side effect fires on the acceptance edge, not on valid;
  • that reset is asynchronously asserted and active low;
  • that ready may be combinational from valid, 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 builtWhat is still missing
A fabric that routes correctlyThe rules a block must obey to use it
Signals that workSignal names anyone else would recognise
A transaction that completesA written statement of when it may not
Properties in a testbenchProperties anybody else can bind
One systemAnything 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

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.