Wishbone · Module 4
WE_O
One bit decides which data path carries the meaningful value. It is a direction selector and never a write trigger, and a slave that ignores it writes a register on every read with the previous write's data.
Four chapters have used one bit without examining it. Chapter 4.4 needed it to know which side's output mattered; Chapter 4.5 needed it in both capture conditions.
How does one bit change the meaning of both data paths — and why must it never, by itself, cause a register to be written?
1. Direction Is a Property of the Transfer
Chapter 2.3 established that a bus needs a direction bit because both endpoints can drive data and only one of them should be believed at a time. Wishbone spends exactly one wire on it.
The WE_O signal description — "The write enable output [WE_O] indicates whether the current local bus cycle is a READ or WRITE cycle. The signal is negated during READ cycles, and is asserted during WRITE cycles." And RULE 3.60 puts it in the list of master outputs that must be qualified with STB_O.
That is the whole content, and the economy is deliberate: a single bit, meaningful only under the strobe, decided by the master before the transfer begins. Note that the direction semantics come from the signal description rather than from a numbered rule — there is no "RULE n: WE_O means write" to cite, and a claim that there is should be checked.
What follows from that, as engineering consequence rather than specification:
Direction is per-transfer, not per-cycle. A master sets it when it presents a transfer and holds it until termination — the same obligation ADR_O carries, and from the same source: RULE 3.60 lists ADR_O, DAT_O(), SEL_O() and WE_O together as signals the master must qualify with STB_O (Chapter 4.3 §3). Changing direction mid-transfer would change what the slave is being asked to do while it is doing it.
There is no third state. No "read-modify-write" transfer, no "exchange" that moves data both ways. A read-modify-write sequence is a read transfer followed by a write transfer, and Chapter 4.9 explains what keeps another master from intervening between them.
The slave does not choose. A slave may refuse a direction — a read-only register answering a write with ERR_O — but it cannot reinterpret one. Answering a write by performing a read is not a legal response; refusing it is.
2. What Each Side Does With It
The master drives it and uses it internally. Chapter 4.5 §3 showed !we_q in the read-capture condition: a master must not capture read data during a write, because there is none.
The slave receives it and gates three things:
Its write path — state changes only under a qualified write. This is the one that breaks.
Its read path — a slave need only produce read data on a read. Driving '0 otherwise is the convention from Chapter 4.4 §3.
Its legality check — a write to a read-only location is an error; a read of the same location is fine. The offset alone does not determine legality; the offset and the direction do.
3. Why WE_I Alone Must Never Cause a Write
This is the section the chapter exists for.
WE_I is a level, not an event. It is a bit on a wire whose value is only meaningful within a qualified transfer. Between transfers it holds whatever the last master left there — masters are not required to clear it, and RULE 3.60 asks only that it be qualified with STB_O, never that it be cleared when the strobe is low.
So a slave whose write path is gated on WE_I alone writes a register on every cycle the line happens to be high, including cycles when no transfer exists at all.
The correct condition is a conjunction, and each term excludes a different wrong cycle:
| Term | Excludes |
|---|---|
CYC_I & STB_I | cycles with no transfer — bus residue |
WE_I | read transfers |
| a legal, writable offset | transfers aimed elsewhere, or at read-only state |
Drop CYC_I & STB_I: registers change with no software access at all.
Drop WE_I: registers change on every read, taking the previous write's value — the fingerprint from Chapter 4.5 §5.
Drop the offset check: writes land in the wrong register, or in one that reported an error.
4. RTL — Direction on Both Sides
// ─────────────────────────────────────────────────────────────────────────
// wb_we_master — a master issuing both directions from one request port.
//
// PURPOSE. Show that direction is captured once, held for the transfer, and
// then used internally to decide what the master does with the result.
//
// Reset: SYNCHRONOUS, ACTIVE HIGH (RULES 2.30, 3.00).
// ─────────────────────────────────────────────────────────────────────────
module wb_we_master #(
parameter int unsigned AW = 30,
parameter int unsigned DW = 32
) (
input logic clk_i,
input logic rst_i,
input logic go_i,
input logic we_i, // 1 = write, 0 = read
input logic [AW-1:0] adr_i,
input logic [DW-1:0] wdat_i,
output logic done_o,
output logic [DW-1:0] rdat_o,
output logic cyc_o,
output logic stb_o,
output logic we_o, // qualified with stb_o, RULE 3.60
output logic [AW-1:0] adr_o,
output logic [DW-1:0] dat_o,
input logic [DW-1:0] dat_i,
input logic ack_i,
input logic err_i,
input logic rty_i
);
logic active_q, we_q;
logic [AW-1:0] adr_q;
logic [DW-1:0] wdat_q;
assign cyc_o = active_q;
assign stb_o = active_q;
assign we_o = we_q; // held stable for the whole transfer
assign adr_o = adr_q;
// NOTE — dat_o is driven from wdat_q UNCONDITIONALLY, including during a
// read. That is legal: RULE 3.60 qualifies DAT_O with STB_O, not with WE_O.
// It is also the behaviour that punishes a slave which ignores WE_I, and
// it is left in deliberately rather than papered over, because a master
// that clears DAT_O on reads hides the bug in every slave it talks to.
assign dat_o = wdat_q;
logic terminated;
assign terminated = ack_i | err_i | rty_i;
always_ff @(posedge clk_i) begin
if (rst_i) begin
active_q <= 1'b0; // RULE 3.20
we_q <= 1'b0;
adr_q <= '0;
wdat_q <= '0;
rdat_o <= '0;
done_o <= 1'b0;
end else begin
done_o <= 1'b0;
if (!active_q) begin
if (go_i) begin
active_q <= 1'b1;
we_q <= we_i; // direction decided ONCE, here
adr_q <= adr_i;
wdat_q <= wdat_i;
end
end else if (terminated) begin
active_q <= 1'b0;
done_o <= 1'b1;
// we_q selects what the RESULT means: a read yields data, a write
// yields only completion (Chapter 4.5 Section 3).
if (ack_i && !we_q) rdat_o <= dat_i;
end
end
end
endmodule// ─────────────────────────────────────────────────────────────────────────
// wb_we_slave — a slave that gates BOTH paths and its legality check on
// direction, using a single named term for every write path.
//
// The three-register map deliberately includes one read-only and one
// write-only location, so that direction changes legality in both ways.
// ─────────────────────────────────────────────────────────────────────────
module wb_we_slave #(
parameter int unsigned OFF_AW = 8,
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,
output logic [DW-1:0] dat_o,
output logic ack_o,
output logic err_o,
output logic [DW-1:0] ctrl_o, // observable application state
output logic kick_o // write-only command pulse
);
localparam logic [OFF_AW-1:0] W_CTRL = 'd0; // read / write
localparam logic [OFF_AW-1:0] W_STATUS = 'd1; // read-only
localparam logic [OFF_AW-1:0] W_CMD = 'd2; // write-only
logic [DW-1:0] ctrl_q;
assign ctrl_o = ctrl_q;
logic xfer;
assign xfer = cyc_i & stb_i; // RULES 3.30 & 3.35
logic known_off;
always_comb begin
unique case (adr_i)
W_CTRL, W_STATUS, W_CMD: known_off = 1'b1;
default: known_off = 1'b0;
endcase
end
// ── DIRECTION CHANGES LEGALITY IN BOTH DIRECTIONS ──────────────────────
// A write to a read-only offset is illegal; a read of it is fine.
// A read of a write-only offset is illegal; a write to it is fine.
// The offset alone never decides. The offset AND we_i decide.
logic illegal_dir;
assign illegal_dir = xfer & known_off &
(( we_i & (adr_i == W_STATUS)) |
(~we_i & (adr_i == W_CMD)));
assign err_o = xfer & (~known_off | illegal_dir);
assign ack_o = xfer & ~err_o;
// ── THE SINGLE NAMED WRITE CONDITION ───────────────────────────────────
// Every write path below uses this term and nothing else. That is the
// structural defence described in Section 3: there is only one term to
// use, so a later edit cannot add a path that forgets one of the four.
logic write_ok;
assign write_ok = xfer & we_i & known_off & ~illegal_dir;
always_ff @(posedge clk_i) begin
if (rst_i) begin
ctrl_q <= '0;
kick_o <= 1'b0;
end else begin
kick_o <= 1'b0; // default: one-cycle pulse
if (write_ok) begin
unique case (adr_i)
W_CTRL: ctrl_q <= dat_i;
W_CMD: kick_o <= 1'b1; // command, not storage
default: ;
endcase
end
end
end
// Read path: gated on a READ, so the slave produces nothing during a
// write and the merged return path stays clean (Chapter 4.4 Section 3).
always_comb begin
dat_o = '0;
if (xfer && !we_i && known_off) begin
unique case (adr_i)
W_CTRL: dat_o = ctrl_q;
W_STATUS: dat_o = {{(DW-2){1'b0}}, 1'b1, 1'b0};
default: dat_o = '0; // W_CMD read is illegal, err_o set
endcase
end
end
endmoduleReading the pair
Purpose. The master shows direction being decided once and used twice — on the wire and internally. The slave shows it gating three separate things.
Ownership. The master drives WE_O; the slave never drives it.
Combinational logic. Master: almost none. Slave: the qualified transfer, offset recognition, the direction-legality term, the two termination outputs, the single write_ok, and the read multiplexer.
Sequential logic. Master: request registers, direction, captured read data, done_o. Slave: one application register and one command pulse.
Timing. we_q is loaded at the go_i edge and is stable for the whole transfer — no mid-transfer change is possible, because nothing else writes it while active_q is high.
Qualification. write_ok is the only write condition in the module. illegal_dir is what makes W_STATUS and W_CMD differ from W_CTRL.
Reset. Synchronous, active high, per RULES 2.30 and 3.00.
Simplifications. No byte lanes — Chapter 4.7 adds them, and the interaction with WE_O is the substance of that chapter. No wait states.
Failure modes. Section 5.
5. Waveform — The Missing WE_I Bug, Traced
WE_O: the read that writes
9 cyclesCycle 4 is the legal part. The write has finished and the master still has 0xAAAA on DAT_O. RULE 3.60 qualifies that output with STB_O alone, so there is nothing wrong here — and the master in Section 4 does exactly this, deliberately.
Cycle 6 is the bug. The broken slave's write condition is xfer & known_off with no we_i, so a read satisfies it. The register is written with whatever DAT_I carries, which is the previous write's value.
Cycle 8 is why it survives testing. The register took 0xAAAA — the value it already held. A read-back test sees the right answer. Insert a write to a different register between the two accesses and the same read now corrupts ctrl with that unrelated value, which is the point at which the bug finally becomes visible.
6. Failure Modes and Discriminating Evidence
Symptom: a register changes when software reads it.
Candidate causes. The slave's write path omits WE_I.
Discriminating evidence. Write value X to a different register, then read the suspect one, then read it again. If the second read returns X, that is conclusive — the value migrated from an unrelated write, which no other bug produces.
Likely RTL location. The write condition. Look for cyc_i & stb_i without we_i.
Property. P1 in Section 7.
Symptom: writes to a read-only register succeed silently.
Candidate causes. The legality check tests the offset but not the direction, so W_STATUS is treated as an ordinary register.
Discriminating evidence. Write to it and check ERR_O in the same cycle. No error asserted means the check is missing entirely rather than being bypassed.
Likely RTL location. The illegal_dir term, or its absence.
Symptom: a command register triggers its action on reads as well as writes.
Candidate causes. The same missing WE_I, on a command pulse rather than a storage register. This is worse than the storage case because there is no state to inspect — the action simply happens.
Discriminating evidence. Trigger on the command pulse and check we_i in the cycle before. Low is conclusive.
Likely RTL location. The W_CMD branch's condition.
Property. P2.
Symptom: a master's read result is sometimes the value it last wrote.
Candidate causes. The master's capture condition omits !we_q, so a write termination overwrites the captured read register with whatever the fabric returns.
Discriminating evidence. Perform a read, then a write, then examine the captured register without reading again. If it changed, the master is the problem, not the slave.
Likely RTL location. The master's capture, per Chapter 4.5 §3.
7. Verification
// ─────────────────────────────────────────────────────────────────────────
// wb_we_checker — direction properties.
//
// P1, P2, P4 are SPECIFICATION-derived: they follow from the WE_O signal
// description's definition of direction together with RULE 3.30, which
// forbids a slave from responding to slave signals while CYC_I is negated.
// P3 is LOCAL DESIGN POLICY — RULE 3.60 requires WE_O to be QUALIFIED with
// STB_O, and reading that as a stability obligation across a multi-cycle
// transfer is this design's interpretation, not a stated rule.
// ─────────────────────────────────────────────────────────────────────────
module wb_we_checker #(
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 ack_o,
input logic err_o,
input logic [DW-1:0] ctrl_q,
input logic kick_o
);
default disable iff (rst_i);
// P1 — SPECIFICATION-derived. Stored state changes only under a qualified
// WRITE. This is the property that catches the missing-we_i bug, and
// it catches it on the FIRST read, long before a read-back test would.
property p_state_needs_qualified_write;
@(posedge clk_i) $changed(ctrl_q) |-> $past(cyc_i && stb_i && we_i);
endproperty
a_state_needs_qualified_write : assert property (p_state_needs_qualified_write)
else $error("register changed outside a qualified write");
// P2 — SPECIFICATION-derived. A command pulse is a side effect, and the
// same reasoning applies: a read must not cause one. Worth asserting
// SEPARATELY from P1 because a command leaves no state to inspect.
property p_command_needs_qualified_write;
@(posedge clk_i) kick_o |-> $past(cyc_i && stb_i && we_i);
endproperty
a_command_needs_qualified_write : assert property (p_command_needs_qualified_write)
else $error("command pulse issued outside a qualified write");
// P3 — LOCAL POLICY. Direction is stable while a transfer is presented
// and unterminated. Catches a master that recomputes we_o from a
// combinational source that can change mid-transfer.
property p_we_stable_during_transfer;
@(posedge clk_i) (cyc_i && stb_i && !ack_o && !err_o) |=> $stable(we_i);
endproperty
a_we_stable_during_transfer : assert property (p_we_stable_during_transfer)
else $error("WE changed while a transfer was outstanding");
// P4 — SPECIFICATION-derived. A transfer the slave refused must not have
// changed state. Direction-illegal accesses are refused, so this is
// what makes the read-only register actually read-only.
property p_refused_changes_nothing;
@(posedge clk_i) ($changed(ctrl_q) || kick_o) |-> !$past(err_o);
endproperty
a_refused_changes_nothing : assert property (p_refused_changes_nothing)
else $error("state changed on a transfer terminated with ERR_O");
endmoduleP1 and P2 are the same idea applied to two kinds of effect, and separating them is deliberate. P1 protects storage, which a test can read back. P2 protects actions, which a test cannot — a command that fired has no residue to inspect, so the assertion is the only place the violation is visible.
P3's antecedent excludes terminated cycles because the master is entitled to change direction for the next transfer on the cycle after a termination.
Tooling limitation. Icarus has no SVA support; reviewed by inspection only.
8. Common Mistakes
"WE_I means write, so a slave can write when it sees it."
Wrong mental model: WE_I is a command that arrives.
Concrete bug: a write path gated on WE_I without the qualifiers, acting on whatever the line holds between transfers.
Observable evidence: registers changing with no software access, often at reset release when bus lines settle.
Correct model: WE_I is a level that is only meaningful inside a qualified transfer. It selects a direction; the qualifiers decide whether there is anything to select for.
"Leaving old data on DAT_O during a read is sloppy and should be fixed."
Wrong mental model: a master should clear its output when it is not writing.
Concrete bug: none in the master — but "fixing" it hides a real bug in every slave the master talks to, because the missing-WE_I slave now captures zero instead of a recognisable value and the corruption becomes harder to identify rather than less frequent.
Observable evidence: a system that works with one master and fails with another, which is an integration nightmare to diagnose.
Correct model: RULE 3.60 qualifies DAT_O with STB_O alone. Masters may leave stale data there, so slaves must be correct against that. Fix the slave.
"A read-back test proves the write path is correctly qualified."
Wrong mental model: write X, read X back, the path is right.
Concrete bug: the missing-WE_I slave passes this test, because the read re-writes the register with the value the preceding write left on DAT_O — the same value.
Observable evidence: a green test suite and a field failure when the access order changes.
Correct model: the test must interleave — write A to register 1, write B to register 2, read register 1, read register 1 again. Or assert P1, which catches it on the first read with no test design at all.
9. Interview Reasoning
The symptom names the cause. Corruption correlated with reads rather than writes points at a write path that is not gated on WE_I.
The mechanism. The polling loop reads the status register. The master presents that read with WE_O low — but RULE 3.60 qualifies the master's DAT_O with STB_O only, so whatever it last wrote is still sitting on that bus. The slave's write condition, missing we_i, is satisfied by the read. It writes.
Why the control register specifically. If the slave decodes the offset correctly, it writes whichever register the read addressed — so polling status corrupts status. If control is what changes, the offset decode is shared between paths in a way that maps the read's offset onto the control register, which is a second bug worth checking.
The one observation that confirms it. Write a distinctive value to an unrelated register, then read the suspect one twice. If the second read returns that distinctive value, the migration is proven — no other mechanism moves a value from one register to another across a read.
Why it was not caught earlier. A read-back test writes then immediately reads, so the value the read re-writes is the value that was supposed to be there. The test passes. It takes an interleaved access pattern, or assertion P1, to expose it.
The fix, and the structural version of the fix. Add we_i. Then name the full conjunction once — qualified transfer, write, legal writable offset — and use that single term for every write path in the module, so the next register added cannot reintroduce the bug.
10. Understanding Check
11. What's Next
Direction is settled. Address, both data paths and direction now all have their meaning and their window.
But every write so far has been the full data width. Real software writes single bytes into registers that hold four of them, and the bus has no narrower transfer to offer — the address still points at a whole word.
How does a 32-bit bus write one byte without disturbing the other three?
Chapter 4.7 — SEL_O answers it. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
DAT_O
Both masters and slaves have a DAT_O, and they mean opposite things. What each must drive, when it must be valid, and why a master may legally leave stale write data on the bus during a read.
- Related topic
Data Flow
One Wishbone access, followed through every block in both directions: what the master drives, where the address changes form, which signals are broadcast and which are decoded, and how read data and termination find their way back to exactly one requester.
- Related topic
DAT_I
A master's DAT_I is returned read data; a slave's is incoming write data. Neither may be believed without the condition that qualifies it — and sampling one cycle late returns the previous transaction's value.
- Related topic
STB_O
Bus wires always carry values; STB_O is what turns a set of values into a request. Qualification, the termination every strobe is owed, and why silence is the one response a slave may never give.
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.
