Skip to content
VLSI Mentor

Wishbone · Module 7

Data Transfer

Write data leaves the master, crosses the bus and lands in specific bytes of one register. The byte-lane mask is the only place where data the transfer never mentioned can be destroyed.

Chapter 7.1 drew the byte mask as one box in the write path. It is the stage where a write either lands in the bits it named or destroys the ones beside them.

How does write data travel from the requester into the correct bits of the correct register?

1. The Forward Payload Path

A block diagram of the write data path arranged left to right in five stages. On the left the client supplies a data word. The master captures it into a private register at the acceptance edge. That register drives the bus data output, which the interconnect carries to the selected slave's data input. Inside the slave a byte lane mask, driven by the select signals, decides which bytes of the addressed register will be replaced. The result is committed into the register file, where unselected byte lanes keep their previous contents.CLIENTa value to writeCAPTURElatched onceDAT_Oqualified by STB_OBYTE MASKSEL picks the lanesREGISTERselected lanes replaced12
Figure 1 — write data from client to committed bits. Five stages, each corrupting the value in a distinguishable way.
StageOwnerCharacteristic failureEvidence
Clientclientasked for the wrong valuebus data matches the request; the request was wrong
Capturemasterpayload not latchedbus data drifts mid-transfer (7.4)
DAT_Omasterqualified by STB_O under RULE 3.60
Byte maskslaveignores or misreads SEL_Ineighbouring bytes change
Registerslavewrong case arma different register changes

Row four is unique to writes. Every other row puts the wrong value somewhere the transfer named. That one damages data the transfer never mentioned, which is why its symptom appears in unrelated code.

2. Data Belongs to a Transaction, Not to a Wire

DAT_O carries a level on every cycle, exactly as DAT_I did in Chapter 6.3. The question is never "is there data on the bus" but "does it belong to the transfer being presented".

RULE 3.60"MASTER interfaces MUST qualify the following signals with [STB_O]: [ADR_O], [DAT_O()], [SEL_O()], [WE_O], and [TAGN_O]."

And §3.2.2 is more specific for a write. RULE 3.75 makes its timing normative, and that timing requires the master's address, data, WE_O and byte selects to remain valid until the rising clock edge following the negation of STB_O.

Read that carefully — it is stronger than "stable while the strobe is asserted." The payload must still be coherent at the edge that ends the transfer, which is the edge at which Chapter 7.1's slave commits. A payload that decayed one edge early would decay exactly when the slave needed it.

3. The Byte-Lane Mask

SEL_O has one bit per byte lane of the data bus. Chapter 4.7 established the mapping as a statement about wires: SEL_O[n] corresponds to DAT[8n+7 : 8n], always, and RULES 3.90 and 3.100 bind the data organisation to the specification's tables.

On a read the mask says which lanes the master will believe. On a write it says which lanes the slave will replace — and by omission, which it must leave alone.

The intuitive form first, because it is the one to reach for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for each lane n:
    if SEL[n]:
        register[lane n] <= write_data[lane n]
    // else: not assigned at all, so the lane keeps its value

The unselected lane is not written with anything. It is not written with zero, not written with the old value explicitly — it is simply not the target of an assignment, so the flop holds. That is why a per-lane conditional cannot corrupt a neighbour: there is no code path that touches it.

The mask formulation computes the same thing as one expression:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lane_mask = {{8{SEL[3]}}, {8{SEL[2]}}, {8{SEL[1]}}, {8{SEL[0]}}}
next      = (write_data & lane_mask) | (register & ~lane_mask)

Both are correct. The per-lane loop is what this module uses, because a reviewer can see at a glance that unselected lanes are untouched. The mask version makes that a property of two bitwise operations, which is harder to check by eye and easier to get subtly wrong — invert one ~ and every unselected byte zeroes.

What must never appear is the whole-word assignment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
register <= write_data;        // ignores SEL entirely

That is correct only for a full-word write and destroys three bytes on every narrow one.

4. RTL — The Masked Update, Isolated

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_lane — the byte-lane masked update, pulled out on its own.
//
// PURPOSE. This is stage 4 of Figure 1, isolated from the surrounding slave
// so the update rule is reviewable without the qualification logic around
// it. It is a pure function: old value + new value + lane mask -> result.
//
// NOT A WISHBONE SLAVE — no clock, no qualifiers, terminates nothing.
//
// Both formulations from Section 3 appear here so they can be compared,
// and an assertion-free equivalence check is left to Section 8's property.
// ─────────────────────────────────────────────────────────────────────────
module wb_write_lane #(
  parameter int unsigned DW = 32
) (
  input  logic [DW-1:0]   old_i,
  input  logic [DW-1:0]   wdat_i,
  input  logic [DW/8-1:0] sel_i,
  output logic [DW-1:0]   next_o,        // per-lane form (what we use)
  output logic [DW-1:0]   next_mask_o    // mask form (for comparison)
);
  localparam int unsigned NL = DW/8;

  // ── PER-LANE FORM ─────────────────────────────────────────────────────
  // next_o is seeded with the OLD value, then selected lanes are
  // overwritten. Seeding first is what makes the unselected lanes keep
  // their contents, and it is also what prevents a latch: every bit of
  // next_o is assigned on every path through the block.
  always_comb begin
    next_o = old_i;
    for (int unsigned n = 0; n < NL; n++) begin
      if (sel_i[n]) next_o[n*8 +: 8] = wdat_i[n*8 +: 8];
    end
  end

  // ── MASK FORM ─────────────────────────────────────────────────────────
  // Identical result, built from a replicated lane mask. Shown because it
  // appears in a great deal of production RTL and a reader should be able
  // to recognise it — but note how much harder it is to confirm by eye
  // that an unselected lane survives. Dropping the `~` on the second term
  // zeroes every unselected byte and still looks plausible.
  logic [DW-1:0] lane_mask;
  always_comb begin
    for (int unsigned n = 0; n < NL; n++) begin
      lane_mask[n*8 +: 8] = {8{sel_i[n]}};
    end
  end
  assign next_mask_o = (wdat_i & lane_mask) | (old_i & ~lane_mask);
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_regs_lane — the running peripheral rebuilt around wb_write_lane.
//
// PURPOSE. Show the composition explicitly: qualification, a commit event,
// and a masked update that is now a separately reviewable block.
//
// Functionally identical to Chapter 7.1's wb_write_regs. The difference is
// that the update rule is no longer buried in a for-loop inside an
// always_ff, so it can be argued about — and tested — on its own.
//
// DECLARED COMMIT POLICY (LOCAL, unchanged from Chapter 7.1):
//   commits the addressed register on the same rising clock edge for which
//   it returns ACK_O for a qualified write.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00).
// ─────────────────────────────────────────────────────────────────────────
module wb_write_regs_lane #(
  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 [DW-1:0]     dat_o,
  output logic              ack_o,
  output logic              err_o,
  output logic [DW-1:0]     control_o,
  output logic [DW-1:0]     outdata_o
);
  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;
  localparam logic [DW-1:0] ID_VALUE = 32'h5742_0701;

  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

  logic writable, known_off;
  always_comb begin
    unique case (adr_i)
      O_CTRL, O_OUT: begin known_off = 1'b1; writable = 1'b1; end
      O_ID:          begin known_off = 1'b1; writable = 1'b0; end
      default:       begin known_off = 1'b0; writable = 1'b0; end
    endcase
  end

  logic illegal;
  assign illegal = ~known_off | (we_i & ~writable);
  assign err_o   = xfer &  illegal;
  assign ack_o   = xfer & ~illegal;

  logic commit;
  assign commit = xfer & we_i & ack_o;           // the declared policy

  // ── THE MASKED UPDATE, ONE INSTANCE PER REGISTER ──────────────────────
  // Each register gets its own lane block. The register's own current
  // value goes in as `old_i`, so a lane the write did not select comes
  // back out unchanged and the flop reloads what it already held.
  logic [DW-1:0] ctrl_next, out_next;
  wb_write_lane #(.DW(DW)) u_ctrl_lane (
    .old_i(ctrl_q), .wdat_i(dat_i), .sel_i(sel_i),
    .next_o(ctrl_next), .next_mask_o());
  wb_write_lane #(.DW(DW)) u_out_lane (
    .old_i(outdata_q), .wdat_i(dat_i), .sel_i(sel_i),
    .next_o(out_next), .next_mask_o());

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      ctrl_q    <= '0;
      outdata_q <= '0;
    end else if (commit) begin
      // Only the ADDRESSED register loads. The other lane block is still
      // computing a result every cycle — that costs nothing and is simply
      // not used, which is the normal shape for a small register file.
      unique case (adr_i)
        O_CTRL:  ctrl_q    <= ctrl_next;
        O_OUT:   outdata_q <= out_next;
        default: ;                                // unreachable under commit
      endcase
    end
  end

  always_comb begin
    dat_o = '0;                                   // RULE 3.65
    if (xfer && !we_i && ack_o) begin
      unique case (adr_i)
        O_CTRL:  dat_o = ctrl_q;
        O_OUT:   dat_o = outdata_q;
        O_ID:    dat_o = ID_VALUE;
        default: dat_o = '0;
      endcase
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_regs_nosel — THE IGNORED-SEL BUG, isolated for simulation.
//
// NOT A REFERENCE DESIGN. Identical to wb_write_regs_lane except that the
// commit assigns the WHOLE WORD instead of the masked result:
//
//     ctrl_q <= dat_i;        instead of      ctrl_q <= ctrl_next;
//
// Every bus signal it produces is conformant. It acknowledges correctly,
// it refuses read-only offsets correctly, and for a FULL-WORD write it is
// indistinguishable from the correct slave. Only a narrow write exposes
// it, and what it destroys is data the transfer never named.
// ─────────────────────────────────────────────────────────────────────────
module wb_write_regs_nosel #(
  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 [DW-1:0]     dat_o,
  output logic              ack_o,
  output logic              err_o,
  output logic [DW-1:0]     control_o,
  output logic [DW-1:0]     outdata_o
);
  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 writable;
  always_comb begin
    unique case (adr_i)
      O_CTRL, O_OUT: writable = 1'b1;
      default:       writable = 1'b0;
    endcase
  end
  assign err_o = xfer & we_i & ~writable;
  assign ack_o = xfer & ~(we_i & ~writable);

  logic commit; assign commit = xfer & we_i & ack_o;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      ctrl_q <= '0; outdata_q <= '0;
    end else if (commit) begin
      // ── THE BUG: sel_i is never consulted ───────────────────────────
      unique case (adr_i)
        O_CTRL:  ctrl_q    <= dat_i;
        O_OUT:   outdata_q <= dat_i;
        default: ;
      endcase
    end
  end

  assign dat_o = '0;                              // read path omitted
endmodule

Reading the group

Purpose. The lane block is the update rule alone; the composed slave shows it in place; the third slave omits it so the damage can be measured.

Interface. wb_write_lane is a pure combinational function — no clock, no reset, no qualifiers. The two slaves present identical Wishbone interfaces.

State. Lane block: none, so no reset is needed and none is present. A reviewer checking RULE 3.20 should confirm a block is stateless rather than hunt for a missing reset. The slaves hold two registers each.

Combinational logic. Lane block: the seeded per-lane overwrite and the mask formulation. Slaves: xfer, legality, two exclusive terminations, commit, and two lane instances.

Sequential logic. One masked load per commit, into the addressed register only.

Write start. The slave has no start event — only the first cycle in which xfer is true. commit is the event it manufactures, and it is true for exactly one cycle per transfer.

Address. adr_i, the local offset from the decode (Chapter 6.2).

Write data. dat_i, consumed in the commit cycle.

Byte enables. sel_i, consumed in the same cycle by the lane block.

Commit. xfer & we_i & ack_o, the declared policy from Chapter 7.1.

Waiting. Neither slave waits. Chapter 7.4 adds latency, and the ack_o term in commit stops being redundant there.

Reset. Synchronous, active high, per RULES 2.30 and 3.00.

Failure modes. Section 7.

Simplifications. The mask formulation's output is left unconnected in the composed slave — it exists to be compared in simulation, not to be used twice. wb_write_regs_nosel omits the read path entirely since it exists only to be written to.

5. Waveform — One Byte In, Three Bytes Preserved

A narrow write: SEL selects one lane

7 cycles
Seven clock cycles showing a byte write into a register that already holds a known value. Before the transfer the control register holds eleven twenty-two thirty-three forty-four. In cycle two the master asserts both qualifiers with write enable high, drives word address two, places the value aa bb cc dd on the data output and asserts only select bit one. The slave acknowledges in the same cycle and commits at the edge ending it. From cycle three the control register reads eleven twenty-two cc forty-four, showing that only byte lane one was replaced and the other three lanes kept their previous contents.SEL=0010: lane 1 onlySEL=0010: lane 1 only3 bytes survived3 bytes survivedCLK_ICYC_OSTB_OWE_OADR_O----0x2--------------------DAT_O----AABBCCDD--------------------SEL_O0000001000000000000000000000ACK_ICONTROL11223344112233441122CC441122CC441122CC441122CC441122CC44t0t1t2t3t4t5t6
Figure 2 — CONTROL holds 0x11223344; a single-byte write to lane 1 changes only that byte.

SEL_O = 0010 selects lane 1, which is DAT[15:8] — the byte 0xCC of 0xAABBCCDD.

CONTROL goes 0x112233440x1122CC44. Byte 1 became 0xCC; bytes 0, 2 and 3 kept 0x44, 0x22 and 0x11.

What wb_write_regs_nosel shows instead is 0xAABBCCDD — the whole word, with three bytes of unrelated state destroyed. The bus trace is identical in both cases, which Section 8 measures.

Note the lane arithmetic, because it is the step people get backwards. Lane 1 is bits 15 down to 8, not bits 8 up to 15 of the value — the mapping is positional on the bus. Chapter 4.7 §2 covered the endianness question this raises, and it is a system decision made in the master, not in the slave.

6. Simulation — Partial Write and the Ignored Mask

Simulation C — the correct slave.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === SIMULATION C - partial write ===
  CONTROL seeded              0x11223344
  write DAT=0xaabbccdd SEL=0010
    per-lane result           0x1122cc44
    mask-form result          0x1122cc44   (identical)
    CONTROL after commit      0x1122cc44
    bytes changed                      1
    bytes preserved                    3

Only lane 1 moved, and the two formulations from Section 3 agreed exactly — which is worth knowing, because the mask form is what a reader will meet in production RTL.

Simulation C2 — the same stimulus against the ignored-SEL slave.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  === the same write, SEL ignored ===
  CONTROL seeded              0x11223344
    CONTROL after commit      0xaabbccdd
    bytes changed                      4
    bytes destroyed that the write never named   3
  bus-trace differences between the two slaves:  0

Three bytes of unrelated state were destroyed, and the two slaves produced byte-identical bus traces. A protocol checker attached to either passes.

This is the write-side twin of Chapter 6.3's finding. There, a conformant bus hid a master that captured at the wrong edge. Here it hides a slave that overwrote data nobody asked it to touch — and unlike the read case, the damage is permanent.

7. Failure Modes and Discriminating Evidence

Symptom: the right register holds the wrong value; all four bytes are wrong.

Candidate causes. The master captured the wrong data, or the client asked for the wrong value.

Discriminating evidence. Compare req_dat_i in the acceptance cycle against DAT_O on the bus. Equal means the client asked for it; different means the master's capture is at fault. One comparison splits them.

Likely RTL location. The master's dat_q load, or nothing — the bug may be upstream of the bus entirely.

Symptom: the right register holds the wrong value; only some bytes are wrong.

Candidate causes. The byte mask selected the wrong lanes, or the lane mapping is reversed.

Discriminating evidence. Compare SEL_O against which bytes actually moved. If lane 1 was selected and lane 2 changed, the mapping is off by one; if lane 3 changed when lane 0 was selected, the lanes are reversed — which is the endianness confusion Chapter 4.7 §2 warned about, and it lives in the master.

Symptom: bytes the write never mentioned changed.

Candidate causes. The commit assigns the whole word and ignores SEL_I.

Discriminating evidence. Seed the register with a known non-zero value, write one byte, read back. Neighbouring bytes taking the write data's bytes is conclusive — and the giveaway is that they take the write data's bytes rather than zero.

Likely RTL location. The commit assignment. Look for reg_q <= dat_i with no mask.

Property. P1 in Section 8.

Symptom: a different register changed.

Candidate causes. Wrong address on the bus, wrong decode, or a wrong case arm in the commit.

Discriminating evidence. ADR_O, then the decoded select, then the local offset, then which register's enable asserted. The first mismatch names the stage — the same walk Chapter 6.2 §7 used for reads.

Symptom: nothing changed and the transfer succeeded.

Candidate causes. SEL_O was all-zero.

Discriminating evidence. SEL_O = 0000 with ACK returned. A legal transfer that selects no lanes commits nothing — the slave did exactly what it was told. Chapter 4.7 §4 treated an empty mask as almost always a master bug and worth erroring on.

Symptom: a write to a read-only register silently succeeds.

Candidate causes. The writable term omits that offset.

Discriminating evidence. Check ERR_O for that transfer. Absent means the slave never classified it as read-only, which is a legality bug rather than a data-path one.

8. Verification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_write_data_checker — payload and masking properties.
//
// P1 is a DESIGN OBLIGATION derived from the SEL_O() signal description
// (SEL names where valid data is PLACED on a write) — the specification
// constrains what SEL MEANS, and "therefore an unselected lane must not
// change" is a sound inference about a slave's internal state rather than
// a stated rule. P2 and P3 are SPECIFICATION. P4 is LOCAL POLICY.
// ─────────────────────────────────────────────────────────────────────────
module wb_write_data_checker #(
  parameter int unsigned DW = 32,
  parameter int unsigned NL = 4
) (
  input logic          clk_i,
  input logic          rst_i,
  input logic          cyc_o,
  input logic          stb_o,
  input logic          we_o,
  input logic [DW-1:0] dat_o,
  input logic [NL-1:0] sel_o,
  input logic          ack_i,
  input logic          err_i,
  input logic          rty_i,
  // white-box, slave side
  input logic          commit,
  input logic [DW-1:0] ctrl_q,
  input logic [DW-1:0] ctrl_prev     // ctrl_q one cycle earlier
);
  default disable iff (rst_i);

  // P1 — DESIGN OBLIGATION, and the property this chapter exists for.
  //      An UNSELECTED lane of the committed register must be unchanged.
  //      Generated per lane so a failure names WHICH lane was destroyed,
  //      turning "the register is wrong" into a one-line diagnosis.
  generate
    for (genvar n = 0; n < NL; n++) begin : g_lane
      property p_unselected_lane_preserved;
        @(posedge clk_i)
          (ctrl_q[n*8 +: 8] != ctrl_prev[n*8 +: 8]) |-> $past(sel_o[n]);
      endproperty
      a_unselected_lane_preserved :
        assert property (p_unselected_lane_preserved)
        else $error("lane %0d of CONTROL changed while unselected", n);
    end
  endgenerate

  // P2 — SPECIFICATION (RULE 3.60, and §3.2.2 via RULE 3.75). The whole
  //      payload holds still while the write is outstanding. The end
  //      boundary is the termination: omitting it would forbid a legal
  //      back-to-back write on the next cycle, which is the over-strong
  //      trap Chapter 4.6 Section 10 described.
  property p_payload_stable;
    @(posedge clk_i) (cyc_o && stb_o && we_o && !(ack_i||err_i||rty_i))
      |=> ($stable(dat_o) && $stable(sel_o) && $stable(we_o));
  endproperty
  a_payload_stable : assert property (p_payload_stable)
    else $error("RULE 3.60 / 3.2.2: write payload moved while outstanding");

  // P3 — SPECIFICATION (RULE 3.60). A write presents a non-empty payload
  //      qualification. NOT a rule that SEL must be non-zero — an empty
  //      mask is legal and simply commits nothing — so this checks only
  //      that SEL is driven, never unknown, while qualified.
  property p_sel_driven;
    @(posedge clk_i) (cyc_o && stb_o) |-> !$isunknown(sel_o);
  endproperty
  a_sel_driven : assert property (p_sel_driven)
    else $error("SEL_O unknown while a transfer was presented");

  // P4 — LOCAL POLICY. State changes only at a commit event, checked
  //      against the slave's own declared term rather than against ACK_I
  //      (Chapter 7.1 Section 11 explains why that distinction matters).
  property p_state_only_on_commit;
    @(posedge clk_i) (ctrl_q != ctrl_prev) |-> $past(commit);
  endproperty
  a_state_only_on_commit : assert property (p_state_only_on_commit)
    else $error("LOCAL: register changed outside a commit event");
endmodule

P1 is generated per lane deliberately, for the reason Chapter 4.7 §7 gave: a single property over the whole word reports "data changed" and leaves the engineer to work out which byte and why. Per-lane generation names the lane in the message, and the lane number maps directly onto a SEL bit and a DAT range.

P1 needs white-box access and there is no alternative — the bus during the ignored-SEL bug is fully conformant, as Section 6 measured. That is the sixth time this course has met that boundary, after lost atomicity (4.9), repeated writes (5.3), the RULE 3.55 stall (5.4), capture-edge bugs (6.3) and repeated read side effects (6.5). Chapter 5.8 §8 collected the pattern.

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

9. Common Mistakes

"SEL can be ignored on a 32-bit bus."

Wrong mental model: everything writes whole words anyway.

Concrete bug: the whole-word assignment. Correct until the first narrow write, then it destroys three bytes.

Observable evidence: neighbouring bytes taking the write data's bytes, with a perfectly conformant bus trace.

Correct model: SEL_O() names where valid data is placed on a write. Ignoring it is only safe if the interface contract says partial writes are unsupported — and that is a claim about today's driver, not about the interface.

"Partial write means the unselected bytes become zero."

Wrong mental model: the mask clears what it does not select.

Concrete bug: a mask formulation with a dropped ~, or an update that seeds next with zero instead of the old value.

Observable evidence: unselected bytes reading zero rather than their previous contents — which looks like the whole-word bug but is subtly different, and the difference tells you which line to fix.

Correct model: unselected lanes are not assigned at all. Seeding with the old value is what expresses that.

"Once the data is on DAT_O, the slave owns it."

Wrong mental model: the bus hands data over.

Concrete bug: a master that changes DAT_O mid-transfer, believing the slave already has it.

Observable evidence: a committed value that matches neither the requested one nor anything the client currently holds.

Correct model: the data belongs to a qualified transaction, and §3.2.2 requires it to stay valid until the edge after the strobe negates. A combinational slave has nowhere to keep an early copy — PERMISSION 3.10 describes exactly such a slave.

10. Interview Reasoning

The implementation I would write:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_comb begin
  next = reg_q;                          // seed with the OLD value
  for (int n = 0; n < 4; n++)
    if (sel[n]) next[n*8 +: 8] = wdat[n*8 +: 8];
end
...
if (commit) reg_q <= next;

The argument for a reviewer is the seed line. next starts as the current contents, so a lane the write did not select is never the target of an assignment and comes back out unchanged. There is no code path that touches it — that is a stronger claim than "the mask happens to preserve it", and a reviewer can check it by eye in one line.

It also prevents a latch, because every bit of next is assigned on every path through the always_comb.

The mask formulation is equivalent and I would recognise it but not reach for it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lane_mask = {{8{sel[3]}}, {8{sel[2]}}, {8{sel[1]}}, {8{sel[0]}}};
next      = (wdat & lane_mask) | (reg_q & ~lane_mask);

Same result — I measured both forms agreeing exactly. But confirming that an unselected byte survives now requires reasoning about two bitwise terms and one inversion, and dropping that ~ zeroes every unselected byte while still looking entirely plausible.

What I would put in the testbench, because the failure is invisible otherwise. Seed the register with a distinctive non-zero value — 0x11223344, not zero — then write one byte and check the other three. Seeding with zero hides the whole-word bug completely, because zeroed neighbours and preserved-zero neighbours are the same bits.

And the assertion, generated per lane so a failure names the byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(reg_q[n*8 +: 8] != reg_prev[n*8 +: 8]) |-> $past(sel[n])

Why the assertion matters more here than on a read. A bus-level checker cannot see this at all — I measured zero trace differences between the correct slave and one that ignores SEL entirely. The damage is to internal state, and it is permanent.

11. Understanding Check

12. What's Next

The payload now reaches the right bits of the right register, with the lanes the transfer never named left alone.

Both slaves so far have committed on the same edge they acknowledged, because that is the policy Chapter 7.1 declared. That coincidence is a design choice, not a law — and a slave that separates the two has to keep them coherent by other means.

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

Chapter 7.3 — ACK Timing 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.