Skip to content
VLSI Mentor

Wishbone · Module 7

ACK Timing

A write slave's acknowledge ends the transfer; it is not a write-enable. Separating protocol termination from internal commit is what lets a slave register its response — and what makes a stale-context bug possible.

Both slaves so far committed on the same edge they acknowledged, because that is the policy Chapter 7.1 declared. This chapter takes that coincidence apart.

When should a write slave acknowledge, and how does its termination relate to the state update?

1. What a Write Acknowledge Promises

On a read, ACK_O carries a coherence promise about data travelling back — RULE 3.65 ties the slave's DAT_O to its termination (Chapter 6.4).

A write acknowledge carries no data at all. Its promise is different and, in one way, larger:

The transfer is over, and this slave has taken responsibility for the payload it was presented.

Four things must hold when it asserts, and each breaks differently:

Must be trueBroken bySymptom
this transfer is addressed to meacknowledging without the selectwrong slave responds; two acknowledges merge
a transfer existsACK_O tied high, or gated on STB_I alonecompletions with no request
the write is legal herea missing writability checkread-only register silently overwritten
the payload will be committed correctlya commit using stale contextACK with the wrong register changed

The fourth row is this chapter's. It cannot happen in a slave whose commit and acknowledge are one event — there is nothing stored to go stale. It becomes possible the moment the response is registered, and Section 4 builds one that gets it wrong.

What the acknowledge does not promise. That the value was sensible, that the device has finished acting on it, or that a downstream effect has completed. Chapter 4.10 §1 made the general point; for a write it means a slave with an internal write buffer may legitimately acknowledge on acceptance rather than on the data reaching its final destination.

PERMISSION 3.30"The assertion of [ACK_O], [ERR_O], and [RTY_O] MAY be asynchronous to the [CLK_I] signal (i.e. there is a combinatorial logic path between [STB_I] and [ACK_O])."

OBSERVATION 3.45"...slave wait states are easiest implemented using a registered [ACK_O] signal."

ShapeACK_OCommitCost
Combinationalsame cycle as the requestsame edgelong loopback
Registered, commit-then-acknext cyclethe cycle before the ack+1 cycle; needs stored context
Registered, ack-with-commitnext cyclesame edge as the ack+1 cycle; needs stored context

All three are conformant. The specification constrains only the termination; the commit column is entirely design policy.

The second row is the interesting one, and it is not a mistake. A slave that captures the request, commits it, and acknowledges afterwards has a perfectly defensible policy — it acknowledges only once the state change has actually happened. A property written as "state changes only when ACK_I was asserted" fails on that correct design, which is exactly why Chapter 7.1 §11 insisted properties be written against the slave's own commit term.

3. RTL — Registered Response, Done Correctly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_ack_reg — registered write response with preserved context.
//
// PURPOSE. Break the combinational loopback by putting a flop between the
// request and the response. The cost is one cycle per write. The NEW
// obligation — and the reason this module is longer than Chapter 7.1's —
// is that a registered response must REMEMBER WHAT IT IS COMMITTING.
//
// ── DECLARED COMMIT POLICY (LOCAL, and DIFFERENT from Chapter 7.1) ────
//   This slave CAPTURES the whole write payload at the edge it first sees
//   a qualified write, COMMITS from that captured copy on the next edge,
//   and returns ACK_O in that same second cycle.
//
//   So commit and ACK are still one edge here — but both are one cycle
//   LATER than the request, and both are driven from STORED context
//   rather than from the bus. That stored context is the new risk.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00).
// ─────────────────────────────────────────────────────────────────────────
module wb_write_ack_reg #(
  parameter int unsigned OFF_AW = 4,
  parameter int unsigned DW     = 32
) (
  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              ack_o,
  output logic              err_o,
  output logic [DW-1:0]     control_o,
  output logic [DW-1:0]     outdata_o
);
  localparam int unsigned NL = DW/8;
  localparam logic [OFF_AW-1:0] O_CTRL = 4'd2;
  localparam logic [OFF_AW-1:0] O_OUT  = 4'd6;
  localparam logic [OFF_AW-1:0] O_ID   = 4'd4;

  logic [DW-1:0] ctrl_q, outdata_q;
  assign control_o = ctrl_q;
  assign outdata_o = outdata_q;

  logic xfer;
  assign xfer = cyc_i & stb_i;                   // RULES 3.30 & 3.35

  // ── CAPTURED REQUEST CONTEXT ──────────────────────────────────────────
  // pend_q says a response is owed. The other four registers are the
  // PAYLOAD COPY. All five load in the same edge, from the same cycle's
  // inputs, so they cannot describe different transactions.
  logic                pend_q;
  logic [OFF_AW-1:0]   off_q;
  logic [DW-1:0]       dat_q;
  logic [DW/8-1:0]     sel_q;
  logic                we_q;

  logic writable_q;
  always_comb begin
    unique case (off_q)
      O_CTRL, O_OUT: writable_q = 1'b1;
      default:       writable_q = 1'b0;
    endcase
  end
  logic known_q;
  always_comb begin
    unique case (off_q)
      O_CTRL, O_OUT, O_ID: known_q = 1'b1;
      default:             known_q = 1'b0;
    endcase
  end

  // ── TERMINATION ───────────────────────────────────────────────────────
  // Asserted only while a response is pending AND the master is still
  // presenting a transfer. The second term matters: without it this slave
  // would drive a termination into a bus that is no longer asking, which
  // RULES 3.30 and 3.35 forbid and which corrupts the NEXT master's
  // transfer in a shared fabric.
  logic legal_q;
  assign legal_q = known_q & ~(we_q & ~writable_q);
  assign ack_o   = pend_q & xfer &  legal_q;
  assign err_o   = pend_q & xfer & ~legal_q;

  // ── THE COMMIT ────────────────────────────────────────────────────────
  // Driven from the CAPTURED context — off_q, dat_q, sel_q, we_q — not
  // from the bus. That single choice is the correctness of this module:
  //
  //   from the copy : the write committed is the write that was accepted
  //   from the bus  : the write committed is whatever is on the wires NOW
  //
  // With a conformant master the two are identical, because RULE 3.60 and
  // §3.2.2 require the payload to hold still. Committing from the copy
  // means this slave is coherent BY CONSTRUCTION rather than by relying
  // on someone else's conformance.
  logic commit;
  assign commit = ack_o & we_q;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      pend_q     <= 1'b0;
      off_q      <= '0;
      dat_q      <= '0;
      sel_q      <= '0;
      we_q       <= 1'b0;
      ctrl_q     <= '0;
      outdata_q  <= '0;
    end else begin
      if (!pend_q) begin
        // Capture the whole transaction in one edge.
        if (xfer) begin
          pend_q <= 1'b1;
          off_q  <= adr_i;
          dat_q  <= dat_i;
          sel_q  <= sel_i;
          we_q   <= ~(~we_i);                   // explicit copy of we_i
        end
      end else begin
        // The response was presented this cycle; clear the pending flag so
        // a held strobe cannot produce a second acknowledge for the same
        // request, and so a stale response cannot outlive its transfer.
        pend_q <= 1'b0;
      end

      if (commit) begin
        unique case (off_q)
          O_CTRL: for (int unsigned n = 0; n < NL; n++)
                    if (sel_q[n]) ctrl_q[n*8 +: 8] <= dat_q[n*8 +: 8];
          O_OUT:  for (int unsigned n = 0; n < NL; n++)
                    if (sel_q[n]) outdata_q[n*8 +: 8] <= dat_q[n*8 +: 8];
          default: ;
        endcase
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_ack_stale — THE LOST-CONTEXT BUG, isolated for simulation.
//
// NOT A REFERENCE DESIGN. Identical to wb_write_ack_reg except that the
// commit reads the BUS — adr_i, dat_i, sel_i — instead of the captured
// copy. Every other line, including the acknowledge, is unchanged.
//
// Against a conformant master the two are the same, because RULE 3.60 and
// §3.2.2 require the payload to hold still. The bug is therefore INVISIBLE
// in a well-behaved system, and appears only when paired with a master
// that moves its payload — at which point it commits a transaction that
// was never requested, and acknowledges it.
// ─────────────────────────────────────────────────────────────────────────
module wb_write_ack_stale #(
  parameter int unsigned OFF_AW = 4,
  parameter int unsigned DW     = 32
) (
  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              ack_o,
  output logic              err_o,
  output logic [DW-1:0]     control_o,
  output logic [DW-1:0]     outdata_o
);
  localparam int unsigned NL = DW/8;
  localparam logic [OFF_AW-1:0] O_CTRL = 4'd2;
  localparam logic [OFF_AW-1:0] O_OUT  = 4'd6;

  logic [DW-1:0] ctrl_q, outdata_q;
  assign control_o = ctrl_q;
  assign outdata_o = outdata_q;

  logic xfer; assign xfer = cyc_i & stb_i;

  logic              pend_q, we_q;
  logic [OFF_AW-1:0] off_q;

  logic writable_q;
  always_comb begin
    unique case (off_q)
      O_CTRL, O_OUT: writable_q = 1'b1;
      default:       writable_q = 1'b0;
    endcase
  end

  assign ack_o = pend_q & xfer &  writable_q;
  assign err_o = pend_q & xfer & ~writable_q;

  logic commit; assign commit = ack_o & we_q;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      pend_q <= 1'b0; off_q <= '0; we_q <= 1'b0;
      ctrl_q <= '0; outdata_q <= '0;
    end else begin
      if (!pend_q) begin
        if (xfer) begin pend_q <= 1'b1; off_q <= adr_i; we_q <= we_i; end
      end else begin
        pend_q <= 1'b0;
      end

      // ── THE BUG: adr_i / dat_i / sel_i, not off_q / dat_q / sel_q ────
      if (commit) begin
        unique case (adr_i)
          O_CTRL: for (int unsigned n = 0; n < NL; n++)
                    if (sel_i[n]) ctrl_q[n*8 +: 8] <= dat_i[n*8 +: 8];
          O_OUT:  for (int unsigned n = 0; n < NL; n++)
                    if (sel_i[n]) outdata_q[n*8 +: 8] <= dat_i[n*8 +: 8];
          default: ;
        endcase
      end
    end
  end
endmodule

Reading the pair

Purpose. Both turn a qualified write into a committed register and a termination, one cycle after the request. The first commits from a captured copy; the second reads the bus.

Interface. Identical Wishbone slave interfaces. A master cannot tell them apart — which is why RULE 3.55 requires it to work with either, and why the bug is an integration hazard rather than a local defect.

State. wb_write_ack_reg: the pending flag plus a four-field payload copy, plus the two registers. The stale variant keeps only the offset and direction — the three registers it is missing are the bug.

Combinational logic. xfer, writability and legality from the stored offset, the two exclusive terminations, and commit.

Sequential logic. Capture when idle; clear the pending flag once answered; the masked load on commit.

Write start. The edge at which pend_q sets — the slave's own manufactured start event.

Address. off_q in the correct slave, adr_i in the broken one.

Write data. dat_q versus dat_i. Byte enables. sel_q versus sel_i.

Commit. ack_o & we_q — the declared policy, one cycle after the request.

ACK. Asserted in the second cycle, and only while the master is still presenting.

Waiting. Exactly one wait state. Chapter 7.4 generalises to N.

Reset. Synchronous, active high. Clearing pend_q prevents a response surviving a reset.

Failure modes. Section 6.

Simplifications. we_q <= ~(~we_i) is written that way only to make the copy explicit against the stale variant's we_q <= we_i; they are identical and the double negation would be removed in production. O_ID appears in the legality check but has no commit path, since it is read-only.

4. Waveform — Commit and Acknowledge, One Cycle Late

Registered write response

7 cycles
Seven clock cycles showing a registered write response. In cycle two the master asserts both qualifiers with write enable high, word address two, data value zero zero zero zero zero zero zero f and all four byte selects. The slave does not acknowledge in that cycle; instead at the edge ending cycle two it captures the whole payload into its pending registers. In cycle three the slave asserts its acknowledge, and at the edge ending cycle three it commits the captured value into the control register. From cycle four the control register reads zero zero zero zero zero zero zero f, both qualifiers have been negated and the acknowledge has fallen.request captured at this edgerequest captured at thisedgeACK + commit at this edgeACK + commit at this edgeCLK_ICYC_OSTB_OWE_OADR_O----0x20x2----------------DAT_O----0000000F0000000F----------------pend_qACK_ICONTROL0000000000000000000000000000000F0000000F0000000F0000000Ft0t1t2t3t4t5t6
Figure 1 — the registered slave. The request is captured at edge 2 and both commit and ACK happen at edge 3.

Edge-by-edge, and note there are now two significant edges rather than one.

Before edge 2. The master presents the whole payload. The slave is idle — pend_q low, ack_o low, because the acknowledge requires pend_q.

At edge 2 — the capture edge. The slave samples a qualified transfer and loads off_q, dat_q, sel_q, we_q and pend_q. Nothing has been committed and nothing acknowledged. The master sees no termination and holds.

Between edges 2 and 3. The master's payload must still be coherent — §3.2.2 requires it until the edge after the strobe negates, and the correct slave no longer depends on that, while the stale one does.

At edge 3 — the commit and termination edge. ack_o is asserted through the cycle, so the master samples it here; in the same instant commit loads ctrl_q from the captured copy.

After edge 3. CONTROL reads 0x0000000F, the master has released, and ACK_O falls with the strobe per RULE 3.50.

Compare Chapter 7.1's Figure 2, where all of this happened at one edge. The same transfer, the same result, one more cycle — and one more register's worth of state that can be wrong.

5. Simulation — Latency, and the Stale-Context Trap

Both slaves, correct payload, conformant master:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === combinational vs registered write response ===
                                   combinational   registered
    writes issued                         3             3
    total busy cycles                     3             6
    cycles per write                      1             2
    CONTROL after 0x0000000f              0x0000000f    0x0000000f
    OUTPUT  after 0xdeadbeef              0xdeadbeef    0xdeadbeef
    commit events                         3             3

Identical results, double the cycles, and exactly one commit per write in both — three writes, three commits.

The stale-context slave, paired with a master that moves its payload:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === lost context: registered slave + drifting payload ===
    requested        word 2 (CONTROL)  data 0x0000000f  sel 1111
    payload at the commit edge   word 6 (OUTPUT)  data 0xdeadbeef
    slave's remembered offset    2
    slave's committed offset     6
    CONTROL after                0x00000000   (never written)
    OUTPUT  after                0xdeadbeef   (never requested)
    terminated with              ACK

The slave acknowledged a write to CONTROL and committed one to OUTPUT_DATA. It remembered the right offset — off_q is 2 — and committed to a different one, because the commit read the bus.

Neither the master nor the slave reported anything wrong. The master issued one write and received one ACK; the client was told the write to CONTROL succeeded. CONTROL was never touched and a register the software never addressed now holds a value it never asked for.

Note which component is at fault, because it is not obvious. The master violated RULE 3.60 by moving its payload. But the slave is the one that turned a protocol violation into silent, permanent, mis-targeted state — and a slave committing from its captured copy would have written CONTROL correctly despite the master's bug. Correct-by-construction beats correct-if-my-partner-behaves.

6. Failure Modes and Discriminating Evidence

Symptom: ACK returned and a different register changed.

Candidate causes. A registered slave committing from the bus rather than from its captured context, combined with something that moved the payload.

Discriminating evidence. Compare the slave's stored offset against what its commit actually indexed in the commit cycle. A difference is conclusive, and it is one port connection.

Likely RTL location. The commit's address/data/select sources.

Property. P3 in Section 8.

Symptom: ACK returned and no register changed.

Candidate causes. The commit term omits we_q, so a read satisfies it — or sel_q was zero, or the target is read-only and should have errored.

Discriminating evidence. Check ERR_O first. Then check SEL_O. Then check whether commit asserted at all. The three are distinguishable in that order and the first two are free.

Symptom: one write produces two acknowledges.

Candidate causes. The pending flag is not cleared, so a held strobe re-triggers the response.

Discriminating evidence. ACK_O asserted in two consecutive cycles with an unchanged address. Whether that is a bug depends on the master — PERMISSION 3.35 lets a slave hold ACK_O and RULE 3.55 requires masters to cope — but for a write it is worse than for a read, because a second acknowledge may accompany a second commit.

Likely RTL location. The pending flag's clearing condition.

Symptom: a registered response arrives after the master released.

Candidate causes. The termination is derived from pend_q without checking that a transfer is still presented.

Discriminating evidence. ACK_O asserted with STB_I negated — a RULE 3.50 violation, contradicting OBSERVATION 3.10's "automatically negate". In a shared fabric it completes the next master's transfer, so the symptom appears elsewhere.

Likely RTL location. The termination expression. wb_write_ack_reg includes xfer for exactly this.

Symptom: the design works combinationally and breaks when the response is registered.

Candidate causes. Registering the acknowledge made the transfer two cycles long, so a commit gated on the presented request now fires twice.

Discriminating evidence. Count commits against completed writes. The bug appeared because latency was introduced, not because the commit path changedChapter 7.4 measures it.

7. The Timing Trade

The combinational write path is a loop the specification names:

OBSERVATION 3.50"In large high speed designs the asynchronous assertion of [ACK_O], [ERR_O], and [RTY_O] could lead to unacceptable delay times, caused by the loopback delay from the MASTER to the SLAVE and back to the MASTER."

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
master flop → decode → slave qualification → ACK_O → response merge → master flop

A write's loop is shorter than a read's. Chapter 6.4 §3 traced the read version, which additionally carries a read multiplexer and a full data-width merge on the return leg. A write returns one bit, so the return leg is narrow — which means the combinational form often closes on writes when it would not on reads.

What the commit costs is separate and usually not the problem. The register load is a sequential path — DAT_I through the byte mask to a flop's D input — and it is cut by the flop itself. It does not participate in the loopback at all. Conflating the two is common and leads people to register the response when the timing pressure was actually on the data path, or vice versa.

The decision procedure. Build combinational, synthesise, and read the critical path. If the loopback is not critical, keep it. If it is, register the response — and budget the new correctness obligation, which Section 8's P3 is.

8. Verification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_ack_checker — write-side termination properties.
//
// P1 and P2 are SPECIFICATION (RULES 3.35 and 3.50). P3 is a DESIGN
// OBLIGATION that EXISTS ONLY for a registered slave — a combinational one
// stores no context and cannot violate it. P4 is LOCAL POLICY.
// ─────────────────────────────────────────────────────────────────────────
module wb_write_ack_checker #(
  parameter int unsigned OFF_AW = 4,
  parameter int unsigned DW     = 32
) (
  input logic              clk_i,
  input logic              rst_i,
  input logic              cyc_i,
  input logic              stb_i,
  input logic              ack_o,
  input logic              err_o,
  // white-box, registered slaves only
  input logic              pend_q,
  input logic              commit,
  input logic [OFF_AW-1:0] off_q,        // the remembered offset
  input logic [OFF_AW-1:0] commit_off    // what the commit actually indexed
);
  default disable iff (rst_i);

  // P1 — SPECIFICATION (RULE 3.35). The termination is a RESPONSE to a
  //      qualified transfer. Catches a tied-high ACK_O, one derived from
  //      STB_I alone, and a registered response that outlives its request.
  property p_ack_is_a_response;
    @(posedge clk_i) (ack_o || err_o) |-> (cyc_i && stb_i);
  endproperty
  a_ack_is_a_response : assert property (p_ack_is_a_response)
    else $error("RULE 3.35: termination asserted without CYC_I & STB_I");

  // P2 — SPECIFICATION (RULE 3.50, OBSERVATION 3.10), contrapositive form.
  property p_ack_negates_with_stb;
    @(posedge clk_i) !stb_i |-> (!ack_o && !err_o);
  endproperty
  a_ack_negates_with_stb : assert property (p_ack_negates_with_stb)
    else $error("RULE 3.50: termination asserted with STB_I negated");

  // P3 — DESIGN OBLIGATION, REGISTERED SLAVES ONLY. The transaction
  //      committed is the transaction that was accepted. Stated as: at a
  //      commit, the indexed offset equals the remembered one.
  //
  //      This property has no meaning for a combinational slave, which has
  //      no off_q to compare against. Registering a response to break a
  //      timing path therefore ADDS a correctness obligation — a trade
  //      worth making deliberately rather than by habit.
  property p_commit_matches_context;
    @(posedge clk_i) commit |-> (commit_off == off_q);
  endproperty
  a_commit_matches_context : assert property (p_commit_matches_context)
    else $error("registered slave committed a different transaction");

  // P4 — LOCAL POLICY. At most one commit per pending response: the
  //      pending flag is cleared, so a held strobe cannot re-trigger.
  property p_one_commit_per_pend;
    @(posedge clk_i) commit |=> !commit;
  endproperty
  a_one_commit_per_pend : assert property (p_one_commit_per_pend)
    else $error("LOCAL: a second commit followed the first");
endmodule

P3 is this chapter's contribution to the verification plan, and its most interesting property is that it does not apply to every slave. A combinational write slave is coherent by construction.

That is the general shape worth carrying beyond Wishbone: adding state to break a timing path adds an invariant, and the invariant is that the state stays consistent with what it describes. Chapter 6.4 §12 reached the same conclusion from the read side; on a write the cost of violating it is permanent rather than a wrong answer.

Tooling limitation. Icarus Verilog has no SVA support and cannot execute any of these. They are reviewed by inspection; both slaves are elaborated and the simulations in Section 5 were run.

9. Common Mistakes

"ACK writes the register."

Wrong mental model: the termination is a write-enable.

Concrete bug: no RTL bug directly — but a property written from this model fires on a correct commit-then-ack slave, and the over-correction moves a real commit event to satisfy a broken checker.

Observable evidence: an assertion failing on a design that is behaving exactly as its datasheet says.

Correct model: ACK_O is a protocol termination. The commit is a declared internal policy, and properties about it must be written against the design's own term.

"Registered responses are safer."

Wrong mental model: flops reduce risk.

Concrete bug: the lost-context slave — a failure mode that cannot exist in the combinational version, and which Section 5 measured committing to the wrong register with a clean ACK.

Observable evidence: a register the software never addressed holding a value, with no error anywhere.

Correct model: a registered response must remember what it is committing. It buys timing closure and costs a cycle plus a new invariant.

"If the master violates the protocol, the resulting corruption is the master's fault."

Wrong mental model: conformance assigns blame cleanly.

Concrete bug: a slave that commits from the bus. Paired with a drifting master it mis-targets state; paired with a conformant one it is invisible.

Observable evidence: a slave that passes every test it has and corrupts state in one particular integration.

Correct model: a design that is correct only while its partner behaves is a latent integration failure. Committing from the captured copy costs three registers and removes the dependency.

10. Interview Reasoning

Because the commit happens in a different cycle from the request, and by then the bus may be showing something else.

A combinational write slave has no such problem. Its acknowledge and its commit are functions of the inputs present right now, so they necessarily describe the same transaction. There is nothing to preserve because nothing is deferred.

A registered slave defers by at least a cycle. At the moment it commits it must know which offset, what data and which byte lanes it was asked about — and there are two places to get them. The captured copy, loaded when the request arrived. Or the bus.

Against a conformant master the two are identical, because RULE 3.60 qualifies the payload with STB_O and §3.2.2 requires it valid until the edge after the strobe negates. So the bug is invisible in a well-behaved system — which is exactly what makes it dangerous. It passes every unit test until it meets a specific partner.

What happens when it does. I measured it: the slave was asked to write CONTROL with 0x0000000F, remembered offset 2 correctly, and committed to offset 6 with 0xDEADBEEF — because the payload had moved by the commit cycle. It returned ACK. CONTROL was never written; OUTPUT_DATA holds a value nobody requested. No error anywhere.

The part I would emphasise in a review. The master violated the protocol, but the slave is what converted a protocol violation into silent, permanent, mis-targeted state. A slave committing from its own copy writes CONTROL correctly despite the master's bug. That is the difference between correct-by-construction and correct-if-my-partner-behaves, and it costs three registers.

And the verification consequence. P3 — at a commit, the indexed offset equals the remembered one — only exists for registered slaves. Choosing to register a response to close timing is not just a cycle of latency; it adds an invariant that has to be written, bound and budgeted.

11. Understanding Check

12. What's Next

The termination is now a design decision with a declared policy behind it, and the registered form carries a new invariant.

Both registered slaves inserted exactly one cycle. Real peripherals take longer — and while they do, the master is holding a payload that will mutate the device the moment the slave is ready.

What must remain true while a write waits for the slave?

Chapter 7.4 — Wait States answers it. 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.