Skip to content
VLSI Mentor

Wishbone · Module 2

Shared Resources

Two initiators wired to one target is not a wiring problem with a wiring solution. A single-port target has one address input and one completion output, so access must be serialised — and the rule that matters most is not who goes first but that ownership cannot change while a transaction is in flight.

Every chapter so far has had one initiator. That assumption did quiet work: whose transaction is this was never a question, and nothing could be taken away mid-flight.

Real systems break it immediately. A CPU and a DMA engine both need the SRAM, and the moment they do, two new questions exist that no amount of correct decode or correct data path answers.

What happens when more than one initiator wants the same hardware resource?

1. Why This Is Not a Wiring Problem

A processor core and a direct memory access engine both need to reach a single shared SRAM. The SRAM is a single-port target: it has one address input, one direction input, one write data input and one completion output, so it can serve only one initiator at a time. An arbiter sits between the two initiators and the target. It takes a request from each, grants the target to exactly one of them, routes that initiator's request through to the SRAM, and routes the SRAM's response back to whichever initiator currently holds the grant. The critical rule is that the grant must not move while a transaction is still in flight.CPUinitiator 0DMA engineinitiator 1Arbitergrants exactly oneRequest muxroutes the winnerShared SRAMONE port, one opinionGrant is heldfor the whole transactionResponse routingback to the owner only12
Figure 1 — two initiators, one single-port target. The arbiter exists because the target has one of everything.

The instinctive fix is to wire both initiators to the target and let it sort them out. It does not work, and the reason is worth stating precisely because it is not the reason people expect.

It is not primarily about electrical contention. On-chip, these are not tri-state nets; two drivers on one input is a multiple-driver error the tools catch.

It is that the target has one of everything. One address input takes one address. One completion output makes one statement. Even if the wires could somehow carry both requests, the SRAM could only be reading one location. Two simultaneous accesses to a single-port resource is not a thing the hardware can represent, so something must choose, and the choice must happen before the target sees anything.

And there is a second consequence people miss. Once you have chosen, the response has to find its way back to the right initiator. Chapter 2.3's read multiplexer routed by target; now there is a second routing question, by initiator, in the opposite direction.

2. RTL 1 — A Fixed-Priority Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// fixed_arbiter — grant a shared target to one of two initiators.
//
// PURPOSE. Serialise access to a single-port resource, and — more
// importantly — HOLD the grant for the whole of a transaction.
//
// Fixed priority: initiator 0 wins ties, always. That is a deliberate
// choice for an introductory arbiter, and Section 6 is honest about what it
// costs. Fairness policies are Module 17's subject.
//
// Generic educational interface — NOT Wishbone signal names.
// ─────────────────────────────────────────────────────────────────────────
module fixed_arbiter (
  input  logic       clk,
  input  logic       rst_n,

  input  logic [1:0] req,        // one bit per initiator: wants the target
  // `xfer_done` is the acceptance edge of the granted initiator's
  // transaction — the term Chapter 2.5 built everything from. The arbiter
  // needs it to know when releasing the grant is safe.
  input  logic       xfer_done,

  output logic [1:0] grant,      // one-hot, or all-zero when idle
  output logic       locked      // 1 = a transaction is in flight
);
  logic [1:0] grant_q;
  logic       locked_q;

  // ── Combinational: who WOULD win if the grant were free. Fixed priority
  //    is a priority encoder: initiator 0 first, then 1.
  logic [1:0] winner;
  always_comb begin
    if      (req[0]) winner = 2'b01;
    else if (req[1]) winner = 2'b10;
    else             winner = 2'b00;
  end

  // ── Outputs. While locked, the REGISTERED grant is presented and the
  //    combinational winner is ignored entirely. This is the whole point of
  //    the module: the grant cannot move mid-transaction even if a
  //    higher-priority initiator raises its request.
  assign grant  = locked_q ? grant_q : winner;
  assign locked = locked_q;

  // ── Sequential ───────────────────────────────────────────────────────
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      grant_q  <= 2'b00;
      locked_q <= 1'b0;
    end else if (!locked_q) begin
      // Free: if anybody is asking, lock the winner in for the duration.
      if (winner != 2'b00) begin
        grant_q  <= winner;
        locked_q <= 1'b1;
      end
    end else begin
      // Locked: the ONLY release is the granted transaction completing.
      // There is no timeout and no pre-emption — see Section 6.
      if (xfer_done) begin
        locked_q <= 1'b0;
        grant_q  <= 2'b00;
      end
    end
  end
endmodule

Reading this module

Purpose. Choose one initiator, and keep that choice stable until its transaction ends.

Interface contract. req[i] means initiator i has a transaction it wants to start or has in flight. xfer_done is the acceptance edge of whichever transaction is currently granted. grant is one-hot or zero.

Combinational behaviour. winner is a two-level priority encoder. The output multiplexer is the design decision: while locked_q is set, winner is not consulted at all. A design that simply assigned grant = winner would be a correct arbiter for single-cycle transactions and a corrupting one for anything that waits.

Sequential behaviour. Two registers. grant_q remembers who owns the target; locked_q remembers that a transaction is in flight. The lock is taken when the target is free and someone asks, and released only on xfer_done.

Cycle by cycle, with both initiators asking and a target that waits two cycles:

Cyclereqlocked_qgrantWhat happens
011001Both ask. Priority picks 0. Lock taken at the edge.
111101Initiator 1 still asking and cannot win. Target busy.
211101Still waiting. Grant unmoved.
311101xfer_done — transaction 0 accepted. Lock released at edge.
410010Initiator 1 finally wins.

Assumptions and simplifications. Two initiators; fixed priority; no pre-emption; no timeout; and xfer_done is assumed to belong to the granted initiator, which the surrounding routing must guarantee.

How it could fail.

  • assign grant = winner without the lock. The grant moves the moment a higher-priority request arrives. Section 5 is what that does.
  • Releasing on req falling instead of xfer_done. An initiator that drops its request early frees the target while its transaction is still outstanding.
  • Releasing on any completion rather than the granted one. Another target's completion frees this one's lock.

Scaling. Two initiators is a two-level priority encoder. Sixteen is a sixteen-level chain with real delay, and fixed priority's starvation problem grows with the count. Both are Module 17's subject.

3. RTL 2 — Routing the Request and the Response

The arbiter chooses; something still has to move the signals.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// shared_port — route the granted initiator to a single-port target, and
// route the target's response back to the owner ONLY.
//
// PURPOSE. Show that arbitration is two routing problems, not one. The
// forward path is a multiplexer selected by grant. The return path is a
// DEMULTIPLEXER, also selected by grant, and getting it wrong delivers one
// initiator's data to the other with no error anywhere.
//
// Generic educational interface — NOT Wishbone signal names.
// ─────────────────────────────────────────────────────────────────────────
module shared_port #(
  parameter int unsigned AW = 32,
  parameter int unsigned DW = 32
) (
  input  logic [1:0]      grant,        // one-hot or zero, from the arbiter

  // ── Initiator 0 ──────────────────────────────────────────────────────
  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 ──────────────────────────────────────────────────────
  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,

  // ── The shared target ────────────────────────────────────────────────
  output logic            t_valid,
  output logic            t_write,
  output logic [AW-1:0]   t_addr,
  output logic [DW-1:0]   t_wdata,
  output logic [3:0]      t_byte_en,
  input  logic            t_ready,
  input  logic [DW-1:0]   t_rdata,
  input  logic            t_error,

  output logic            xfer_done     // acceptance edge, back to the arbiter
);
  // ── Forward path: a multiplexer. An ungranted initiator's `valid` never
  //    reaches the target, which is what makes serialisation real rather
  //    than advisory.
  always_comb begin
    unique case (grant)
      2'b01: begin
        t_valid   = i0_valid;   t_write = i0_write;
        t_addr    = i0_addr;    t_wdata = i0_wdata;
        t_byte_en = i0_byte_en;
      end
      2'b10: begin
        t_valid   = i1_valid;   t_write = i1_write;
        t_addr    = i1_addr;    t_wdata = i1_wdata;
        t_byte_en = i1_byte_en;
      end
      default: begin
        t_valid   = 1'b0;       t_write = 1'b0;
        t_addr    = '0;         t_wdata = '0;
        t_byte_en = 4'h0;
      end
    endcase
  end

  // ── Return path: a DEMULTIPLEXER. The response is broadcast to both
  //    initiators' data inputs — which is harmless, because data without a
  //    completion means nothing (Chapter 2.4) — but the COMPLETION goes to
  //    exactly one. That gating is what makes the response private.
  assign i0_rdata = t_rdata;
  assign i1_rdata = t_rdata;
  assign i0_ready = (grant == 2'b01) & t_ready;
  assign i1_ready = (grant == 2'b10) & t_ready;
  assign i0_error = (grant == 2'b01) & t_error;
  assign i1_error = (grant == 2'b10) & t_error;

  // The acceptance edge of whichever transaction is granted.
  assign xfer_done = t_valid & t_ready;
endmodule

Reading this module

Purpose. Make the grant physically effective in both directions.

The forward path is Chapter 2.3's multiplexer with the roles reversed: many initiators, one target. The default branch matters — with no grant, t_valid must be low, or the target sees a request nobody made from whatever the unselected initiator happens to be driving.

The return path is the half that is easy to get wrong. Read data is broadcast to both initiators, and that is safe: Chapter 2.4 established that payload without a qualifying control signal is meaningless, so an initiator that is not being told done will never look. The completion is what is gated, and gating it is what makes the response private.

Why xfer_done is derived here rather than in the arbiter. It is t_valid & t_ready — the acceptance term of the transaction that is actually on the target's port. Deriving it from any initiator's signals instead would let a non-granted initiator's activity release the lock.

How it could fail.

  • Ungated i0_ready/i1_ready. Both initiators see every completion. Each believes its own transaction finished, and both capture the same read data. Section 5.
  • A missing default branch. t_valid becomes a latch, and the target sees stale requests when nothing is granted.
  • xfer_done derived from i0_valid & t_ready. Initiator 0's activity releases initiator 1's lock.

4. Verification — Ownership Properties

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// Ownership and arbitration properties. These encode this chapter's rules —
// not any bus's protocol, and not a fairness policy, which is Module 17.
// ─────────────────────────────────────────────────────────────────────────
module arb_checker (
  input logic       clk,
  input logic       rst_n,
  input logic [1:0] req,
  input logic [1:0] grant,
  input logic       locked,
  input logic       xfer_done,
  input logic       t_valid,
  input logic       i0_ready,
  input logic       i1_ready
);
  default disable iff (!rst_n);

  // P1 — grants are mutually exclusive. Two grants means two initiators
  //      driving one target port: the forward multiplexer would have two
  //      matching branches and the target one incoherent request.
  property p_grant_onehot0;
    @(posedge clk) $onehot0(grant);
  endproperty
  a_grant_onehot0 : assert property (p_grant_onehot0)
    else $error("grant=%b — two initiators cannot own one port", grant);

  // P2 — a grant implies a request, EXCEPT while locked. The exception is
  //      the interesting part: a granted initiator may legitimately still
  //      be holding a transaction whose `req` has already been satisfied,
  //      so the naive form of this property is too strong.
  property p_grant_implies_req;
    @(posedge clk) (|grant && !locked) |-> (req & grant) != 2'b00;
  endproperty
  a_grant_implies_req : assert property (p_grant_implies_req)
    else $error("granted an initiator that is not requesting");

  // P3 — the grant does not move while a transaction is in flight. THE
  //      property of this chapter. Everything in Section 5 is a violation
  //      of exactly this.
  property p_grant_stable_in_flight;
    @(posedge clk) (locked && !xfer_done) |=> $stable(grant);
  endproperty
  a_grant_stable_in_flight : assert property (p_grant_stable_in_flight)
    else $error("grant changed while a transaction was outstanding");

  // P4 — at most one initiator is ever told a transfer completed. The
  //      response is private; two completions means both initiators believe
  //      the same transaction was theirs.
  property p_single_completion;
    @(posedge clk) !(i0_ready && i1_ready);
  endproperty
  a_single_completion : assert property (p_single_completion)
    else $error("both initiators saw a completion for one transfer");

  // P5 — an ungranted target port is quiet. With no grant, no request may
  //      reach the target at all.
  property p_quiet_when_ungranted;
    @(posedge clk) (grant == 2'b00) |-> !t_valid;
  endproperty
  a_quiet_when_ungranted : assert property (p_quiet_when_ungranted)
    else $error("a request reached the target with no grant");
endmodule

Why P2 carries an exception. The obvious property — a grant implies a request — fires on correct behaviour. Once an initiator is granted and its transaction is in flight, its req may legitimately have been deasserted, while the grant must stay. Writing the naive form and then "fixing" the design to satisfy it would break the lock. An assertion that is too strong is worse than none, because it pressures the design in the wrong direction.

Why P4 is expressed on the initiators' completions rather than on the grant. It catches the specific bug where the grant is correct and the response routing is not — which is a different file, and a failure message naming the completion points at it directly.

5. Failure Modes and Discriminating Evidence

Symptom: the DMA engine occasionally writes data intended for the CPU, or reads a value the CPU asked for.

Candidates. The grant moved mid-transaction. Or the completion is not gated by grant.

Discriminating evidence. Trigger on locked && $changed(grant). If that ever fires, it is the grant — the arbiter released or switched while a transaction was outstanding, so the response came back to a different owner than the one who asked. If the grant is stable, check i0_ready and i1_ready: both high in one cycle means the response routing is ungated.

Likely RTL location. The arbiter's output multiplexer, or shared_port's completion gating.

Property that catches it. P3 for the first, P4 for the second.

Symptom: the CPU stalls for long, irregular periods under DMA load.

Candidates. Fixed-priority starvation, if the DMA is the higher priority. Or long transactions holding the lock.

Discriminating evidence. Measure the time between the CPU's req rising and its grant. If it correlates with DMA activity and the DMA is priority 0, it is starvation — a policy problem, not a bug. If both initiators are being served but each transaction is long, the lock duration is the issue and the answer is shorter transactions, not a different arbiter.

Property that catches it. None of the above. Starvation is a liveness property; the assertions here are all safety properties and none of them can express eventually. That distinction is worth carrying.

Symptom: the target sees requests when nothing should be accessing it.

Candidates. A missing default in the forward multiplexer, so t_valid latches. Or an ungranted initiator's valid reaching the target.

Discriminating evidence. Check t_valid while grant is zero. High is conclusive.

Property that catches it. P5.

Symptom: a transaction is released early and the target completes into nothing.

Candidates. The lock released on req falling rather than on xfer_done.

Discriminating evidence. Compare the cycle locked_q clears against the cycle t_valid && t_ready occurs. Clearing first is the bug.

Likely RTL location. The arbiter's release condition.

6. Fixed Priority — What It Costs

Being honest about the arbiter that was just built.

What it gets right. It is small, it is fast, and it is trivially deterministic — the same request pattern always produces the same grant sequence, which makes it easy to reason about and easy to test.

What it gets wrong: starvation. Initiator 0 wins every tie. If initiator 0 requests continuously, initiator 1 never runs. Not rarely — never. And this is not a corner case: a DMA engine moving a large buffer requests continuously by design.

The consequence is worse than slowness. A starved initiator's latency is unbounded, so any real-time deadline it has is unmeetable, and the failure is load-dependent — it appears only when the other initiator is busy, which is exactly when the system is under test least.

What the alternatives trade. Round-robin gives every initiator a turn and costs a little state and a little delay. Weighted schemes let one initiator have more turns without excluding the other. Priority with an anti-starvation escape hatch keeps low latency for the urgent initiator and bounds the other's wait. Each buys fairness with area and complexity, and choosing among them requires knowing the initiators' latency requirements — which is a system question.

Module 17 is where those are developed. What matters here is recognising that fixed priority is a policy choice with a known failure mode, not a default.

7. Common Mistakes

"Two initiators can share a target if the wiring is right."

Wrong mental model: it is a connectivity problem.

Concrete failure: multiple drivers on the target's inputs — caught by the tools — or, if muxed without arbitration, two requests alternating into one target that has no idea two conversations are happening.

Observable evidence: elaboration errors, or a target receiving interleaved fragments of two transactions.

Correct model: a single-port target has one of everything. Two simultaneous accesses cannot be represented, so serialisation is required before the target sees anything.

"The arbiter just has to pick a winner."

Wrong mental model: arbitration is the choice.

Concrete failure: a combinational grant = winner. Initiator 0's request arrives mid-transaction, the grant switches, and the target — which latched initiator 1's address — completes to initiator 0.

Observable evidence: data appearing in the wrong initiator, only when both are active, and more often as transactions get longer.

Correct model: arbitration is choosing and holding. The hold is the harder half and the one that causes the bugs.

"Fixed priority is fine because the low-priority initiator is not important."

Wrong mental model: priority reflects importance, so a starved initiator is an acceptable loss.

Concrete failure: the low-priority initiator has a deadline — a UART that must be serviced before its receive register overflows — and misses it whenever the other initiator is busy.

Observable evidence: data loss correlated with system load, absent on an idle bench.

Correct model: priority orders access, not importance. A low-priority initiator with a real-time requirement needs a bounded wait, which fixed priority cannot provide at all.

"If the grant is right, the response goes to the right place."

Wrong mental model: routing is one problem.

Concrete failure: the forward multiplexer is correct and completions are broadcast ungated. Both initiators see ready, both believe their transaction finished, both capture the same read data.

Observable evidence: one initiator receiving a plausible value it never asked for, while its own transaction is silently lost.

Correct model: arbitration is two routing problems in opposite directions. The forward path selects; the return path de-selects, and the completion is what must be gated.

8. Interview Reasoning

Needed because a single-port target has one of everything — one address input, one direction, one data input, one completion output. Two simultaneous accesses are not something the hardware can represent, so access must be serialised before the target sees anything.

Not just a multiplexer because of time. A multiplexer answers which input right now. Arbitration must answer which initiator for the duration of a transaction, and a transaction spans an unbounded number of cycles once targets can wait.

The distinction has teeth. A combinational grant = winner is a correct multiplexer and a broken arbiter: the moment a higher-priority request arrives mid-transaction, the grant switches, and the target — which latched the first initiator's address — completes to the second.

And there is a second routing problem people forget: the response must return to the owner. Read data can be broadcast harmlessly, because data without a qualifying completion is meaningless, but the completion must be gated by grant or both initiators believe the transfer was theirs.

9. Understanding Check

10. What's Next

Contention is now a design rather than a hazard: a single-port target forces serialisation, an arbiter chooses and holds, the forward path multiplexes while the return path gates completion, and fixed priority is a policy with a named failure mode rather than a default.

Six chapters have each built one piece. None has assembled them.

How do address, data, control, transactions, decoding and ownership combine into one working communication fabric — and where, when you look at the whole thing at once, does the case for a published standard become undeniable?

Chapter 2.7 — SoC Communication builds the complete system, traces three real accesses through it end to end, and closes Module 2 by handing the unresolved question to Module 3. The full path is on the Wishbone curriculum index.

Continue learning

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.