Wishbone · Module 8
Block Transfer Cycle
Holding CYC_O across several transfers separates cycle duration from transfer duration, gives the master its own throttle, and makes address advancement a correctness problem.
Chapters 8.1 and 8.2 counted 1 and 1, and both noted that the coincidence was conditional.
What changes when one bus cycle contains several transfers?
1. Not a Burst
Engineers arriving from AXI or AHB will map a block cycle onto a burst. The mapping fails in three places, and each failure produces a different wrong expectation.
| AXI-style burst | Wishbone Classic BLOCK | |
|---|---|---|
| Length | declared up front (AWLEN) | not declared — not on the bus at all |
| Address sequence | inferred from a declared mode | the master drives every address |
| Last beat | signalled (WLAST) | inferred from CYC_O negating |
| Per-transfer handshake | one per beat | one per transfer, same as a single cycle |
| Slave knowledge | knows the block's shape | knows only "another qualified transfer" |
The last row is the one with engineering consequences. A Classic slave cannot tell a block cycle from a sequence of single cycles by looking at any one transfer. It sees a qualified transfer, answers it, and sees another. CYC_O staying asserted is the only difference, and most slaves do not look at it for that purpose.
Which means a correct single-transfer slave already supports block cycles, with no changes — as long as it obeys RULE 3.30 and gates on CYC_I & STB_I. That is a real and slightly surprising benefit of the simplicity.
And it means the master carries all the structure. Transfer count, address sequence, when to stop — all master-side state, none of it on the wires.
2. The Master Can Throttle Too
Modules 6 and 7 showed one throttle: the slave withholds its termination and the master waits. A block cycle has a second one, and it is the reason CYC_O and STB_O must be separable signals.
The BLOCK sections describe wait states being inserted by the master — by negating STB_O — as well as by the slave.
| Throttle | Mechanism | State on the bus |
|---|---|---|
| Slave-side | withhold ACK_O | CYC & STB asserted, no termination |
| Master-side | negate STB_O | CYC asserted, STB negated |
That second state never occurred in Modules 6 or 7. Chapter 8.1's simulation measured gaps = 0 in every run and called it the PERMISSION 3.40 signature.
It is not an error and not a stall in the usual sense. It means: I still own this operation, and I am not asking for anything at this instant. A master uses it when it needs a cycle to compute the next address, or when its client has not supplied the next data word yet.
And it closes the loop on PERMISSION 3.40:
"If a MASTER doesn't generate wait states, then [STB_O] and [CYC_O] MAY be assigned the same signal."
A block master that ever pauses cannot tie them. The permission's precondition is exactly the thing a block master gives up.
3. The Running System Gains a Window
Block transfers need sequential addresses, and the peripheral from Modules 6 and 7 is a register map rather than a memory — its offsets are meaningful individually, not as a run.
So the device gains a small window, and nothing else changes:
| Byte | Word | Register | Access |
|---|---|---|---|
0x08 | 2 | CONTROL | read/write |
0x18 | 6 | OUTPUT_DATA | read/write |
0x30–0x3C | 12–15 | WINDOW[0..3] | read/write, 4 sequential words |
Why it exists, stated plainly: four consecutive addresses that mean something as a run, so a block cycle has a natural target. Everything Modules 6 and 7 named keeps its offset and its access rules, and word 7 stays unmapped exactly as Chapter 6.1 §8 and Chapter 6.6's Trace B rely on.
It is deliberately four words. A larger window would invite a memory-controller tutorial, which is not this module's job.
4. RTL — A Block Master and a Window
// ─────────────────────────────────────────────────────────────────────────
// wb_block_master — a block cycle controller.
//
// PURPOSE. Hold ONE bus cycle across N transfers. This is Chapter 8.2's
// wb_single_cycle_ctrl with exactly three additions:
// * a transfer counter
// * an address that advances
// * a cycle that does NOT end when a transfer terminates
// Everything else is unchanged, which is the point: a block cycle is a
// small extension of the single-cycle skeleton, not a new protocol.
//
// ── THE CENTRAL CORRECTNESS RULE ─────────────────────────────────────
// The address and the transfer index advance on TERMINATION, never on
// elapsed clocks. A transfer may be presented for many cycles (slave
// waits) and is still ONE transfer — the same counting rule Chapters
// 6.5 and 7.4 established inside slaves, here applied to sequencing.
//
// CYC/STB ARE SEPARATE. This master pauses between transfers (S_GAP), so
// PERMISSION 3.40's precondition does not hold and the two signals cannot
// share a register.
//
// DIRECTION. we_o is a passthrough, constant for the whole block. Nothing
// in the sequencing branches on it — BLOCK READ and BLOCK WRITE are the
// same controller, per Chapter 8.2 Section 1.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00, 3.20).
// ─────────────────────────────────────────────────────────────────────────
module wb_block_master #(
parameter int unsigned AW = 30,
parameter int unsigned DW = 32,
parameter int unsigned CW = 4 // transfer-count width
) (
input logic clk_i,
input logic rst_i,
// ── local client side ───────────────────────────────────────────────
input logic req_i,
input logic req_we_i,
input logic [AW-1:0] req_base_i, // first WORD address
input logic [CW-1:0] req_len_i, // number of transfers, >= 1
input logic [DW-1:0] req_dat_i, // write payload (see SIMPLIFICATIONS)
input logic gap_en_i, // insert a master wait between transfers
output logic busy_o,
output logic done_o, // one pulse when the CYCLE completes
output logic [CW-1:0] xfers_o, // transfers completed this cycle
output logic [DW-1:0] last_dat_o, // last read value captured
// ── Wishbone MASTER interface ───────────────────────────────────────
output logic cyc_o,
output logic stb_o,
output logic we_o,
output logic [AW-1:0] adr_o,
output logic [DW-1:0] dat_o,
output logic [DW/8-1:0] sel_o,
input logic [DW-1:0] dat_i,
input logic ack_i,
input logic err_i,
input logic rty_i
);
// ── STATE ─────────────────────────────────────────────────────────────
// S_XFER : a transfer is presented (CYC=1, STB=1)
// S_GAP : the cycle is held, nothing asked (CYC=1, STB=0) <-- NEW
// This is the master-side throttle of Section 2, and the state
// that could not exist in Chapters 8.1 and 8.2.
typedef enum logic [1:0] { S_IDLE, S_XFER, S_GAP } state_e;
state_e state_q;
logic [AW-1:0] adr_q;
logic [CW-1:0] left_q;
logic we_q;
logic [DW-1:0] dat_q;
logic gap_q;
// ── THE SEPARATION ────────────────────────────────────────────────────
// cyc_o covers BOTH active states; stb_o covers only S_XFER.
assign cyc_o = (state_q == S_XFER) || (state_q == S_GAP);
assign stb_o = (state_q == S_XFER);
assign we_o = we_q;
assign adr_o = adr_q;
assign dat_o = dat_q;
assign sel_o = '1; // full-word transfers; see Module 13
assign busy_o = (state_q != S_IDLE);
logic terminated;
assign terminated = ack_i | err_i | rty_i;
always_ff @(posedge clk_i) begin
if (rst_i) begin
state_q <= S_IDLE; // RULE 3.20 via the encoding
adr_q <= '0;
left_q <= '0;
we_q <= 1'b0;
dat_q <= '0;
gap_q <= 1'b0;
done_o <= 1'b0;
xfers_o <= '0;
last_dat_o <= '0;
end else begin
done_o <= 1'b0;
unique case (state_q)
S_IDLE: begin
if (req_i && (req_len_i != '0)) begin
// ── CYCLE START ─────────────────────────────────────────────
// Once per block, not once per transfer.
state_q <= S_XFER;
adr_q <= req_base_i;
left_q <= req_len_i;
we_q <= req_we_i;
dat_q <= req_dat_i;
gap_q <= gap_en_i;
xfers_o <= '0;
end
end
S_XFER: begin
// ── TRANSFER END ────────────────────────────────────────────
// Everything here is gated on `terminated`. Nothing advances on
// elapsed clocks — that is the bug wb_block_master_fastadv has.
if (terminated) begin
if (ack_i && !we_q) last_dat_o <= dat_i; // Chapter 6.3's window
xfers_o <= xfers_o + CW'(1);
if (left_q == CW'(1)) begin
// ── CYCLE END ─────────────────────────────────────────────
// The LAST transfer's termination is also the cycle's end,
// so CYC_O and STB_O negate together — which is how a block
// cycle ends in Classic, there being no last-transfer flag.
state_q <= S_IDLE;
done_o <= 1'b1;
end else begin
// ── NEXT TRANSFER ─────────────────────────────────────────
// Advance ONLY here. The address moves because a transfer
// COMPLETED, not because a clock passed.
left_q <= left_q - CW'(1);
adr_q <= adr_q + AW'(1);
// Written as if/else rather than a ternary: assigning an enum
// from a conditional expression needs an explicit cast, and
// Icarus rejects the bare form (the portability wart recorded
// in Chapter 4.3).
if (gap_q) state_q <= S_GAP;
else state_q <= S_XFER;
end
end
end
S_GAP: begin
// The cycle is held with nothing presented. A slave sees CYC_I
// asserted and STB_I negated: RULE 3.35 forbids it to terminate
// and it simply waits. One cycle here; a real master might stay
// until its client supplies the next word.
state_q <= S_XFER;
end
default: state_q <= S_IDLE;
endcase
end
end
endmodule// ─────────────────────────────────────────────────────────────────────────
// wb_window_slave — four sequential words, so a block has a target.
//
// PURPOSE. Give the running peripheral a small run of addresses. It is a
// register file, not a memory controller — four words, no latency model,
// no burst awareness.
//
// NOTE WHAT IS ABSENT: this slave has NO knowledge of block cycles. It
// answers each qualified transfer on its own terms. That is Section 1's
// claim in executable form — a correct single-transfer slave already
// supports blocks, provided it gates on CYC_I & STB_I per RULE 3.30.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00).
// ─────────────────────────────────────────────────────────────────────────
module wb_window_slave #(
parameter int unsigned OFF_AW = 4,
parameter int unsigned DW = 32,
parameter int unsigned LAT = 0 // wait states, for SIM D
) (
input logic clk_i,
input logic rst_i,
input logic cyc_i,
input logic stb_i,
input logic we_i,
input logic [OFF_AW-1:0] adr_i,
input logic [DW-1:0] dat_i,
input logic [DW/8-1:0] sel_i,
output logic [DW-1:0] dat_o,
output logic ack_o,
output logic err_o,
// observable for QA
output logic [DW-1:0] w0_o,
output logic [DW-1:0] w1_o,
output logic [DW-1:0] w2_o,
output logic [DW-1:0] w3_o
);
localparam int unsigned NL = DW/8;
localparam logic [OFF_AW-1:0] W_BASE = 4'd12; // words 12..15
logic [DW-1:0] win_q [4];
assign w0_o = win_q[0];
assign w1_o = win_q[1];
assign w2_o = win_q[2];
assign w3_o = win_q[3];
logic xfer;
assign xfer = cyc_i & stb_i; // RULES 3.30 & 3.35
logic in_window;
assign in_window = (adr_i >= W_BASE) && (adr_i <= (W_BASE + 4'd3));
logic [1:0] idx;
assign idx = adr_i[1:0]; // words 12..15 -> 0..3
// ── LATENCY, only so SIM D can delay one transfer ──────────────────────
// `ready` is declared and assigned before the block that uses it.
logic [7:0] waited_q;
logic ready;
assign ready = (waited_q >= 8'(LAT));
always_ff @(posedge clk_i) begin
if (rst_i) waited_q <= '0;
else if (!xfer) waited_q <= '0;
else if (!ready) waited_q <= waited_q + 8'd1;
else waited_q <= '0;
end
assign err_o = xfer & ready & ~in_window;
assign ack_o = xfer & ready & in_window;
logic commit;
assign commit = xfer & we_i & ack_o; // Module 7's commit policy
always_ff @(posedge clk_i) begin
if (rst_i) begin
for (int unsigned i = 0; i < 4; i++) win_q[i] <= '0;
end else if (commit) begin
for (int unsigned n = 0; n < NL; n++)
if (sel_i[n]) win_q[idx][n*8 +: 8] <= dat_i[n*8 +: 8];
end
end
always_comb begin
dat_o = '0; // RULE 3.65
if (xfer && !we_i && ack_o) dat_o = win_q[idx];
end
endmodule// ─────────────────────────────────────────────────────────────────────────
// wb_block_master_fastadv — THE EARLY-ADVANCE BUG, isolated.
//
// NOT A REFERENCE DESIGN. Identical to wb_block_master except that the
// address and the counter advance on every clock the transfer is
// presented, rather than on termination:
//
// if (terminated) ... becomes unconditional advance
//
// Against a zero-wait-state slave every transfer terminates in the cycle
// it is presented, so the two masters are INDISTINGUISHABLE. Introduce one
// wait state and the address runs ahead of the transfers.
// ─────────────────────────────────────────────────────────────────────────
module wb_block_master_fastadv #(
parameter int unsigned AW = 30,
parameter int unsigned DW = 32,
parameter int unsigned CW = 4
) (
input logic clk_i,
input logic rst_i,
input logic req_i,
input logic req_we_i,
input logic [AW-1:0] req_base_i,
input logic [CW-1:0] req_len_i,
input logic [DW-1:0] req_dat_i,
output logic busy_o,
output logic done_o,
output logic [CW-1:0] xfers_o,
output logic cyc_o,
output logic stb_o,
output logic we_o,
output logic [AW-1:0] adr_o,
output logic [DW-1:0] dat_o,
output logic [DW/8-1:0] sel_o,
input logic [DW-1:0] dat_i,
input logic ack_i,
input logic err_i,
input logic rty_i
);
logic active_q, we_q;
logic [AW-1:0] adr_q;
logic [CW-1:0] left_q;
logic [DW-1:0] dat_q;
assign cyc_o = active_q;
assign stb_o = active_q;
assign we_o = we_q;
assign adr_o = adr_q;
assign dat_o = dat_q;
assign sel_o = '1;
assign busy_o = active_q;
always_ff @(posedge clk_i) begin
if (rst_i) begin
active_q <= 1'b0; adr_q <= '0; left_q <= '0; we_q <= 1'b0;
dat_q <= '0; done_o <= 1'b0; xfers_o <= '0;
end else begin
done_o <= 1'b0;
if (!active_q) begin
if (req_i && (req_len_i != '0)) begin
active_q <= 1'b1; adr_q <= req_base_i; left_q <= req_len_i;
we_q <= req_we_i; dat_q <= req_dat_i; xfers_o <= '0;
end
end else begin
if (ack_i || err_i || rty_i) xfers_o <= xfers_o + CW'(1);
// ── THE BUG: advance every clock, regardless of termination ────
if (left_q == CW'(1)) begin
active_q <= 1'b0; done_o <= 1'b1;
end else begin
left_q <= left_q - CW'(1);
adr_q <= adr_q + AW'(1);
end
end
end
end
endmoduleReading the group
Purpose. The block master holds one cycle across N transfers; the window gives it somewhere to go; the fast-advance master shows what happens when sequencing keys off clocks instead of terminations.
Interface. The client asks for a base address and a length. done_o pulses once per cycle, not once per transfer — that is the client-visible consequence of the whole chapter.
State. Three states, an address, a remaining count, a direction, a payload and a gap flag. state_q is no longer a single "active" bit, and that is precisely the change from Chapter 8.2's controller.
Combinational logic. The two qualifier expressions — which now differ — and the bus outputs from state.
Sequential logic. Capture at request; advance on termination; release after the last.
Cycle start. Once, at S_IDLE → S_XFER.
Transfer start. Repeatedly, at each entry to S_XFER.
Termination. Sampled as a level in S_XFER; everything that advances is inside that branch.
Next transfer. Via S_GAP if enabled, otherwise straight back to S_XFER — a back-to-back block with no gap.
Cycle end. When the last transfer terminates. CYC_O and STB_O negate together, which is how a Classic block ends.
Reset. Synchronous, active high; S_IDLE negates both qualifiers.
Failure modes. wb_block_master_fastadv is one; Section 7 covers the rest.
Simplifications. One write payload for the whole block rather than a per-transfer stream — a real block-write master takes a word per transfer from its client, and that plumbing would obscure the sequencing this chapter is about. SEL_O is tied to all-ones; Module 13 owns byte selects. The window has no realistic latency source; LAT exists only so SIM D can delay a transfer.
5. Waveform — One Cycle, Four Transfers
A block read cycle: CYC spans, STB repeats
10 cyclesCount it. Bus cycles = 1. Transfers = 4 — four acknowledges. CYC_O asserted for 6 clocks.
Cycle 4 is the state Modules 6 and 7 never produced. CYC_O asserted, STB_O negated: the master owns the cycle and is asking for nothing. The slave does nothing, correctly — RULE 3.35 requires a termination to be generated from the AND of CYC_I and STB_I.
The address advances only after an acknowledge. It reads 0xC for transfer 0, 0xD for transfer 1, and then stands still through the gap in cycle 4 before moving to 0xE for transfer 2 — which the slave delays, so 0xE holds across cycles 5 and 6 too. It is driven by completions, not by clocks, and Section 6 measures what happens when it is not.
The cycle ends at the last transfer's termination, with both qualifiers negating together. Nothing on the bus announced that transfer 3 was the last — the master simply stopped, which is how a Classic block ends.
6. Simulation — Three Block Runs
SIM C — four transfers, immediate response.
=== SIM C - block, immediate ACK ===
bus cycles begun 1
transfers completed 4
reads / writes 4 / 0
wait cycles 0
gaps (CYC & !STB) 0
clocks with CYC high 4
addresses presented 0xc 0xd 0xe 0xf
done pulses 1One cycle, four transfers, four clocks. The address sequence is exactly the four window words, and done_o pulsed once — for the cycle, not per transfer.
SIM D — every transfer delayed by one wait state, plus a master gap.
=== SIM D - block with waits and a master gap ===
bus cycles begun 1
transfers completed 4
wait cycles 4
gaps (CYC & !STB) 3
clocks with CYC high 11
addresses presented 0xc 0xd 0xe 0xf
address changes while a transfer was outstanding 0
done pulses 1Still one cycle and still four transfers. The cycle grew from 4 clocks to 11 — four slave wait states and three master gaps added to the four terminating clocks — and the content did not change.
gaps = 3 — the master-side throttle, and the number that was zero in every Module 6, 7, 8.1 and 8.2 run. That single counter is the clearest evidence a trace is a block cycle rather than a run of single cycles.
Note what the two throttle counters separate. waits = 4 is the slave holding four clocks; gaps = 3 is the master holding three. Both lengthen the same cycle, and only by reading STB_O can you tell which side spent the time — the distinction Section 2 argued is new in this chapter.
address changes while a transfer was outstanding = 0 is the correctness check: the address moved between transfers and never during one.
SIM E — the early-advance master, same stimulus.
=== SIM E - early address advance (1 wait state) ===
correct master addresses presented 0xc 0xd 0xe 0xf
transfers completed 4
addresses acknowledged 0xc 0xd 0xe 0xf
fast-advance addresses presented 0xc 0xd 0xe 0xf
transfers completed 2
address changes while outstanding 2
addresses acknowledged 0xd 0xfRead the two address lines, not the transfer count. Both masters presented the same four window words. The broken one acknowledged only two of them — 0xd and 0xf.
WINDOW[0] and WINDOW[2] were never read at all. The fast-advance master put 0xc on the bus, the slave took its one wait state, and before that transfer could terminate the master had already moved to 0xd. The acknowledge that arrived on the next clock terminated a transfer whose address was 0xd. The same thing happened to 0xe. Two of the four requested words silently vanished, and the two that did return were the wrong ones for the slots the master was filling.
This is the failure shape worth remembering: the addresses look right on a bus trace. Every word 0xc–0xf appears. Nothing is out of range, nothing is unmapped, no ERR_I is raised, and the cycle terminates normally. Only by correlating each address against its own termination does the loss become visible — which is exactly what address changes while outstanding = 2 counts, and it is a RULE 3.60 violation twice over.
The master's own bookkeeping records the damage: transfers completed = 2 against a request for four. A block master that reports fewer transfers than it asked for has usually failed this way — the counter is honest even when the bus trace looks clean.
At zero wait states the two masters are identical — SIM C's run is bit-for-bit the same for both. The bug needs latency to appear, which is the same structural property Chapter 6.2 and Chapter 7.4 both measured — and the reason a block master's unit test must include a waiting slave.
7. Failure Modes and Discriminating Evidence
Symptom: a block skips addresses, and the transfer count is short.
Candidate causes. The address advances on elapsed clocks rather than on terminations.
Discriminating evidence. Watch ADR_O across a presented transfer. Any change before its termination is a RULE 3.60 violation and is conclusive. Do not look for gaps in the presented address sequence — SIM E shows there may be none. Compare the addresses acknowledged against the addresses presented: under one wait state the acknowledged set is half the presented set, and it is the acknowledged set that says which words actually moved.
Likely RTL location. The advance's enable — outside the terminated branch.
Property. P3 in Section 8.
Symptom: a block repeats an address.
Candidate causes. The advance is inside the termination branch but the state machine re-enters S_XFER without it — or the termination is detected twice for one transfer.
Discriminating evidence. Count acknowledges against distinct addresses. More acknowledges than addresses means a transfer was counted twice; equal counts with a repeated address means the advance was skipped on one iteration.
Symptom: the block stops after the first transfer.
Candidate causes. The master released CYC_O at the first termination — the single-cycle behaviour it inherited.
Discriminating evidence. CYC_O negating at the first acknowledge. The monitor reports this as several bus cycles rather than one, which is the cleanest signature: cycles = 4, transfers = 4 is a run of single cycles, cycles = 1, transfers = 4 is a block.
Likely RTL location. The cycle-end condition — wb_block_master ends the cycle only when left_q == 1.
Symptom: a slave responds correctly to single cycles and misbehaves inside a block.
Candidate causes. The slave keys something off CYC_I rising, or holds internal state across what it assumes are cycle boundaries.
Discriminating evidence. CYC_I rises once for the whole block. A slave counting cycle starts, or resetting per-cycle state on that edge, sees one where it expected four.
Correct model: a slave should key on the qualified transfer, never on the cycle. wb_window_slave has no CYC_I edge logic at all.
Symptom: the master hangs mid-block with CYC_O high and STB_O low.
Candidate causes. The gap state has no exit — a master waiting for a client that never supplies the next word.
Discriminating evidence. gaps climbing without bound while transfers is static. Distinguishable from a slave-side hang, where STB_O would be high with no termination.
8. Verification
// ─────────────────────────────────────────────────────────────────────────
// wb_block_checker — block-cycle properties.
//
// P1 and P2 are SPECIFICATION (RULES 3.25, 3.60). P3 and P4 are LOCAL
// DESIGN POLICY describing THIS master's sequencing — the specification
// says nothing about how a block master computes its addresses, so nothing
// here can be a Wishbone rule.
//
// NOTE what is ABSENT: Chapter 8.1's P3 ("the cycle ends with its
// transfer"). A block master violates it by design, and this checker
// REPLACES it with P4 rather than simply dropping it.
// ─────────────────────────────────────────────────────────────────────────
module wb_block_checker #(
parameter int unsigned AW = 30,
parameter int unsigned CW = 4
) (
input logic clk_i,
input logic rst_i,
input logic cyc_o,
input logic stb_o,
input logic [AW-1:0] adr_o,
input logic ack_i,
input logic err_i,
input logic rty_i,
// white-box
input logic [CW-1:0] left_q,
input logic [CW-1:0] xfers_q,
input logic done_o
);
default disable iff (rst_i);
logic terminated;
assign terminated = ack_i | err_i | rty_i;
// P1 — SPECIFICATION (RULE 3.25). A strobe never exists outside a cycle.
// Unchanged from Chapters 8.1 and 8.2: this rule does not care how
// many transfers the cycle contains.
property p_stb_inside_cyc;
@(posedge clk_i) stb_o |-> cyc_o;
endproperty
a_stb_inside_cyc : assert property (p_stb_inside_cyc)
else $error("RULE 3.25: STB_O asserted outside a bus cycle");
// P2 — SPECIFICATION (RULE 3.60). The address is stable while its
// transfer is outstanding. This is what the fast-advance master
// violates, and it fires on the FIRST early advance rather than
// when the address sequence is noticed to be wrong.
property p_addr_stable_while_outstanding;
@(posedge clk_i) (cyc_o && stb_o && !terminated) |=> $stable(adr_o);
endproperty
a_addr_stable_while_outstanding :
assert property (p_addr_stable_while_outstanding)
else $error("RULE 3.60: ADR_O moved while a transfer was outstanding");
// P3 — LOCAL POLICY. The transfer index advances ONLY on a termination.
// The sequencing form of the counting rule Chapters 6.5 and 7.4
// established for side effects.
property p_index_advances_on_termination;
@(posedge clk_i) $changed(xfers_q) |-> $past(cyc_o && stb_o && terminated);
endproperty
a_index_advances_on_termination :
assert property (p_index_advances_on_termination)
else $error("LOCAL: transfer index advanced without a termination");
// P4 — LOCAL POLICY, REPLACING Chapter 8.1's P3. The cycle ends only
// when the LAST transfer terminates — not when any transfer does.
// This is the block-cycle form of "the cycle ends with its work".
property p_cycle_ends_only_at_last;
@(posedge clk_i) ($fell(cyc_o)) |-> $past(terminated && (left_q == CW'(1)));
endproperty
a_cycle_ends_only_at_last : assert property (p_cycle_ends_only_at_last)
else $error("LOCAL: cycle ended before the last transfer");
endmoduleP4 is the point of this checker. Chapter 8.1 §7 wrote a property saying the cycle ends with its transfer and labelled it LOCAL POLICY with a note that it would have to be deleted here. It is not deleted — it is replaced, and the replacement says the same thing at the right granularity.
That is what a policy property should do when a design grows. Dropping it would lose the invariant entirely; keeping it unchanged would fail on a correct design.
Tooling limitation. Icarus Verilog has no SVA support and cannot execute any of these. They are reviewed by inspection; all three modules are elaborated and the three simulations above were run.
9. Common Mistakes
"A block transfer is a Wishbone burst."
Wrong mental model: importing AXI/AHB burst structure.
Concrete bug: a slave written to expect a declared length or a last-beat flag, or a master that assumes the slave infers the address sequence.
Observable evidence: a slave that works with single cycles and mis-sequences inside a block; or RTL looking for CTI_O/BTE_O in a B3 Classic design, where they do not exist.
Correct model: Classic has no length, no last-beat signal and no address mode. The master drives every address and stops by negating both qualifiers. B4's pipelined mode adds tags; that is a different profile.
"The address should advance every clock during a block."
Wrong mental model: one transfer per clock.
Concrete bug: the fast-advance master. Measured: it presented 0xC 0xD 0xE 0xF — the correct addresses — but acknowledged only 0xD and 0xF, completing 2 of the 4 requested transfers.
Observable evidence: a short transfer count, and an acknowledged-address set smaller than the presented one. The presented sequence alone looks correct, which is why this bug is easy to miss.
Correct model: advance on termination. A transfer may occupy many clocks and is still one transfer.
"ACK ends the block."
Wrong mental model: the termination releases the cycle.
Concrete bug: a master that negates CYC_O at the first acknowledge, turning one block into four single cycles.
Observable evidence: the monitor reports cycles = 4, transfers = 4 instead of cycles = 1, transfers = 4.
Correct model: ACK terminates a transfer. The master's cycle logic decides when the cycle ends — per RULE 3.25, it must span from the first transfer to the last.
"CYC high with STB low means something is wrong."
Wrong mental model: the qualifiers always move together.
Concrete bug: none — but a reader who believes this will misdiagnose a normal master gap as a fault and look for a bug that is not there.
Observable evidence: time wasted investigating a correct trace.
Correct model: it is the master-side throttle, described in the BLOCK sections and counted by the monitor as gaps. It is the signature of a block cycle.
10. Interview Reasoning
Because a transfer may occupy many clocks and is still one transfer — the address belongs to the transfer, not to the clock.
The mechanism when it is wrong. With a slave inserting one wait state, each transfer is presented for two clocks. A master advancing every clock moves the address underneath its own outstanding transfer — a RULE 3.60 violation, since the address is qualified by STB_O and must be stable while the transfer is presented.
What I measured. The correct master presented 0xC 0xD 0xE 0xF and acknowledged all four. The fast-advance master presented the same four addresses and acknowledged only 0xD and 0xF — two address changes while a transfer was outstanding, and two of the four window words never read.
The part I would emphasise in an interview is that the presented addresses were identical. The broken master did not walk out of range, raise an error, or produce an obviously malformed trace. The damage is entirely in the pairing — which address was live when each acknowledge arrived. A defect that leaves the address sequence looking correct is the argument for checking address stability across a transfer rather than eyeballing the address list.
Two distinct consequences, which is worth separating. The slave serves whatever address happens to be present when it terminates, so the data is wrong. And the master's transfer count and the number of words it believes it moved come apart, so the sequence is wrong. Either alone would be a bug; together they make the trace hard to read.
Why it survives testing. At zero wait states every transfer terminates in the cycle it is presented, so the two masters are byte-identical. The bug needs latency to exist at all — the same structural property that hid the read-address drift in Chapter 6.2 and the write payload drift in Chapter 7.4.
The rule to state generally. Anything that advances a sequence — an address, an index, a pointer, a descriptor — advances on completion, never on elapsed time. That is the same counting rule as "a side effect fires once per accepted transfer", applied to sequencing instead of to state.
And the property that catches it is conformance rather than policy: (cyc && stb && !terminated) |=> $stable(adr). No white-box access needed, so it catches third-party masters too.
11. Understanding Check
12. What's Next
CYC_O and STB_O are now visibly different signals, a cycle's duration is visibly different from its content, and sequencing is visibly a function of completions rather than clocks.
Every transfer in that block was independent — four reads that happened to be adjacent. The next cycle type holds two transfers that are not independent at all: the second one's data is computed from the first one's result, and the whole point is that nothing intervenes between them.
How can one bus cycle contain a read followed by a related write — and what exactly does that guarantee?
Chapter 8.4 — Read-Modify-Write Cycle answers it. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
Need for Standardized Interconnects
An address map answers where a register lives. It says nothing about which wires carry the request, when they are valid, how the target reports completion, or what happens on an error. Three peripherals with three private interfaces produce three adapters, three verification efforts and three ways to be wrong — which is the argument for standardising the interface rather than the map.
- Related topic
The Open Hardware Movement
Source availability and reusability are different properties. A published core tells you what it does; it does not tell you what it requires, and requirements are what integration runs on. What reusable open IP needs beyond the RTL — licensing, documentation, an interface contract, verification, maintenance — and where open hardware is honestly weaker than its advocates claim.
- Related topic
SoC Communication
Six chapters built the pieces; this one assembles them into a working fabric and traces three real accesses through it. The result works, and reading the nine unwritten rules a third party would need is what makes the case for a published protocol concrete rather than theoretical.
- Related topic
Transaction Lifecycle
One Wishbone transaction from a master's decision to act through the slave's termination and back to the caller: what is fixed by the protocol, what every implementation may vary, and what a real simulation of the assembled system shows at each step.
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.
