Wire the arbiter's accept_i to the request handshake instead of the response, and the first slow read goes to the wrong master. The bench below prints FAIL @75 ns: response to master 1, which has nothing outstanding. Each piece of this topic can pass its own test and still fail like this once they are connected.
This lesson builds one small, complete interconnect from the earlier lessons: arb_4 from Arbitration, the base and limit decode from Address Decode, the AND-OR mux from Mux Trees and Read Paths, and bus_timeout and the response codes from Error Paths and Timeouts. Two masters share it, and three slaves and a default slave answer.
Follow the accent line from master 1 down and back up. Every transaction takes the same route; only the slave box changes.
Arbiter: one owner at a time. The grant is the owner's name until its reply, so the reply can be routed by it.
Decode: the address picks exactly one of the four boxes. Anything outside the three ranges goes to the default slave.
Return: the mux picks the reply by the select registered at issue, and m_rvalid goes back only to the grant holder.
Target | Range | Select | Answers |
|---|---|---|---|
RAM, 1024 words | 0x0000_1000 to 0x0000_1FFF | sel0 | Next cycle, OKAY |
CSR block | 0x0000_2000 to 0x0000_2FFF | sel1 | Next cycle. ID at 0x000 (read-only, 0xC5A0_0001), SCRATCH at 0x004; any other access SLVERR |
Slow peripheral | 0x0000_3000 to 0x0000_3FFF | sel2 | Four registers at 0x000 to 0x00C, three cycles after issue. Offsets 0x010 and up never answer |
Default slave | everything else | default_err | Next cycle, DECERR |
A request is a valid/ready offer: the master holds m_valid, m_addr, m_write and m_wdata until m_ready takes it. The reply is one m_rvalid pulse with m_rdata and m_resp, using the error lesson's codes: OKAY 2'b00, SLVERR 2'b10, DECERR 2'b11. One transaction is in flight across the whole bus.
The slow peripheral's upper offsets model a slave with no error path for its holes: it simply never answers. bus_timeout with LIMIT = 4 watches only that slave, which normally answers in 3.
Almost all of the glue is these lines of bus_fabric. Read them in order: issue, remember, answer, route.
// snippet: the glue in bus_fabric (full listing below)
arb_4 #(.N(2), .RR(1)) arb (.clk(clk), .rst_n(rst_n), .req(m_valid), .accept_i(rsp_fire), .grant(grant));
assign issue = |(grant & m_valid) && !busy; // the owner's request is taken once
assign m_ready = issue ? grant : 2'b00;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin busy <= 0; sel_q <= 0; end
else if (issue) begin busy <= 1; sel_q <= sel; end // remember who must answer
else if (rsp_fire) begin busy <= 0; sel_q <= 0; end
bus_timeout #(.LIMIT(LIMIT)) tmo (.clk, .rst_n, .req(busy && sel_q[2]), .ack(rv[2]), .timeout(timeout));
assign rsp_fire = busy && (|(sel_q & rv) || timeout);
assign m_rvalid = rsp_fire ? grant : 2'b00; // routed to the owner, never to a requester`accept_i(rsp_fire)`: the arbitration lesson's rule. The ownership interval ends with the reply, not with the request, so grant still names the owner when the reply comes back.
`sel_q`: the decode result is registered at issue. The master may change m_addr while it waits, so the reply cannot be steered by the live address.
Timeout: bus_timeout is unchanged. When it fires, the fabric completes the transaction with SLVERR and pulses abort to the slow slave, which drops its pending request. That is the coordinated isolation the error lesson asks for: no late reply can follow.
Both masters request in the same cycle. Master 0 reads 0x0000_3040, a hole in the slow peripheral; master 1 reads 0x0000_5000, which is unmapped.
Each slot is one clock cycle. In m_valid, grant and m_rvalid the left bit is master 1, so grant 01 is master 0. m_resp is the response code, and sel_q lists default, slow, CSR and RAM from the left.
Slots 1 and 2: both masters offer (m_valid 11). The registered grant goes to master 0 in slot 2, and its request is taken at the end of slot 2.
Slots 3 to 7: sel_q is 0100, the slow peripheral, and nothing answers. In slot 7 timeout rises and the fabric sends master 0 m_rvalid 01 with m_resp 10, SLVERR.
Slot 8: the grant clears for the arbitration cycle. Master 1 is still holding its offer, as valid/ready requires.
Slots 9 and 10: master 1 owns the bus, sel_q becomes 1000, the default slave, and its reply is m_rvalid 10 with m_resp 11, DECERR.
The directed part of the bench runs this pair and the two normal cases, printing each reply as the master sees it:
@75 ns m0 read 00003000 -> resp=00 data=00000000
@105 ns m1 read 00002004 -> resp=00 data=00000000
@135 ns m0 write 00001010 -> resp=00 data=00000000
@165 ns m1 read 00002000 -> resp=00 data=c5a00001
@195 ns m0 read 00001010 -> resp=00 data=12345678
@225 ns m1 read 00005000 -> resp=11 data=00000000
@295 ns m0 read 00003040 -> resp=10 data=00000000m0 write, then m1 read: master 0 writes 0x1234_5678 to RAM; master 1 reads the CSR ID 0xC5A0_0001 in the next grant; master 0 reads its word back.
The first line is a normal slow read, three cycles after issue. It is the case that exposes the routing bug below.
After the directed cases, each master issues 1500 random transactions with 0 to 3 idle cycles between them. Addresses mix RAM words, legal and illegal CSR offsets, slow registers, slow holes and unmapped addresses. The bench keeps its own copy of the map and of every register, updated in the order the fabric takes requests.
Every reply is checked for its owner (a reply to a master with nothing outstanding fails), its code and, for reads, its data. Two watchdogs bound progress: 12 cycles for an offer to be taken and 40 for a reply.
PASS: 3007 transactions checked: RAM 1398, CSR 570, slow 609, default 430; SLVERR 387 (80 of them timeouts), DECERR 430; longest wait to be taken 8 cyclesThe longest wait, 8 cycles, is master 1's wait in the waveform: slot 1 before any grant, master 0's issue in slot 2, five cycles waiting for the timeout, and the arbitration cycle in slot 8.
The five items of the Integration Checklist, applied to this design.
Checklist item | How this design meets it | Evidence |
|---|---|---|
Memory map matches software headers | Three base and limit pairs in bus_decode3; the bench holds its own copy of the map | Every reply's code checked, including 430 DECERR on unmapped addresses |
Clock and reset per bridge documented | One clock and one reset; no bridge | Not exercised here; a fabric with bridges lists each one |
Width and endianness aligned | 32 bits end to end; slaves index words with addr[11:2] | Write and read back 0x1234_5678; no byte strobes in this bus |
Backpressure verified under load | Masters hold offers until m_ready; random idle gaps | Ordered per-owner checks of 3007 replies; replies cannot be stalled here |
Debug paths do not starve production | Round-robin between two masters; grant held per transaction | No offer waited more than 8 cycles to be taken |
// bad-example: the grant is released when the request is taken
arb_4 #(.N(2), .RR(1)) arb (.clk(clk), .rst_n(rst_n), .req(m_valid), .accept_i(issue), .grant(grant));FAIL @75 ns: response to master 1, which has nothing outstanding
FATAL: bus_accept_on_issue.sv:293: FAIL @455 ns: watchdog, master 0 waited 40 cycles for a responseMaster 0's slow read was issued, the grant cleared, master 1 took the bus, and three cycles later the slow reply arrived and was routed to master 1. Master 0 never hears back and the watchdog ends the run.
The second classic is a default slave that never answers. Change its rv <= req to rv <= 1'b0 and the unmapped read of master 1 hangs the whole bus: no timeout watches the default slave, so master 1 keeps the grant and master 0's next offer is never taken.
@195 ns m0 read 00001010 -> resp=00 data=12345678
FATAL: bus_silent_default.sv:285: FAIL @325 ns: master 0 waited 12 cycles for its request to be takenWithout the two watchdogs both runs would hang silently until the simulator's time limit. Every bus bench needs a bound on waiting, not just a scoreboard.
The CSR block. 0x2008 is inside its range, so decode selects sel1, not the default slave. The CSR block has no register at offset 0x008, so it answers SLVERR, 2'b10. DECERR is only for addresses no slave owns.
With LIMIT = 4 the timeout fires before the reply, so every slow register access gets SLVERR and the abort discards the real answer. The bench fails at once with FAIL @95 ns: master 0 got resp=10 data=00000000, expected resp=00 data=00000000, and ends with 529 errors. A latency of 5 still passes, with no margin. Raise LIMIT above the slave's worst-case latency with margin, and record that bound in the integration spec.
The complete listing below contains every file of this project, each starting at a // ---- file: <name> ---- line. Save each part under its name in one folder, then run the script.
File | What it holds |
|---|---|
| The design: arbiter, decoder, timeout, three slaves, default slave and the fabric top |
| The bench: two random masters and a scoreboard of every transaction |
run.sh
#!/bin/sh
set -e
iverilog -g2012 -o sim.out bus_fabric.sv tb_bus_fabric.sv
vvp -n sim.outExpected output: the line a correct design makes the simulation print at the end of its run.
PASS: 3007 transactions checked: RAM 1398, CSR 570, slow 609, default 430; SLVERR 387 (80 of them timeouts), DECERR 430; longest wait to be taken 8 cyclesThe whole fabric with its bench: arb_4 and bus_timeout exactly as in their lessons, bus_decode3, the four slaves, bus_fabric, and the random-traffic bench with its reference model.
// ---- file: bus_fabric.sv ----
// arb_4 from the arbitration lesson and bus_timeout from the error-paths lesson, unchanged.
module arb_4 #(
parameter int N = 4, // number of masters; N must be at least 1
parameter int RR = 1 // 1: round-robin, 0: fixed priority (lowest index wins)
)(
input logic clk, rst_n,
input logic [N-1:0] req,
input logic accept_i, // currently granted ownership interval completed
output logic [N-1:0] grant // registered owner; retained until accepted
);
logic [N-1:0] grant_q, grant_next;
// N = 1 guard ($clog2(1) is 0, so [$clog2(N)-1:0] would be [-1:0]).
localparam int AW = (N > 1) ? $clog2(N) : 1;
logic [AW-1:0] ptr;
int idx;
always_comb begin
grant_next = {N{1'b0}};
idx = 0;
if (|req) begin
if (RR == 0) begin
for (int i = 0; i < N; i++)
if (req[i] && grant_next == {N{1'b0}}) grant_next[i] = 1'b1;
end else begin
for (int k = 0; k < N; k++) begin
idx = ptr + k;
if (idx >= N) idx = idx - N;
if (req[idx] && grant_next == {N{1'b0}}) grant_next[idx] = 1'b1;
end
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
grant_q <= {N{1'b0}};
ptr <= '0;
end else if (!(|grant_q)) begin
grant_q <= grant_next;
end else if (accept_i) begin
grant_q <= '0;
// The cursor advances from the completed owner, never from a stalled candidate.
for (int i = 0; i < N; i++)
if (grant_q[i]) begin
if (i == N-1) ptr <= '0;
else ptr <= AW'(i + 1);
end
end
end
assign grant = grant_q;
endmodule
module bus_timeout #(
parameter int LIMIT = 8 // must be at least 1
)(
input logic clk, rst_n,
input logic req, // transaction in flight
input logic ack, // slave answered
output logic timeout // qualified level, not a sticky fault or response
);
// LIMIT = 0 would give W = $clog2(1) = 0, a zero-width counter and an illegal cast,
// so the width stays elaborable and the check below rejects the configuration.
localparam int W = (LIMIT > 0) ? $clog2(LIMIT + 1) : 1;
initial begin
if (LIMIT < 1) $error("bus_timeout: LIMIT must be at least 1");
end
logic [W-1:0] count;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0;
else if (!req || ack) count <= '0;
else if (count < LIMIT[W-1:0]) count <= count + 1'b1;
end
assign timeout = (count == LIMIT[W-1:0]) && req && !ack;
endmodule
// bus_decode3: the decode lesson's base/limit pairs, extended to three slaves.
module bus_decode3 #(
parameter [31:0] BASE0 = 32'h0000_1000, LIMIT0 = 32'h0000_1FFF, // RAM
parameter [31:0] BASE1 = 32'h0000_2000, LIMIT1 = 32'h0000_2FFF, // CSR block
parameter [31:0] BASE2 = 32'h0000_3000, LIMIT2 = 32'h0000_3FFF // slow peripheral
)(
input logic [31:0] addr,
output logic sel0, sel1, sel2,
output logic default_err
);
assign sel0 = (addr >= BASE0) && (addr <= LIMIT0);
assign sel1 = (addr >= BASE1) && (addr <= LIMIT1);
assign sel2 = (addr >= BASE2) && (addr <= LIMIT2);
assign default_err = !(sel0 || sel1 || sel2);
endmodule
// Every slave takes a one-cycle req pulse and answers once with rv, rdata and resp.
// resp codes as in the error-paths lesson: OKAY 2'b00, SLVERR 2'b10, DECERR 2'b11.
module ram_slave ( // 1024 words, answers on the next cycle
input logic clk, rst_n, req, write,
input logic [31:0] addr, wdata,
output logic rv, output logic [31:0] rdata, output logic [1:0] resp
);
logic [31:0] mem [0:1023];
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin rv <= 0; rdata <= 0; resp <= 2'b00; end
else begin
rv <= req;
if (req) begin
if (write) mem[addr[11:2]] <= wdata;
rdata <= write ? 32'h0 : mem[addr[11:2]];
resp <= 2'b00;
end
end
endmodule
module csr_slave ( // ID (read-only) at 0x000, SCRATCH at 0x004
input logic clk, rst_n, req, write,
input logic [31:0] addr, wdata,
output logic rv, output logic [31:0] rdata, output logic [1:0] resp
);
localparam [31:0] ID = 32'hC5A0_0001;
logic [31:0] scratch;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin rv <= 0; rdata <= 0; resp <= 2'b00; scratch <= 0; end
else begin
rv <= req;
if (req) begin
rdata <= 32'h0;
resp <= 2'b10; // SLVERR unless a legal access below
if (addr[11:0] == 12'h000 && !write) begin rdata <= ID; resp <= 2'b00; end
if (addr[11:0] == 12'h004) begin
resp <= 2'b00;
if (write) scratch <= wdata; else rdata <= scratch;
end
end
end
endmodule
module slow_slave #(parameter int LAT = 3) ( // four registers at 0x000-0x00C, answers after LAT cycles
input logic clk, rst_n, req, write, abort, // offsets 0x010 and up never answer (no error path)
input logic [31:0] addr, wdata,
output logic rv, output logic [31:0] rdata, output logic [1:0] resp
);
logic [31:0] regs [0:3];
logic pend, hole, wr_q;
logic [1:0] idx_q;
logic [31:0] wdata_q;
int cnt;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
rv <= 0; rdata <= 0; resp <= 2'b00; pend <= 0; hole <= 0; cnt <= 0;
for (int i = 0; i < 4; i++) regs[i] <= 32'h0;
end
else begin
rv <= 0;
if (req) begin
pend <= 1; cnt <= 1; hole <= (addr[11:0] >= 12'h010);
idx_q <= addr[3:2]; wr_q <= write; wdata_q <= wdata;
end else if (abort) begin
pend <= 0; // the fabric gave up: no late reply
end else if (pend && !hole) begin
if (cnt == LAT - 1) begin
pend <= 0; rv <= 1; resp <= 2'b00;
if (wr_q) begin regs[idx_q] <= wdata_q; rdata <= 32'h0; end
else rdata <= regs[idx_q];
end else cnt <= cnt + 1;
end
end
endmodule
module default_slave ( // unmapped address: DECERR on the next cycle
input logic clk, rst_n, req,
output logic rv, output logic [31:0] rdata, output logic [1:0] resp
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) rv <= 0;
else rv <= req;
assign rdata = 32'h0;
assign resp = 2'b11;
endmodule
// bus_fabric: two masters, one transaction at a time. The arbiter's grant is the owner from
// issue until the response; accept_i is the response, so the owner cannot change mid-flight.
module bus_fabric #(parameter int LIMIT = 4) (
input logic clk, rst_n,
input logic [1:0] m_valid, // request: valid/ready with addr, write, wdata
output logic [1:0] m_ready,
input logic [1:0][31:0] m_addr, m_wdata,
input logic [1:0] m_write,
output logic [1:0] m_rvalid, // response: one pulse to the owner
output logic [31:0] m_rdata,
output logic [1:0] m_resp
);
logic [1:0] grant;
logic owner, busy, issue, rsp_fire, timeout;
logic [31:0] addr;
logic [3:0] sel, sel_q; // {default, slow, csr, ram}
logic [3:0] rv;
logic [3:0][31:0] rd; // per-slave read data, response code
logic [3:0][1:0] rs;
arb_4 #(.N(2), .RR(1)) arb (.clk(clk), .rst_n(rst_n), .req(m_valid), .accept_i(rsp_fire), .grant(grant));
assign owner = grant[1];
assign addr = m_addr[owner];
assign issue = |(grant & m_valid) && !busy; // the owner's request is taken once
assign m_ready = issue ? grant : 2'b00;
bus_decode3 dec (.addr(addr), .sel0(sel[0]), .sel1(sel[1]), .sel2(sel[2]), .default_err(sel[3]));
ram_slave ram (.clk, .rst_n, .req(issue && sel[0]), .write(m_write[owner]), .addr, .wdata(m_wdata[owner]),
.rv(rv[0]), .rdata(rd[0]), .resp(rs[0]));
csr_slave csr (.clk, .rst_n, .req(issue && sel[1]), .write(m_write[owner]), .addr, .wdata(m_wdata[owner]),
.rv(rv[1]), .rdata(rd[1]), .resp(rs[1]));
slow_slave slw (.clk, .rst_n, .req(issue && sel[2]), .write(m_write[owner]), .addr, .wdata(m_wdata[owner]),
.abort(timeout), .rv(rv[2]), .rdata(rd[2]), .resp(rs[2]));
default_slave dfl (.clk, .rst_n, .req(issue && sel[3]), .rv(rv[3]), .rdata(rd[3]), .resp(rs[3]));
bus_timeout #(.LIMIT(LIMIT)) tmo (.clk, .rst_n, .req(busy && sel_q[2]), .ack(rv[2]), .timeout(timeout));
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin busy <= 0; sel_q <= 0; end
else if (issue) begin busy <= 1; sel_q <= sel; end // remember who must answer
else if (rsp_fire) begin busy <= 0; sel_q <= 0; end
// Response mux: AND-OR qualified by the registered select, as in bus_rmux.
assign rsp_fire = busy && (|(sel_q & rv) || timeout);
assign m_rdata = timeout ? 32'h0 : ({32{sel_q[0]}} & rd[0]) | ({32{sel_q[1]}} & rd[1])
| ({32{sel_q[2]}} & rd[2]) | ({32{sel_q[3]}} & rd[3]);
assign m_resp = timeout ? 2'b10 : ({2{sel_q[0]}} & rs[0]) | ({2{sel_q[1]}} & rs[1])
| ({2{sel_q[2]}} & rs[2]) | ({2{sel_q[3]}} & rs[3]);
assign m_rvalid = rsp_fire ? grant : 2'b00; // routed to the owner, never to a requester
endmodule
Tool: Icarus Verilog 13.0 (iverilog -g2012, vvp), run on 2026-10-17. Command: iverilog -g2012 -s tb_bus_fabric -o bus_fabric.vvp bus_fabric.sv && vvp -n bus_fabric.vvp, where bus_fabric.sv is the page's full listing.
Result: PASS: 3007 transactions checked: RAM 1398, CSR 570, slow 609, default 430; SLVERR 387 (80 of them timeouts), DECERR 430; longest wait to be taken 8 cycles. The directed reply lines are copied from the same run.
Variants of the same listing, one line changed each: accept_i(issue) gives FAIL @75 ns: response to master 1, then the 40-cycle watchdog; a silent default slave gives the 12-cycle request watchdog at 325 ns; slow-slave latency 6 gives 529 errors, latency 4 and 5 pass.
Waveform: wave-src/bus-integration/fabric.sv (the same RTL, a two-request scenario) through node scripts/tools/gen-waves.mjs memory-buses:bus-integration; it matched the hand-written expectation.
Scope and limits: one clock, one transaction in flight, masters always accept replies, no byte strobes, fixed seed. No formal proof; the random run covers the sequences it happened to draw.