AMBA CHI · Module 8 · Request Flows
WriteUnique Flow
Module 8 turns to writes, and WriteUnique is the write that does not assume you already hold the line. A core issues it to write a coherent line it may not have cached — the home does the coherence work, invalidating every other copy, and grants a DBID for the data. Its two forms matter: WriteUniqueFull replaces a whole line, while WriteUniquePtl writes only some bytes, carrying byte-enables. The partial form is subtle: untouched bytes must keep their existing values, so the home merges the new bytes over the old line rather than overwrite it. Skip the merge and the untouched bytes are corrupted with whatever rode the disabled lanes. Representative model, not the specification.
Intermediate16 min readAMBA CHIWriteUniqueByte EnablesMergePartial Write
Module 8 · Chapter 8.5 · Request Flows
Project thread — 8.1–8.4 were the read flows. This chapter opens the writes with WriteUnique — a write that needs no cached line. 8.6 is WriteBack — evicting a line you already own.
1. Learning Outcomes
By the end of this chapter you should be able to:
- State that WriteUnique writes a coherent line without the requester holding it cached first.
- Trace the flow — REQ → DBID grant → WriteData → Comp — with the home invalidating other copies.
- Distinguish WriteUniqueFull (whole line) from WriteUniquePtl (partial, with byte-enables).
- Explain the merge: enabled bytes take new data, disabled bytes keep the existing line's data.
- Diagnose why skipping the merge corrupts the bytes a partial write never touched.
- Implement a representative byte-enable merge in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
Not every write starts from ownership. A device streaming data, a producer publishing a small field, an agent that writes but does not cache — all need to write a coherent line without first fetching it into a cache and taking ownership. WriteUnique is that write: it hands the data and the address to the home and lets the home make it coherent.
Its lesson is the partial write. Writing a whole line is easy — replace it. Writing some bytes of a line is where correctness gets sharp: the bytes you do not write must survive unchanged, which means the new bytes have to be merged over the old line, not stamped on top of it. This merge, driven by byte-enables, is one of the most common sources of silent data corruption in memory systems — write a 16-bit field and clobber the two bytes beside it, and nothing errors; the data is just wrong. This is the flow where byte-granular correctness lives.
3. Key Terms
4. Previous Chapter Connection
The read flows (8.1–8.4) brought a line to a requester; the write flows push data from one. WriteUnique is the first, and it leans on two things you already have: the write handshake of Chapter 7.2 — request, DBID grant, WriteData, complete — and the invalidating snoop of Chapter 8.2, since a coherent write needs every other copy gone.
What is new is that the requester does not own the line first. Where ReadUnique fetched a line so the requester could write it locally, WriteUnique writes through the home without caching, and the home takes on both jobs: invalidate the other copies and, for a partial write, merge the new bytes with the line's existing data. It is a write that carries its own coherence and its own byte-enables.
5. Core Concept — write a line you do not hold; the home makes it coherent
A WriteUnique writes data to a coherent line without the requester needing a cached copy. The home performs the coherence and the merge; the requester just supplies address, data, and byte-enables.
- Request. The requester sends WriteUnique (Full or Ptl) to the home. It does not hold the line, and will not cache it.
- Grant. The home grants a DBID (Chapter 7.2) — the requester may send data only after this.
- Invalidate others. The home snoops every other holder with an invalidating snoop (SnpCleanInvalid), clearing them to I and collecting the line's current data (needed for a partial merge). No stale copy may survive the write.
- WriteData + merge. The requester sends the WriteData, tagged with the DBID and carrying byte-enables. For Full, the whole line is replaced. For Ptl, the home merges: enabled bytes take the new data, disabled bytes keep the existing line's bytes. The result is written to memory.
- Complete. The home signals Comp. The requester holds no cached copy — it wrote through.
The synthesis:
WriteUnique writes a coherent line without owning it first. The home does the coherence — invalidate every other copy — and, for a partial write, the merge — enabled bytes new, disabled bytes preserved. The requester supplies data and byte-enables under a DBID grant and keeps nothing cached. Full replaces the line; Ptl edits some bytes and must leave the rest intact.
6. Engineering Mental Model — mail an edit to the records office
You want to change a couple of fields on the master form, but you do not keep a copy of the form yourself.
- You mail the records office the new values and a note saying which fields to change (WriteUniquePtl with byte-enables). You do not first request the whole form.
- The office pulls back any outstanding copies so no one is working from an old version (invalidate others), and takes the current master.
- Then it applies only your changed fields onto the current master, leaving every other field exactly as it was (the merge), and files the result.
- You keep no copy — you wrote through the office.
The danger is if the clerk, instead of applying only your fields, replaces the whole form with a page that has only your two fields filled in and the rest blank. Every field you did not mention is now wiped. That is a partial write without a merge — and it is the bug this chapter turns on.
7. Engineering Diagram — a WriteUniquePtl
Read top to bottom: request, an invalidating snoop that also fetches the current data, the DBID grant, the partial data, then merge and complete. The requester never cached the line; the home did all the coherence and the merge.
8. Full versus Partial — and the Merge
The two forms differ in one thing: whether every byte is written.
| Form | Bytes written | Byte-enables | Home must merge? |
|---|---|---|---|
| WriteUniqueFull | the whole line | all enabled | no — replace |
| WriteUniquePtl | some bytes | per-byte BE | yes — enabled new, disabled old |
The rule to carry: Full replaces, Ptl merges. For a full write, every byte is valid, so the home simply installs the new line. For a partial write, only the enabled bytes are valid; the disabled bytes on the WriteData beat are meaningless, so the home must take those bytes from the existing line. Both forms invalidate every other copy — the coherence step is identical; only the data assembly differs.
9. The Partial-Write Merge — byte-enables preserve untouched bytes
The merge is the heart of WriteUniquePtl, and it is worth stating exactly.
- Byte-enables select. Each byte of the line has a BE bit: 1 means the WriteData's byte is valid and replaces the old; 0 means the WriteData's byte is meaningless and the old byte must be kept.
- Disabled lanes carry garbage. On a partial beat, the disabled byte lanes hold don't-care data — often zero, often stale. They must never reach the line.
- The home merges. The home builds
new[b] = BE[b] ? write[b] : old[b]per byte — pulling the current line (from the snooped holder or memory) for the disabled bytes. - Then writes the whole line. Memory receives the merged line — the new bytes where written, the old bytes everywhere else.
The point to carry:
A partial write is an edit, not a replacement. The requester supplies values for some bytes; the home is responsible for the rest, which must be exactly what was there before. That is why the home fetches the current line and merges by byte-enable: the disabled lanes on the write beat are not data, they are placeholders, and treating them as data overwrites bytes the writer never intended to touch. The byte-enable is the line between an edit and a clobber.
10. Flow Walkthrough — WriteUniquePtl with a holder
RN0 writes bytes 0–1 of a line homed at HN; RN1 holds the line, and the line currently reads 0xAABBCCDD.
- REQ. RN0 sends WriteUniquePtl with byte-enables 0011 (bytes 0 and 1). It holds no cached copy.
- Invalidate + fetch. HN snoops RN1 with SnpCleanInvalid: RN1 goes to I and returns the current line 0xAABBCCDD (needed for the merge).
- Grant. HN returns DBIDResp — RN0 may now send data.
- WriteData. RN0 sends the partial data: bytes 0–1 = 0x3344, byte-enables 0011. The upper two byte lanes are don't-care.
- Merge + write. HN merges: enabled bytes 0–1 take 0x3344; disabled bytes 2–3 keep the old 0xAABB. The merged line 0xAABB3344 is written to memory. HN signals Comp.
End state: memory holds 0xAABB3344 — the low two bytes updated, the high two preserved; every other copy invalidated; RN0 cached nothing. Had the home skipped the merge and written the raw beat, bytes 2–3 would have been clobbered — the DebugLab.
11. RTL / Hardware View — the byte-enable merge
The core of WriteUniquePtl at the home is one combinational merge: for each byte, take the new data if enabled, else the old line's byte. Representative, 32-bit line with 4 byte-enables.
// Representative WriteUnique partial-write MERGE at the home (educational).
// For each byte: enabled -> take the new WriteData byte; disabled -> keep the
// existing line byte. This preserves the bytes the partial write does not touch.
// WriteUnique also invalidates every other copy (coherence), shown as a flag.
module chi_writeunique_merge #(parameter BYTES = 4) (
input logic [BYTES*8-1:0] old_line, // current line (from snoop or memory)
input logic [BYTES*8-1:0] wr_data, // WriteData beat (valid only in enabled lanes)
input logic [BYTES-1:0] byte_en, // 1 = write this byte, 0 = keep old
output logic [BYTES*8-1:0] new_line, // merged result written to memory
output logic invalidate_others
);
always_comb begin
for (int i = 0; i < BYTES; i++)
new_line[i*8 +: 8] = byte_en[i] ? wr_data[i*8 +: 8] // enabled: new byte
: old_line[i*8 +: 8]; // disabled: keep old byte
end
// WriteUnique clears every other copy so no stale data survives the write.
assign invalidate_others = 1'b1;
endmoduleThe same behavior in Verilog-2001:
// Representative WriteUnique partial-write MERGE at the home (Verilog-2001).
module chi_writeunique_merge #(parameter BYTES = 4) (
input [BYTES*8-1:0] old_line,
input [BYTES*8-1:0] wr_data,
input [BYTES-1:0] byte_en,
output reg [BYTES*8-1:0] new_line,
output invalidate_others
);
integer i;
always @* begin
for (i = 0; i < BYTES; i = i + 1)
new_line[i*8 +: 8] = byte_en[i] ? wr_data[i*8 +: 8]
: old_line[i*8 +: 8];
end
assign invalidate_others = 1'b1;
endmoduleAnd in VHDL:
-- Representative WriteUnique partial-write MERGE at the home (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity chi_writeunique_merge is
generic ( BYTES : integer := 4 );
port (
old_line : in std_logic_vector(BYTES*8-1 downto 0);
wr_data : in std_logic_vector(BYTES*8-1 downto 0);
byte_en : in std_logic_vector(BYTES-1 downto 0);
new_line : out std_logic_vector(BYTES*8-1 downto 0);
invalidate_others : out std_logic
);
end entity;
architecture rtl of chi_writeunique_merge is
begin
process (old_line, wr_data, byte_en)
begin
for i in 0 to BYTES-1 loop
if byte_en(i) = '1' then
new_line(i*8+7 downto i*8) <= wr_data(i*8+7 downto i*8); -- enabled: new
else
new_line(i*8+7 downto i*8) <= old_line(i*8+7 downto i*8); -- disabled: keep old
end if;
end loop;
end process;
invalidate_others <= '1';
end architecture;All three preserve the disabled bytes by pulling them from old_line, and take the enabled bytes from wr_data. The invalidate_others flag marks the coherence step. The DebugLab shows the corruption when the disabled bytes are not preserved.
12. Verification View — enabled written, disabled preserved
The properties that define a correct partial write: enabled bytes get the new data, disabled bytes keep the old, and other copies are invalidated.
// Bind to chi_writeunique_merge (BYTES = 4).
// 1. Every ENABLED byte equals the new WriteData byte.
property p_enabled_written;
@(*) (byte_en[0] -> new_line[7:0] == wr_data[7:0]) &&
(byte_en[1] -> new_line[15:8] == wr_data[15:8]) &&
(byte_en[2] -> new_line[23:16] == wr_data[23:16]) &&
(byte_en[3] -> new_line[31:24] == wr_data[31:24]);
endproperty
// 2. Every DISABLED byte keeps the OLD line's byte — untouched.
property p_disabled_preserved;
@(*) (!byte_en[0] -> new_line[7:0] == old_line[7:0]) &&
(!byte_en[1] -> new_line[15:8] == old_line[15:8]) &&
(!byte_en[2] -> new_line[23:16] == old_line[23:16]) &&
(!byte_en[3] -> new_line[31:24] == old_line[31:24]);
endproperty
// 3. WriteUnique invalidates every other copy.
property p_invalidate_others;
@(*) invalidate_others == 1'b1;
endpropertyThe system point, beyond the checks:
A partial write carries two kinds of bytes: data (the enabled lanes) and placeholders (the disabled lanes). Correctness is entirely about telling them apart — the byte-enables are that distinction, and the merge is where it is enforced. The disabled lanes must be treated as absent, their positions filled from the current line, not from the beat. This is why WriteUnique fetches the line before merging: to have real bytes for the positions the writer left blank. Lose the distinction — write the whole beat — and the placeholders masquerade as data, silently rewriting bytes outside the write's intent. Byte-granular writes are correct only if the merge is.
- What it proves: enabled bytes take new data, disabled bytes keep old, other copies invalidated.
- What it does not prove: the home actually fetched the current line — that is the snoop/memory path (Chapter 7.4).
- Bug signature: a disabled byte changing — a placeholder lane written as if it were data.
13. Testbench — full and partial merges
Drives full and partial byte-enable patterns and checks the merged line against the expected bytes.
module tb_chi_writeunique_merge;
localparam BYTES = 4;
logic [31:0] old_line, wr_data, new_line;
logic [3:0] byte_en;
logic invalidate_others;
int errors = 0;
chi_writeunique_merge #(.BYTES(BYTES)) dut (.*);
task automatic check(input logic [3:0] be, input logic [31:0] exp, input string name);
byte_en = be; #1;
if (new_line !== exp || invalidate_others !== 1'b1) begin
errors++; $display("FAIL %s: got %08h exp %08h", name, new_line, exp);
end else $display("PASS %s: %08h", name, new_line);
endtask
initial begin
old_line = 32'hAABBCCDD;
wr_data = 32'h11223344;
check(4'b1111, 32'h11223344, "full (replace all)");
check(4'b0011, 32'hAABB3344, "low half (bytes 0,1)");
check(4'b1000, 32'h11BBCCDD, "top byte (byte 3)");
check(4'b0000, 32'hAABBCCDD, "none (all preserved)");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS full (replace all): 11223344
PASS low half (bytes 0,1): AABB3344
PASS top byte (byte 3): 11BBCCDD
PASS none (all preserved): AABBCCDD
ALL TESTS PASSED14. DebugLab — a partial write with no merge
A partial write with no merge
PARTIAL WRITE WRITTEN WITHOUT MERGE -> UNTOUCHED BYTES CLOBBEREDA field is written correctly, but an adjacent field in the same cache line is silently wiped — set to zero or garbage — even though nothing wrote to it. It only happens with partial writes; full-line writes are fine.
The disabled bytes were overwritten by the beat's placeholder lanes:
line before: 0xAABBCCDD
WriteUniquePtl: wr_data = 0x00003344, byte_en = 0011 (only low 2 bytes valid)
correct merge : new = 0xAABB3344 (keep AA BB, write 33 44)
buggy (no merge): new = 0x00003344 (wrote the WHOLE beat)
-> bytes 2,3 clobbered from AABB to 0000The enabled bytes (33 44) are right; the disabled bytes were taken from the beat's zero-filled lanes instead of the old line.
The home wrote the entire WriteData beat to the line, ignoring the byte-enables. From that point the disabled lanes — which hold no valid data — were treated as data and stored, overwriting the bytes the partial write intended to preserve.
A partial write's disabled lanes are placeholders, not data. WriteUniquePtl carries byte-enables precisely because only some bytes are valid; the rest must come from the existing line. Writing the raw beat conflates the two, so bytes outside the write's intent are corrupted. This is a byte-granular data-integrity bug, distinct from a coherence-coverage error (Chapter 8.2): coherence was fine — every other copy was invalidated — but the data assembly dropped the merge.
Merge the WriteData with the current line by byte-enable: new[b] = BE[b] ? write[b] : old[b], as the outcome logic does. Fetch the current line (via the invalidating snoop or memory) so the disabled positions have real bytes to keep. Only the enabled bytes change; every other byte in the line is preserved exactly.
15. Common Mistakes
- Writing a partial beat without merging. Assumption: store the whole beat. Bug: untouched bytes clobbered (the DebugLab). Prevention: merge by byte-enable.
- Not fetching the current line for a partial. Assumption: memory already has it. Bug: no real bytes for disabled lanes. Prevention: snoop/read the current line first.
- Skipping invalidation. Assumption: writing through is enough. Bug: a stale sharer survives the write. Prevention: invalidate every other copy.
- Caching the line after WriteUnique. Assumption: a write installs the line. Bug: unexpected retained state. Prevention: WriteUnique writes through; the requester keeps nothing.
- Sending data before the DBID. Assumption: data follows the request. Bug: dropped write (Chapter 7.2). Prevention: wait for the DBID grant.
- Ignoring byte-enables on Full. Assumption: Full needs BEs too. Bug: over-thinking. Prevention: Full replaces the whole line; all bytes valid.
16. Engineering Checklist
- Issue WriteUnique without caching the line; the home makes it coherent.
- Wait for the DBID grant before sending WriteData (Chapter 7.2).
- Invalidate every other copy — no stale copy survives the write.
- For Ptl, fetch the current line and merge: enabled new, disabled preserved.
- For Full, simply replace the whole line.
- Keep no cached copy after the write; close with Comp.
17. Key Takeaways
- WriteUnique writes a coherent line without the requester holding it cached — the home makes it coherent.
- The flow is the write handshake (REQ → DBID → WriteData → Comp) plus invalidating every other copy.
- WriteUniqueFull replaces a whole line; WriteUniquePtl writes some bytes with byte-enables.
- A partial write must be merged: enabled bytes take new data, disabled bytes keep the old line's bytes.
- Skipping the merge lets the beat's placeholder lanes clobber untouched bytes — silent corruption.
- Invalidate others, merge by byte-enable, cache nothing; the model here is representative.
18. Quick Revision
WriteUnique flow. WriteUnique writes a coherent line the requester need not hold cached — the home does the coherence and the merge, the requester supplies data and byte-enables under a DBID grant and keeps nothing. The flow is the write handshake (REQ → DBID → WriteData → Comp) plus an invalidating snoop (SnpCleanInvalid) that clears every other copy. Two forms: WriteUniqueFull replaces the whole line; WriteUniquePtl writes only some bytes, carrying byte-enables. The partial form's rule: the home merges —
new[b] = BE[b] ? write[b] : old[b]— taking enabled bytes from the WriteData and disabled bytes from the existing line, so bytes the write never touched are preserved. The disabled lanes of a partial beat are placeholders, not data; writing the raw beat clobbers untouched bytes — a silent corruption of a neighbouring field, distinct from a coherence error. Invalidate others, fetch and merge by byte-enable, cache nothing. Representative model; 8.6 is the WriteBack flow — evicting a line you already own dirty.
Coming Next
Chapter 8.6 — WriteBack Flow. WriteUnique wrote a line the requester did not own; WriteBack is the opposite — evicting a line the requester does own dirty, returning it to memory. Chapter 8.6 walks the flow: how a cache gives up a dirty line under the DBID handshake, why it is the natural end of a line's residence in a cache, and how it differs from the write-through of WriteUnique — the eviction path that keeps memory current as caches make room.