Skip to content
VLSI Mentor

Wishbone · Module 8

Read-Modify-Write Cycle

One CYC_O across a read and a related write is the whole of what the specification defines. Measured runs show the same conformant master losing mutual exclusion when only the arbiter's policy changes.

Chapter 8.3 put four independent transfers under one cycle. This chapter puts two transfers under one cycle that are not independent at all: the second one's data is computed from the first one's result.

What does holding the cycle across both of them actually buy you?

1. The Phase Structure

An RMW cycle is one bus cycle containing exactly two transfers, in a fixed order and with a fixed relationship:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BUS CYCLE                       ← CYC_O asserted throughout
  ├── TRANSFER 0   READ         ← STB_O, WE_O low,  terminated by ACK
  │      ↓ the value read feeds the value written
  └── TRANSFER 1   WRITE        ← STB_O, WE_O high, terminated by ACK
                                ← CYC_O negates with the last STB_O

Compare that hierarchy against Chapter 8.3's and the structural difference is small: one cycle, several transfers, CYC_O spanning. The differences are in the details, and all three matter.

WE_O changes inside the cycle. Every cycle in Modules 6, 7, 8.1, 8.2 and 8.3 had one direction. This one does not. WE_O is low for transfer 0 and high for transfer 1, which means it is not a per-cycle constant — it is qualified by STB_O like every other master output (RULE 3.60), and this is the first cycle in the module that proves it.

The transfer count is fixed at two. A block cycle's length is a master's business; an RMW's is the cycle type's definition.

The second transfer's data depends on the first transfer's result. A block's four transfers could be issued in any order without changing the outcome. These two cannot be reordered, cannot be separated, and cannot be issued concurrently.

That dependency is why the cycle type exists. It is also, as Section 2 shows, the only thing the specification does anything about.

2. What the Specification Guarantees — Precisely

This section is the chapter. Everything else here is mechanism; this is the part that is usually stated wrongly, including in places that ought to know better.

The claim to be careful with

"Holding CYC_O makes the operation atomic. No other master can get in between the read and the write."

The first sentence overstates a real mechanism. The second sentence is not something Wishbone says.

What section 3.4 actually contains

I checked, and the boundary is sharp. Section 3.4 specifies the timing of a read phase followed by a write phase under one continuous assertion of CYC_O. It contains no locking mechanism, no arbitration rule, and no statement about other masters at all. LOCK_O does exist in B3 — and §3.4 does not use it. The RMW walkthrough and its normative figure name CYC_O, STB_O, ACK_I, WE_O and the payload signals, and no lock; the BLOCK figure, by contrast, shows LOCK_O alongside CYC_O. Chapter 15.2 takes up what that difference means. There is also nothing a slave can inspect to learn that it is inside an RMW rather than serving two unrelated cycles.

So the honest decomposition has three parts, and only the first is Wishbone's:

What it isWho provides it
Protocol structureOne CYC_O spanning a read transfer and a write transferRULE 3.85 / §3.4 — the specification
Bus exclusivityNobody else is granted the shared bus while that CYC_O is assertedThe arbiter, by policy
Target exclusivityNothing else can reach the location by another routeThe system, by construction

Wishbone defines row one. It deliberately does not define row two — arbitration is an integrator decision, and the closest the specification comes is an observation about common practice:

RECOMMENDATION 3.05 notes that arbitration logic "often uses [CYC_I] to select between MASTER interfaces."

Read the word "often". It is a description of what designers usually do, in a RECOMMENDATION, not a requirement placed on anybody. An arbiter that re-grants at every transfer boundary violates nothing. Section 7 builds one, runs the same conformant master against it, and measures what happens.

Row three is outside the bus entirely. Chapter 4.9 measured a lost update through a slave's second port with CYC_O held perfectly throughout — no bus-level mechanism can address that, because the competing access never appeared on the bus.

The formulation worth memorising

Holding CYC_O across both halves is the protocol half of atomicity, and it is the half you are responsible for.

Indivisibility additionally requires an arbiter that honours the request and a target with no second path. Neither is supplied by the specification, and neither is visible on your interface.

Why be this careful about a phrasing? Because the loose version is not merely imprecise — it points debugging in the wrong direction. An engineer who believes CYC_O is the guarantee will inspect the master when a semaphore fails, find it perfectly conformant, and have nowhere to go. Section 8 measures a failure of exactly that shape.

3. The Counter Signature

Chapter 8.1's monitor gives this chapter a sharp instrument, because an RMW has a counter signature nothing else in the module produces:

Cyclecyclestransfersreadswrites
Single read1110
Single write1101
Block read of 41440
RMW1211
A read then a write, as two cycles2211

Look at the last two rows. Reads, writes and transfers are identical. The only number that differs is the bus-cycle count — and a bus-cycle count is a count of CYC_O rising edges.

That is the entire observable difference between an RMW and a read followed by a write. Same addresses, same data, same acknowledges, same total clocks on a quiet bus. One integer, derived from one signal, separates a semaphore operation from two unrelated accesses.

Which explains why the bug in Section 7 is so easy to ship. A master that drops CYC_O between the halves still reads the right word, still computes the right value, still writes it back, and still reports success. Every transfer it performs is conformant. The only evidence is a counter nobody was reading.

4. The Running System Gains a Semaphore

The peripheral picks up one register, at the first free word below the block window:

ByteWordRegisterAccessIntroduced
0x000STATUSRO6.1
0x041COUNTRO, free-running6.1
0x082CONTROLRW6.1
0x0C3INPUT_DATARO6.3
0x104IDRO6.1
0x145IRQread-to-clear6.5
0x186OUTPUT_DATARW7.1
0x208EVENTSW1C7.4
0x249COMMANDWO, pulses7.4
0x2810SEMAPHORERW8.4
0x30–0x3C12–15WINDOW[0..3]RW8.3

SEMAPHORE is a plain read/write register. It has no test-and-set behaviour, no lock bit, no side effect, and no knowledge of RMW cycles. That is deliberate and it is not a simplification — Classic Wishbone gives a slave nothing to key off. The slave cannot distinguish an RMW's read from any other read, so any protection has to come from somewhere else. Building the slave "smarter" would hide the very thing this chapter is measuring.

Word 7 remains unmapped, because Chapter 6.1 and Chapter 6.6 both read it to produce an ERR — the reason a new register goes at word 10 rather than filling the first free slot.

5. RTL — A Cycle, a Register, and a Policy

Three modules, and the third is the interesting one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// wb_rmw_master — a test-and-set built as ONE Wishbone RMW cycle.
//
// §3.4 defines the RMW cycle as a read phase and a write phase under a
// single assertion of CYC_O. This master implements exactly that and
// nothing more. It does NOT "lock" anything: Classic Wishbone has no lock
// signal. Holding CYC_O is a bus REQUEST that an arbiter may or may not
// honour (RECOMMENDATION 3.05 says arbitration "often" uses CYC_I).
//
// MOD_CLK selects where the modify happens:
//   0 — combinationally from DAT_I at the read's termination. The write is
//       presented on the very next edge, with no gap. This matches the
//       timing figure in §3.4.
//   1 — in a registered state between the halves. CYC_O stays asserted with
//       STB_O negated for one clock: a MASTER-inserted wait state, the same
//       mechanism Chapter 8.3 used inside a block.
// ─────────────────────────────────────────────────────────────────────────
module wb_rmw_master #(
  parameter int unsigned AW      = 30,
  parameter int unsigned DW      = 32,
  parameter bit          MOD_CLK = 1'b0
) (
  input  logic            clk_i,
  input  logic            rst_i,
  input  logic            req_i,
  input  logic [AW-1:0]   req_adr_i,
  input  logic [DW-1:0]   req_mask_i,

  output logic            busy_o,
  output logic            done_o,
  output logic            got_o,        // the bit was clear when WE read it
  output logic [DW-1:0]   read_dat_o,
  output logic [DW-1:0]   wrote_dat_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
);
  typedef enum logic [1:0] { S_IDLE, S_READ, S_MOD, S_WRITE } state_e;
  state_e state_q;

  logic [AW-1:0] adr_q;
  logic [DW-1:0] mask_q, wr_q;
  logic          terminated;

  // ── CYC_O spans BOTH halves. This is the whole protocol mechanism. ──────
  assign cyc_o = (state_q == S_READ) || (state_q == S_MOD) || (state_q == S_WRITE);
  // STB_O is negated during the modify, so the two signals are NOT the same
  // wire. PERMISSION 3.40's shortcut is unavailable to an RMW master.
  assign stb_o = (state_q == S_READ) || (state_q == S_WRITE);

  assign we_o   = (state_q == S_WRITE);
  assign adr_o  = adr_q;
  assign dat_o  = wr_q;
  assign sel_o  = '1;
  assign busy_o = (state_q != S_IDLE);

  assign terminated = cyc_o && stb_o && (ack_i || err_i || rty_i);

  // The modify itself: set the masked bit.
  logic [DW-1:0] modified;
  assign modified = dat_i | mask_q;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      state_q <= S_IDLE; adr_q <= '0; mask_q <= '0; wr_q <= '0;
      done_o <= 1'b0; got_o <= 1'b0; read_dat_o <= '0; wrote_dat_o <= '0;
    end else begin
      done_o <= 1'b0;
      case (state_q)
        S_IDLE: if (req_i) begin
          adr_q <= req_adr_i; mask_q <= req_mask_i; state_q <= S_READ;
        end

        S_READ: if (terminated) begin
          read_dat_o <= dat_i;
          // The TEST, taken from the value the read actually returned.
          got_o      <= ((dat_i & mask_q) == '0);
          // The SET. Computed here so the write can be presented on the
          // very next edge with no gap.
          wr_q       <= modified;
          // Icarus rejects an enum assigned from a ternary (Chapter 4.3),
          // so the MOD_CLK choice is written as if/else.
          if (MOD_CLK) state_q <= S_MOD;
          else         state_q <= S_WRITE;
        end

        // CYC_O high, STB_O low: the bus is held and nothing is presented.
        S_MOD: state_q <= S_WRITE;

        S_WRITE: if (terminated) begin
          wrote_dat_o <= wr_q;
          state_q     <= S_IDLE;
          done_o      <= 1'b1;
        end

        default: state_q <= S_IDLE;
      endcase
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// wb_sem_slave — the running peripheral's SEMAPHORE word.
//
// Word 10 (byte 0x28) is a plain read/write register. It has NO notion of a
// read-modify-write cycle, no lock, and no CYC_I edge logic. That is the
// point: in Classic Wishbone a slave cannot tell an RMW from two unrelated
// single cycles, so whatever protection an RMW provides is NOT provided here.
// ─────────────────────────────────────────────────────────────────────────
module wb_sem_slave #(
  parameter int unsigned OFF_AW = 4,
  parameter int unsigned DW     = 32,
  parameter int unsigned LAT    = 0        // slave wait states, for SIM G
) (
  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,

  output logic [DW-1:0]       sem_o,       // observation only
  output int unsigned         writes_o     // committed writes, observation
);
  localparam logic [OFF_AW-1:0] O_ID  = 4'd4;    // byte 0x10  RO
  localparam logic [OFF_AW-1:0] O_SEM = 4'd10;   // byte 0x28  RW
  localparam logic [DW-1:0] ID_VALUE = 32'h5742_0801;

  logic [DW-1:0] sem_q;
  logic          mapped, valid, term;
  logic [2:0]    wait_q;

  assign mapped = (adr_i == O_ID) || (adr_i == O_SEM);
  // RULE 3.30: a transfer is qualified by the AND of CYC_I and STB_I.
  assign valid  = cyc_i && stb_i;

  assign term  = valid && (wait_q == LAT[2:0]);
  assign ack_o = term &&  mapped;
  assign err_o = valid && !mapped && (wait_q == LAT[2:0]);

  always_comb begin
    dat_o = '0;
    if (valid && !we_i) begin
      case (adr_i)
        O_ID:  dat_o = ID_VALUE;
        O_SEM: dat_o = sem_q;
        default: dat_o = '0;
      endcase
    end
  end

  assign sem_o = sem_q;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      sem_q <= '0; wait_q <= '0; writes_o <= '0;
    end else begin
      if (!valid)      wait_q <= '0;
      else if (!term)  wait_q <= wait_q + 3'd1;
      else             wait_q <= '0;

      // ── COMMIT, once per accepted transfer (Module 7's rule) ────────────
      if (term && mapped && we_i && (adr_i == O_SEM)) begin
        sem_q    <= dat_i;
        writes_o <= writes_o + 1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// wb_arb2 — a two-master shared-bus arbiter, with the ONE policy decision
// that determines whether an RMW cycle is indivisible made a parameter.
//
// Wishbone does not define arbitration. RECOMMENDATION 3.05 observes only
// that arbitration logic "often uses [CYC_I] to select between MASTER
// interfaces" — OFTEN, not MUST. Both arbiters below are legal Wishbone.
//
//   HONOUR_CYC = 1  the grant is held for as long as the granted master
//                   asserts CYC_I. An open cycle is never broken into.
//   HONOUR_CYC = 0  the grant is re-evaluated after every TERMINATED
//                   transfer and handed to the other requester. Fair,
//                   lower worst-case latency, and it will split an RMW.
//
// An ungranted master sees no termination and simply waits, which is what a
// master holding STB_O on a bus it has not been granted must do.
// ─────────────────────────────────────────────────────────────────────────
module wb_arb2 #(
  parameter int unsigned AW         = 30,
  parameter int unsigned DW         = 32,
  parameter bit          HONOUR_CYC = 1'b1
) (
  input  logic            clk_i,
  input  logic            rst_i,

  input  logic            m0_cyc_i, m0_stb_i, m0_we_i,
  input  logic [AW-1:0]   m0_adr_i,
  input  logic [DW-1:0]   m0_dat_i,
  input  logic [DW/8-1:0] m0_sel_i,
  output logic [DW-1:0]   m0_dat_o,
  output logic            m0_ack_o, m0_err_o,

  input  logic            m1_cyc_i, m1_stb_i, m1_we_i,
  input  logic [AW-1:0]   m1_adr_i,
  input  logic [DW-1:0]   m1_dat_i,
  input  logic [DW/8-1:0] m1_sel_i,
  output logic [DW-1:0]   m1_dat_o,
  output logic            m1_ack_o, m1_err_o,

  output logic            s_cyc_o, s_stb_o, s_we_o,
  output logic [AW-1:0]   s_adr_o,
  output logic [DW-1:0]   s_dat_o,
  output logic [DW/8-1:0] s_sel_o,
  input  logic [DW-1:0]   s_dat_i,
  input  logic            s_ack_i, s_err_i,

  output logic            gnt_o,        // which master owns the bus
  output int unsigned     steals_o      // grants taken from an OPEN cycle
);
  logic gnt_q;
  logic sel_cyc, other_cyc, term;

  assign gnt_o     = gnt_q;
  assign sel_cyc   = gnt_q ? m1_cyc_i : m0_cyc_i;
  assign other_cyc = gnt_q ? m0_cyc_i : m1_cyc_i;

  assign s_cyc_o = sel_cyc;
  assign s_stb_o = gnt_q ? m1_stb_i : m0_stb_i;
  assign s_we_o  = gnt_q ? m1_we_i  : m0_we_i;
  assign s_adr_o = gnt_q ? m1_adr_i : m0_adr_i;
  assign s_dat_o = gnt_q ? m1_dat_i : m0_dat_i;
  assign s_sel_o = gnt_q ? m1_sel_i : m0_sel_i;

  // Terminations reach ONLY the granted master.
  assign m0_ack_o = (gnt_q == 1'b0) ? s_ack_i : 1'b0;
  assign m0_err_o = (gnt_q == 1'b0) ? s_err_i : 1'b0;
  assign m1_ack_o = (gnt_q == 1'b1) ? s_ack_i : 1'b0;
  assign m1_err_o = (gnt_q == 1'b1) ? s_err_i : 1'b0;
  assign m0_dat_o = s_dat_i;
  assign m1_dat_o = s_dat_i;

  assign term = s_cyc_o && s_stb_o && (s_ack_i || s_err_i);

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      gnt_q <= 1'b0; steals_o <= '0;
    end else begin
      if (!sel_cyc) begin
        // The owner has no cycle open: switching costs nothing.
        if (other_cyc) gnt_q <= ~gnt_q;
      end else if (!HONOUR_CYC && term && other_cyc) begin
        // ── THE POLICY THAT SPLITS AN RMW ────────────────────────────────
        // The owner's cycle is still OPEN (sel_cyc is asserted) and the bus
        // is handed away anyway, at a transfer boundary.
        gnt_q    <= ~gnt_q;
        steals_o <= steals_o + 1;
      end
    end
  end
endmodule

Reading the group

Purpose. The master shows what §3.4 requires; the slave shows what a slave can and cannot contribute; the arbiter shows where the rest of the answer lives.

Interface. req_adr_i and req_mask_i name the word and the bit. got_o reports whether that bit was clear in the value the read returned — a test-and-set's only result.

Combinational logic. In the master, cyc_o and stb_o are two different expressions, which is the whole design. In the arbiter, every slave-facing signal is a mux on one grant bit, and steals_o counts only the handovers taken while sel_cyc was still asserted.

Sequential logic. Four master states; one grant bit; one semaphore register.

Timing. CYC_O asserts at the same edge as the first STB_O, satisfying RULE 3.25's "no later than", and negates at the write's termination. With MOD_CLK = 0 the write is presented in the clock immediately after the read terminates, which is §3.4's own figure. With MOD_CLK = 1 one gap clock separates them.

Reset. Active high, synchronous, consistent with the module.

Simplifications. One word, one bit, no RTY_I handling — Chapter 4.12 owns retry, and an RMW that retries raises a question this chapter does not need. SEL_O is tied high; Module 13 owns byte selects. The arbiter is two masters and one slave, because a third of either changes nothing about the policy being measured.

Not a reference arbiter. wb_arb2 exists to make one policy switchable. A production arbiter needs fairness, starvation bounds and a timeout, none of which are here.

6. Waveform — One Cycle, Two Directions

RMW: CYC_O spans a read and a write

10 cycles
Ten clock cycles showing a read-modify-write cycle on word ten. The cycle signal rises at cycle one and remains asserted continuously through cycle five. The strobe signal is asserted in cycles one and two for the read, negated in cycle three which is the master's modify gap, and asserted again in cycles four and five for the write. The write enable signal is low during the read and high during the write, changing inside the single bus cycle. The address holds word ten throughout. Two acknowledges appear, one at cycle two ending the read and one at cycle five ending the write. The semaphore register in the slave changes from zero to one at cycle six, one clock after the write terminates.read terminates; value testedread terminates; valuetestedmodify: CYC high, STB lowmodify: CYC high, STB lowwrite terminates; cycle endswrite terminates; cycleendsCLK_ICYC_OSTB_OWE_OADR_O----0xA0xA0xA0xA0xA----------------ACK_ISEM0x00x00x00x00x00x00x10x10x10x1t0t1t2t3t4t5t6t7t8t9
Figure 1 — an RMW cycle with one slave wait state per half and a registered modify. Traced from the simulation in Section 7.

Read CYC_O and STB_O as two independent lines, because in this figure they finally are. CYC_O is asserted for five clocks. STB_O is asserted for four of them, in two separate runs.

Cycle 3 is the modify. CYC_O high, STB_O low — the master owns the cycle, is presenting nothing, and is computing. The slave does nothing, correctly, because RULE 3.30 qualifies a transfer with the AND of both signals.

That clock is the master's own wait state, the same mechanism Chapter 8.3 put between block transfers, used here for a different reason. It is also the reason an RMW master cannot take PERMISSION 3.40's shortcut of driving CYC_O and STB_O from one register.

WE_O changes at cycle 4, inside the cycle. This is the first figure in the module where direction is not a property of the bus cycle. WE_O is qualified by STB_O, not by CYC_O, so it is free to change in a clock where nothing is presented — and it does.

ADR_O holds word 10 for all five clocks. Both halves address the same location; that is what makes the pair an RMW rather than two errands.

The semaphore changes at cycle 6, one clock after the write's termination — the write commits at the terminating edge and is visible on the next, exactly the PRESENTED-versus-COMMITTED distinction Chapter 7.1 established.

7. Simulation — Four Runs, Two of Which Should Worry You

SIM F — one RMW cycle, no contention, no wait states.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM F - one RMW cycle, no contention, no wait states ===
    bus cycles begun        1
    transfers completed     2
    reads / writes          1 / 1
    wait cycles             0
    gaps (CYC & !STB)       0
    clocks with CYC high    2
    master says it acquired 1
    SEMAPHORE after         0x00000001
    slave writes committed  1

cycles = 1, transfers = 2, reads = 1, writes = 1 — the signature from Section 3, measured. Two clocks total, because with MOD_CLK = 0 the modify is combinational and the write is presented in the clock immediately after the read terminates. gaps = 0: this RMW inserted no master wait state at all.

SIM F(b) — the same operation with the modify given its own state.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM F(b) - same, modify registered in its own state ===
    bus cycles begun        1
    transfers completed     2
    gaps (CYC & !STB)       1
    clocks with CYC high    3
    SEMAPHORE after         0x00000001

The modify cost exactly one clock, and it shows up as a gap, not as a wait. Cycle length went from 2 to 3 — a 50% increase on the shortest possible RMW — while cycles and transfers did not move. The counter that changed is the one that attributes the delay to the master, which is how you tell "my slave is slow" from "my master is thinking".

This is a real design choice, not an artifact. A combinational modify puts dat_i, the bit operation and the write-data mux in one path between the slave's read data and the master's output register. A registered modify buys timing closure with a clock of cycle length — and on a shared bus, a clock of everybody else's latency too.

SIM G — one slave wait state in each half.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM G - one RMW cycle, one slave wait state per half ===
    bus cycles begun        1
    transfers completed     2
    reads / writes          1 / 1
    wait cycles             2
    gaps (CYC & !STB)       0
    clocks with CYC high    4
    SEMAPHORE after         0x00000001

The signature is unchanged: still one cycle, still two transfers. The cycle doubled in length and its content did not change — the same result Chapter 8.3 measured for blocks, and the reason cycle length is never a proxy for work done.

waits = 2, gaps = 0 attributes all of it to the slave, cleanly.

SIM H — two masters race for the same bit

Both masters run the same conformant wb_rmw_master, both start on the same clock, both test-and-set bit 0 of SEMAPHORE. Only the arbiter's HONOUR_CYC parameter differs between the two runs.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM H - two masters race for the semaphore ===
    (a) conformant masters, arbiter HONOURS CYC_I
        master 0 believes it acquired   1
        master 1 believes it acquired   0
        grants taken from an open cycle 0
        SEMAPHORE                       0x00000001
        slave writes committed          2
    (b) SAME masters, arbiter does NOT honour CYC_I
        master 0 believes it acquired   1
        master 1 believes it acquired   1
        grants taken from an open cycle 3
        SEMAPHORE                       0x00000001
        slave writes committed          2

In (a) exactly one master acquired the lock. That is mutual exclusion, and it is what a semaphore is for.

In (b) both masters acquired it. Two threads now believe they own the same resource.

Nothing about either master changed between the runs. Same RTL, same request, same address, same mask, same wait states. Both masters held CYC_O across both halves in both runs, and a RULE 3.25 checker watching either master's port would pass in (b) exactly as it passed in (a).

Here is the (b) grant trace, taken from the simulation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   clk  gnt  s_CYC s_STB s_WE s_ACK | m0_cyc m1_cyc  SEM  steals
     1    0     1     1    0    0   |   1      1     0x0    0
     2    0     1     1    0    1   |   1      1     0x0    0     m0 reads 0 -> got
     3    1     1     1    0    0   |   1      1     0x0    1     <- STOLEN
     4    1     1     1    0    1   |   1      1     0x0    1     m1 reads 0 -> got
     5    0     1     1    1    0   |   1      1     0x0    2     <- STOLEN
     6    0     1     1    1    1   |   1      1     0x0    2     m0 writes 1
     7    1     1     1    1    0   |   0      1     0x1    3     <- STOLEN
     8    1     1     1    1    1   |   0      1     0x1    3     m1 writes 1

Read m0_cyc down the trace: it is asserted continuously from clock 1 to clock 6. Master 0 never released the bus. The arbiter took it anyway, three times, at transfer boundaries.

The resulting order is read, read, write, write — the textbook lost update, reached without a single protocol violation anywhere.

SIM I — the complementary failure, with a correct arbiter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIM I - split masters, arbiter HONOURS CYC_I ===
        master 0 believes it acquired   1
        master 1 believes it acquired   1
        grants taken from an open cycle 0
        SEMAPHORE                       0x00000001
        slave writes committed          2

wb_rmw_master_split is identical to wb_rmw_master except for one lineassign cyc_o = stb_o; — so the bus is released between the halves. The arbiter is the honouring one and takes nothing from an open cycle: steals = 0. It cannot. There is no open cycle to take from; the master published two single cycles and the arbiter interleaved them exactly as it is supposed to.

Both masters acquired the lock again. Same outcome as (b), opposite cause.

The number that is the same in every run

Look at SEMAPHORE across all four contention runs: 0x00000001. Look at slave writes committed: 2.

The final memory state is identical whether mutual exclusion held or not. Both masters write the same value, and setting an already-set bit is idempotent — the same property Chapter 7.4 found in W1C registers, here working against you.

So the failure is invisible in the register. It exists only in the masters' beliefsgot_o, which never appears on the bus. You cannot debug this by dumping the semaphore, and you cannot find it by checking the slave. The evidence is the bus-cycle count on each master's port, the grant trace, and nothing else.

8. Failure Modes and Discriminating Evidence

Symptom: two masters both acquire a semaphore that only one should hold.

This is the chapter's central symptom and it has three distinct causes. They are distinguished by two observations, and the order matters because the first is cheap.

Observation 1 — was CYC_O continuously asserted on the losing master's port, from its read's presentation to its write's termination?

If it negated: the master is the fault. It performed two single cycles. cycles = 2 on the monitor, and the negation is one clock wide and easy to miss by eye — trigger on cyc_o falling while the master's state machine says an RMW is in progress. This is SIM I. Likely location: the cyc_o assignment, usually because both qualifiers come from one expression.

If it stayed asserted: the master is conformant and the fault is elsewhere. Go to observation 2.

Observation 2 — did the competing access appear on the shared bus at all?

If it did: the arbiter regranted during an open cycle. This is SIM H(b). Discriminating evidence: the grant signal changing while the previous owner's CYC_I is still asserted — count it, as wb_arb2 does with steals_o. Likely location: the arbiter's grant condition. Nothing in the master will ever show this, which is why engineers stall here.

If it did not: the change came through a back door, and no bus-level mechanism will help. Discriminating evidence: the semaphore's value changing on a clock where the slave's ACK_O did not fire. Likely location: a second port, a debug interface, a CPU-side alias of the same register. Chapter 4.9 measured this case.

Symptom: a semaphore works in unit test and fails in the system.

Candidate causes. The unit test had one master, so no arbiter policy was exercised.

Discriminating evidence. Count masters in the testbench. An RMW's protection property is untestable with a single master — the cycle will always complete undisturbed. SIM F and SIM G pass identically for wb_rmw_master and wb_rmw_master_split, which is the point: a single-master test cannot tell them apart.

The fix is a testbench, not RTL: two masters, a shared slave and a real arbiter, which is what the rig in Section 7 is.

Symptom: an RMW cycle hangs with CYC_O high and STB_O low.

Candidate causes. The modify state has no exit — usually a multi-cycle computation whose completion signal never arrives.

Discriminating evidence. gaps climbing without bound while transfers stays at 1. Distinguishable from a slave-side hang, where STB_O would be high with no termination. Same discriminator as the block master's stall in Chapter 8.3.

Symptom: the write half is issued at a different address from the read half.

Candidate causes. The address is recomputed rather than held.

Discriminating evidence. Compare ADR_O at the two terminations. They must be equal — an RMW to two addresses is not an RMW. wb_rmw_master holds adr_q from the request until the cycle ends and never recomputes it.

9. Verification

Four properties, labelled by what they are.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for wb_rmw_master. SPECIFICATION vs LOCAL POLICY is labelled
// on each one, because the distinction is the whole subject of Section 2.
//
// NOTE ON EXECUTION: these are SystemVerilog assertions. Icarus Verilog
// does not support SVA, so they were reviewed by inspection and are NOT
// claimed to have been executed. The measured results in Section 7 come
// from procedural checks in the testbench, which Icarus does run.
// ─────────────────────────────────────────────────────────────────────────
module wb_rmw_master_props (
  input logic clk_i, rst_i,
  input logic cyc_o, stb_o, we_o, ack_i, err_i,
  input logic [29:0] adr_o,
  input logic busy_o
);
  default clocking cb @(posedge clk_i); endclocking
  default disable iff (rst_i);

  logic terminated;
  assign terminated = cyc_o && stb_o && (ack_i || err_i);

  // P1 — SPECIFICATION (RULE 3.25). CYC_O is asserted no later than the
  //      edge that qualifies STB_O. Carried unchanged from 8.1 and 8.3.
  P1_cyc_qualifies: assert property ( stb_o |-> cyc_o );

  // P2 — SPECIFICATION (RULE 3.60). ADR_O is stable while a presented
  //      transfer is outstanding. Both halves of an RMW are subject to it.
  P2_adr_stable: assert property (
    (cyc_o && stb_o && !(ack_i || err_i)) |=> $stable(adr_o)
  );

  // P3 — LOCAL POLICY. This master performs exactly two transfers per bus
  //      cycle, a read then a write, and the cycle ends with the write.
  //      This is the migrated descendant of 8.1's P3 and 8.3's P4: same
  //      intent, granularity matched to the cycle type.
  P3_two_transfers: assert property (
    $fell(cyc_o) |-> $past(terminated && we_o)
  );

  // P4 — LOCAL POLICY, and the one worth arguing about. Between the read's
  //      termination and the write's termination, CYC_O never negates.
  //      This is the property that distinguishes wb_rmw_master from
  //      wb_rmw_master_split, and it is expressible from the master's own
  //      port alone.
  P4_cyc_spans: assert property (
    (terminated && !we_o) |=> (cyc_o throughout (terminated && we_o)[->1])
  );
endmodule

What P4 can and cannot establish, stated carefully.

P4 proves the master's half of Section 2's decomposition, completely. If it holds, this master never released the bus between its read and its write. That is exactly RULE 3.85's requirement, and it is the part you own.

P4 says nothing about whether the operation was indivisible. SIM H(b) satisfies P4 on both masters' ports and loses mutual exclusion anyway. A property written from one master's pins cannot see a grant it did not receive.

Catching the SIM H(b) failure needs a property somewhere else — on the arbiter, asserting that the grant does not change while the granted master's CYC_I is asserted. That property is not a Wishbone conformance property. It encodes an integration decision, and writing it down is how a project makes that decision explicit instead of assumed.

This is the sharpest instance of a limit this module keeps finding. Chapter 4.9 put it as: protocol conformance can be checked from the wires, intent cannot. Here the gap is measurable. Every property above passes in SIM H(b), and the system is broken.

10. Common Mistakes

"Holding CYC_O makes the access atomic."

Wrong mental model: CYC_O is a lock.

What is true: holding CYC_O is the protocol structure §3.4 requires, and it is a request for the bus that an arbiter may honour.

Concrete bug: SIM H(b). Two conformant masters holding CYC_O correctly, an arbiter that regrants at transfer boundaries, and both masters acquire the same lock.

Observable evidence: grants taken from an open cycle = 3, with both masters' CYC_O continuously asserted.

Correct model: protocol structure from the specification; exclusivity from the arbiter and the system. Say which one you mean.

"A slave can tell it is inside an RMW and protect the location."

Wrong mental model: the slave sees a special cycle.

What is true: Classic Wishbone gives the slave nothing. No lock signal, no cycle-type tag. A slave sees a qualified read and later a qualified write, exactly as it would for two unrelated cycles.

Concrete bug: a design that relies on the slave "noticing" the RMW and rejecting an intervening access. There is nothing to notice with.

Observable evidence: wb_sem_slave has no CYC_I edge logic at all, and every run in Section 7 works through it identically.

Correct model: if the target needs to enforce something, that is a feature you design into the slave — and it is then your protocol, not Wishbone's.

"WE_O is constant for a bus cycle."

Wrong mental model: direction is a property of the cycle.

What is true: WE_O is a master output qualified by STB_O (RULE 3.60), like ADR_O and SEL_O. It must be stable while a transfer is outstanding — not for the cycle.

Concrete bug: a bus monitor, a coverage model or a scoreboard keyed on "cycle direction". It will mis-classify every RMW, usually by recording only the first half.

Observable evidence: Figure 1, cycle 4: WE_O rises with CYC_O still asserted from cycle 1.

Correct model: direction belongs to the transfer. The same correction as Chapter 8.2's point that "SINGLE READ / WRITE" is one cycle type.

"Every Wishbone interface supports RMW."

Wrong mental model: the cycle types are a mandatory set.

What is true: PERMISSION 3.60 says interfaces MAY be designed so that they do not support RMW, exactly as PERMISSION 3.55 says for BLOCK. RULE 3.85 binds only those that do support it.

Concrete bug: issuing an RMW to a peripheral whose datasheet never claimed to support one, then reasoning about what "should" happen. Nothing is promised.

Observable evidence: the DATASHEET is where support is declared. RULE 2.15, quoted in Chapter 4.11, makes that documentation normative for optional signals, and cycle-type support is declared the same way.

Correct model: check the datasheet at both ends. Cycle-type support is a declared capability, not an assumption.

"A single-master testbench proves the RMW works."

Wrong mental model: correctness is a property of the master alone.

What is true: the protection property cannot be exercised without a competitor and an arbiter.

Concrete bug: wb_rmw_master and wb_rmw_master_split produce identical results in SIM F and SIM G. A single-master suite passes both.

Observable evidence: the difference appears only in SIM H and SIM I, which need two masters.

Correct model: an RMW's unit test is a system test with at least two masters and a real arbiter, or it has not tested the feature.

11. Interview Reasoning

Answer the protocol question first, then the system question, and keep them apart. The failure mode of this question is answering "it's atomic" and stopping.

What the specification defines. Section 3.4 defines a bus cycle containing a read transfer and a write transfer, with CYC_O asserted across both halves. RULE 3.85 requires interfaces that support RMW to meet that timing; PERMISSION 3.60 allows an interface not to support it at all. That is the complete normative content, and it is one requirement about one signal.

What the specification calls it for. The section says the cycle "is used for indivisible semaphore operations." That is a statement of purpose. Section 3.4 does not say what prevents another master from intervening, and it provides no locking mechanism.

So the guarantee decomposes into three parts and Wishbone supplies one. The protocol structure is mine to get right. Bus exclusivity depends on an arbiter that treats CYC_I as a claim — and RECOMMENDATION 3.05 says arbitration logic "often" uses CYC_I, which is an observation about practice, not a requirement. Target exclusivity depends on there being no second path into the location.

What I measured. Two conformant masters, same RTL, racing for one semaphore bit. With an arbiter that holds the grant while CYC_I is asserted, exactly one acquired it. With an arbiter that regrants at transfer boundaries, both did — three grants taken while a cycle was open, and neither master violated anything.

And the detail I would make sure to mention: the semaphore register ended at 0x00000001 in both runs, with two committed writes in both. The final memory state is identical whether mutual exclusion held or not. The failure exists only in what each master believes, so it is invisible to a memory dump and to the slave.

What I would say I own. Holding CYC_O across both halves, and asserting it. What I would raise as an integration question: the arbiter's regrant policy, in writing, early — because by the time a semaphore fails intermittently in a system, the master will look correct and will be correct.

12. Understanding Check

13. What's Next

Four cycle types are now on the table, three of them measured in this module and all four sharing a single qualification mechanism.

They have been introduced one at a time, each with its own signature, its own failure modes and its own relationship between CYC_O and STB_O. What they have not had is a single place where they are set side by side — same counters, same rules, same questions.

How do the Wishbone cycle types actually compare, and how do you tell from a trace which one you are looking at?

Chapter 8.5 — Cycle Types puts them in one table and one decision procedure. The full path is on the Wishbone curriculum index.

Continue learning

Standards & specifications

Governing standard
Wishbone SoC Interconnection Architecture (OpenCores)(opens OpenCores in a new tab)

Defines the Wishbone signal set, the bus cycles built from it and the interface rules a portable IP core must follow. It deliberately leaves interconnect topology, address map and arbitration policy to the integrator, so those are system decisions rather than requirements of the specification.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Wishbone curriculum.