Wishbone · Module 13
Partial Writes
A masked write has two terms and one is invisible when missing. Measured: 0xA1B2C3D4 becoming 0x00000044 through a design whose mask is entirely correct.
Chapter 13.2 built byte enables two ways and both worked. Both were written by someone who already knew the answer.
What does a partial write have to preserve, and what does a design look like that preserves nothing?
1. Preservation, Not Restoration
The unselected lanes are never disturbed. That sounds like a detail of wording and it is the difference between two architectures.
A design that preserves never writes the unselected lanes at all. There is no window in which they hold something else.
A design that restores would read the old word, combine, and write back — which is a read-modify-write, a genuine bus operation with its own cycle type, its own atomicity question, and its own chapter. Module 15 builds it.
They are not the same thing and they do not cost the same. A partial write is one transfer; the preservation happens inside the target, in the same clock, because the target already holds the old value. A read-modify-write is two transfers with the old value travelling across the bus and back.
The confusion matters because it changes what you look for when debugging. If a partial write is losing data, the fault is inside one target. If a read-modify-write is losing data, the fault may be another master writing in the window between the two halves — a completely different investigation, and one that does not apply here at all.
2. RTL — The Term Removed
// ── THE DEFECT — mask the incoming data and forget the old word ─────────
// wb_broken_mask_reg. NOT A REFERENCE DESIGN.
//
// q <= dat_i & mask;
//
// The reasoning that produces it is half-right, which is why it survives a
// reading: "only the selected lanes are valid, so mask the data." Masking
// the data IS correct — it is the (dat & mask) term. What is missing is the
// other term, and its absence does not look like an absence.
//
// The result is that every unselected lane is written with ZERO. The
// transfer does not merely fail to update them; it destroys them. Chapter
// 13.3 measures exactly what is lost.
module wb_broken_mask_reg #(
parameter int unsigned DW = 32,
parameter int unsigned GRAN = 8,
parameter logic [31:0] RESET = 32'hA1B2_C3D4,
localparam int unsigned SELW = DW / GRAN
) (
input logic clk_i,
input logic rst_i,
input logic we_i,
input logic [SELW-1:0] sel_i,
input logic [DW-1:0] dat_i,
output logic [DW-1:0] q_o
);
logic [DW-1:0] q, mask;
wb_sel_to_mask #(.DW(DW), .GRAN(GRAN)) u_mask (.sel_i(sel_i), .mask_o(mask));
always_ff @(posedge clk_i) begin
if (rst_i) q <= DW'(RESET);
// ── THE DEFECT: no (q & ~mask) term. ──
else if (we_i) q <= dat_i & mask;
end
assign q_o = q;
endmoduleReading it
It differs from Chapter 13.2's style B by one term. Same mask expander, same commit condition, same reset value. q <= dat_i & mask instead of q <= (q & ~mask) | (dat_i & mask).
Its mask is entirely correct. It instantiates the same wb_sel_to_mask, which Chapter 13.1 audited exhaustively. Every diagnostic aimed at the select path exonerates this design, which is what makes it durable.
And the reasoning that produces it is half true. "Only the selected lanes are valid, so mask the data" is correct — that is the (dat & mask) term. What is missing is a statement about the lanes the sentence never mentioned.
3. Simulation — SIM C: With and Without the Preserve Term
Three registers, identical stimulus. Per-lane enables, the masked merge, and the merge with (old & ~mask) deleted. Each resets to 0xA1B2C3D4 before every row.
=== SIM C - the preserve term, present and absent ===
identical stimulus to three registers: per-lane enables,
expanded-mask merge, and the merge missing (old & ~mask).
SEL lane-enable mask-merge dat & mask lost
0001 0xa1b2c344 0xa1b2c344 0x00000044 0xa1b2c300
0011 0xa1b23344 0xa1b23344 0x00003344 0xa1b20000
0101 0xa122c344 0xa122c344 0x00220044 0xa100c300
1000 0x11b2c3d4 0x11b2c3d4 0x11000000 0x00b2c3d4
1111 0x11223344 0x11223344 0x11223344 0x00000000
The two correct columns are identical on every row.
The third zeroes every lane it was not given, and the
'lost' column is exactly the state it destroyed.
On SEL=1111 all three agree - a full-word write has no
unselected lanes, so the missing term has nothing to do.Reading it
Row 1 is the headline. A one-byte write to a register holding 0xA1B2C3D4:
| result | |
|---|---|
| per-lane enables | 0xa1b2c344 |
| masked merge | 0xa1b2c344 |
dat & mask | 0x00000044 |
Three bytes destroyed by a write that was only ever about one. The lost column, 0xa1b2c300, is exactly the state that disappeared.
The two correct columns are identical on every row, which is the equivalence Chapter 13.2 claimed, measured rather than asserted.
Row 3, SEL = 0101, is where the damage is least intuitive. Correct gives 0xa122c344; the broken design gives 0x00220044. The surviving bytes are not a prefix or a suffix — they are the selected lanes, scattered, with holes punched between them. A design that assumes a contiguous transfer would not even produce this shape.
4. Simulation — SIM D: All Sixteen Patterns, Lane by Lane
The five rows above are examples. This is the property. Every select pattern, against old = 0xA1B2C3D4 and dat = 0x11223344 — words chosen so that every lane differs between them, so a preserved lane and a replaced lane can never be confused.
=== SIM D - every select pattern, checked lane by lane ===
all 16 patterns against old=0xa1b2c3d4 dat=0x11223344
lane checks performed 64
selected lane not updated 0
unselected lane not preserved 0
the two styles disagreed 0
Non-contiguous patterns are in this sweep - 0101, 1010,
1001 - and behave like every other pattern. A bitmap has
no notion of contiguity to violate.Reading it
Sixty-four lane checks — sixteen patterns times four lanes — and three counters at zero.
"selected lane not updated" and "unselected lane not preserved" are separate counters on purpose. A design could satisfy one and fail the other, and they point at different code: the first at the data path, the second at the missing preserve term. Collapsing them into one pass/fail throws away the diagnosis.
"the two styles disagreed" runs the equivalence check on every pattern. Per-lane enables and the masked merge produce bit-identical results sixteen times out of sixteen.
And the sweep includes the patterns nothing else would have chosen. 0101, 1010, 1001, 0110 — non-contiguous lane sets, which behave exactly like the contiguous ones because a bitmap has no notion of contiguity to violate.
One write, two registers, three bytes
8 cyclesThe two lowest rows are identical. Both designs write the selected lane correctly, and a read of that byte returns the right value from either. Nothing about the selected lane distinguishes them.
The two upper rows are the entire difference, and it happens in one clock with no termination, no error and no indication of any kind. The transfer was acknowledged normally.
5. A Bitmap Is Not a Size
This is the misconception the sweep exists to destroy, and it is worth naming precisely.
SEL is described as a bitmap. The signal description: "Each individual select signal correlates to one of eight active bytes on the 64-bit data port." One bit, one lane. Four bits give sixteen patterns.
A size encoding would be a different abstraction. Two bits encoding "byte / halfword / word" gives four values and needs a separate offset to say where. It cannot express 0101 at all, and it cannot express 0000.
| select bitmap | size encoding | |
|---|---|---|
| values for a 4-lane port | 16 | 3 or 4 |
expresses 0101 | yes | no |
| expresses "no lanes" | yes | no |
| needs a separate offset | no | yes |
On legality, the honest statement is narrow. The specification does not forbid non-contiguous patterns — no rule enumerates permitted ones. That is permission by absence, not explicit blessing, and it is worth saying as the weaker claim.
What is a real question is whether a given target supports them. The specification's operand nomenclature covers BYTE, WORD, DWORD and QWORD; a scattered lane set is not one of those operands. Whether a core handles one is a datasheet matter under RULE 2.15's maximum-operand-size requirement — and a core built as a per-lane bitmap, like every register in this module, handles them without ever being asked to.
The design consequence is what makes this practical. A target implemented per-lane gets all sixteen patterns for free. A target implemented as a size decoder has a hole in its behaviour that no contiguous test will find.
6. Bus Lanes and Register Meaning Are Different Layers
SEL decides which lanes participate. It does not decide what participating means.
For an ordinary read/write register they are nearly the same thing — a participating lane takes the data — which is why this chapter can treat them as one until now.
They separate as soon as the register has its own rule. A write-1-clear status register, a command port, a FIFO write port: for each, "this lane took part" is the input to a rule, not the whole of it.
| register kind | a participating lane means |
|---|---|
| ordinary RW | take the written value |
| write-1-clear | clear the bits that were written as 1 |
| command | fire the side effect |
| FIFO port | enqueue the delivered bytes |
The order is always the same: mask first, register rule second. A lane that did not participate was not written at all, so the register's rule has nothing to apply to it.
Chapter 13.5 measures all three, including a command register that fires from a lane it was never given. This chapter stops here — the distinction is the point, and the register bank that depends on it is a chapter away. Designing register semantics themselves belongs to Module 24.
7. Failure Modes and Discriminating Evidence
Symptom: a byte write clears the rest of the register.
Candidate causes. The merge missing its preserve term — dat & mask with no old & ~mask.
Discriminating evidence. Whether the unselected lanes became zero or took DAT's values. Zero means the preserve term is missing. DAT's values means SEL is ignored entirely. The two look similar in a bug report and have different fixes, and one byte-write from a non-zero register separates them.
Likely RTL location: the register's update expression, not the mask.
Symptom: whole-word writes work and byte writes corrupt data.
Candidate causes. Any preservation defect at all.
Discriminating evidence. That the symptom is conditional on SEL != all-ones is itself the diagnosis. SIM C's last row shows why: with every lane selected there is nothing to preserve, so the defect is dormant. Look at the update expression, not at the transfers that work.
Symptom: a scattered select pattern behaves differently from a contiguous one.
Candidate causes. A target that decodes SEL as a size and an offset rather than as a bitmap.
Discriminating evidence. Compare 0011 against 0101. Both select two lanes. A bitmap treats them alike; a size decoder handles the first and mishandles the second. SIM D's sweep is this test generalised.
Symptom: data loss appears in a variable nothing wrote to.
Candidate causes. A neighbouring byte in the same word destroyed by someone else's partial write.
Discriminating evidence. Whether the lost bytes share a word with a byte that was legitimately written. If so, the corruption is a partial-write defect in whatever owns that word. The routine that appears to be at fault is the victim, and the one at fault reported success.
8. Verification
The properties in Chapter 13.1 Section 10 cover this chapter without change. P2 says a selected lane takes the data; P3 says an unselected lane is unchanged, and P3 is precisely what wb_broken_mask_reg violates on every write where SEL is not all-ones.
P3 is the reason the property is written per lane. A word-wide statement — "the result equals the merge" — would also catch this defect, but it would not say which lanes were wrong, and the per-lane form fails once for every destroyed byte.
SIM D is those two properties in executable form. Icarus cannot run the assertions; it can run sixty-four lane comparisons, and the counters are separated along the same lines the properties are.
9. Common Mistakes
"Partial write means zero the other bytes."
Wrong mental model: the transfer defines the whole word.
What is true: the transfer defines the lanes it selected and says nothing about the others. SIM C measures 0xA1B2C3D4 becoming 0x00000044 in a design that believes otherwise.
"DAT & mask implements a masked write."
Wrong mental model: masking the data is the operation.
What is true: it is one of two terms. Masking the data is right; the missing half is old & ~mask. The expression looks complete because nothing in it is wrong — what is wrong is what is absent.
"Only contiguous select patterns matter."
Wrong mental model: real software only does byte, halfword and word accesses.
What is true: a bitmap has sixteen patterns and a per-lane design handles all of them identically. A target that special-cases contiguity has untested behaviour; SIM D covers the whole space for the cost of a loop.
"A partial write is a read-modify-write."
Wrong mental model: preservation requires reading.
What is true: the target already holds the old value — no read crosses the bus. A partial write is one transfer. Module 15's read-modify-write is a different operation with different atomicity concerns, and conflating them sends a debugging session to the wrong place entirely.
"If writes read back correctly, the register is fine."
Wrong mental model: read-back is a complete test.
What is true: a broken register reads back perfectly for whole-word writes. SIM C's last row is the proof. The test that finds it needs a partial write from a non-zero state, which is two conditions that a naive test satisfies neither of.
10. Interview Reasoning
Start from where each bit's new value comes from.
Every bit of the register is in exactly one of two sets after the write. If its lane was selected, the new value comes from the write data. If not, it comes from the register itself. Those sets are complementary, which is what makes a mask the right tool.
So with MASK = expand(SEL):
NEW = (old & ~MASK) | (dat & MASK)The first term keeps the bits the transfer did not claim; the second supplies the ones it did.
Say why the | is safe. MASK and ~MASK partition the word, so the two terms are disjoint — no bit gets a contribution from both. This is a concatenation, not a combination, which is exactly what made Chapter 12.4's ORed response mux unsafe by contrast: there the terms overlapped.
And name the failure. Dropping the first term gives dat & MASK, which zeroes every unselected lane. Measured: 0xA1B2C3D4 becoming 0x00000044 on a one-byte write.
11. Understanding Check
Because SEL = 1111 selects every lane, so there is nothing to preserve.
The broken design's defect is that it zeroes unselected lanes. With all four selected there are no unselected lanes — the missing (old & ~mask) term would contribute nothing even if it were there, because ~mask is all zeros.
So the three results are identical, and the register looks correct.
This is why the row is published rather than trimmed. It shows the exact conditions under which the defect hides, and those conditions are the common case during bring-up. A test suite made of whole-word writes gives a clean pass on a register that destroys data.
12. What's Next
The equation is derived, the missing term is measured, and the property holds across the entire select space.
Every select pattern so far arrived ready-made. Software does not have select patterns — it has pointers.
Where does a select pattern come from, and what happens when the bytes software wants do not fit in one word?
Chapter 13.4 — Alignment translates byte addresses into ADR plus SEL, measures what changes when the endianness does, and classifies every offset and size the teaching configuration allows. 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
The Open Hardware Movement
Source availability and reusability are different properties. A published core tells you what it does; it does not tell you what it requires, and requirements are what integration runs on. What reusable open IP needs beyond the RTL — licensing, documentation, an interface contract, verification, maintenance — and where open hardware is honestly weaker than its advocates claim.
- 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.
