Wishbone · Module 15
Atomic Operations
Two masters each increment a counter from 10. Every transfer is conformant and the counter ends at 11 — including under the RMW cycle exactly as the specification defines it.
Module 14 grouped many transfers under one cycle and found that grouping guarantees nothing about interference. It named LOCK_O and did not use it.
This module is about the operation that needs what grouping does not provide.
Two masters each add one to a shared counter. Every transfer is correct. Why is the answer wrong?
1. What the Specification Actually Defines
B3 has a cycle type for this, and Chapter 8.4 introduced its structure. Here is its own description of what it is for:
The RMW (read-modify-write) cycle is used for indivisible semaphore operations. During the first half of the cycle a single read data transfer is performed. During the second half of the cycle a write data transfer is performed. The
CYC_Osignal remains asserted during both halves of the cycle.
Three facts, and they are worth separating carefully.
It is a defined cycle type. RULE 3.85 binds its timing; PERMISSION 3.60 makes supporting it optional, exactly as PERMISSION 3.55 does for BLOCK — the same optionality Chapter 14.1 noted for block cycles.
Its structure is fixed: read half, then write half, CYC_O across both. RULE 3.25 requires CYC_O for the duration of SINGLE, BLOCK and RMW cycles.
And "used for indivisible semaphore operations" is a statement of purpose. It says what the cycle is for. It is not a mechanism, and Section 6 measures the difference.
2. Three Things Called Atomic
The word is useless without a scope, and these are three different claims:
| what is excluded | who provides it | |
|---|---|---|
| bus-ownership atomicity | no other bus master is granted the bus during the interval | the interconnect |
| resource atomicity | nothing changes the protected state during the interval | the whole system |
| system atomicity | the operation is indivisible as software understands it | the architecture |
They are not the same and one does not imply the next. Chapter 15.3 measures a system with perfect bus-ownership atomicity, no second master at all, and a lost update — because the competitor was inside the slave.
This module teaches the first, bounds the second, and is honest about the third.
3. The Running System
One shared counter, two masters, and the smallest thing that can give them both access.
The counter slave does not know how to increment. That is deliberate: a slave with an "add one" command would make this entire module disappear, because the operation would be a single transfer with nothing to interleave. The increment is built out of a bus read and a bus write, which is what makes the interval between them exist.
It also does not know it is inside an RMW. Nothing in the Classic profile tells it. RECOMMENDATION 3.15 advises an optional TGC_O() cycle tag named RMW_O that could identify the cycle type — it is advisory, it is not present here, and even where it is present it identifies a type rather than conferring a guarantee.
So the slave sees a read transfer and later a write transfer, exactly as it would see two unrelated cycles. Chapter 15.3 takes up what follows from that.
4. RTL — The Shared Counter
// ─────────────────────────────────────────────────────────────────────────
// wb_counter_slave — one shared word that two masters both want to increment.
//
// IT DOES NOT KNOW HOW TO INCREMENT, AND THAT IS THE POINT. A slave with an
// "add one" command would make the whole module vanish: the operation would
// be a single transfer and nothing could interleave. The increment here is
// built out of a bus READ and a bus WRITE, which is what makes the interval
// between them exist at all.
//
// IT ALSO DOES NOT KNOW IT IS INSIDE AN RMW. Nothing in the Classic profile
// tells it. RECOMMENDATION 3.15 advises an optional TGC_O() cycle tag named
// [RMW_O] that could identify the cycle type, but it is advisory and this
// slave does not receive one. It sees a read transfer, and later a write
// transfer, exactly as it would see two unrelated cycles.
//
// COMMIT SEMANTICS, inherited from Module 7 and used unchanged since:
// the register changes on an acknowledged write and at no other time. The
// commit is gated on `wr_fire`, which is true for exactly one clock per
// answered write phase — a held ACK cannot produce two increments, which
// would make every measurement in this module meaningless.
//
// THE EVENT INPUT IS REAL HARDWARE, NOT SCAFFOLDING. A shared counter in a
// real peripheral usually has something that also increments it — a packet
// arriving, a timer wrapping, an interrupt being posted. Chapter 15.3 uses
// it to separate BUS exclusivity from RESOURCE exclusivity, and it needs a
// deterministic priority:
//
// LOCAL SLAVE POLICY: a bus write and an event on the same clock both
// apply, with the event added to the written value. Neither is dropped.
// This is a choice; another slave could let the write win and lose the
// event. It is written as one expression so the precedence cannot drift.
// ─────────────────────────────────────────────────────────────────────────
module wb_counter_slave #(
parameter int unsigned OFF_AW = 4,
parameter int unsigned DW = 32,
parameter int unsigned WAITS = 0, // wait states per transfer
parameter logic [31:0] RESET = 32'd10,
// THE DEFECT, available as a parameter so that Chapter 15.4's checker
// can be shown to detect something. With it set, the commit is gated on
// the transfer being PRESENTED rather than on its being ANSWERED - so a
// phase that spans several clocks commits on every one of them.
//
// This is the repeated-side-effect failure Modules 5, 7, 9 and 11 each
// measured in their own way. It is included here because an RMW built
// on a slave that executes a write more than once is meaningless, and
// because a commit-count checker that has never failed proves nothing.
parameter bit COMMIT_EVERY_CLOCK = 1'b0
) (
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,
// an internal hardware source that also updates the counter
input logic event_i,
// write-protect. While asserted a write phase is answered with ERR and
// commits nothing; reads are unaffected. A real shared register often
// has one, and Chapter 15.4 uses it to produce a genuine error on the
// write half of an RMW without building a special faulty slave.
input logic wr_block_i,
// observation only — not part of the Wishbone interface
output logic [DW-1:0] count_o,
output int unsigned wr_commits_o,
output int unsigned rd_serviced_o,
output int unsigned events_o
);
localparam logic [OFF_AW-1:0] O_COUNT = OFF_AW'('h0);
logic [DW-1:0] count_q;
logic [7:0] held_q;
int unsigned nwr_q, nrd_q, nev_q;
logic xfer, mapped, ready, rd_fire, wr_fire;
assign xfer = cyc_i && stb_i; // RULE 3.30: CYC_I qualifies
assign mapped = (adr_i == O_COUNT);
// The slave throttles by withholding its termination, which is the
// specification's mechanism. held_q counts clocks this phase has gone
// unanswered and is cleared on the answer, so each phase of a two-phase
// RMW gets its own latency.
assign ready = xfer && (held_q >= 8'(WAITS));
// A write while write-protected is refused, exactly as an unmapped
// offset is. The commit is gated on the same condition as the
// acknowledgement, so a refused write cannot change the register -
// LOCAL SLAVE POLICY, and the reason Chapter 15.4 can say the counter
// is unchanged after an error without appealing to the specification.
logic refused;
assign refused = !mapped || (we_i && wr_block_i);
assign ack_o = ready && !refused;
assign err_o = ready && refused;
assign rd_fire = ready && !refused && !we_i;
assign wr_fire = ready && !refused && we_i; // exactly one clock
// The commit condition. Correct: the answered edge, once per phase.
// Defective: every clock the write is presented.
logic commit_now;
logic presented_wr;
assign presented_wr = xfer && mapped && we_i && !wr_block_i;
assign commit_now = COMMIT_EVERY_CLOCK ? presented_wr : wr_fire;
always_comb begin
dat_o = '0;
if (rd_fire) dat_o = count_q;
end
always_ff @(posedge clk_i) begin
if (rst_i) begin
count_q <= DW'(RESET);
held_q <= 8'd0; nwr_q <= 0; nrd_q <= 0; nev_q <= 0;
end else begin
if (!xfer || ready) held_q <= 8'd0;
else held_q <= held_q + 8'd1;
// One expression, so a bus write and a simultaneous event cannot
// race: the write supplies the base and the event adds to it.
if (commit_now) count_q <= dat_i + (event_i ? DW'(1) : DW'(0));
else count_q <= count_q + (event_i ? DW'(1) : DW'(0));
if (commit_now) nwr_q <= nwr_q + 1;
if (rd_fire) nrd_q <= nrd_q + 1;
if (event_i) nev_q <= nev_q + 1;
end
end
assign count_o = count_q;
assign wr_commits_o = nwr_q;
assign rd_serviced_o = nrd_q;
assign events_o = nev_q;
endmoduleReading it
wr_fire is the commit, and it is true for exactly one clock per answered write phase. Gating the register update on the acknowledged edge — Chapter 5.6's distinction — rather than on the presented transfer is the discipline Modules 5, 7, 9 and 11 each established in their own way — and here it is load-bearing in a new way.
An RMW built on a slave that executes a write more than once is meaningless. Chapter 15.4 runs that slave deliberately, as one of three defects used to prove the checkers can fail.
refused folds two rejections into one term — an unmapped offset, which is Chapter 10.2's ordinary ERR, and a write-protected register — so the acknowledgement and the commit cannot disagree about whether a transfer was accepted. Chapter 15.4's error experiment depends on that.
The event input is real hardware, not scaffolding. A shared counter in a real peripheral usually has something that also increments it. Its priority is written as one expression so it cannot drift, and it is what Chapter 15.3 needs.
5. RTL — A Master That Reads, Adds One, and Writes
// ─────────────────────────────────────────────────────────────────────────
// wb_incr_master — reads the shared counter, adds one, writes it back.
//
// THE HOLD POLICY IS A PARAMETER, and that is the whole experimental design.
// Chapters 15.1 and 15.2 compare four policies against identical stimulus,
// so every measured difference has exactly one cause. Nothing else in the
// module changes between them.
//
// HOLD_NONE two independent cycles. CYC_O drops between read and write.
// HOLD_CYC one cycle across both halves. CYC_O retained, no LOCK_O.
// This is the B3 RMW cycle as §3.4 defines it.
// HOLD_LOCK CYC_O retained AND LOCK_O asserted across both halves.
// HOLD_WRITE LOCK_O asserted for the write half only.
// HOLD_READ LOCK_O released after the read, before the write.
//
// WHAT §3.4 ACTUALLY SAYS, because HOLD_CYC is named after it:
// "During the first half of the cycle a single read data transfer is
// performed. During the second half of the cycle a write data transfer
// is performed. The [CYC_O] signal remains asserted during both halves
// of the cycle."
// and RULE 3.25 requires CYC_O for the duration of SINGLE, BLOCK and RMW
// cycles. LOCK_O appears nowhere in the RMW section and nowhere in its
// normative figure — which is why HOLD_CYC and HOLD_LOCK are different
// parameters rather than the same one.
//
// THE SAVED READ IS THE OTHER HALF OF CORRECTNESS. `saved_q` captures
// DAT_I at the read phase's answered edge, and the write data is derived
// from `saved_q` — never from live DAT_I, which by RULE 3.65 is meaningful
// only while the slave is terminating and belongs to a phase that is over.
// A master that recomputed from the wire would be reading whatever the
// second phase's bus carries.
//
// ONE CLIENT REQUEST PRODUCES ONE CLIENT COMPLETION. `done_o` pulses once,
// with `ok_o` or `err_o`, whatever happened on the bus.
// ─────────────────────────────────────────────────────────────────────────
module wb_incr_master #(
parameter int unsigned AW = 30,
parameter int unsigned DW = 32,
// 0 NONE, 1 CYC, 2 LOCK, 3 WRITE-only, 4 READ-only
parameter int unsigned HOLD = 2
) (
input logic clk_i,
input logic rst_i,
input logic start_i,
input logic [AW-1:0] adr_i,
output logic busy_o,
output logic done_o,
output logic ok_o,
output logic err_o,
// Wishbone master port
output logic cyc_o,
output logic stb_o,
output logic lock_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,
// observation only
output logic [DW-1:0] saved_o,
output logic [2:0] state_o
);
localparam int unsigned HOLD_NONE = 0;
localparam int unsigned HOLD_CYC = 1;
localparam int unsigned HOLD_LOCK = 2;
localparam int unsigned HOLD_WRITE = 3;
localparam int unsigned HOLD_READ = 4;
initial begin
if (HOLD > 4) $fatal(1, "wb_incr_master: HOLD must be 0..4");
end
typedef enum logic [2:0] {
S_IDLE, S_READ, S_GAP, S_WRITE, S_DONE
} state_e;
state_e state_q;
logic [DW-1:0] saved_q;
logic [AW-1:0] adr_q;
logic err_q;
logic phase_done;
assign phase_done = cyc_o && stb_o && (ack_i || err_i);
// CYC_O spans both halves except under HOLD_NONE, where the cycle ends
// after the read and a second cycle begins for the write. S_GAP exists
// only for HOLD_NONE and is the clock in which CYC_O is low.
assign cyc_o = (state_q == S_READ) || (state_q == S_WRITE) ||
((HOLD != HOLD_NONE) && (state_q == S_GAP));
assign stb_o = (state_q == S_READ) || (state_q == S_WRITE);
// LOCK_O per the policy under test.
always_comb begin
case (HOLD)
HOLD_LOCK: lock_o = cyc_o; // whole cycle
HOLD_WRITE: lock_o = (state_q == S_WRITE); // write half only
HOLD_READ: lock_o = (state_q == S_READ); // read half only
default: lock_o = 1'b0; // NONE and CYC
endcase
end
assign we_o = (state_q == S_WRITE);
assign adr_o = adr_q;
assign sel_o = '1;
assign dat_o = saved_q + DW'(1); // derived from the SAVED read
assign busy_o = (state_q != S_IDLE);
assign done_o = (state_q == S_DONE);
assign ok_o = (state_q == S_DONE) && !err_q;
assign err_o = (state_q == S_DONE) && err_q;
assign saved_o = saved_q;
assign state_o = state_q;
always_ff @(posedge clk_i) begin
if (rst_i) begin
state_q <= S_IDLE; saved_q <= '0; adr_q <= '0; err_q <= 1'b0;
end else begin
case (state_q)
S_IDLE: begin
err_q <= 1'b0;
if (start_i) begin adr_q <= adr_i; state_q <= S_READ; end
end
S_READ: if (phase_done) begin
if (err_i) begin
// LOCAL MASTER POLICY: an error on the read abandons the
// compound operation. No write phase is presented, so nothing
// is committed and there is nothing to undo.
err_q <= 1'b1;
state_q <= S_DONE;
end else begin
saved_q <= dat_i; // captured at the answered edge
state_q <= S_GAP;
end
end
// One clock between the halves. Under HOLD_NONE CYC_O is low here
// and the write becomes a separate cycle; under every other policy
// CYC_O is retained and this is the master wait state the
// specification's own RMW walkthrough shows between the halves.
S_GAP: state_q <= S_WRITE;
S_WRITE: if (phase_done) begin
// LOCAL MASTER POLICY: an error on the write is reported as a
// failure. Whether the slave committed before answering ERR is
// the slave's business - nothing here rolls anything back, and
// Wishbone provides no mechanism that would.
if (err_i) err_q <= 1'b1;
state_q <= S_DONE;
end
S_DONE: state_q <= S_IDLE;
default: state_q <= S_IDLE;
endcase
end
end
endmoduleReading it
HOLD is the only thing that differs between the four experiments in this chapter and the next. Everything else — the address, the arithmetic, the saved read, the state machine — is shared, so each measured difference has exactly one cause.
saved_q is the other half of correctness and it is easy to overlook. The write data is saved_q + 1, derived from what the read returned, never from live DAT_I. RULE 3.65 makes a slave's DAT_O() meaningful only while it is terminating; by the write half the read's termination is long over. A master that recomputed from the wire would be using whatever the second phase happens to carry.
HOLD_CYC is named after §3.4 because that is exactly what it implements — CYC_O across both halves, no LOCK_O, because the RMW section names no lock and its normative figure does not show one. Whether that is enough is Section 7's measurement.
The error policy is stated in the RTL rather than implied. An error on the read abandons the operation before any write is presented. An error on the write is reported as a failure. Neither is a rollback, and the comments say so where a reader would otherwise assume it.
6. Simulation — SIM A: Two Increments, No Protection
Both masters are released on the same clock. Each performs a read cycle, then a separate write cycle — CYC_O drops between them. Every transfer is protocol-correct.
=== SIM A - two increments, no protection ===
each master does a read cycle, then a separate write
cycle. Every transfer is protocol-correct.
policy HOLD_NONE - CYC drops between read and write
counter before 10
A read 10
B read 10
A wrote 11
B wrote 11
read phases served 2
write commits 2
counter after 11
expected after two 12
result LOST UPDATE
Both masters read the same value, both computed the same
result, and both wrote it. Two increments, one increment
of effect. Nothing on the bus went wrong.Reading it
A read 10 and B read 10. That one line is the entire defect. Everything after it follows correctly from a value that had already gone stale.
Both masters computed 11 and both wrote 11. Neither made an arithmetic error. The write commits column says 2 — both writes reached the register and both committed. Nothing was dropped.
Counter after: 11. Expected after two increments: 12.
Read the read-phases line too. Two reads served, two writes committed, four transfers, four terminations. A protocol checker watching this bus sees a completely conformant trace, and it is conformant — the failure is not a protocol event.
7. Simulation — SIM B: The RMW Cycle, With and Without a Lock
Now give both masters the cycle the specification defines. Both rows keep CYC_O asserted across the read half and the write half, which is what §3.4 requires. They differ only in whether LOCK_O is asserted with it.
=== SIM B - the RMW cycle, with and without a lock ===
Both rows keep CYC_O asserted across the read half and the
write half, which is what section 3.4 requires. They differ
only in whether LOCK_O is asserted with it.
policy A read B read after expected result
CYC retained, no LOCK 10 10 11 12 LOST UPDATE
CYC retained + LOCK 10 11 12 12 preserved
owner changes inside a protected interval:
CYC only 0
CYC + LOCK 0
write commits CYC only 2 CYC + LOCK 2Reading it — this is the result the module exists for
The first row is the B3 RMW cycle exactly as §3.4 defines it. Read half, write half, CYC_O asserted across both, nothing omitted. It loses an update.
The second row is the same cycle with LOCK_O asserted alongside. Both updates survive.
Two things follow, and both matter.
First: retaining CYC_O across both halves is necessary and it is not sufficient. The structure the specification defines is the structure; it is not a guarantee of exclusion. The measurement is the argument — nothing was changed between the rows except one signal.
Second: the difference is not a Wishbone rule being obeyed or broken. No numbered rule in the Classic chapter constrains an arbiter at all, and the introduction says so plainly: "Arbitration methodology is defined by the end user (priority arbiter, round-robin arbiter, etc.)." The interconnect in both rows is the same one, and it honours LOCK_O because that is what LOCK_O's description says an INTERCON does.
The gap is the whole question
10 cyclesCycle 3 is the entire subject of this module. CYC_O is still asserted — the cycle has not ended — but STB_O is negated, which is how the specification's own RMW walkthrough shows a master pausing between the halves: "MASTER negates STB_O to introduce a wait state."
An arbiter that re-evaluates when the current master is not presenting can grant the bus on that clock. With the LOCK_O (none) row, nothing prevents it. With LOCK_O (held), its description does.
The saved read row is why the intrusion matters. The master has already captured 10 and will write 11 regardless of what happens at cycle 3. It has no way to notice that the value changed underneath it.
8. Failure Modes and Discriminating Evidence
Symptom: two increments produce one.
Candidate causes. Both masters read the same value. Ownership released between the halves. An arbiter that ignored a lock. One write never committed.
Discriminating evidence. Each master's read value, side by side. Two masters reading the same number is conclusive and immediately separates a staleness problem from a commit problem. If the reads differ and the total is still wrong, the fault is downstream — a missing or duplicated commit, which Chapter 15.4's commit counter measures.
Likely RTL location: the hold policy, or the interconnect.
Symptom: the final value is right and the operation count is wrong.
Candidate causes. A slave committing more than once per write phase.
Discriminating evidence. The slave's own register-update count against the number of successful operations. For an increment this defect is invisible in the value — writing 11 three times leaves 11 — so only the commit count sees it. Chapter 15.4 measures six commits for two increments with a correct final value.
Symptom: the write data does not match what was read.
Candidate causes. The master recomputed from live DAT_I in the write half instead of from the saved read.
Discriminating evidence. The captured read value against the data driven in the write phase. They must differ by exactly the transformation. RULE 3.65 makes DAT_I meaningless outside a termination, so a master reading it in the write half is using undefined data by construction.
Symptom: the counter is correct but an operation reported failure.
Candidate causes. An error on one phase, with the other phase's effect already committed.
Discriminating evidence. Which phase terminated with ERR, and whether a commit occurred. An error on the read leaves nothing committed. An error on the write may or may not — that depends on the slave, not on Wishbone, and Chapter 15.4 is explicit about which.
9. Verification
// ─────────────────────────────────────────────────────────────────────────
// wb_rmw_props — what a read-modify-write promises, and who promises it.
//
// NOTE ON EXECUTION: Icarus Verilog does not support SVA. These properties
// were reviewed by inspection and are NOT claimed to have been executed.
// The numbers in this module come from procedural checks, which Icarus
// does run — and three of those checks were additionally validated against
// targeted broken designs (Chapter 15.4), because a checker that has only
// ever passed has not been shown to check anything.
//
// ONE of these is SPEC-DERIVED. The rest are local, and that ratio is the
// honest picture: §3.4 fixes the structure of the cycle and says nothing
// about exclusion.
// ─────────────────────────────────────────────────────────────────────────
module wb_rmw_props #(
parameter int unsigned AW = 30,
parameter int unsigned DW = 32
) (
input logic clk_i,
input logic rst_i,
input logic cyc_i,
input logic stb_i,
input logic lock_i,
input logic we_i,
input logic ack_i,
input logic err_i,
input logic [DW-1:0] dat_i,
input logic [DW-1:0] dat_o,
input logic [DW-1:0] saved_i,
input logic [2:0] state_i, // 0 IDLE 1 READ 2 GAP 3 WRITE 4 DONE
input logic done_i,
input logic ok_i,
input logic err_rep_i
);
default disable iff (rst_i);
localparam logic [2:0] S_IDLE = 3'd0, S_READ = 3'd1, S_GAP = 3'd2,
S_WRITE = 3'd3, S_DONE = 3'd4;
logic phase_end;
assign phase_end = cyc_i && stb_i && (ack_i || err_i);
// P1 — SPEC-DERIVED. §3.4: "During the first half of the cycle a single
// read data transfer is performed. During the second half of the cycle a
// write data transfer is performed. The [CYC_O] signal remains asserted
// during both halves of the cycle." Plus RULE 3.25, which requires
// CYC_O for the duration of SINGLE, BLOCK and RMW cycles.
// Stated as: a presented phase always carries CYC_O, and the write half
// is never presented before the read half has been answered.
property p_write_after_read;
@(posedge clk_i) (state_i == S_WRITE) |-> cyc_i;
endproperty
a_write_after_read: assert property (p_write_after_read);
property p_read_precedes_write;
@(posedge clk_i) $rose(state_i == S_WRITE) |-> $past(state_i == S_GAP);
endproperty
a_read_precedes_write: assert property (p_read_precedes_write);
// P2 — LOCAL MASTER POLICY. The write data is derived from the SAVED
// read, never from live DAT_I. RULE 3.65 makes a slave's DAT_O()
// meaningful only while it is terminating, and by the write half the
// read's termination is long over — a master recomputing from the wire
// would be using whatever the second phase happens to carry.
property p_write_derived_from_saved;
@(posedge clk_i) (state_i == S_WRITE) |-> (dat_o == saved_i + DW'(1));
endproperty
a_write_derived_from_saved: assert property (p_write_derived_from_saved);
// P3 — LOCAL MASTER POLICY. The saved value does not move once captured,
// for the whole of the write half.
property p_saved_stable;
@(posedge clk_i) (state_i == S_GAP || state_i == S_WRITE) |=>
(state_i == S_WRITE) -> $stable(saved_i);
endproperty
a_saved_stable: assert property (p_saved_stable);
// P4 — LOCAL MASTER POLICY, and the definition of the protected
// interval. LOCK_O is asserted from the start of the read half through
// the answered write. Under the HOLD_LOCK policy this is what the
// ownership guarantee is built on; under the other policies it is
// deliberately false, which is the subject of Chapters 15.1 and 15.2.
property p_lock_spans_operation;
@(posedge clk_i) (state_i == S_READ || state_i == S_GAP ||
state_i == S_WRITE) |-> lock_i;
endproperty
a_lock_spans_operation: assert property (p_lock_spans_operation);
// P5 — LOCAL MASTER POLICY. No early release: the operation does not
// drop CYC_O between the halves. This is what separates an RMW cycle
// from two transfers with a data dependency.
property p_no_release_between_halves;
@(posedge clk_i) (state_i == S_GAP) |-> cyc_i;
endproperty
a_no_release_between_halves: assert property (p_no_release_between_halves);
// P6 — LOCAL MASTER POLICY. Metadata holds still while a phase waits.
property p_metadata_stable_in_wait;
@(posedge clk_i) (cyc_i && stb_i && !(ack_i || err_i)) |=>
(cyc_i && stb_i) -> ($stable(we_i) && $stable(dat_o));
endproperty
a_metadata_stable_in_wait: assert property (p_metadata_stable_in_wait);
// P7 — LOCAL MASTER POLICY. One client request, one client completion,
// classified. Never both, never neither.
property p_one_completion;
@(posedge clk_i) done_i |-> (ok_i ^ err_rep_i);
endproperty
a_one_completion: assert property (p_one_completion);
// P8 — LOCAL MASTER POLICY. An error anywhere in the operation is
// reported as a failure. NOT a rollback claim: nothing here undoes a
// write the slave may already have committed.
property p_err_not_reported_as_success;
@(posedge clk_i) (phase_end && err_i) |=> ##[0:2] (done_i -> err_rep_i);
endproperty
a_err_not_reported_as_success: assert property (p_err_not_reported_as_success);
endmoduleOne of these is specification-derived and seven are not, which is the honest ratio. §3.4 and RULE 3.25 fix the structure — read half, write half, CYC_O across both — and say nothing about exclusion.
P4 is deliberately false of three of this module's four policies. It states that LOCK_O spans the operation, which is true under HOLD_LOCK and false under HOLD_NONE, HOLD_CYC, HOLD_READ and HOLD_WRITE. A property that holds for one configuration and not another has to say which, and the comment does.
P8 is written carefully to avoid claiming something the design does not do. It says an error is not reported as success. It does not say the operation was undone, because nothing undoes it.
10. Common Mistakes
"Read then write is atomic."
Wrong mental model: sequence implies indivisibility.
What is true: the gap between them is where the failure lives. SIM A measures two correct reads, two correct writes, and one increment of effect.
"Both transfers were Wishbone-correct, so the operation is correct."
Wrong mental model: correctness composes.
What is true: compound correctness is a system property. Four conformant transfers produced a wrong answer, and no rule about transfers is violated anywhere in the trace.
"Keeping CYC_O asserted means nobody else can touch the resource."
Wrong mental model: tenure is exclusion.
What is true: measured false in SIM B's first row. CYC_O was retained across both halves, exactly as §3.4 requires, and an update was still lost. CYC_O requests; LOCK_O holds — the specification's own sentence.
"An RMW cycle is atomic because the specification says it is for indivisible operations."
Wrong mental model: a statement of purpose is a guarantee.
What is true: §3.4 defines a cycle structure. LOCK_O is not named in it and does not appear in its figure. The exclusion comes from the interconnect, and arbitration methodology is "defined by the end user".
"An RMW is one bus transfer."
Wrong mental model: one cycle, one transfer.
What is true: two transfers under one cycle — the same cycle-versus-transfer distinction Chapter 14.1 established, applied to an operation with a dependency between the halves.
"The slave knows it is an RMW."
Wrong mental model: the cycle type reaches the target.
What is true: nothing tells it. RECOMMENDATION 3.15's TGC_O()/RMW_O tag is advisory and optional, and even where present identifies a type rather than conferring exclusion.
11. Interview Reasoning
Because correctness of a transfer says nothing about the interval between transfers.
Walk the interleaving. Counter at 10. A reads 10. B reads 10 — still 10, because A has not written yet. A writes 11. B writes 11. Two increments, one increment of effect.
Every transfer in that sequence is conformant. Correct address, correct data, correct termination. There is no moment in the trace at which anything is wrong, which is what makes it a different class of defect from anything a protocol checker catches.
The stale read is the defect and it is not an event. B's read returned the true value of the counter at the moment it was performed. It became wrong later, when A wrote.
So the fix is not a better transfer — it is exclusion over an interval. Name the interval precisely: from before the read is observable through the write that commits the derived value. Chapter 15.2 measures what happens when a lock covers the wrong part of it.
12. Understanding Check
Because both writes wrote the same value, and the second one overwrote the first with a number that was already there.
A wrote 11 to a counter holding 10. The counter became 11. That update is real and it committed.
B then wrote 11 to a counter holding 11. That update also committed — the write commits column says 2 — and it changed nothing, because B had computed its value from a counter that read 10.
Nothing was dropped. Both writes happened. The second one carried a value derived from stale information, so applying it was indistinguishable from doing nothing.
Which is why "did the write commit" is the wrong question here. Both did. The right question is what value each master used to compute what it wrote, and that is the read column.
13. What's Next
The failure is established and the cycle is defined: read half, write half, one CYC_O — and a measurement showing that this is the structure rather than the guarantee.
LOCK_O fixed it in SIM B. Asserting it somewhere is not the same as asserting it over the right interval.
Exactly which clocks have to be protected, and what happens to a lock that covers the wrong ones?
Chapter 15.2 — Synchronization defines the protected interval precisely and measures two locks that are real, correctly asserted, and useless. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
CYC_O
STB_O presents one transfer; CYC_O frames the tenure it belongs to. Holding it across transfers is what makes a read-modify-write atomic — and what atomicity still does not guarantee.
- Related topic
Synchronization
Two locks that are real, correctly asserted and useless. Measured: protecting the read loses an update, and protecting the write loses the same one.
- Related topic
Shared Resources
A perfect lock, an arbiter that honours it, and no second master at all — and one update is still lost, because the competitor was inside the slave.
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.
