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:
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
// ─────────────────────────────────────────────────────────────────────────
// 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// ─────────────────────────────────────────────────────────────────────────
// 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
endmoduleReading 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 cyclesCycle 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
// ─────────────────────────────────────────────────────────────────────────
// 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");
endmoduleP1 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
Related tutorials
- Related topic
Need for Standardized Interconnects
An address map answers where a register lives. It says nothing about which wires carry the request, when they are valid, how the target reports completion, or what happens on an error. Three peripherals with three private interfaces produce three adapters, three verification efforts and three ways to be wrong — which is the argument for standardising the interface rather than the map.
- Related topic
Data Transfer
Selection routes a request; it does not move a value. Write data fans out, read data fans in through a multiplexer that every target widens, byte lanes decide which parts of a word participate, and a register narrower than the bus has to be placed on a lane rather than merely connected.
- Related topic
SoC Communication
Six chapters built the pieces; this one assembles them into a working fabric and traces three real accesses through it. The result works, and reading the nine unwritten rules a third party would need is what makes the case for a published protocol concrete rather than theoretical.
- Related topic
Transaction Lifecycle
One Wishbone transaction from a master's decision to act through the slave's termination and back to the caller: what is fixed by the protocol, what every implementation may vary, and what a real simulation of the assembled system shows at each step.
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.
