Skip to content
VLSI Mentor

Wishbone · Module 4

SEL_O

How a 32-bit bus writes one byte without disturbing the other three: byte lanes, the per-lane conditional write, where endianness actually lives, and why ignoring the mask destroys data the transfer never named.

Every transfer so far has moved a whole data word. Real software does not: it writes single bytes, half-words, and fields inside registers that hold four bytes each. The address bus cannot help — Chapter 4.3 established that a Wishbone address names a word, not a byte.

How does a 32-bit bus write one byte without disturbing the other three?

1. Why the Address Bus Cannot Do This

Chapter 4.3 §2 explained that a Wishbone ADR_O on a 32-bit bus addresses words: the byte-offset bits are not present on the bus at all, because every transfer moves a word and there is nothing for them to select.

That is precisely the problem. Software says "store this byte at 0x1003". The interface converts that to word address 0x400 — and now every byte-level distinction is gone. The word address names all four bytes equally.

SEL_O restores what the address dropped. The address says which word; SEL_O says which bytes within it. Together they name a byte range, and neither alone can.

Why not just have a byte-addressed bus? Because then every transfer would move one byte, and a 32-bit read would take four transfers. The word-plus-mask arrangement is what lets the common case — a full-word access — cost one transfer while still permitting the narrow case. Nearly every SoC bus makes the same trade; Chapter 2.3 §4 set out the general form.

2. Lane Numbering and the Endianness Trap

SEL_O[n] corresponds to DAT[8n+7 : 8n]. That is a statement about wires, and it is unambiguous: lane 0 is the low eight bits of the data bus, always.

Which byte address lane 0 holds is a different question, and it is the one that causes trouble.

On a little-endian system, byte address 0 within the word lives in lane 0. A byte store to address 0x1000 sets SEL_O = 4'b0001.

On a big-endian system, byte address 0 lives in the highest lane. The same store sets SEL_O = 4'b1000.

3. The Slave's Obligation on a Write

This is the heart of the chapter.

A slave receiving a write must update only the selected lanes of the addressed word. The unselected lanes hold bytes belonging to other variables, other fields, other software entirely — and the slave is the only thing standing between a narrow write and their destruction.

The correct structure is a per-lane conditional write:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for each lane n:
    if (write is qualified) and (SEL_I[n]):
        storage[word][lane n] <= DAT_I[8n+7 : 8n]

The incorrect structure is a whole-word write, which stores all four lanes and takes the unselected ones from whatever DAT_I happens to carry there. The master is not required to drive anything meaningful on unselected lanes, so those bytes are undefined — and in practice they are the remains of a previous transfer.

The damage is to data the transfer never mentioned. Software writes one byte and three neighbouring bytes change. Nothing in the access that failed points at the bytes that were destroyed, which is what makes this class of bug so expensive: the symptom appears in unrelated code, at an unrelated time.

4. RTL — A Byte-Lane Register File

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_sel_slave — a byte-addressable register file, correctly lane-gated.
//
// PURPOSE. This is the reference structure for every Wishbone slave that
// holds storage rather than fixed fields: a per-lane conditional write.
//
// LANE CONVENTION. sel_i[n] covers dat[8n+7 : 8n]. That is a WIRE fact and
// is fixed. Which BYTE ADDRESS lands in which lane is an endianness
// decision made by the MASTER (Section 2) and is not this module's business.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00).
// ─────────────────────────────────────────────────────────────────────────
module wb_sel_slave #(
  parameter int unsigned OFF_AW = 4,           // 16 words
  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 [(DW/8)-1:0]     sel_i,          // qualified with stb_i
  input  logic [OFF_AW-1:0]     adr_i,
  input  logic [DW-1:0]         dat_i,
  output logic [DW-1:0]         dat_o,
  output logic                  ack_o,
  output logic                  err_o
);
  localparam int unsigned NL    = DW / 8;       // lanes
  localparam int unsigned WORDS = 1 << OFF_AW;

  logic [DW-1:0] mem_q [WORDS];

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

  // A write with SEL_I == 0 selects nothing. It is not an error in the
  // specification's terms, but it is almost always a master bug, so this
  // slave reports it rather than silently doing nothing — an EDUCATIONAL
  // IMPLEMENTATION CHOICE, flagged as such, not a specification rule.
  logic empty_sel;
  assign empty_sel = xfer & (sel_i == '0);

  assign err_o = empty_sel;
  assign ack_o = xfer & ~err_o;

  logic write_ok;
  assign write_ok = xfer & we_i & ~err_o;       // Chapter 4.6's single term

  // ── THE PER-LANE CONDITIONAL WRITE ─────────────────────────────────────
  // Each lane is gated INDEPENDENTLY. An unselected lane is not written at
  // all, so it keeps the neighbouring byte it already holds.
  //
  // The whole-word alternative —
  //     if (write_ok) mem_q[adr_i] <= dat_i;
  // — stores all NL lanes, taking unselected ones from whatever the master
  // left on dat_i. That destroys three neighbours on every byte write, and
  // the destroyed bytes belong to data the transfer never named.
  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      for (int unsigned w = 0; w < WORDS; w++) mem_q[w] <= '0;
    end else if (write_ok) begin
      for (int unsigned n = 0; n < NL; n++) begin
        if (sel_i[n]) mem_q[adr_i][n*8 +: 8] <= dat_i[n*8 +: 8];
      end
    end
  end

  // ── READ PATH ──────────────────────────────────────────────────────────
  // On a read, unselected lanes carry nothing a master should believe.
  // Driving zero there is an educational choice that makes the discipline
  // VISIBLE in simulation: a master that ignores SEL on reads sees zeros
  // in the lanes it did not ask for, rather than plausible data that hides
  // the mistake. A slave returning the full word is equally conformant.
  always_comb begin
    dat_o = '0;
    if (xfer && !we_i && !err_o) begin
      for (int unsigned n = 0; n < NL; n++) begin
        if (sel_i[n]) dat_o[n*8 +: 8] = mem_q[adr_i][n*8 +: 8];
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_sel_master — converting a BYTE address and a SIZE into a word address
// plus a lane mask. This is where endianness actually lives.
//
// LITTLE-ENDIAN. Byte offset 0 within a word occupies lane 0. Stated here
// because it is a CHOICE, not a specification requirement (Section 2).
// ─────────────────────────────────────────────────────────────────────────
module wb_sel_master #(
  parameter int unsigned BYTE_AW = 32,
  parameter int unsigned DW      = 32
) (
  input  logic                    clk_i,
  input  logic                    rst_i,

  input  logic                    go_i,
  input  logic                    we_i,
  input  logic [BYTE_AW-1:0]      badr_i,       // BYTE address from the CPU
  input  logic [1:0]              size_i,       // 0=byte 1=half 2=word
  input  logic [DW-1:0]           wdat_i,       // right-justified payload
  output logic                    done_o,
  output logic                    misaligned_o,

  output logic                    cyc_o,
  output logic                    stb_o,
  output logic                    we_o,
  output logic [(DW/8)-1:0]       sel_o,
  output logic [BYTE_AW-3:0]      adr_o,        // WORD address
  output logic [DW-1:0]           dat_o,
  input  logic                    ack_i,
  input  logic                    err_i,
  input  logic                    rty_i
);
  localparam int unsigned NL = DW / 8;

  logic          active_q;
  logic [(NL)-1:0] sel_q;
  logic [BYTE_AW-3:0] adr_q;
  logic [DW-1:0] dat_q;
  logic          we_q;

  assign cyc_o = active_q;
  assign stb_o = active_q;
  assign we_o  = we_q;
  assign sel_o = sel_q;
  assign adr_o = adr_q;
  assign dat_o = dat_q;

  // ── LANE MASK FROM BYTE OFFSET AND SIZE ────────────────────────────────
  // Little-endian: offset 0 -> lane 0.
  logic [1:0] boff;
  assign boff = badr_i[1:0];

  function automatic logic [NL-1:0] mask_of(input logic [1:0] sz,
                                            input logic [1:0] off);
    unique case (sz)
      2'd0: mask_of = NL'(4'b0001) << off;              // one byte
      2'd1: mask_of = NL'(4'b0011) << off;              // half-word
      default: mask_of = {NL{1'b1}};                    // full word
    endcase
  endfunction

  // ── ALIGNMENT ──────────────────────────────────────────────────────────
  // A half-word at offset 1 or 3, or a word at any non-zero offset, would
  // need a mask that straddles two words. Wishbone has no such transfer.
  // Detecting this HERE, in the master, is the right place: the alternative
  // is a silently truncated mask and a corrupted neighbour.
  logic bad_align;
  always_comb begin
    unique case (size_i)
      2'd0:    bad_align = 1'b0;
      2'd1:    bad_align = boff[0];
      default: bad_align = (boff != 2'd0);
    endcase
  end

  // ── PAYLOAD PLACEMENT ──────────────────────────────────────────────────
  // The CPU hands over a right-justified value; the bus expects it in the
  // lanes SEL names. A byte destined for lane 2 must be shifted there.
  // Forgetting this shift is the "byte always lands in lane 0" bug.
  logic [DW-1:0] placed;
  assign placed = wdat_i << (8 * boff);

  logic terminated;
  assign terminated = ack_i | err_i | rty_i;

  always_ff @(posedge clk_i) begin
    if (rst_i) begin
      active_q     <= 1'b0;
      sel_q        <= '0;
      adr_q        <= '0;
      dat_q        <= '0;
      we_q         <= 1'b0;
      done_o       <= 1'b0;
      misaligned_o <= 1'b0;
    end else begin
      done_o       <= 1'b0;
      misaligned_o <= 1'b0;
      if (!active_q) begin
        if (go_i) begin
          if (bad_align) begin
            // Refuse locally. Never issue a transfer whose mask cannot
            // express what was asked for.
            done_o       <= 1'b1;
            misaligned_o <= 1'b1;
          end else begin
            active_q <= 1'b1;
            we_q     <= we_i;
            sel_q    <= mask_of(size_i, boff);
            adr_q    <= badr_i[BYTE_AW-1:2];
            dat_q    <= placed;
          end
        end
      end else if (terminated) begin
        active_q <= 1'b0;
        done_o   <= 1'b1;
      end
    end
  end
endmodule

Reading the pair

Purpose. The slave shows the per-lane write that protects neighbours; the master shows where byte addresses, sizes and endianness are converted into a lane mask.

Ownership. The master drives SEL_O; the slave consumes SEL_I and never drives it.

Combinational logic. Slave: qualification, the empty-mask check, terminations, and a lane-gated read multiplexer. Master: mask_of, alignment, and the payload shift.

Sequential logic. Slave: the word array, written lane by lane. Master: the request registers including the computed mask.

Timing. The mask is computed combinationally from the request and registered alongside the address, so it is stable for the whole transfer — RULE 3.60 lists SEL_O() and ADR_O together as signals the master qualifies with STB_O.

Qualification. write_ok carries forward Chapter 4.6's single-named-term discipline, and sel_i[n] gates each lane on top of it.

Reset. Synchronous, active high. The slave clears its array, which is an educational choice — real memories do not.

Simplifications. mask_of assumes a 32-bit bus in its literals. A parameterised version would build the mask from sz arithmetically; the fixed form is clearer and the NL'() casts keep it width-correct.

Failure modes. Section 6.

5. Waveform — A Byte Write Between Two Full Words

SEL_O: one byte in, three bytes preserved

9 cycles
Nine clock cycles showing three transfers to the same word address. The first transfer is a full word write with all four select bits asserted and the value eleven twenty-two thirty-three forty-four on the data output; after it the stored word holds that value. The second transfer is a single byte write with only select bit two asserted and the byte ninety-nine positioned in lane two; after it the stored word holds eleven ninety-nine thirty-three forty-four, showing that lanes zero, one and three were left untouched. The third transfer is a full word read which returns the modified word, confirming that the narrow write changed exactly one byte.full word: SEL = 1111full word: SEL = 1111byte write: SEL = 0100 onlybyte write: SEL = 0100 onlyread back: 3 bytes survivedread back: 3 bytes survivedCLK_ISTB_OWE_OSEL_O000011110000010000001111000000000000DAT_O01122334411223344009900000099000000990000009900000099000000990000ACK_Imem000000000000000011223344112233441199334411993344119933441199334411993344t0t1t2t3t4t5t6t7t8
Figure 1 — a full-word write, a single-byte write to lane 2 of the same word, and a read back. Only lane 2 changes.

Cycle 3 is the whole lesson. SEL_O = 4'b0100 selects lane 2 alone. DAT_O carries 0x00990000 — the byte 0x99 shifted into lane 2 by the master, with the other lanes carrying nothing meaningful.

Cycle 5 shows what a correct slave did: 0x11993344. Lane 2 changed; lanes 0, 1 and 3 kept 0x44, 0x33 and 0x11.

What a whole-word slave would show instead: 0x00990000 — the entire DAT_I, stored unconditionally. Three bytes belonging to other data, gone, with nothing in the access that caused it pointing at them.

6. Failure Modes and Discriminating Evidence

Symptom: writing one byte corrupts its three neighbours.

Candidate causes. The slave performs a whole-word write and ignores SEL_I entirely.

Discriminating evidence. Write a full word of 0xAABBCCDD, then a single byte to lane 2, then read back. A correct slave returns a word with three original bytes intact; a whole-word slave returns the raw DAT_I, typically with zeros in the unselected lanes. The zeros are the tell — real neighbouring data does not become zero by accident.

Likely RTL location. The write statement. Look for mem_q[adr] <= dat_i; with no per-lane loop.

Property. P1 in Section 7.

Symptom: byte writes always land in the lowest byte of the word.

Candidate causes. The master computes SEL_O correctly but does not shift the payload into the selected lane.

Discriminating evidence. Capture SEL_O and DAT_O on the same cycle. If SEL_O is 0100 while the payload sits in DAT_O[7:0], the mask and the data disagree — conclusive, and visible in a single cycle without any read-back.

Likely RTL location. The payload placement — a missing << (8 * boff).

Symptom: data is byte-reversed within words but otherwise consistent.

Candidate causes. Endianness mismatch between the master's mask generation and the system's expectation, or a slave that applies its own endian conversion on top of the master's.

Discriminating evidence. Write 0x01 to byte address 0 and read the containing word. Little-endian yields 0x00000001; big-endian yields 0x01000000. One transfer settles it. Then check whether the master or the slave is the one disagreeing by looking at which lane SEL_O actually selected.

Likely RTL location. mask_of in the master, or any lane-reversal in the slave — which should not exist at all.

Symptom: a narrow access silently does nothing.

Candidate causes. SEL_O computed as zero — an unhandled size encoding, or a mask shifted past the top lane by an unchecked offset.

Discriminating evidence. Check SEL_O during the transfer. All zero is conclusive, and the slave in Section 4 reports it as an error precisely so this does not pass unnoticed.

Property. P3.

Symptom: an unaligned half-word access corrupts the next word.

Candidate causes. The master shifts the mask without checking alignment, so a half-word at offset 3 produces a mask that should straddle two words and instead truncates.

Discriminating evidence. Issue a half-word access at an odd byte offset and watch SEL_O. A mask of 1000 for a two-byte access is conclusive — one lane for a two-lane request.

Likely RTL location. The alignment check, or its absence.

7. Verification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// wb_sel_checker — byte-lane properties.
//
// P1 is the important one and is SPECIFICATION-derived: the SEL_O() signal
// description defines it as naming where valid data is PLACED on writes,
// which means an unselected lane carries nothing the slave may act on.
//
// P2 is SPECIFICATION-derived in the same way, for the read direction.
// P3 and P4 are LOCAL DESIGN POLICY — the specification does not forbid a
// zero mask, and the stability obligation is inherited by analogy with
// RULE 3.60's qualification requirement rather than stated as a stability
// rule for SEL in its own right.
// ─────────────────────────────────────────────────────────────────────────
module wb_sel_checker #(
  parameter int unsigned DW = 32,
  parameter int unsigned NL = 4
) (
  input logic           clk_i,
  input logic           rst_i,
  input logic           cyc_i,
  input logic           stb_i,
  input logic           we_i,
  input logic           ack_o,
  input logic           err_o,
  input logic [NL-1:0]  sel_i,
  input logic [DW-1:0]  word_q,        // the addressed word, white-box
  input logic [DW-1:0]  word_prev      // its value one cycle earlier
);
  default disable iff (rst_i);

  // P1 — SPECIFICATION-derived, and the property this chapter exists for.
  //      Every UNSELECTED lane of the addressed word must be unchanged.
  //      Generated per lane so a failure names WHICH lane was destroyed,
  //      which turns a vague "data corrupted" into a one-line diagnosis.
  generate
    for (genvar n = 0; n < NL; n++) begin : g_lane
      property p_unselected_lane_preserved;
        @(posedge clk_i)
          (word_q[n*8 +: 8] != word_prev[n*8 +: 8]) |-> $past(sel_i[n]);
      endproperty
      a_unselected_lane_preserved :
        assert property (p_unselected_lane_preserved)
        else $error("lane %0d changed while unselected", n);
    end
  endgenerate

  // P2 — SPECIFICATION-derived. No lane changes at all outside a qualified
  //      write, which is Chapter 4.6's P1 restated for this storage.
  property p_no_change_without_write;
    @(posedge clk_i) (word_q != word_prev) |-> $past(cyc_i && stb_i && we_i);
  endproperty
  a_no_change_without_write : assert property (p_no_change_without_write)
    else $error("stored word changed outside a qualified write");

  // P3 — LOCAL POLICY. A qualified transfer selects at least one lane.
  //      The specification does not require this; a zero mask is simply a
  //      transfer that moves nothing. It is asserted because in practice it
  //      is always a master bug, and a silent no-op is expensive to find.
  property p_nonempty_mask;
    @(posedge clk_i) (cyc_i && stb_i) |-> (sel_i != '0);
  endproperty
  a_nonempty_mask : assert property (p_nonempty_mask)
    else $error("qualified transfer with an empty byte-lane mask");

  // P4 — LOCAL POLICY, read out of RULE 3.60. SEL is stable while a
  //      transfer is outstanding. The end boundary is the termination, for
  //      exactly the reason Chapter 4.6 Section 10 set out.
  property p_sel_stable;
    @(posedge clk_i) (cyc_i && stb_i && !ack_o && !err_o) |=> $stable(sel_i);
  endproperty
  a_sel_stable : assert property (p_sel_stable)
    else $error("SEL changed while a transfer was outstanding");
endmodule

P1 is generated per lane deliberately. A single property over the whole word would report "data changed" and leave the engineer to work out which byte and why. A per-lane assertion names the lane in its message, and since the lane number maps directly to a mask bit and a DAT range, the failure message is close to the fix.

word_prev is passed in rather than derived with $past so the property reads as a comparison between two concrete values. $past(word_q) would be equivalent and is the more idiomatic form; the explicit signal is used here because it survives the inspection-only review that Icarus forces.

Tooling limitation. Icarus has no SVA support; reviewed by inspection only.

8. Common Mistakes

"A slave can ignore SEL_I if software always writes whole words."

Wrong mental model: the mask is an optimisation the slave may skip.

Concrete bug: a whole-word write. It works perfectly until the first byte write, then destroys three neighbours.

Observable evidence: neighbouring data zeroed after a narrow access, with nothing in the failing access naming the data that was lost.

Correct model: SEL_O() names where valid data is placed, so unselected lanes carry nothing meaningful, and a slave acting on them is acting on residue. "Software always writes whole words" is a claim about today's driver, not about the interface.

"SEL_O[0] is byte address 0."

Wrong mental model: lane order and byte-address order are the same thing.

Concrete bug: a big-endian master paired with a little-endian assumption, or a slave that reverses lanes to "help".

Observable evidence: data byte-reversed within words, consistently — consistent enough to survive a long way into integration.

Correct model: SEL_O[n] is DAT[8n+7:8n], which is a wire fact. Which byte address lives there is an endianness decision made in the master, and the specification requires explicit conversion between interfaces that disagree.

"Computing the mask is enough — the data will find its lane."

Wrong mental model: asserting SEL_O[2] somehow routes the payload to lane 2.

Concrete bug: a correct mask with a right-justified payload. The slave writes lane 2 with whatever DAT_I[23:16] holds, which is not the byte software asked to store.

Observable evidence: byte writes landing with the wrong value rather than in the wrong place — and SEL_O looks right, so the mask logic is the last place anyone checks.

Correct model: mask and placement are two separate obligations on the master. SEL_O says which lane; the payload shift puts the byte there.

9. Interview Reasoning

The slave is performing a whole-word write and ignoring SEL_I.

Why the neighbours die. SEL_I names the lanes carrying valid data. A master driving a single-byte write places the byte in its lane and leaves the other three lanes carrying nothing meaningful — it is not required to drive anything sensible there, and the SEL_O() description — valid data is placed in the selected lanes — is what makes that legal. A slave that stores all four lanes takes three bytes of residue and commits them.

The two transfers that prove it. Write 0xAABBCCDD as a full word. Then write 0x99 as a single byte to lane 2. Read back.

  • Correct slave: 0xAA99CCDD.
  • Whole-word slave: whatever the master left on the bus — typically 0x00990000, with zeros in the unselected lanes.

The zeros are the discriminator. Genuine neighbouring data does not become zero spontaneously. Seeing exactly the master's DAT_O reflected in memory identifies the mechanism without any further instrumentation.

Why it is expensive in the field. The corrupted bytes belong to variables the failing access never mentioned. The symptom appears in unrelated code, so the search starts in the wrong module. This is the argument for the per-lane assertion in Section 7: it fires at the moment of corruption, names the lane, and turns a cross-module hunt into a one-line fix.

The fix. A per-lane conditional write. Not a mask-and-merge computed in one expression — that is equivalent in effect but obscures which lanes are being written, and the loop form is what a reviewer can check at a glance.

10. Understanding Check

11. What's Next

The transfer is now fully described: which word, which bytes, which direction, and what data. Every field a Wishbone transfer carries has been covered.

Except that none of it means anything yet. A slave has been told to look at these signals "when a transfer is qualified" in five consecutive chapters, and the signal that does the qualifying has never been examined.

What turns a set of stable bus values into a transfer the slave must act on?

Chapter 4.8 — STB_O 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.