Skip to content

RTL Design Patterns · Chapter 13 · Case Studies

Multi-Master Bus Arbiter + Controller

This case study is where arbitration becomes a real bus: several masters, such as a CPU, a DMA engine, and a debug port, all need to reach one shared slave over one set of address and data wires that can carry only one transaction at a time. Nothing here is new. The arbiter core is a round-robin arbiter so no master starves, the ownership lock is a request and grant handshake that holds grant for a whole transaction so the bus is never re-arbitrated mid-transfer, and the bus controller is a safe FSM that runs an address phase, a data phase, and a done strobe. You will wire these into one parameterized bus, drive heavy contention, and prove that every transaction completes correctly with no bus contention. The two signature bugs, a grant that changes mid-transaction and a master driving without grant, are built and verified in SystemVerilog, Verilog, and VHDL.

Advanced20 min readRTL Design PatternsBus ArbiterRound-RobinOwnership LockBus Controller FSMAtomicityFairnessBus Contention

Chapter 13 · Section 13.4 · Case Studies

1. The Engineering Problem

You have three masters — a CPU load/store unit, a DMA engine, and a debug port — and one shared slave, a small memory. Between them runs one bus: one address bus, one write-data bus, one read-data bus back. Only one master can drive those wires at a time; two masters driving the shared address bus in the same cycle is not two transactions, it is a short — contention that resolves to X. So you need something to decide, each transaction, whose address and data ride the bus, to route them to the addressed slave, and to route the slave's read data back to the right master. That decider is an arbiter, and the machine that runs the chosen transfer beat by beat is a bus controller.

The naive picture — an arbiter that picks a winner every cycle and a mux that steers that winner onto the bus — is almost right, and its failure is the whole reason this is a capstone. A real transaction is not one cycle. A master puts an address on the bus in one beat and its data in the next beat (or several beats for a burst); the transfer is atomic — it must run start to finish without another master's wires cutting in. If the arbiter re-arbitrates every cycle, then the instant a higher-priority master raises its request mid-transfer, the grant flips, the bus mux switches to the new master, and the original master's transaction is sliced in half: its address beat went to the slave but its data beat came from someone else. The transfer is corrupted, silently, and only under contention.

So the arbiter must do two jobs that pull in opposite directions. It must be fair — no master waits forever, every master is served within a bounded number of transactions no matter how greedy its neighbours are (Chapter 10.5) — and it must be atomic — once a master is granted, it keeps the bus for its entire transaction, no mid-transfer preemption. Fairness says 'take turns'; atomicity says 'but finish your turn.' The structure that satisfies both is a round-robin arbiter (10.2) wrapped in an ownership lock (10.4): pick fairly, then hold the grant until the transaction signals done, and only then advance the rotation. That is the pattern this page builds.

the-need.v — many masters, one bus, and a transaction that must not be cut in half
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Several masters want one shared slave over ONE set of bus wires:
   wire [N-1:0]      req;        // each master asserts req when it has a transaction
   wire [N-1:0]      grant;      // one-hot: exactly one owner drives the bus
   wire [WIDTH-1:0]  addr;       // the shared address bus (muxed to the OWNER)
   wire [WIDTH-1:0]  wdata;      // the shared write-data bus (muxed to the OWNER)
 
   // WRONG -- re-arbitrate every cycle. A higher-priority master asking mid-transfer
   //   flips grant, the mux switches, and the in-flight transaction is corrupted:
   //   assign grant = req & (~req + 1);   // pure combinational pick, no lock => preemption
 
   // RIGHT -- round-robin pick, then LOCK the grant on the owner until xfer_done, then
   //   advance the pointer past the winner and re-arbitrate. Fair AND atomic.
   //   (the pattern this page builds -- arbiter core + ownership lock + controller FSM)

2. Mental Model

3. Pattern Anatomy

The arbitered bus is three blocks bound by one lock. Take the topology first (who drives whom), then the transaction timeline (what happens beat by beat), then the two pieces of discipline that decide correctness: the ownership lock that freezes arbitration, and the mutual exclusion the bus mux enforces.

The topology — N masters, an arbiter, a shared bus, one slave. Each master has a request line and its own address/write-data it wants to drive. The arbiter takes all N requests and produces a one-hot grant. The grant does two things: it feeds the ownership lock (which holds it steady for the transaction) and it selects the N-to-1 bus mux that steers only the owner's address and write-data onto the shared bus. Address decode on the shared address selects the slave; the slave's read-data returns on the shared read bus and is routed back to the owner. One owner drives the bus; everyone else is masked off.

Multi-master bus topology — N masters through the arbiter and ownership lock, muxed onto one shared bus to the slave

data flow
Multi-master bus topology — N masters through the arbiter and ownership lock, muxed onto one shared bus to the slavereq[N-1:0]one-hot pickgrant (HELD)addr / wdatashared busN masterseach raises REQ and offers its own addr / wdataround-robin arb(10.2)masked/unmasked priority -> one-hot pick, fairownership lock(10.4)latch winner as OWNER; HOLD grant until xfer_doneN-to-1 bus muxgrant steers ONLY the owner's addr / wdata onto the busslave (decoded)addr decode selects it; rdata returns to the owner
N masters share one bus to one slave. The round-robin arbiter (10.2) reduces the N request lines to a one-hot pick; the ownership lock (10.4) latches that pick as the OWNER and HOLDS the grant for the whole transaction, so it does not change mid-transfer. The one-hot grant selects an N-to-1 bus mux that steers ONLY the owner's address and write-data onto the shared bus -- every other master is masked off, which is what makes the bus mutually exclusive (one driver, never two). Address decode on the shared address selects the slave; its read-data returns on the shared read bus and is routed back to the owner. Nothing here knows it is a bus of any particular protocol; it arbitrates, locks, muxes, and decodes. Identical in structure across SystemVerilog, Verilog, and VHDL.

The transaction timeline — request, grant, address, data, release, re-arbitrate. A master with work raises REQUEST and holds it. When the arbiter grants it and the lock latches it as owner, the controller runs the transaction: an ADDRESS beat drives the owner's address (and, for a write, sets the direction) onto the shared bus; then one or more DATA beats move the payload — the slave latches write-data, or returns read-data. When the last beat completes the controller raises xfer_done; the owner drops its request, the lock releases, and the arbiter advances the pointer past this owner and re-arbitrates. The grant is stable from the moment it is latched until xfer_done — that stability is atomicity.

Transaction timeline — one master's bus transaction, request to re-arbitrate

data flow
Transaction timeline — one master's bus transaction, request to re-arbitratewon arbitrationcontroller startsafter 1 beatlast beatdonegrant freedREQUESTmaster asserts req, holds it until grantedGRANTarbiter picks it; lock latches OWNER, grant HELDADDRESSowner's addr on the shared bus; decode selects slaveDATA1+ beats: write-data latched / read-data returnedRELEASExfer_done -> owner drops req -> lock frees the busRE-ARBptr advances PAST this owner; arbiter picks the next
One transaction, left to right in time. The master REQUESTS and holds; the arbiter GRANTS and the lock latches it as OWNER, freezing the grant. The controller then runs the transfer: an ADDRESS beat (owner's address on the shared bus, address-decoded to the slave) then one or more DATA beats (write-data latched by the slave, or read-data returned). The whole ADDRESS-plus-DATA span runs under the SAME held grant -- that stability is atomicity, and it is exactly what a per-cycle re-arbitration would break. When the last beat completes the controller raises xfer_done; the owner drops its request, the lock RELEASES, and only then does the arbiter advance its pointer PAST this owner and RE-ARBITRATE. Because the pointer advances only at release, fairness (round-robin rotation across transactions) and atomicity (each transaction whole) hold at once. Identical across SystemVerilog, Verilog, and VHDL.

The ownership lock — the grant register that freezes arbitration. The arbiter's combinational round-robin pick is a candidate; it is only the winner in cycles where no owner is active. The lock is a tiny FSM (IDLE, BUSY) with an OWNER register: in IDLE, if any request is active, latch the pick into OWNER and go BUSY; in BUSY, hold OWNER unchanged until the controller raises xfer_done, then clear OWNER and return to IDLE. The grant driven to the mux is OWNER, never the raw pick. While BUSY the pointer does not advance and the pick is ignored — the whole point. This is the request/grant discipline of 10.4: grant held through the transaction, request stable until release.

Mutual exclusion — the bus mux is the enforcement. The shared address and write-data buses are an N-to-1 mux (Chapter 1.1) selected by the one-hot grant: addr = master_addr[owner_index], and likewise for write-data. Because the grant is one-hot (at most one bit set), at most one master's wires reach the bus — mutual exclusion is structural, not a rule you hope masters obey. A master that instead drives the shared bus directly whenever it has a request is a second driver on the same net; two drivers is contention, and the value on the bus goes X. Masters must present their address/data to the mux and let the grant gate them — never drive the shared net themselves.

Round-robin fairness across transactions. Because the pointer advances to winner + 1 only at release, the winner of a transaction drops to the bottom of the rotation for the next arbitration. A busy master cannot win two transactions in a row while another master is asking: it wins, runs its (atomic) transaction, releases, and the pointer has moved past it, so the next requester at or after the new pointer wins. Worst-case wait is bounded — every asking master is served within N transactions (10.5). Fairness is a property across transactions; atomicity is a property within one; the lock is what lets both coexist.

4. Real RTL Implementation

This is the core of the page. We build the arbitered bus incrementally — (a) the round-robin arbiter core, (b) the request/grant ownership-lock layer, (c) the bus controller FSM — then the integrated, parameterized arbitered_bus, and show it plus its contention self-checking testbench and the grant-lost-mid-burst bug-vs-lock fix in SystemVerilog, Verilog, and VHDL. The idea is identical across the three; only the spelling differs.

Three conventions run through every block, each an earlier lesson made concrete:

  • The pointer advances only at release. The round-robin core picks combinationally every cycle, but the pointer that rotates priority is updated only when a transaction completes (xfer_done), not per cycle — that is what makes the arbiter fair per transaction while a transaction stays atomic.
  • The grant to the bus is the OWNER register, never the raw pick. The lock latches the winner and holds it; the bus mux and every master read the held grant. This is the one thing that keeps a transaction from being sliced.
  • The shared bus is a one-hot-grant mux, and only the owner drives it. Address and write-data are muxed by grant; the slave is chosen by address decode. Masters offer their wires to the mux and are gated by grant — they never drive the shared net directly.

4a. Building block (i) — the round-robin arbiter core (10.2)

The core is the 10.2 round-robin arbiter almost verbatim: mask requests at or after the pointer, run a lowest-set-bit priority encoder, fall back to the unmasked encoder on wrap, and expose the one-hot pick. The only capstone change is that here the pointer will be advanced by the lock at release (§4b), not on every grant — so this block exposes the pick and lets the caller own the pointer update.

rr_core.sv — round-robin pick: masked + unmasked priority (10.2), pointer advanced by the lock
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rr_core #(
       parameter int N = 4                            // number of masters
   )(
       input  logic [N-1:0]           req,            // N requests, valid in parallel
       input  logic [$clog2(N)-1:0]   ptr,            // rotating priority origin (owned by the lock)
       output logic [N-1:0]           pick            // one-hot fair winner (0 if idle)
   );
       logic [N-1:0] mask, req_masked;
       logic [N-1:0] pick_masked, pick_unmasked;
 
       // MASK: keep only requests at index ptr or higher -> priority resolves FROM the
       // pointer, not from index 0. This is what makes the pick rotate (10.2).
       always_comb begin
           for (int i = 0; i < N; i++) mask[i] = (i >= ptr);
           req_masked    = req & mask;
           // lowest-set-bit priority encoder: v & (~v + 1) is one-hot (or 0).
           pick_masked   = req_masked & (~req_masked + 1'b1);   // first req at-or-after ptr
           pick_unmasked = req        & (~req + 1'b1);          // WRAP: first req from 0
           // fallback: masked winner if any, else wrap to the unmasked winner.
           pick          = (|req_masked) ? pick_masked : pick_unmasked;
       end
   endmodule

The pick is fair if the pointer is advanced past each winner. In 10.2 that happened every grant; here the transaction spans many cycles, so the pointer must advance exactly once per transaction — at release. That deferral is §4b's job.

4b. Building block (ii) — the ownership-lock layer (10.4)

The lock turns the per-cycle pick into a per-transaction owner. It is a two-state FSM with an OWNER register: latch the pick on entry to BUSY, hold it until xfer_done, then clear it and advance the round-robin pointer to winner + 1. The grant the rest of the system sees is OWNER — held for the whole transaction. This is shown as part of the integrated module (§4d); the standalone reasoning — grant driven from a register not the pick, pointer advanced only at release — is exactly the 10.4 handshake and the 10.2 rotation combined.

4c. Building block (iii) — the bus controller FSM (4.1)

The controller is a safe FSM (4.1) that runs one transaction while an owner holds the bus: ADDRESS (drive the owner's address onto the shared bus for one beat; decode selects the slave), then DATA (move BEATS beats of payload — the slave latches write-data or returns read-data), then raise xfer_done for one cycle and go back to IDLE. It starts when the lock reports an owner and signals xfer_done when the last data beat completes; that strobe is what tells the lock to release. The full RTL is in the integrated module below.

4d. The integrated arbitered bus — parameterized, in SystemVerilog

One module: rr_core + the ownership lock + the controller FSM + the N-to-1 bus mux + a simple slave, parameterized on N (masters) and WIDTH. Each master offers m_addr[i] / m_wdata[i] and a req[i]; the bus drives bus_addr / bus_wdata from the owner and returns bus_rdata; the slave here is a small write-then-read memory selected by the top address bit.

arbitered_bus.sv — integrated: rr_core + ownership lock + controller FSM + bus mux + slave
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module arbitered_bus #(
       parameter int N     = 4,                        // number of masters
       parameter int WIDTH = 8,                        // address/data width
       parameter int BEATS = 2,                        // data beats per transaction
       parameter int PW    = (N > 1) ? $clog2(N) : 1
   )(
       input  logic                 clk,
       input  logic                 rst,               // synchronous, active-high
       input  logic [N-1:0]         req,               // req[i] = master i has a transaction
       input  logic [WIDTH-1:0]     m_addr  [N],       // each master's address
       input  logic [WIDTH-1:0]     m_wdata [N],       // each master's write-data
       output logic [N-1:0]         grant,             // one-hot OWNER (held across the transaction)
       output logic                 xfer_done,         // one-cycle: this transaction completed
       output logic [WIDTH-1:0]     bus_addr,          // shared address bus (owner-muxed)
       output logic [WIDTH-1:0]     bus_wdata,         // shared write-data bus (owner-muxed)
       output logic [WIDTH-1:0]     bus_rdata          // shared read-data bus (from slave)
   );
       // ------------------- round-robin arbiter core (10.2) --------------------
       logic [PW-1:0]  ptr;                             // rotation origin, advanced at release
       logic [N-1:0]   pick;
       rr_core #(.N(N)) core (.req(req), .ptr(ptr), .pick(pick));
 
       // encode a one-hot vector to an index (for the pointer update)
       function automatic logic [PW-1:0] idx_of(input logic [N-1:0] oh);
           idx_of = '0;
           for (int i = 0; i < N; i++) if (oh[i]) idx_of = i[PW-1:0];
       endfunction
 
       // ------------------- ownership lock (10.4) + controller FSM (4.1) -------
       typedef enum logic [1:0] { IDLE, ADDRESS, DATA } state_t;
       state_t                    state;
       logic [N-1:0]              owner;                // registered one-hot owner (the LOCK)
       logic [$clog2(BEATS+1)-1:0] beat;                // data beats remaining
 
       always_ff @(posedge clk) begin
           if (rst) begin
               state <= IDLE; owner <= '0; beat <= '0; ptr <= '0; xfer_done <= 1'b0;
           end else begin
               xfer_done <= 1'b0;                       // default: strobe is one cycle
               unique case (state)
                   IDLE: if (|req) begin
                             owner <= pick;             // LATCH the fair winner as OWNER (lock)
                             state <= ADDRESS;          //   ...and hold it; controller starts
                         end
                   ADDRESS: begin
                             beat  <= BEATS[$clog2(BEATS+1)-1:0]; // set up the data beats
                             state <= DATA;             // address beat done -> data phase
                         end
                   DATA: if (beat == 1) begin           // last data beat completing
                             xfer_done <= 1'b1;         //   signal the transaction is done
                             // RELEASE: free the owner and advance the pointer PAST it.
                             ptr   <= (idx_of(owner) == N-1) ? '0 : idx_of(owner) + 1'b1;
                             owner <= '0;
                             state <= IDLE;              //   re-arbitrate next cycle
                         end else begin
                             beat  <= beat - 1'b1;       // ...more beats; grant STAYS locked
                         end
               endcase
           end
       end
 
       assign grant = owner;                            // grant is the HELD one-hot owner
 
       // ------------------- N-to-1 bus mux by grant (mutual exclusion) ---------
       // Only the OWNER's address/data reach the shared bus. Because owner is one-hot,
       // at most one master drives the bus -- mutual exclusion is structural.
       logic [WIDTH-1:0] sel_addr, sel_wdata;
       always_comb begin
           sel_addr  = '0; sel_wdata = '0;
           for (int i = 0; i < N; i++)
               if (owner[i]) begin sel_addr = m_addr[i]; sel_wdata = m_wdata[i]; end
       end
       assign bus_addr  = (state != IDLE) ? sel_addr  : '0;   // drive only while owned
       assign bus_wdata = (state == DATA) ? sel_wdata : '0;   // write-data during DATA
 
       // ------------------- simple slave: address-decoded write-then-read -------
       // Two 1-word "slaves" selected by the top address bit; latch on the ADDRESS beat.
       logic [WIDTH-1:0] mem0, mem1, addr_q;
       always_ff @(posedge clk) begin
           if (rst) begin mem0 <= '0; mem1 <= '0; addr_q <= '0; end
           else begin
               if (state == ADDRESS) addr_q <= sel_addr;      // capture the transaction address
               if (state == DATA) begin                        // write beats land in the decoded slave
                   if (addr_q[WIDTH-1]) mem1 <= sel_wdata;
                   else                 mem0 <= sel_wdata;
               end
           end
       end
       assign bus_rdata = addr_q[WIDTH-1] ? mem1 : mem0;       // read-back from the decoded slave
   endmodule

Two lines carry the whole design. owner <= pick (only in IDLE) and holding owner unchanged through ADDRESS and DATA is the ownership lock — the grant cannot change mid-transaction. ptr <= idx_of(owner) + 1 (only at release, when beat == 1) is the round-robin rotation — the pointer advances past the winner exactly once per transaction, so fairness is preserved without breaking atomicity. The bus mux gated on owner[i] is the mutual exclusion — one driver, structurally. The contention testbench drives every master every cycle and proves all three at once.

arbitered_bus_tb.sv — heavy contention: single owner, atomic transfers, fairness, correct data
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module arbitered_bus_tb;
       localparam int N = 3, WIDTH = 8, BEATS = 2, PW = 2;
       logic               clk = 0, rst;
       logic [N-1:0]       req, grant;
       logic               xfer_done;
       logic [WIDTH-1:0]   m_addr [N], m_wdata [N];
       logic [WIDTH-1:0]   bus_addr, bus_wdata, bus_rdata;
       int                 errors = 0;
       int                 served [N];                  // transactions completed per master
       always #5 clk = ~clk;
 
       arbitered_bus #(.N(N), .WIDTH(WIDTH), .BEATS(BEATS)) dut (
           .clk(clk), .rst(rst), .req(req), .m_addr(m_addr), .m_wdata(m_wdata),
           .grant(grant), .xfer_done(xfer_done),
           .bus_addr(bus_addr), .bus_wdata(bus_wdata), .bus_rdata(bus_rdata));
 
       // MUTUAL EXCLUSION: grant is at most one-hot, and only a requester is granted.
       always @(posedge clk) if (!rst) begin
           assert ($onehot0(grant))
               else begin $error("TWO owners on the bus: grant=%b", grant); errors++; end
           assert ((grant & req) == grant)
               else begin $error("granted a non-requester: req=%b grant=%b", req, grant); errors++; end
       end
 
       // ATOMICITY: once a master owns the bus, its grant must not change until xfer_done.
       logic [N-1:0] grant_q;
       always @(posedge clk) if (!rst) begin
           if (|grant_q && !xfer_done)
               assert (grant == grant_q)
                   else begin $error("grant changed MID-TRANSACTION: %b -> %b", grant_q, grant); errors++; end
           grant_q <= grant;
       end
 
       // FAIRNESS + count: tally which master completed each transaction.
       always @(posedge clk) if (!rst && xfer_done)
           for (int i = 0; i < N; i++) if (grant[i]) served[i]++;
 
       initial begin
           rst = 1; req = '0;
           for (int i = 0; i < N; i++) begin m_addr[i] = i[WIDTH-1:0]; m_wdata[i] = (8'hA0 + i); served[i] = 0; end
           grant_q = '0;
           @(posedge clk); #1; rst = 0;
           req = '1;                                    // HEAVY CONTENTION: everyone asks, always
           repeat (12 * (2 + BEATS)) @(posedge clk);    // run many transactions
           req = '0;
           repeat (4) @(posedge clk);
           // FAIRNESS: with all masters always asking, round-robin serves each within N
           // transactions, so the counts must be within 1 of each other (no starvation).
           for (int i = 0; i < N; i++)
               for (int j = 0; j < N; j++)
                   if (served[i] > served[j] + 1) begin
                       $error("UNFAIR: master %0d served %0d, master %0d served %0d", i, served[i], j, served[j]);
                       errors++;
                   end
           if (errors == 0) $display("PASS: single owner, atomic transfers, fair rotation under contention");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

4e. The signature bug — the grant changes mid-transaction (buggy vs fixed)

Every arbitered-bus failure that survives a single-beat test lives in the ownership lock. The sharpest is having no lock: the grant is the raw combinational round-robin pick, recomputed every cycle. Single-beat transfers pass — they finish in the same cycle the grant is stable. But the instant a transaction spans more than one beat and a higher-priority-in-rotation master raises its request mid-transfer, the pick flips, the bus mux switches, and the in-flight transaction is corrupted: its address beat went to the slave, its data beat came from a different master. It 'works' only for single-beat transfers with no contention.

bus_lock.sv — BUGGY (per-cycle re-arbitration, no lock) vs FIXED (latch owner, hold until done)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: grant is the RAW combinational pick, recomputed every cycle. There is no
   //   OWNER register and no hold. A master that raises req mid-transfer changes the
   //   pick -> grant flips -> the bus mux switches -> the burst is sliced. clk/rst are
   //   unused here: having no state IS the bug.
   module bus_grant_bad #(parameter int N = 4, parameter int PW = 2) (
       input  logic         clk, rst,
       input  logic [N-1:0] req,
       input  logic [PW-1:0] ptr,
       input  logic         xfer_done,               // ignored: nothing to release
       output logic [N-1:0] grant
   );
       logic [N-1:0] mask, req_masked;
       always_comb begin
           for (int i = 0; i < N; i++) mask[i] = (i >= ptr);
           req_masked = req & mask;
           // per-cycle pick with NO lock -> re-arbitrates mid-transaction -> preemption
           grant = (|req_masked) ? (req_masked & (~req_masked + 1'b1))
                                 : (req        & (~req        + 1'b1));
       end
   endmodule
 
   // FIXED: latch the pick into OWNER and HOLD it until xfer_done. The grant to the bus
   //   is OWNER, so it cannot change mid-transaction; the pointer (not shown) advances
   //   only at release, preserving fairness.
   module bus_grant_good #(parameter int N = 4, parameter int PW = 2) (
       input  logic         clk, rst,
       input  logic [N-1:0] req,
       input  logic [PW-1:0] ptr,
       input  logic         xfer_done,
       output logic [N-1:0] grant
   );
       typedef enum logic {IDLE, BUSY} state_t;
       state_t       state;
       logic [N-1:0] owner;
       logic [N-1:0] mask, req_masked, pick;
       always_comb begin
           for (int i = 0; i < N; i++) mask[i] = (i >= ptr);
           req_masked = req & mask;
           pick       = (|req_masked) ? (req_masked & (~req_masked + 1'b1))
                                      : (req        & (~req        + 1'b1));
       end
       always_ff @(posedge clk)
           if (rst) begin state <= IDLE; owner <= '0; end
           else unique case (state)
               IDLE: if (|req)   begin owner <= pick; state <= BUSY; end   // LATCH + LOCK
               BUSY: if (xfer_done) begin owner <= '0; state <= IDLE; end   // RELEASE
           endcase                                                          //   else HOLD
       assign grant = owner;                            // HELD -> no mid-transaction change
   endmodule
bus_lock_tb.sv — a mid-transfer latecomer must NOT steal a live multi-beat transaction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module bus_lock_tb;
       localparam int N = 2, PW = 1;
       logic          clk = 0, rst, xfer_done;
       logic [N-1:0]  req, grant_bad, grant_good;
       logic [PW-1:0] ptr = '0;
       int            errors = 0;
       always #5 clk = ~clk;
 
       bus_grant_bad  #(.N(N), .PW(PW)) bad  (.clk(clk), .rst(rst), .req(req), .ptr(ptr), .xfer_done(xfer_done), .grant(grant_bad));
       bus_grant_good #(.N(N), .PW(PW)) good (.clk(clk), .rst(rst), .req(req), .ptr(ptr), .xfer_done(xfer_done), .grant(grant_good));
 
       initial begin
           rst = 1; req = '0; xfer_done = 0; @(posedge clk); #1; rst = 0;
           // Master 1 (lower priority) starts a multi-beat transaction, alone.
           req = 2'b10; @(posedge clk); #1;
           // Both grants land on master 1 to begin its burst.
           assert (grant_good == 2'b10) else begin $error("good: master 1 not owner"); errors++; end
           // Mid-burst, higher-priority master 0 raises its request (no xfer_done yet).
           req = 2'b11; @(posedge clk); #1;
           // FIXED: grant stays LOCKED on master 1 -> atomic.
           assert (grant_good == 2'b10)
               else begin $error("FIXED preempted: grant=%b", grant_good); errors++; end
           // BUGGY: grant JUMPS to master 0 mid-transaction -> the burst is sliced.
           if (grant_bad == 2'b10) begin $error("bug not shown: buggy arbiter held"); errors++; end
           else $display("observed: buggy grant flipped to %b mid-transaction (burst corrupted)", grant_bad);
           // Complete master 1's transaction; the FIXED arbiter releases and hands off.
           xfer_done = 1; @(posedge clk); #1; xfer_done = 0;
           assert (grant_good == 2'b01 || grant_good == 2'b00)
               else begin $error("FIXED no clean release: grant=%b", grant_good); errors++; end
           if (errors == 0) $display("PASS: the locked arbiter keeps the transaction whole; the pulse arbiter preempts");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

4f. The integrated arbitered bus in Verilog

The same three blocks with reg/wire typing and localparam states. The lock (latch owner, hold to xfer_done), the pointer update at release, and the grant-gated bus mux are unchanged; only the spelling differs. Verilog cannot pass an unpacked port array in the classic style, so the masters' addresses and data are flattened into N*WIDTH buses and sliced with an indexed part-select.

arbitered_bus.v — integrated arbitered bus in Verilog (flattened master buses, locked owner)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module arbitered_bus #(
       parameter N = 4, WIDTH = 8, BEATS = 2, PW = 2
   )(
       input  wire                 clk, rst,
       input  wire [N-1:0]         req,
       input  wire [N*WIDTH-1:0]   m_addr_flat,       // N master addresses packed
       input  wire [N*WIDTH-1:0]   m_wdata_flat,      // N master write-data packed
       output wire [N-1:0]         grant,
       output reg                  xfer_done,
       output wire [WIDTH-1:0]     bus_addr, bus_wdata,
       output wire [WIDTH-1:0]     bus_rdata
   );
       // ---- round-robin core (10.2): masked + unmasked priority ----
       reg  [PW-1:0] ptr;
       wire [N-1:0]  mask_v, req_masked, pick_m, pick_u, pick;
       genvar g;
       generate for (g = 0; g < N; g = g + 1) begin : mk
           assign mask_v[g] = (g >= ptr);
       end endgenerate
       assign req_masked = req & mask_v;
       assign pick_m     = req_masked & (~req_masked + 1'b1);   // first req at-or-after ptr
       assign pick_u     = req        & (~req        + 1'b1);   // wrap: first req from 0
       assign pick       = (|req_masked) ? pick_m : pick_u;
 
       // ---- ownership lock (10.4) + controller FSM (4.1) ----
       localparam [1:0] IDLE = 2'd0, ADDRESS = 2'd1, DATA = 2'd2;
       reg [1:0]     state;
       reg [N-1:0]   owner;
       reg [15:0]    beat;
       integer       i;
       reg [PW-1:0]  widx;
 
       always @(*) begin                                // one-hot owner -> index (for ptr update)
           widx = 0;
           for (i = 0; i < N; i = i + 1) if (owner[i]) widx = i[PW-1:0];
       end
 
       always @(posedge clk) begin
           if (rst) begin state<=IDLE; owner<=0; beat<=0; ptr<=0; xfer_done<=1'b0; end
           else begin
               xfer_done <= 1'b0;
               case (state)
                   IDLE: if (|req) begin owner <= pick; state <= ADDRESS; end   // LATCH + LOCK
                   ADDRESS: begin beat <= BEATS; state <= DATA; end
                   DATA: if (beat == 1) begin
                             xfer_done <= 1'b1;                                   // last beat -> done
                             ptr   <= (widx == N-1) ? 0 : widx + 1'b1;            // advance PAST winner (release)
                             owner <= 0; state <= IDLE;
                         end else beat <= beat - 1'b1;                            // hold grant, more beats
               endcase
           end
       end
       assign grant = owner;
 
       // ---- N-to-1 bus mux by grant (mutual exclusion) ----
       reg [WIDTH-1:0] sel_addr, sel_wdata;
       always @(*) begin
           sel_addr = 0; sel_wdata = 0;
           for (i = 0; i < N; i = i + 1) if (owner[i]) begin
               sel_addr  = m_addr_flat [i*WIDTH +: WIDTH];
               sel_wdata = m_wdata_flat[i*WIDTH +: WIDTH];
           end
       end
       assign bus_addr  = (state != IDLE) ? sel_addr  : {WIDTH{1'b0}};
       assign bus_wdata = (state == DATA) ? sel_wdata : {WIDTH{1'b0}};
 
       // ---- address-decoded slave: write-then-read, selected by the top addr bit ----
       reg [WIDTH-1:0] mem0, mem1, addr_q;
       always @(posedge clk) begin
           if (rst) begin mem0<=0; mem1<=0; addr_q<=0; end
           else begin
               if (state == ADDRESS) addr_q <= sel_addr;
               if (state == DATA) begin
                   if (addr_q[WIDTH-1]) mem1 <= sel_wdata; else mem0 <= sel_wdata;
               end
           end
       end
       assign bus_rdata = addr_q[WIDTH-1] ? mem1 : mem0;
   endmodule
arbitered_bus_tb.v — self-checking Verilog: single owner, atomic, fair under contention (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module arbitered_bus_tb;
       parameter N = 3, WIDTH = 8, BEATS = 2;
       reg               clk, rst;
       reg  [N-1:0]      req;
       reg  [N*WIDTH-1:0] m_addr_flat, m_wdata_flat;
       wire [N-1:0]      grant;
       wire              xfer_done;
       wire [WIDTH-1:0]  bus_addr, bus_wdata, bus_rdata;
       reg  [N-1:0]      grant_q;
       integer           errors, i, s0, s1, s2, k;
 
       arbitered_bus #(.N(N), .WIDTH(WIDTH), .BEATS(BEATS), .PW(2)) dut (
           .clk(clk), .rst(rst), .req(req),
           .m_addr_flat(m_addr_flat), .m_wdata_flat(m_wdata_flat),
           .grant(grant), .xfer_done(xfer_done),
           .bus_addr(bus_addr), .bus_wdata(bus_wdata), .bus_rdata(bus_rdata));
 
       initial clk = 0;  always #5 clk = ~clk;
 
       // MUTUAL EXCLUSION + ATOMICITY, checked every cycle.
       always @(posedge clk) if (!rst) begin
           if (!((grant & (grant - 1)) == 0))                              // one-hot-or-zero
               begin $display("FAIL: TWO owners grant=%b", grant); errors = errors + 1; end
           if ((grant & req) !== grant)                                    // granted a requester
               begin $display("FAIL: granted non-requester req=%b grant=%b", req, grant); errors = errors + 1; end
           if (|grant_q && !xfer_done && grant !== grant_q)                // atomic: no mid-transfer change
               begin $display("FAIL: grant changed mid-transaction %b -> %b", grant_q, grant); errors = errors + 1; end
           grant_q <= grant;
           if (xfer_done) begin
               if (grant[0]) s0 = s0 + 1;
               if (grant[1]) s1 = s1 + 1;
               if (grant[2]) s2 = s2 + 1;
           end
       end
 
       initial begin
           errors = 0; s0 = 0; s1 = 0; s2 = 0; grant_q = 0;
           m_addr_flat  = {8'd2, 8'd1, 8'd0};       // master 2,1,0 addresses
           m_wdata_flat = {8'hA2, 8'hA1, 8'hA0};    // master 2,1,0 write-data
           rst = 1; req = 0; @(posedge clk); #1; rst = 0;
           req = {N{1'b1}};                          // HEAVY CONTENTION: everyone asks always
           for (k = 0; k < 12*(2+BEATS); k = k + 1) @(posedge clk);
           req = 0; repeat (4) @(posedge clk);
           // FAIRNESS: counts within 1 of each other (round-robin, no starvation).
           if (s0 > s1 + 1 || s1 > s0 + 1 || s0 > s2 + 1 || s2 > s0 + 1 || s1 > s2 + 1 || s2 > s1 + 1)
               begin $display("FAIL: unfair served = %0d %0d %0d", s0, s1, s2); errors = errors + 1; end
           if (errors == 0) $display("PASS: single owner, atomic, fair under contention (%0d %0d %0d)", s0, s1, s2);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog lock bug-vs-fix pair is the same story: the buggy arbiter is the raw combinational pick (no owner register), the fixed one latches owner and holds it until xfer_done.

bus_lock.v — BUGGY (pulse pick, re-arbitrates) vs FIXED (locked owner) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: raw combinational pick, no owner register -> re-arbitrates every cycle.
   module bus_grant_bad #(parameter N = 4, PW = 2) (
       input  wire         clk, rst, xfer_done,
       input  wire [N-1:0] req, input wire [PW-1:0] ptr,
       output wire [N-1:0] grant
   );
       wire [N-1:0] mask_v, req_masked;
       genvar g;
       generate for (g = 0; g < N; g = g + 1) begin : mk assign mask_v[g] = (g >= ptr); end endgenerate
       assign req_masked = req & mask_v;
       assign grant = (|req_masked) ? (req_masked & (~req_masked + 1'b1))
                                    : (req        & (~req        + 1'b1));   // BUG: no lock
   endmodule
 
   // FIXED: latch owner, hold until xfer_done -> grant cannot change mid-transaction.
   module bus_grant_good #(parameter N = 4, PW = 2) (
       input  wire         clk, rst, xfer_done,
       input  wire [N-1:0] req, input wire [PW-1:0] ptr,
       output wire [N-1:0] grant
   );
       localparam IDLE = 1'b0, BUSY = 1'b1;
       reg          state;
       reg  [N-1:0] owner;
       wire [N-1:0] mask_v, req_masked, pick;
       genvar g;
       generate for (g = 0; g < N; g = g + 1) begin : mk assign mask_v[g] = (g >= ptr); end endgenerate
       assign req_masked = req & mask_v;
       assign pick = (|req_masked) ? (req_masked & (~req_masked + 1'b1))
                                   : (req        & (~req        + 1'b1));
       always @(posedge clk)
           if (rst) begin state <= IDLE; owner <= 0; end
           else case (state)
               IDLE: if (|req)     begin owner <= pick; state <= BUSY; end   // LATCH + LOCK
               BUSY: if (xfer_done) begin owner <= 0;   state <= IDLE; end   // RELEASE
           endcase
       assign grant = owner;                                                 // HELD
   endmodule

4g. The integrated arbitered bus in VHDL

VHDL states the same machines with an enumerated state type, numeric_std counters, and rising_edge(clk). The lock latches the owner and holds it to xfer_done, the pointer advances only at release, and the bus mux is gated on the one-hot owner — the identical structure.

arbitered_bus.vhd — integrated arbitered bus in VHDL (numeric_std, locked owner, grant-gated mux)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity arbitered_bus is
       generic ( N : integer := 4; WIDTH : integer := 8; BEATS : integer := 2 );
       port (
           clk, rst  : in  std_logic;
           req       : in  std_logic_vector(N-1 downto 0);
           m_addr    : in  std_logic_vector(N*WIDTH-1 downto 0);   -- N master addresses packed
           m_wdata   : in  std_logic_vector(N*WIDTH-1 downto 0);   -- N master write-data packed
           grant     : out std_logic_vector(N-1 downto 0);
           xfer_done : out std_logic;
           bus_addr  : out std_logic_vector(WIDTH-1 downto 0);
           bus_wdata : out std_logic_vector(WIDTH-1 downto 0);
           bus_rdata : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
 
   architecture rtl of arbitered_bus is
       type state_t is (S_IDLE, S_ADDRESS, S_DATA);
       signal state  : state_t := S_IDLE;
       signal owner  : std_logic_vector(N-1 downto 0) := (others=>'0');
       signal ptr    : integer range 0 to N-1 := 0;
       signal beat   : integer range 0 to BEATS := 0;
       signal mem0, mem1, addr_q : std_logic_vector(WIDTH-1 downto 0) := (others=>'0');
 
       -- round-robin pick: masked (at-or-after ptr) with unmasked wrap fallback
       function pick_rr (req : std_logic_vector; ptr : integer) return std_logic_vector is
           variable n    : integer := req'length;
           variable rm   : std_logic_vector(req'range) := (others=>'0');
           variable g    : std_logic_vector(req'range) := (others=>'0');
           variable done : boolean := false;
       begin
           for i in 0 to n-1 loop if i >= ptr then rm(i) := req(i); end if; end loop;
           -- lowest set bit of the masked set (first req at-or-after ptr)
           for i in 0 to n-1 loop if rm(i)='1' and not done then g(i):='1'; done:=true; end if; end loop;
           if not done then                       -- WRAP: lowest set bit of the unmasked set
               for i in 0 to n-1 loop if req(i)='1' and not done then g(i):='1'; done:=true; end if; end loop;
           end if;
           return g;
       end function;
 
       function idx_of (oh : std_logic_vector) return integer is
           variable r : integer := 0;
       begin
           for i in 0 to oh'length-1 loop if oh(i)='1' then r := i; end if; end loop;
           return r;
       end function;
   begin
       process (clk)
           variable pick     : std_logic_vector(N-1 downto 0);
           variable sel_addr : std_logic_vector(WIDTH-1 downto 0);
           variable sel_wdata: std_logic_vector(WIDTH-1 downto 0);
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   state <= S_IDLE; owner <= (others=>'0'); ptr <= 0; beat <= 0;
                   xfer_done <= '0'; mem0 <= (others=>'0'); mem1 <= (others=>'0'); addr_q <= (others=>'0');
               else
                   xfer_done <= '0';
                   pick := pick_rr(req, ptr);
                   -- select the OWNER's address/data (all-zero when no owner)
                   sel_addr := (others=>'0'); sel_wdata := (others=>'0');
                   for i in 0 to N-1 loop
                       if owner(i)='1' then
                           sel_addr  := m_addr (i*WIDTH+WIDTH-1 downto i*WIDTH);
                           sel_wdata := m_wdata(i*WIDTH+WIDTH-1 downto i*WIDTH);
                       end if;
                   end loop;
 
                   case state is
                       when S_IDLE =>
                           if req /= (req'range => '0') then
                               owner <= pick; state <= S_ADDRESS;         -- LATCH + LOCK
                           end if;
                       when S_ADDRESS =>
                           addr_q <= sel_addr;                            -- capture transaction address
                           beat   <= BEATS; state <= S_DATA;
                       when S_DATA =>
                           if addr_q(WIDTH-1)='1' then mem1 <= sel_wdata; else mem0 <= sel_wdata; end if;
                           if beat = 1 then
                               xfer_done <= '1';                          -- last beat -> done
                               if idx_of(owner) = N-1 then ptr <= 0; else ptr <= idx_of(owner)+1; end if;  -- release
                               owner <= (others=>'0'); state <= S_IDLE;
                           else
                               beat <= beat - 1;                          -- hold grant, more beats
                           end if;
                   end case;
               end if;
           end if;
       end process;
 
       grant <= owner;
       -- combinational bus mux by the one-hot owner (mutual exclusion is structural)
       process (owner, state, m_addr, m_wdata, addr_q, mem0, mem1)
           variable a, d : std_logic_vector(WIDTH-1 downto 0);
       begin
           a := (others=>'0'); d := (others=>'0');
           for i in 0 to N-1 loop
               if owner(i)='1' then
                   a := m_addr (i*WIDTH+WIDTH-1 downto i*WIDTH);
                   d := m_wdata(i*WIDTH+WIDTH-1 downto i*WIDTH);
               end if;
           end loop;
           if state /= S_IDLE then bus_addr <= a; else bus_addr <= (others=>'0'); end if;
           if state  = S_DATA  then bus_wdata <= d; else bus_wdata <= (others=>'0'); end if;
       end process;
       bus_rdata <= mem1 when addr_q(WIDTH-1)='1' else mem0;
   end architecture;
arbitered_bus_tb.vhd — self-checking VHDL: single owner, atomic, fair under contention (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity arbitered_bus_tb is end entity;
 
   architecture sim of arbitered_bus_tb is
       constant N : integer := 3; constant WIDTH : integer := 8; constant BEATS : integer := 2;
       signal clk : std_logic := '0';
       signal rst, xfer_done : std_logic;
       signal req, grant, grant_q : std_logic_vector(N-1 downto 0) := (others=>'0');
       signal m_addr, m_wdata : std_logic_vector(N*WIDTH-1 downto 0);
       signal bus_addr, bus_wdata, bus_rdata : std_logic_vector(WIDTH-1 downto 0);
 
       function onehot0 (v : std_logic_vector) return boolean is
           variable c : integer := 0;
       begin
           for i in v'range loop if v(i)='1' then c := c + 1; end if; end loop;
           return c <= 1;
       end function;
   begin
       dut : entity work.arbitered_bus
           generic map (N=>N, WIDTH=>WIDTH, BEATS=>BEATS)
           port map (clk=>clk, rst=>rst, req=>req, m_addr=>m_addr, m_wdata=>m_wdata,
                     grant=>grant, xfer_done=>xfer_done,
                     bus_addr=>bus_addr, bus_wdata=>bus_wdata, bus_rdata=>bus_rdata);
 
       clk <= not clk after 5 ns;
 
       -- MUTUAL EXCLUSION + ATOMICITY every cycle
       check : process (clk) begin
           if rising_edge(clk) and rst = '0' then
               assert onehot0(grant) report "TWO owners on the bus" severity error;
               assert (grant and req) = grant report "granted a non-requester" severity error;
               if grant_q /= (grant_q'range => '0') and xfer_done = '0' then
                   assert grant = grant_q report "grant changed MID-TRANSACTION" severity error;
               end if;
               grant_q <= grant;
           end if;
       end process;
 
       stim : process begin
           -- master 0,1,2 addresses and data packed low-index first
           m_addr  <= std_logic_vector(to_unsigned(2,WIDTH)) & std_logic_vector(to_unsigned(1,WIDTH)) & std_logic_vector(to_unsigned(0,WIDTH));
           m_wdata <= x"A2" & x"A1" & x"A0";
           rst <= '1'; req <= (others=>'0');
           wait until rising_edge(clk); rst <= '0';
           req <= (others=>'1');                         -- HEAVY CONTENTION
           for i in 0 to 12*(2+BEATS) loop wait until rising_edge(clk); end loop;
           req <= (others=>'0');
           for i in 0 to 3 loop wait until rising_edge(clk); end loop;
           report "arbitered_bus self-check complete (single owner, atomic, fair under contention)" severity note;
           wait;
       end process;
   end architecture;

The VHDL lock bug-vs-fix pair is the same one difference: the buggy arbiter returns the combinational pick with no owner register; the fixed one latches the owner and holds it until xfer_done.

bus_lock.vhd — BUGGY (pulse pick, no lock) vs FIXED (locked owner) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   -- shared round-robin pick used by both (masked + unmasked wrap)
   package bus_pick_pkg is
       function pick_rr (req : std_logic_vector; ptr : integer) return std_logic_vector;
   end package;
   package body bus_pick_pkg is
       function pick_rr (req : std_logic_vector; ptr : integer) return std_logic_vector is
           variable n    : integer := req'length;
           variable rm   : std_logic_vector(req'range) := (others=>'0');
           variable g    : std_logic_vector(req'range) := (others=>'0');
           variable done : boolean := false;
       begin
           for i in 0 to n-1 loop if i >= ptr then rm(i) := req(i); end if; end loop;
           for i in 0 to n-1 loop if rm(i)='1' and not done then g(i):='1'; done:=true; end if; end loop;
           if not done then
               for i in 0 to n-1 loop if req(i)='1' and not done then g(i):='1'; done:=true; end if; end loop;
           end if;
           return g;
       end function;
   end package body;
 
   library ieee;
   use ieee.std_logic_1164.all;
   use work.bus_pick_pkg.all;
 
   -- BUGGY: raw combinational pick, no owner register -> re-arbitrates every cycle.
   entity bus_grant_bad is
       generic ( N : integer := 4 );
       port ( clk, rst, xfer_done : in std_logic;
              req : in std_logic_vector(N-1 downto 0);
              ptr : in integer range 0 to N-1;
              grant : out std_logic_vector(N-1 downto 0) );
   end entity;
   architecture rtl of bus_grant_bad is
   begin
       grant <= pick_rr(req, ptr);        -- BUG: no lock, no hold -> preemption mid-transaction
   end architecture;
 
   library ieee;
   use ieee.std_logic_1164.all;
   use work.bus_pick_pkg.all;
 
   -- FIXED: latch owner, hold until xfer_done -> grant cannot change mid-transaction.
   entity bus_grant_good is
       generic ( N : integer := 4 );
       port ( clk, rst, xfer_done : in std_logic;
              req : in std_logic_vector(N-1 downto 0);
              ptr : in integer range 0 to N-1;
              grant : out std_logic_vector(N-1 downto 0) );
   end entity;
   architecture rtl of bus_grant_good is
       type state_t is (S_IDLE, S_BUSY);
       signal state : state_t := S_IDLE;
       signal owner : std_logic_vector(N-1 downto 0) := (others=>'0');
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then state <= S_IDLE; owner <= (others=>'0');
               else
                   case state is
                       when S_IDLE => if req /= (req'range => '0') then
                                          owner <= pick_rr(req, ptr); state <= S_BUSY;  -- LATCH + LOCK
                                      end if;
                       when S_BUSY => if xfer_done = '1' then
                                          owner <= (others=>'0'); state <= S_IDLE;      -- RELEASE
                                      end if;                                           -- else HOLD
                   end case;
               end if;
           end if;
       end process;
       grant <= owner;                    -- HELD -> no mid-transaction change
   end architecture;

Across all three languages the arbitered bus's whole correctness is one sentence: pick a fair winner with the round-robin core, LATCH it as the owner and HOLD the grant until the controller signals xfer_done, drive the shared bus through a mux gated by that one-hot grant, and advance the rotation pointer PAST the owner only at release — so the transaction is atomic and the arbiter is fair at once.

5. Verification Strategy

An arbitered bus's correctness is not 'did the first transaction look right' — it is 'under heavy contention, does every master's transaction complete to the right slave with the right data and in the master's order, is the bus ever driven by two masters at once, is any transaction corrupted by a mid-transfer re-arbitration, and is the arbiter fair.' The invariants, and the whole §4 contention testbench, are:

Under all masters contending, (1) the grant is one-hot-or-zero every cycle and only ever on a requester (mutual exclusion), (2) once a master owns the bus its grant does not change until xfer_done (atomicity — no mid-transfer preemption), (3) each transaction moves the owner's address and data to the addressed slave and reads back correctly (functional correctness), and (4) every asking master is served within N transactions (fairness — no starvation).

Everything below is those invariants made checkable, and they map onto the contention testbenches in §4.

  • Mutual exclusion (single driver). Every cycle, assert the grant is one-hot-or-zero ($onehot0(grant) / (grant & (grant-1)) == 0 / a bit-count <= 1) and that (grant & req) == grant (only a requester is granted). Two owners means two drivers on the shared bus — the second §7 bug — and this assertion catches it every cycle, not just when the data happens to differ.
  • Atomicity (no mid-transfer preemption — the signature check). Register the previous grant; on any cycle where an owner was active last cycle and xfer_done did not fire, assert grant == grant_q. A grant that changes while a transaction is in flight is the signature bug (§7 row 1): the lock is missing or the pointer advanced early. This is the assertion the whole capstone hangs on, and it only fires under contention — a lone master never exercises it.
  • Functional correctness (right slave, right data, in order). For each master, drive a distinct address (that decodes to a known slave) and distinct write-data; after its transaction completes, read the slave back and assert it holds that master's data. Because transactions are serialized by the lock, the slave's final state is a well-defined sequence — check that a master's write is not clobbered by another master mid-transfer (which is exactly what preemption would do).
  • Fairness (bounded wait, no starvation — 10.5). With every master asking every cycle, tally which master completes each transaction and assert the counts stay within one of each other over a long run. A fixed-priority collapse (pointer never advancing) shows here as one master's count running away while others stall — the classic starvation signature. Re-run with a lone persistent requester to confirm it is still served every turn.
  • Drive heavy contention, not a polite one-at-a-time schedule. The only stimulus that exercises all four invariants is everyone asking at once. A testbench that starts one master, lets it finish, then starts the next will pass even a broken (unlocked) arbiter, because it never creates a mid-transfer latecomer. Assert req = all-ones and let it run for many transactions — that is the test that separates a bus that works in a demo from one that works in a chip.
  • Expected waveform. grant is one-hot and stable for the whole ADDRESS-plus-DATA span of each transaction, then moves to the next master in rotation the cycle after xfer_done. bus_addr holds the owner's address during the transaction; bus_wdata carries the owner's data during DATA beats; bus_addr/bus_wdata are zero (or idle) between transactions. A grant that flickers within a transaction is the visual signature of the missing-lock bug; a bus value that goes X under contention is the visual signature of a master driving the shared net without holding grant.

6. Common Mistakes

Grant changes mid-transaction — the missing ownership lock. The signature failure and the reason this is a capstone. If the grant is the raw combinational round-robin pick, recomputed every cycle, then a higher-priority-in-rotation master raising its request mid-transfer flips the grant, switches the bus mux, and slices the in-flight transaction — its address beat went to the slave, its data beat came from someone else. It passes single-beat transfers and any one-at-a-time schedule, and corrupts multi-beat transfers the instant there is contention. The fix is the lock: latch the winner into an OWNER register, drive the grant from that register, and hold it until xfer_done. Full post-mortem in §7 row 1; buggy-vs-fixed RTL in §4e.

A master driving the shared bus without holding grant — bus contention. A master that puts its address or data on the shared net whenever it has a request (rather than only through the grant-gated mux) is a second driver. Two drivers on one net is contention: the value resolves to X in simulation and is a short in silicon. The discipline is structural — masters present their wires to the N-to-1 bus mux and are gated by the one-hot grant; they never drive the shared net directly. Post-mortem in §7 row 2.

Arbiter unfairness — a master starves under load. If the round-robin pointer is not advanced past the winner (it advances to the winner instead of winner+1, or does not advance at all), the rotation collapses back into fixed priority: the same master wins every arbitration and lower-priority masters starve (10.5). Under the contention test this shows as one master's completion count running away. Advance the pointer to winner + 1 (mod N) exactly once per transaction, at release — the deferral is what keeps fairness and atomicity.

Address decode selecting the wrong slave. The shared address must be decoded to select exactly one slave, and the decode must be stable for the whole transaction. Decoding a changing address (e.g. re-reading the master's address after the mux has moved on) or an off-by-one decode routes the transfer to the wrong slave — the write lands in the wrong memory, or the read returns another slave's data. Capture the transaction address on the ADDRESS beat (into an addr_q register) and decode that, so the slave selection is fixed for the transaction even as beats advance.

Advancing the pointer per cycle instead of per transaction. A tempting shortcut is to keep the 10.2 arbiter verbatim and advance the pointer every cycle it produces a grant. But a transaction spans many cycles with the grant held, so a per-cycle advance moves the rotation during a transaction — and if the lock is also missing, re-arbitrates mid-transfer. The pointer must advance exactly once, at xfer_done. Tie the pointer update to release, not to grant-asserted.

Not holding request stable until granted (and through the transaction). A master that pulses its request for one cycle and drops it can be dropped by the arbiter before the lock latches it, or can look like a release mid-transaction. The request must be held from the moment work arrives until the transaction completes — stable until granted, stable through the transaction — which is exactly the request/grant handshake discipline (10.4). A glitchy request is indistinguishable from a release and desynchronizes the lock.

7. DebugLab

The bus that works in a demo and corrupts transfers under load — a grant that moves mid-transaction and a master that drives without grant

The engineering lesson: a shared bus is correct only if exactly one master owns it for the whole of each transaction — and both signature bugs are the same crime, letting more than one master reach the bus, committed at two different stations. The first lets a second master reach the bus in time (the grant moves mid-transaction because there is no lock, so a latecomer slices a live transfer); the second lets a second master reach the bus in space (a master drives the shared net on its request instead of its grant, so two drivers fight in the same cycle). Both pass a one-at-a-time demo and both surface the instant the bus is loaded. Three habits make the bus trustworthy, identical across SystemVerilog, Verilog, and VHDL: lock the grant on the owner until the transaction is done (latch the pick, hold it to xfer_done), drive the shared bus only through a grant-gated mux (never on request), and advance the round-robin pointer past the winner only at release (fair per transaction, atomic within one) — and verify under heavy contention, not a polite schedule, because everyone-asking-at-once is the only test that exercises the lock, the mux, and the rotation at all.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses composing the pieces and getting the atomicity-versus-fairness accounting right.

Exercise 1 — Bound the worst-case wait

For N masters all asking every cycle, with each transaction taking 1 + BEATS cycles (one address beat plus BEATS data beats), derive the worst-case number of cycles a master waits from raising its request to being granted. State (i) the worst-case wait in transactions under round-robin, (ii) the cycles per transaction, and (iii) the resulting cycle bound, and say how it changes if bursts of up to K beats are allowed.

Exercise 2 — Predict the missing-lock failure

An engineer wires the combinational round-robin pick straight to the bus mux (no owner register) and runs 2-beat transactions. Describe precisely (i) what a one-at-a-time testbench reports, (ii) what happens when master 1 is mid-transaction and master 0 raises its request, and (iii) which byte the addressed slave ends up holding. Then give the one-block fix and the assertion that would have exposed it.

Exercise 3 — Add a second slave and an error response

The bus decodes the top address bit to one of two slaves. Add a third address region that maps to no slave, and an error response for it. State (i) where the decode changes, (ii) what the controller drives back to the master for an unmapped address (an error flag on the read-data path), and (iii) how the master's controller should react. Name which earlier pattern the decode and the error flag draw on.

Exercise 4 — Add a read/write direction and a ready handshake on the slave

The current slave latches or returns data in a fixed number of beats. Give the slave a ready line so it can stall a beat (a slow memory). State (i) where the controller must wait on ready and how that changes the beat counter, (ii) why the grant must stay locked across the stall (and what would break if it did not), and (iii) what the fairness bound becomes when a slave can insert wait states. Name which earlier pattern the ready stall draws on.

The patterns this bus composes (the lessons you are assembling here):

  • Round-Robin Arbiters — Chapter 10.2; the arbiter core — masked + unmasked priority, pointer to winner+1, one-hot grant, bounded wait — reused here as the fair picker.
  • Request / Grant Handshake — Chapter 10.4; the ownership lock — latch the winner as owner, hold the grant through the transaction, release on done — the atomicity layer of this bus.
  • Arbiter Fairness — Chapter 10.5; the no-starvation guarantee the contention testbench asserts, and why the pointer must advance past the winner.
  • Fixed-Priority Arbiters — Chapter 10.1; the priority encoder inside the round-robin core, and the starvation it causes if the pointer never rotates.
  • FSM Fundamentals — Chapter 4.1; the bus controller — IDLE, ADDRESS, DATA — is this safe FSM sequencing one transaction.
  • FSM + Datapath — Chapter 4.6; the control-down / status-up split between the controller FSM and its beat counter, address register, and bus mux.
  • Valid / Ready Handshake — Chapter 9.1; the master-side and slave-side handshake discipline that paces each beat and lets a slow slave stall.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this case study walks end to end.
  • What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns into a real bus fabric — not inventing a new one — is the design skill this capstone rehearses.
  • UART Case Study — Chapter 13.1; the sibling capstone — three patterns composed into a real peripheral, verified end to end.

Forward — the other case studies (unlock as they ship):

  • SPI Master Case Study (/rtl-design-patterns/case-study-spi-master) — Chapter 13.2; an FSM + shift register master that drives a source-synchronous serial bus — a single-master transaction sequencer.
  • Streaming FIFO Case Study (/rtl-design-patterns/case-study-streaming-fifo) — Chapter 13.3; buffering the beat stream a bus master produces before it hits a slow slave.
  • CDC Stream Case Study (/rtl-design-patterns/case-study-cdc-stream) — Chapter 13.5; moving a bus stream safely across two unrelated clocks.

Verilog v1 prerequisites (the grammar these controllers transcribe into):

11. Summary

  • An arbitered bus is patterns composed into a real fabric, not a new pattern. It is a round-robin arbiter (10.2) feeding an ownership lock (10.4) driving a bus controller FSM (4.1), with an N-to-1 bus mux (1.1) gated by the one-hot grant and an address decode selecting the slave. Each block is a lesson you already had; the capstone is wiring them so a transaction is atomic and the arbiter is fair at once.
  • The grant must be locked for the whole transaction. Latch the winning pick into an OWNER register, drive the grant from that register, and hold it until the controller raises xfer_done. A grant driven from the raw combinational pick re-arbitrates every cycle, so a mid-transfer latecomer slices the in-flight transaction — the signature bug (§7 row 1). The held grant is atomicity.
  • The bus is a grant-gated mux — mutual exclusion is structural. The shared address/write-data buses are an N-to-1 mux selected by the one-hot grant, so at most one master drives the bus by construction. A master that drives the shared net on its request instead of its grant is a second driver — contention, X (§7 row 2). Drive only through the mux, gated by grant.
  • Fairness and atomicity coexist because the pointer advances at release. Advance the round-robin pointer to winner + 1 (mod N) exactly once per transaction, at xfer_done — so the winner drops to the bottom of the rotation after its turn (fair, bounded wait, no starvation, 10.5) while the grant stayed locked for the whole turn (atomic). Advancing per cycle, or to the winner instead of winner+1, collapses the rotation back into starvation.
  • Prove it with heavy contention, not a polite schedule. Drive every master every cycle and assert one-hot grant (mutual exclusion), grant-unchanged-until-done (atomicity), right-slave-right-data (correctness), and served-counts-within-one (fairness). A one-at-a-time demo passes even a broken, unlocked arbiter; everyone-asking-at-once is the only test that exercises the lock, the mux, and the rotation — self-checking contention testbenches shown in SystemVerilog, Verilog, and VHDL.