Wishbone · Module 16
CPU + DMA Systems
Two initiators in contention, with the clocks counted in four columns: holding work, asking for the bus, owning it, and being answered. Contention cost two clocks out of twenty-one.
Chapter 16.1 built a second initiator and ran it alone. Arranging that was the only reason the system behaved simply.
What happens on the clock both masters want the bus?
1. The Four Layers, and Why They Have to Stay Apart
Layer 1 is the client request. In Chapter 16.1's master it is req_i, busy_o and done_o; in the DMA it is the CTRL register write and the engine's internal index. None of it is on the bus. A master may hold accepted work for any number of clocks without presenting anything.
Layer 2 is ownership. Some piece of logic decides which master's signals reach the shared path. The specification does not define this layer at all. It names the module — "INTERCON: A WISHBONE module that interconnects MASTER and SLAVE interfaces" — and hands you the policy: "Arbitration methodology is defined by the end user."
Layer 3 is the Wishbone transfer. This is the only layer with rules. CYC_O spans the cycle (RULE 3.25), STB_O qualifies the address, data, select and write-enable (RULE 3.60), and the slave answers with exactly one of ACK_O, ERR_O or RTY_O (RULE 3.45).
Layer 4 is what the slave actually does. A write commits; a FIFO pushes; a command fires. Chapter 13.5 and Chapter 15.3 both turned on the distance between layer 3 and layer 4, and it is still there.
The practical value of keeping them apart is diagnostic. When a transaction does not happen, the first question is not why is the bus broken — it is which layer stopped progressing, and the four layers have four different sets of evidence.
2. RTL — The System, End to End
One system serves all four chapters. It is shown whole here because every later measurement refers to a named wire in it.
// ─────────────────────────────────────────────────────────────────────────
// m16_system — the one system Module 16 uses, from 16.1 to 16.4.
//
// CPU master ─┐ ┌─ shared RAM 0x000..0x0FF
// ├─ wb_owner_mux2 ─ wb_split2
// DMA master ─┘ └─ DMA config regs 0x800..0x803
//
// and, when WITH_PRIVATE is set, a second wb_split2 on the CPU's own port:
//
// CPU master ─ wb_split2 ─┬─ to wb_owner_mux2 (shared, adr[10] = 0)
// └─ private RAM (local, adr[10] = 1)
//
// The private leg never reaches the interconnect, so a CPU access to it
// acquires no ownership and contends with nothing. That is Chapter 16.4's
// measurement and the reason the same splitter appears twice.
//
// WORD ADDRESSES throughout - adr[11:0] selects a 32-bit word, never a
// byte. adr[11] chooses RAM versus DMA registers; adr[10] chooses shared
// versus private.
//
// The defect parameters pass straight through to wb_owner_mux2 and are all
// off by default.
// ─────────────────────────────────────────────────────────────────────────
module m16_system #(
parameter int unsigned AW = 12,
parameter int unsigned DW = 32,
parameter int unsigned SW = DW/8,
parameter int unsigned RAM_WAITS = 0,
parameter bit RELEASE_ON_IDLE = 1'b0,
parameter bit HONOUR_LOCK = 1'b1,
parameter bit PREEMPT = 1'b0,
parameter bit MIX_CONTEXT = 1'b0,
parameter bit BROADCAST_RETURN = 1'b0,
parameter bit WITH_PRIVATE = 1'b0
) (
input logic clk_i,
input logic rst_i,
// CPU client contract - LOCAL, not Wishbone
input logic cpu_req_i,
input logic cpu_we_i,
input logic [AW-1:0] cpu_adr_i,
input logic [DW-1:0] cpu_dat_i,
input logic [SW-1:0] cpu_sel_i,
input logic cpu_lock_i,
input logic cpu_hold_i,
output logic cpu_busy_o,
output logic cpu_done_o,
output logic cpu_ok_o,
output logic cpu_xerr_o,
output logic cpu_xrty_o,
output logic [DW-1:0] cpu_rdat_o,
// shared RAM "local resource busy" input - answers RTY while asserted
input logic ram_busy_i,
// DMA observation
output logic dma_busy_o,
output logic dma_done_o,
output logic dma_failed_o,
output logic [15:0] dma_words_o,
// resource observation
output logic [15:0] ram_writes_o,
output logic [15:0] priv_writes_o,
// CPU master port (for the probe)
output logic cpu_cyc_o,
output logic cpu_stb_o,
output logic cpu_mwe_o,
output logic [AW-1:0] cpu_madr_o,
output logic [DW-1:0] cpu_mdat_o,
output logic [SW-1:0] cpu_msel_o,
output logic cpu_ack_o,
output logic cpu_err_o,
output logic cpu_rty_o,
// CPU port AS THE INTERCONNECT SEES IT. With WITH_PRIVATE set these are
// the shared leg of the CPU's own splitter, and they differ from the
// master's own pins on every private access. An instrument wired to the
// master's pins instead reports contention that does not exist.
output logic cpu_icyc_o,
output logic cpu_istb_o,
output logic cpu_iwe_o,
output logic [AW-1:0] cpu_iadr_o,
output logic [DW-1:0] cpu_idat_o,
output logic [SW-1:0] cpu_isel_o,
output logic cpu_iack_o,
output logic cpu_ierr_o,
output logic cpu_irty_o,
// DMA master port (for the probe)
output logic dma_cyc_o,
output logic dma_stb_o,
output logic dma_mwe_o,
output logic [AW-1:0] dma_madr_o,
output logic [DW-1:0] dma_mdat_o,
output logic [SW-1:0] dma_msel_o,
output logic dma_ack_o,
output logic dma_err_o,
output logic dma_rty_o,
// shared downstream path (for the probe)
output logic s_cyc_o,
output logic s_stb_o,
output logic s_we_o,
output logic [AW-1:0] s_adr_o,
output logic [DW-1:0] s_dat_o,
output logic [SW-1:0] s_sel_o,
output logic [DW-1:0] s_rdat_o,
output logic s_ack_o,
output logic s_err_o,
output logic s_rty_o,
output logic [1:0] owner_o,
output logic s_lock_o
);
// ── CPU master ──
logic c_cyc, c_stb, c_lock, c_we;
logic [AW-1:0] c_adr;
logic [DW-1:0] c_dat, c_rdat;
logic [SW-1:0] c_sel;
logic c_ack, c_err, c_rty;
wb_cpu_master #(.AW(AW), .DW(DW), .SW(SW)) u_cpu (
.clk_i(clk_i), .rst_i(rst_i),
.req_i(cpu_req_i), .req_we_i(cpu_we_i), .req_adr_i(cpu_adr_i),
.req_dat_i(cpu_dat_i), .req_sel_i(cpu_sel_i), .req_lock_i(cpu_lock_i),
.hold_i(cpu_hold_i),
.busy_o(cpu_busy_o), .done_o(cpu_done_o), .ok_o(cpu_ok_o),
.err_o(cpu_xerr_o), .rty_o(cpu_xrty_o), .rdat_o(cpu_rdat_o),
.cyc_o(c_cyc), .stb_o(c_stb), .lock_o(c_lock), .we_o(c_we),
.adr_o(c_adr), .dat_o(c_dat), .sel_o(c_sel),
.dat_i(c_rdat), .ack_i(c_ack), .err_i(c_err), .rty_i(c_rty));
assign cpu_cyc_o = c_cyc; assign cpu_stb_o = c_stb;
assign cpu_mwe_o = c_we; assign cpu_madr_o = c_adr;
assign cpu_mdat_o = c_dat; assign cpu_msel_o = c_sel;
assign cpu_ack_o = c_ack; assign cpu_err_o = c_err; assign cpu_rty_o = c_rty;
// ── optional CPU-side local split ──
logic x_cyc, x_stb, x_lock, x_we;
logic [AW-1:0] x_adr;
logic [DW-1:0] x_dat, x_rdat;
logic [SW-1:0] x_sel;
logic x_ack, x_err, x_rty;
logic p_cyc, p_stb, p_we;
logic [AW-1:0] p_adr;
logic [DW-1:0] p_dat, p_rdat;
logic [SW-1:0] p_sel;
logic p_ack;
generate
if (WITH_PRIVATE) begin : g_priv
wb_split2 #(.AW(AW), .DW(DW), .SW(SW), .SEL_BIT(10)) u_cpusplit (
.m_cyc_i(c_cyc), .m_stb_i(c_stb), .m_lock_i(c_lock), .m_we_i(c_we),
.m_adr_i(c_adr), .m_dat_i(c_dat), .m_sel_i(c_sel),
.m_dat_o(c_rdat), .m_ack_o(c_ack), .m_err_o(c_err), .m_rty_o(c_rty),
.a_cyc_o(x_cyc), .a_stb_o(x_stb), .a_lock_o(x_lock), .a_we_o(x_we),
.a_adr_o(x_adr), .a_dat_o(x_dat), .a_sel_o(x_sel),
.a_dat_i(x_rdat), .a_ack_i(x_ack), .a_err_i(x_err), .a_rty_i(x_rty),
.b_cyc_o(p_cyc), .b_stb_o(p_stb), .b_lock_o(), .b_we_o(p_we),
.b_adr_o(p_adr), .b_dat_o(p_dat), .b_sel_o(p_sel),
.b_dat_i(p_rdat), .b_ack_i(p_ack), .b_err_i(1'b0), .b_rty_i(1'b0),
.hit_b_o());
wb_shared_ram #(.AW(AW), .DW(DW), .SW(SW), .WORDS(16), .WAITS(0))
u_priv (.clk_i(clk_i), .rst_i(rst_i), .cyc_i(p_cyc), .stb_i(p_stb),
.we_i(p_we), .adr_i(p_adr), .dat_i(p_dat), .sel_i(p_sel),
.busy_i(1'b0), .dat_o(p_rdat), .ack_o(p_ack),
.err_o(), .rty_o(), .writes_o(priv_writes_o));
end else begin : g_nopriv
assign x_cyc = c_cyc; assign x_stb = c_stb; assign x_lock = c_lock;
assign x_we = c_we; assign x_adr = c_adr; assign x_dat = c_dat;
assign x_sel = c_sel;
assign c_rdat = x_rdat; assign c_ack = x_ack;
assign c_err = x_err; assign c_rty = x_rty;
assign priv_writes_o = 16'd0;
end
endgenerate
assign cpu_icyc_o = x_cyc; assign cpu_istb_o = x_stb;
assign cpu_iwe_o = x_we; assign cpu_iadr_o = x_adr;
assign cpu_idat_o = x_dat; assign cpu_isel_o = x_sel;
assign cpu_iack_o = x_ack; assign cpu_ierr_o = x_err;
assign cpu_irty_o = x_rty;
// ── DMA ──
logic d_cyc, d_stb, d_lock, d_we;
logic [AW-1:0] d_adr;
logic [DW-1:0] d_dat, d_rdat;
logic [SW-1:0] d_sel;
logic d_ack, d_err, d_rty;
logic cfg_cyc, cfg_stb, cfg_we;
logic [AW-1:0] cfg_adr;
logic [DW-1:0] cfg_dat, cfg_rdat;
logic cfg_ack, cfg_err;
wb_teaching_dma #(.AW(AW), .DW(DW), .SW(SW)) u_dma (
.clk_i(clk_i), .rst_i(rst_i),
.c_cyc_i(cfg_cyc), .c_stb_i(cfg_stb), .c_we_i(cfg_we),
.c_adr_i(cfg_adr[1:0]), .c_dat_i(cfg_dat),
.c_dat_o(cfg_rdat), .c_ack_o(cfg_ack), .c_err_o(cfg_err),
.cyc_o(d_cyc), .stb_o(d_stb), .lock_o(d_lock), .we_o(d_we),
.adr_o(d_adr), .dat_o(d_dat), .sel_o(d_sel),
.dat_i(d_rdat), .ack_i(d_ack), .err_i(d_err), .rty_i(d_rty),
.busy_o(dma_busy_o), .done_o(dma_done_o), .failed_o(dma_failed_o),
.words_done_o(dma_words_o));
assign dma_cyc_o = d_cyc; assign dma_stb_o = d_stb;
assign dma_mwe_o = d_we; assign dma_madr_o = d_adr;
assign dma_mdat_o = d_dat; assign dma_msel_o = d_sel;
assign dma_ack_o = d_ack; assign dma_err_o = d_err; assign dma_rty_o = d_rty;
// ── the interconnect ──
logic s_cyc, s_stb, s_lock, s_we;
logic [AW-1:0] s_adr;
logic [DW-1:0] s_dat, s_rdat;
logic [SW-1:0] s_sel;
logic s_ack, s_err, s_rty;
wb_owner_mux2 #(
.AW(AW), .DW(DW), .SW(SW),
.RELEASE_ON_IDLE(RELEASE_ON_IDLE), .HONOUR_LOCK(HONOUR_LOCK),
.PREEMPT(PREEMPT), .MIX_CONTEXT(MIX_CONTEXT),
.BROADCAST_RETURN(BROADCAST_RETURN)
) u_mux (
.clk_i(clk_i), .rst_i(rst_i),
.cpu_cyc_i(x_cyc), .cpu_stb_i(x_stb), .cpu_lock_i(x_lock),
.cpu_we_i(x_we), .cpu_adr_i(x_adr), .cpu_dat_i(x_dat), .cpu_sel_i(x_sel),
.cpu_dat_o(x_rdat), .cpu_ack_o(x_ack), .cpu_err_o(x_err),
.cpu_rty_o(x_rty),
.dma_cyc_i(d_cyc), .dma_stb_i(d_stb), .dma_lock_i(d_lock),
.dma_we_i(d_we), .dma_adr_i(d_adr), .dma_dat_i(d_dat), .dma_sel_i(d_sel),
.dma_dat_o(d_rdat), .dma_ack_o(d_ack), .dma_err_o(d_err),
.dma_rty_o(d_rty),
.s_cyc_o(s_cyc), .s_stb_o(s_stb), .s_lock_o(s_lock), .s_we_o(s_we),
.s_adr_o(s_adr), .s_dat_o(s_dat), .s_sel_o(s_sel),
.s_dat_i(s_rdat), .s_ack_i(s_ack), .s_err_i(s_err), .s_rty_i(s_rty),
.owner_o(owner_o));
assign s_cyc_o = s_cyc; assign s_stb_o = s_stb; assign s_we_o = s_we;
assign s_adr_o = s_adr; assign s_dat_o = s_dat; assign s_sel_o = s_sel;
assign s_rdat_o = s_rdat;
assign s_lock_o = s_lock;
assign s_ack_o = s_ack; assign s_err_o = s_err; assign s_rty_o = s_rty;
// ── downstream split: RAM (adr[11]=0) and DMA registers (adr[11]=1) ──
logic r_cyc, r_stb, r_we;
logic [AW-1:0] r_adr;
logic [DW-1:0] r_dat, r_rdat;
logic [SW-1:0] r_sel;
logic r_ack, r_rty;
wb_split2 #(.AW(AW), .DW(DW), .SW(SW), .SEL_BIT(11)) u_dsplit (
.m_cyc_i(s_cyc), .m_stb_i(s_stb), .m_lock_i(s_lock), .m_we_i(s_we),
.m_adr_i(s_adr), .m_dat_i(s_dat), .m_sel_i(s_sel),
.m_dat_o(s_rdat), .m_ack_o(s_ack), .m_err_o(s_err), .m_rty_o(s_rty),
.a_cyc_o(r_cyc), .a_stb_o(r_stb), .a_lock_o(), .a_we_o(r_we),
.a_adr_o(r_adr), .a_dat_o(r_dat), .a_sel_o(r_sel),
.a_dat_i(r_rdat), .a_ack_i(r_ack), .a_err_i(1'b0), .a_rty_i(r_rty),
.b_cyc_o(cfg_cyc), .b_stb_o(cfg_stb), .b_lock_o(), .b_we_o(cfg_we),
.b_adr_o(cfg_adr), .b_dat_o(cfg_dat), .b_sel_o(),
.b_dat_i(cfg_rdat), .b_ack_i(cfg_ack), .b_err_i(cfg_err),
.b_rty_i(1'b0),
.hit_b_o());
wb_shared_ram #(.AW(AW), .DW(DW), .SW(SW), .WORDS(256), .WAITS(RAM_WAITS))
u_ram (.clk_i(clk_i), .rst_i(rst_i), .cyc_i(r_cyc), .stb_i(r_stb),
.we_i(r_we), .adr_i(r_adr), .dat_i(r_dat), .sel_i(r_sel),
.busy_i(ram_busy_i), .dat_o(r_rdat), .ack_o(r_ack),
.err_o(), .rty_o(r_rty), .writes_o(ram_writes_o));
endmoduleReading it
Read the instance list before the logic. There is a CPU master, a DMA with two ports, one wb_owner_mux2 between the masters and the shared path, one wb_split2 below it, and one RAM. That is the entire system, and the address map is three lines:
| window | address | reached via |
|---|---|---|
| shared RAM | 0x000–0x0FF | the interconnect |
| DMA configuration | 0x800–0x803 | the interconnect |
| CPU private | 0x400–0x40F | the CPU's own splitter, when WITH_PRIVATE is set |
adr[11] chooses RAM or DMA registers; adr[10] chooses shared or private. Word addresses throughout — adr[11:0] selects a 32-bit word, never a byte, which is Chapter 12.1's convention and the reason no shifts appear anywhere in this module.
The DMA appears twice in the wiring and that is the point. Its configuration port hangs off u_dsplit as an ordinary slave, downstream of the interconnect; its master port goes straight into u_mux as a peer of the CPU. A transfer from the CPU to the DMA's registers goes through the interconnect and back down into the same device that is, on the other port, competing for that interconnect.
cpu_icyc_o is the one output that needs explaining now. It is the CPU's CYC_O as the interconnect sees it. With WITH_PRIVATE set they differ — the splitter keeps private accesses off the shared leg entirely — and Chapter 16.4 measures the difference. An instrument wired to the master's own pins instead would report contention that does not exist, which is a mistake this module made once and fixed by moving the probe.
The shape of the diagram is the argument. Two paths at the top, one path at the bottom, and a place in the middle where the number changes from two to one. Everything that can go wrong in Chapter 16.3 goes wrong at that junction.
3. Simulation — SIM C: Requests That Do Not Overlap
The easy case first, because its numbers are the baseline everything else is measured against. The CPU reads one word and releases; only then does the DMA start.
=== SIM C - requests that do not overlap ===
the CPU reads 0x005 and releases; then the DMA copies one word
clk cpu_busy dma_busy cpu_cyc dma_cyc owner s_cyc s_stb adr term
0 0 0 0 0 - 0 0 -
1 0 0 0 0 - 0 0 -
2 1 0 1 0 - 0 0 -
3 1 0 1 0 CPU 1 1 0x005 ACK
4 1 0 0 0 CPU 0 0 -
5 0 0 0 0 - 0 0 -
6 0 0 0 0 - 0 0 -
7 0 0 0 0 - 0 0 -
8 0 0 0 0 - 0 0 -
9 1 0 1 0 - 0 0 -
10 1 0 1 0 CPU 1 1 0x803 ACK
11 1 1 0 1 CPU 0 0 -
12 0 1 0 1 DMA 1 1 0x010 ACK
13 0 1 0 0 DMA 0 0 -
14 0 1 0 1 - 0 0 -
15 0 1 0 1 DMA 1 1 0x030 ACK
16 0 1 0 0 DMA 0 0 -
17 0 1 0 0 - 0 0 -
18 0 0 0 0 - 0 0 -
19 0 0 0 0 - 0 0 -
acquisitions CPU 5 DMA 2 owned clocks CPU 10 DMA 4
clocks a master asserted CYC_O without owning: CPU 5 DMA 2
-> one clock per acquisition, with no competitor presentReading it
Follow one acquisition all the way down: clocks 2, 3, 4, 5.
Clock 2 — cpu_busy is 1 and cpu_cyc is 1, and the owner column reads -. The master has work and is asking, and it does not have the bus. s_cyc and s_stb are both 0: nothing has reached a slave.
Clock 3 — the owner is CPU, s_cyc and s_stb are 1, the address 0x005 is on the shared path, and the termination is ACK. Request, ownership, transfer and effect, all satisfied.
Clock 4 — cpu_cyc has gone but the owner is still CPU. The master is in its wrap state; the interconnect releases on the next clock. Ownership outlives the cycle by one clock, which matters when the other master is waiting.
Clock 5 — owner -. The bus is free.
Now the number at the bottom. Clocks a master asserted CYC_O without owning: CPU 5, DMA 2 — against acquisitions: CPU 5, DMA 2. Exactly one waiting clock per acquisition, with no competitor anywhere in the run.
That is not contention; it is the cost of the ownership state being registered. The mux samples the request lines at a clock edge and the grant appears after it. Any system whose ownership is a flip-flop has this clock, and reading it as contention is the first mistake this measurement prevents.
Clocks 11 through 17 show the handover working. At 11 the CPU still owns and the DMA's CYC_O has just risen — the DMA is asking. At 12 the owner is DMA and its read of 0x010 is answered. At 13 the DMA drops CYC_O for its gap clock and the owner stays DMA — no one else wants it. At 14 the DMA asks again and the owner has dropped to -, because the gap clock released it.
Read clock 14 carefully, because it is the release policy made visible. This interconnect retains an owner only while that owner's CYC_O is asserted. The DMA's one-clock gap between its read and its write is enough to lose the bus — and in this run nothing took it, so the DMA reacquired immediately.
4. Simulation — SIM D: Both Masters on the Same Clock
The CPU's request is timed to rise on the exact clock the DMA starts a phase. This is arranged, because a simultaneous acquisition is what the tie-break rule exists for and it should not be left to luck.
=== SIM D - both masters assert CYC_O on the same clock ===
the CPU request is timed to rise on a DMA phase boundary
clk cpu_busy dma_busy cpu_cyc dma_cyc owner s_cyc s_stb adr term
0 0 0 0 0 - 0 0 -
1 0 0 0 0 - 0 0 -
2 1 0 1 0 - 0 0 -
3 1 0 1 0 CPU 1 1 0x803 ACK
4 1 1 0 1 CPU 0 0 -
5 0 1 0 1 DMA 1 1 0x010 ACK
6 0 1 0 0 DMA 0 0 -
7 1 1 1 1 - 0 0 -
8 1 1 1 1 CPU 1 1 0x007 ACK
9 1 1 0 1 CPU 0 0 -
10 0 1 0 1 DMA 1 1 0x030 ACK
11 0 1 0 0 DMA 0 0 -
12 0 1 0 1 - 0 0 -
13 0 1 0 1 DMA 1 1 0x011 ACK
14 0 1 0 0 DMA 0 0 -
15 0 1 0 1 - 0 0 -
16 0 1 0 1 DMA 1 1 0x031 ACK
17 0 1 0 0 DMA 0 0 -
18 0 1 0 0 - 0 0 -
19 0 0 0 0 - 0 0 -
20 0 0 0 0 - 0 0 -
acquisitions CPU 5 DMA 4
CPU clocks asserting CYC_O without owning 5
DMA clocks asserting CYC_O without owning 6
CPU clocks holding work with CYC_O negated 5
DMA clocks holding work with CYC_O negated 5
extra wait clocks caused by contention: CPU 0 DMA 2
CPU read value 0xaaaa0007 DMA words 2 DMA failed 0
violations: no-owner 0 non-owner term 0 context 0 mid-transfer 0Reading it — clock 7
Clock 7 is the whole simulation. cpu_cyc is 1, dma_cyc is 1, and the owner is -. Two masters are asking and nobody has it.
Clock 8 resolves it: the owner is CPU. The CPU's read of 0x007 is presented and acknowledged. The DMA keeps CYC_O asserted and gets nothing.
Why the CPU? Because wb_owner_mux2 tests cpu_cyc_i before dma_cyc_i when the owner is NONE. That is a LOCAL TEACHING POLICY and nothing more. It is a tie-break on an idle bus, not a priority scheme: once either master owns the path, this test is not reached, so a busy CPU cannot use it to keep the bus. The difference between a tie-break and a priority scheme is exactly that, and Module 17 is where the distinction is designed rather than described.
The DMA waits at clocks 7, 8 and 9 and acquires at 10. Three clocks of CYC_O asserted without the bus.
Now the arithmetic at the bottom, which is the point of the chapter.
CPU clocks asserting
CYC_Owithout owning 5 · DMA 6 CPU clocks holding work withCYC_Onegated 5 · DMA 5 extra wait clocks caused by contention — CPU 0, DMA 2
Four different counts, four different meanings.
"Holding work with CYC_O negated" is layer 1 without layer 3 — the master has accepted a job and is not asking for the bus. For the DMA these are its gap clocks and its index-increment clocks; for the CPU they are the clock between latching a request and entering the transfer state. A bus trace shows none of this. The master looks idle and is not.
"Asserting CYC_O without owning" is layer 3 without layer 2 — the master is asking and is not being heard. A bus trace of the shared path shows none of this either, because the request never reaches the shared path. It is visible only at the master's own port.
"Extra wait clocks caused by contention" is the difference between that count and the number of acquisitions, and it is the only one of the four that is actually about the other master. CPU 0, DMA 2. In a run with 21 shared-bus terminations, competition cost two clocks, and both of them fell on the master that lost the tie-break.
And the last line of the run matters as much as the first. violations: no-owner 0, non-owner term 0, context 0, mid-transfer 0. Both operations completed, correctly, with the right data. Contention is not a fault; it is a delay. Chapter 16.3 is where it becomes a fault.
5. Request Is Not CYC_O, and CYC_O Is Not Ownership
Two conflations, and the measurements above separate both.
First: a pending request is not an asserted CYC_O. The DMA spent five clocks in this run holding a word it had read and had not yet written, with CYC_O negated. It had work. It was not asking. A design that assumes "busy implies requesting" will misread every one of those clocks, and an arbiter built on that assumption will grant the bus to a master that is not ready for it.
Second: an asserted CYC_O is not ownership. Six clocks in this run had dma_cyc high and the owner not DMA.
The specification is careful here in a way that is easy to miss. It does describe CYC_O as a request — in the [CYC_O] signal description:
The
[CYC_O]signal is useful for interfaces with multi-port interfaces (such as dual port memories). In these cases, the[CYC_O]signal requests use of a common bus from an arbiter.
and again, advisorily, in RECOMMENDATION 3.05:
Arbitration logic often uses
[CYC_I]to select between MASTER interfaces.
"In these cases." "Often." The specification is describing a common architecture, not mandating one. CYC_O is this system's request signal because this system chose that, and a design with separate request and grant wires is equally conformant — there is no REQ/GNT pair anywhere in the B3 signal list to make it otherwise.
What is never true in either architecture is that requesting equals owning. That is what the sixth column measures.
6. RTL — The Instrument
Four questions, none of which any single signal answers, and all of which the rest of this module depends on.
// ─────────────────────────────────────────────────────────────────────────
// wb_bus_probe — the instrument, not part of the design.
//
// Everything here is simulation-only observation: int counters, no reset
// requirement beyond clearing, no synthesis intent. A silicon version of
// any of it would be narrow counters behind a status register, and none of
// it is a Wishbone feature.
//
// It answers four questions that no single signal answers:
//
// 1. OWNERSHIP TIMELINE who owns, for how long, how often it moves
// 2. REQUEST-TO-COMPLETION how many clocks a master held work without
// asserting CYC_O, and how many it asserted CYC_O without owning
// 3. RESPONSE ISOLATION did a master see a termination it did not earn
// 4. REQUEST CONTEXT does every shared field come from one master
//
// Question 2 is the one that makes the layers measurable. A master that is
// busy with CYC_O negated has a pending LOCAL request and no bus request
// at all; a master asserting CYC_O without owning has a bus request and no
// bus. Those are different clocks and they are counted separately.
// ─────────────────────────────────────────────────────────────────────────
module wb_bus_probe #(
parameter int unsigned AW = 12,
parameter int unsigned DW = 32,
parameter int unsigned SW = DW/8
) (
input logic clk_i,
input logic rst_i,
// client layer
input logic cpu_busy_i,
input logic dma_busy_i,
// CPU master port
input logic cpu_cyc_i,
input logic cpu_stb_i,
input logic cpu_we_i,
input logic [AW-1:0] cpu_adr_i,
input logic [DW-1:0] cpu_dat_i,
input logic [SW-1:0] cpu_sel_i,
input logic cpu_ack_i,
input logic cpu_err_i,
input logic cpu_rty_i,
// DMA master port
input logic dma_cyc_i,
input logic dma_stb_i,
input logic dma_we_i,
input logic [AW-1:0] dma_adr_i,
input logic [DW-1:0] dma_dat_i,
input logic [SW-1:0] dma_sel_i,
input logic dma_ack_i,
input logic dma_err_i,
input logic dma_rty_i,
// shared downstream path
input logic s_cyc_i,
input logic s_stb_i,
input logic s_we_i,
input logic [AW-1:0] s_adr_i,
input logic [DW-1:0] s_dat_i,
input logic [SW-1:0] s_sel_i,
input logic s_ack_i,
input logic s_err_i,
input logic s_rty_i,
input logic [1:0] owner_i,
// violations
output int unsigned v_xfer_no_owner_o,
output int unsigned v_nonowner_term_o,
output int unsigned v_context_mix_o,
output int unsigned v_owner_change_active_o,
// timeline
output int unsigned owner_changes_o,
output int unsigned cpu_acquires_o,
output int unsigned dma_acquires_o,
output int unsigned cpu_own_clocks_o,
output int unsigned dma_own_clocks_o,
output int unsigned none_clocks_o,
// request-to-completion
output int unsigned cpu_pending_quiet_o,
output int unsigned cpu_req_wait_o,
output int unsigned dma_pending_quiet_o,
output int unsigned dma_req_wait_o,
// terminations seen on the shared path
output int unsigned n_ack_o,
output int unsigned n_err_o,
output int unsigned n_rty_o
);
localparam logic [1:0] OWN_NONE = 2'd0;
localparam logic [1:0] OWN_CPU = 2'd1;
localparam logic [1:0] OWN_DMA = 2'd2;
logic [1:0] prev_owner_q;
logic prev_active_q; // a phase was presented and unanswered
logic s_term, cpu_term, dma_term, ctx_bad;
assign s_term = s_ack_i || s_err_i || s_rty_i;
assign cpu_term = cpu_ack_i || cpu_err_i || cpu_rty_i;
assign dma_term = dma_ack_i || dma_err_i || dma_rty_i;
// The shared request context must be one master's, whole. RULE 3.60
// names the fields that MUST be qualified together by [STB_O]; this
// compares all of them against the owner's own outputs.
always_comb begin
ctx_bad = 1'b0;
if (s_stb_i) begin
if (owner_i == OWN_CPU)
ctx_bad = (s_adr_i != cpu_adr_i) || (s_we_i != cpu_we_i)
|| (s_sel_i != cpu_sel_i)
|| (s_we_i && (s_dat_i != cpu_dat_i));
else if (owner_i == OWN_DMA)
ctx_bad = (s_adr_i != dma_adr_i) || (s_we_i != dma_we_i)
|| (s_sel_i != dma_sel_i)
|| (s_we_i && (s_dat_i != dma_dat_i));
end
end
always_ff @(posedge clk_i) begin
if (rst_i) begin
v_xfer_no_owner_o <= 0; v_nonowner_term_o <= 0;
v_context_mix_o <= 0; v_owner_change_active_o <= 0;
owner_changes_o <= 0;
cpu_acquires_o <= 0; dma_acquires_o <= 0;
cpu_own_clocks_o <= 0; dma_own_clocks_o <= 0; none_clocks_o <= 0;
cpu_pending_quiet_o <= 0; cpu_req_wait_o <= 0;
dma_pending_quiet_o <= 0; dma_req_wait_o <= 0;
n_ack_o <= 0; n_err_o <= 0; n_rty_o <= 0;
prev_owner_q <= OWN_NONE; prev_active_q <= 1'b0;
end else begin
// 1. ownership timeline
if (owner_i == OWN_CPU) cpu_own_clocks_o <= cpu_own_clocks_o + 1;
else if (owner_i == OWN_DMA) dma_own_clocks_o <= dma_own_clocks_o + 1;
else none_clocks_o <= none_clocks_o + 1;
if (owner_i != prev_owner_q) owner_changes_o <= owner_changes_o + 1;
// An ACQUISITION is a transition INTO an owner, which is the event a
// reader means by "it got the bus". Returning to NONE is a release
// and is counted separately by owner_changes_o.
if (owner_i == OWN_CPU && prev_owner_q != OWN_CPU)
cpu_acquires_o <= cpu_acquires_o + 1;
if (owner_i == OWN_DMA && prev_owner_q != OWN_DMA)
dma_acquires_o <= dma_acquires_o + 1;
// an owner change on a clock where the previous clock had an
// unanswered phase in flight
if (owner_i != prev_owner_q && prev_owner_q != OWN_NONE
&& owner_i != OWN_NONE && prev_active_q)
v_owner_change_active_o <= v_owner_change_active_o + 1;
prev_owner_q <= owner_i;
prev_active_q <= s_cyc_i && s_stb_i && !s_term;
// 2. request-to-completion
if (cpu_busy_i && !cpu_cyc_i)
cpu_pending_quiet_o <= cpu_pending_quiet_o + 1;
if (cpu_cyc_i && owner_i != OWN_CPU)
cpu_req_wait_o <= cpu_req_wait_o + 1;
if (dma_busy_i && !dma_cyc_i)
dma_pending_quiet_o <= dma_pending_quiet_o + 1;
if (dma_cyc_i && owner_i != OWN_DMA)
dma_req_wait_o <= dma_req_wait_o + 1;
// 3. response isolation. Both masters are tested on the same clock
// and the hits are SUMMED - two separate non-blocking assignments to
// one counter would silently keep only the last.
v_nonowner_term_o <= v_nonowner_term_o
+ ((cpu_term && owner_i != OWN_CPU) ? 1 : 0)
+ ((dma_term && owner_i != OWN_DMA) ? 1 : 0);
// 4. request context + no transfer without an owner
if (s_cyc_i && s_stb_i && owner_i == OWN_NONE)
v_xfer_no_owner_o <= v_xfer_no_owner_o + 1;
if (ctx_bad) v_context_mix_o <= v_context_mix_o + 1;
if (s_ack_i) n_ack_o <= n_ack_o + 1;
if (s_err_i) n_err_o <= n_err_o + 1;
if (s_rty_i) n_rty_o <= n_rty_o + 1;
end
end
endmoduleReading it
Every counter in this module comes from here, so the definitions matter more than the code.
cpu_pending_quiet_o counts clocks where the client is busy and CYC_O is negated — layer 1 without layer 3. cpu_req_wait_o counts clocks where CYC_O is asserted and the owner is someone else — layer 3 without layer 2. They are never true on the same clock and neither is derivable from the other.
cpu_acquires_o counts transitions into an owner, which is what a reader means by "it got the bus". owner_changes_o counts every transition including releases to NONE, and in a system whose masters drop CYC_O between transfers it is roughly twice the acquisition count. Reporting one and calling it the other inflates the apparent churn by a factor of two.
ctx_bad compares all four qualified fields at once. RULE 3.60 names the set — "MASTER interfaces MUST qualify the following signals with [STB_O]: [ADR_O], [DAT_O()], [SEL_O()], [WE_O]" — and the check tests the shared path against the owner's own outputs across the whole set, not field by field. Data is compared only on writes, because on a read the shared write-data lines carry nothing anyone should believe.
The response-isolation counter is summed rather than assigned twice. Both masters are tested on the same clock, and two separate non-blocking assignments to one counter would keep only the last — a bug this module had and which would have halved every leak count in Chapter 16.3.
And all of it is simulation-only. int unsigned counters are not hardware. A silicon version of any of this would be narrow counters behind a status register, and none of it is a Wishbone feature — a system that exposes none of it can report only that something is stuck.
7. What the Specification Actually Delegates
Three sentences carry the whole of layer 2, and none of them is a RULE.
Features list — Arbitration methodology is defined by the end user (priority arbiter, round-robin arbiter, etc.).
RECOMMENDATION 3.05 — Arbitration logic often uses
[CYC_I]to select between MASTER interfaces. Keeping[CYC_O]asserted may lead to arbitration problems. It is therefore recommended that[CYC_O]is not indefinitely asserted.
OBSERVATION 3.40 — The asynchronous assertion of
[ACK_O],[ERR_O], and[RTY_O]assures that the interface can accomplish one data transfer per clock cycle. Furthermore, it simplifies the design of arbiters in multi-MASTER applications.
A feature bullet, a recommendation and an observation. Those are the only places in B3 Chapter 3 that mention arbitration at all, and not one of them tells an arbiter what to choose.
What the specification does constrain is the shape of the thing being arbitrated. RULE 3.25 says CYC_O spans the cycle. The [CYC_O] description explains what a bus cycle ending means to an arbiter — and the BLOCK section says it outright:
if the SLAVE is a shared (dual port) memory, then an arbiter for that memory can determine when one MASTER is done with it so that another can gain access to the memory.
"Done with it" is CYC_O negated, and that is exactly the release rule wb_owner_mux2 implements. It is still a choice — the specification describes the signal an arbiter can watch, not the policy it must apply when it sees it.
8. Failure Modes and Discriminating Evidence
Four layers, four sets of evidence.
SYMPTOM — a master's transaction never completes.
Candidates. It never asked (layer 1 stuck). It asked and never owned (layer 2). It owned and the slave never answered (layer 3). It was answered and the answer went elsewhere (Chapter 16.3).
Discriminating evidence. In order: busy, CYC_O, owner, s_stb, the termination. The first of those that is not what you expect names the layer. A bus trace alone cannot do this — it starts at s_stb and the first two questions are already unanswerable.
SYMPTOM — the DMA is slower when the CPU is busy, but nothing fails.
Candidates. Ordinary contention. A release policy that holds the bus longer than the transfer. A master asserting CYC_O between requests.
Discriminating evidence. dma_req_wait minus dma_acquires. That difference is contention and nothing else — in this interconnect, where ownership is one registered state and an uncontended acquisition costs exactly one clock. Establish that baseline on your own design before subtracting it. If the difference is large, look at the other master's CYC_O duty cycle, which is Chapter 16.4's subject.
SYMPTOM — a master appears idle in a bus trace but is not making progress.
Candidates. It is in layer 1 — holding work with CYC_O negated.
Discriminating evidence. busy against CYC_O at the master's own port. The shared path shows nothing, by construction. This is the failure mode that most often gets misattributed to the interconnect, because the interconnect is the only thing a shared-bus trace can see.
SYMPTOM — an arbiter grants the bus to a master that immediately does nothing with it.
Candidates. The grant was keyed on something other than CYC_O — a client-level "busy", for instance.
Discriminating evidence. The clock distance between the grant and the first s_stb. One clock is the register. Several means the selected master was not ready, which means the request signal is the wrong one.
9. Common Mistakes
"CYC_O is the arbitration request."
What is true: it very often is, and the specification describes that use directly.
Why stating it as a fact is wrong: the wording is "in these cases" and "often". There is no REQ or GNT signal in B3. A system may use CYC_I, a separate request line, or a bus-agnostic scheduler. If you assume CYC_O is the request when integrating an unfamiliar interconnect, you will misread its timing.
"If my master asserted CYC_O, it owns the bus."
Why it is wrong: six clocks in SIM D say otherwise. Asserting CYC_O is asking. Nothing returns to the master to tell it the answer — which is why a master that needs to know must be told by something outside the protocol.
"If my master has work pending, it is requesting."
Why it is wrong: five clocks in SIM D say otherwise for each master. The DMA's gap clock is work in hand and no request on the wire, deliberately, because it has nothing to present until it decides the write address.
"Two masters with pending work means two outstanding Wishbone transfers."
Why it is wrong: Classic Wishbone has one transfer on the shared path at a time, and this interconnect presents exactly one. Two pending local requests is a fact about two masters; one presented transfer is a fact about one bus. Conflating them leads people to expect responses that can arrive out of order, which in this profile they cannot.
"The CPU wins because CPUs have priority."
Why it is wrong: the CPU wins a tie on an idle bus, and only there. Once either master owns the path the tie-break is not reached, so it cannot be used to hold the bus. A fixed-priority scheme is a different thing and is Module 17's subject — this is three lines of if/else if in a state machine that is otherwise about retention.
10. Interview Reasoning
"What is the difference between requesting the bus and owning it?"
Requesting is something a master does; owning is something the interconnect decides. A master can request for an unbounded number of clocks. Nothing on the Wishbone interface tells it whether it owns — its only evidence is the absence of a termination, which is also what a slow slave looks like.
"Who decides ownership?"
The interconnect, by a policy the integrator chose. "Arbitration methodology is defined by the end user." There is no default and no rule.
"What does a non-owner do while it waits?"
It keeps its request asserted and it must not believe anything it sees on the shared path. In particular it must not treat another master's termination as its own — Chapter 16.3 measures a system that gets this wrong.
"Your DMA's throughput halves when a second peripheral driver runs. Is that a bug?"
Not by itself. Two masters on one shared path share one path; that is what shared means. It becomes a bug when the delay is unbounded or when data is wrong. The measurement that separates them is req_wait minus acquires — contention cost — and whether it has a ceiling. The ceiling is an arbitration property, not a protocol one.
"A bus trace shows the CPU idle for forty clocks while a transaction is outstanding. What are you looking at?"
You are probably looking at the wrong wire. A shared-path trace cannot show a master holding work with CYC_O negated, nor a master asserting CYC_O and not owning. Both look identical to "idle" from downstream of the interconnect. The evidence is at the master's own port.
11. Understanding Check
In SIM C there was no competitor, yet each acquisition still cost one waiting clock. Why?
Because ownership is registered. The mux samples the request lines at a clock edge and the owner appears after it. That clock is structural, not contention, and subtracting it is how the contention cost is obtained.
A master is busy and CYC_O is low for six clocks. Which layer is it in, and what would a shared-bus trace show?
Layer 1, holding work and presenting nothing. The trace would show nothing at all — the master is indistinguishable from idle downstream of the interconnect.
The DMA drops CYC_O for one clock between its read and its write. Under this interconnect's policy, what does that cost it?
Its ownership. The policy retains an owner only while its CYC_O is asserted, so the gap clock releases the bus and the DMA must reacquire. In SIM C nothing took it; in SIM D something did. This is a deliberate property of this local policy, and a policy that held the bus across the gap would trade that cost for a longer tenure.
Someone proposes keying the arbiter on each master's client-level busy instead of CYC_O. What goes wrong?
The arbiter will grant the bus to a master that is not presenting anything. The DMA is busy during its gap clock and its index-increment clock and has nothing to transfer. The grant is wasted and the other master waits for a tenure that does nothing — which is why RECOMMENDATION 3.05's CYC_I is a better signal than any client-level notion of busy.
12. What's Next
Two masters now contend, and the interconnect between them has been treated as a black box that does the right thing.
What exactly does it have to do — and what happens when it does not?
Chapter 16.3 — Bus Ownership opens the interconnect. Ownership turns out to have two directions rather than one, a request mux without a coherent response demux is broken, and three targeted defects are measured against a conformance monitor that finds nothing wrong with any of them.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
Multi-Master Systems
Byte-identical masters against two interconnects. The ownership timeline names clock 19, and three checkers are validated against the defects they claim to catch.
- Related topic
Why Multiple Masters Exist
The same four-word copy performed twice — by the CPU and by a second initiator — with the bus traffic counted both times, and the specification's own definition of a master.
- Related topic
Arbitration Impact
B3 permits a master to hold CYC_O indefinitely and recommends against it in the same chapter. Measured: a lightly loaded CPU cost the DMA more than a saturated one did.
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.
