AMBA CHI · Module 8 · Request Flows
ReadClean Flow
ReadShared shared a copy; ReadUnique took sole ownership; ReadClean sits between them. A core issues a ReadClean to read a line without inheriting responsibility for dirty data. The requester ends clean: Shared Clean if it coexists with a holder, Unique Clean if it is the sole holder — never Dirty. Where ReadUnique forwards a modified line to the requester as dirty, ReadClean cleans it, writing the dirty data back to memory so the copies it leaves are genuinely clean. The obligation is easy to miss: cleaning a dirty holder means writing its data back to memory, or the latest value sits in caches marked clean and is dropped on eviction. Representative model, not the specification.
Intermediate15 min readAMBA CHIReadCleanClean CopySnpCleanWriteback
Module 8 · Chapter 8.3 · Request Flows
Project thread — 8.1 shared (end SC), 8.2 owned (end Unique). This chapter is the third read: ReadClean — a clean copy with no dirty responsibility. 8.4 covers ReadNotSharedDirty.
1. Learning Outcomes
By the end of this chapter you should be able to:
- Place ReadClean between ReadShared and ReadUnique — a clean read without ownership of dirty data.
- State the signature: the requester ends clean (SC or UC), never dirty (UD).
- Explain how a dirty holder is cleaned — its data written back to memory, not forwarded.
- Distinguish the cases by holder state — uncached, shared, unique clean, unique dirty.
- Diagnose why skipping the writeback silently loses a modified line.
- Implement a representative ReadClean outcome in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
ReadClean is the read for when you want the data but not the baggage. Sometimes a core needs to read a line and possibly cache it, but has no intention of writing it and does not want to become responsible for holding it dirty. ReadClean gives exactly that — a clean copy — and it is common enough (instruction fetches, read-mostly data, prefetches) that you will meet it whenever you trace real traffic.
Its lesson is subtle and specific: cleanliness is a claim about memory, not just about a cache line's flag. When ReadClean turns a dirty holder into clean copies, it is asserting that memory now holds the latest value — which is only true if the flow actually wrote that value back. Mark a line clean without cleaning memory and you have created the most dangerous kind of copy: one that looks disposable but holds the only current data. This is the flow where the writeback obligation is easiest to drop and most costly to miss.
3. Key Terms
4. Previous Chapter Connection
Chapters 8.1 and 8.2 gave the two poles: ReadShared ends the requester SC and downgrades holders (they coexist); ReadUnique ends it Unique and invalidates everyone. Both could leave the requester holding dirty data — ReadShared preserves it as shared-dirty responsibility, ReadUnique forwards it as UD.
ReadClean is defined by refusing that. It wants a copy that is clean — matching memory — so the requester carries no dirty responsibility. When it meets a dirty holder, it does not forward the dirty line; it cleans it, writing the data back to memory (Chapter 7.2's writeback path, driven by the home). The requester ends SC or UC, never UD. It is the read that takes the value while leaving the dirty-data duty behind.
5. Core Concept — a clean copy, no dirty responsibility
A ReadClean brings a line to a requester in a clean state — matching memory — and takes on no obligation to write dirty data back. The flow varies by holder state, but the requester's copy is always clean.
- Uncached. No cache holds the line. The home fetches from memory and installs the requester in UC (Unique Clean) — sole holder, clean. No snoop.
- Held shared. Sharers hold the line in SC. The requester joins as another SC holder; memory is already current. No snoop, no writeback.
- Held unique clean (UC). A holder in UC. The home sends SnpClean, downgrading it to SC; the requester ends SC. The line was already clean, so no writeback.
- Held unique dirty (UD). A holder in UD. The home sends SnpClean; the holder returns its dirty data and downgrades to SC, and the home writes the data back to memory. The requester installs a clean SC copy. The dirty data is cleaned, not forwarded.
Across all cases the requester ends clean — SC or UC, never UD — and any dirty data ends up in memory, so every remaining copy genuinely matches it.
The synthesis:
ReadClean fetches a clean copy and leaves the requester with no dirty responsibility. A dirty holder is cleaned — its data written back to memory via SnpClean — rather than forwarded. The requester ends SC (coexisting) or UC (sole), never UD. The invariant that names the flow: after ReadClean, memory is current and every cached copy is clean.
6. Engineering Mental Model — photocopy the master, file the original
Back to the office document, but now you want a read-only, up-to-date copy and none of the filing duties.
- If nobody has it, you take the archive's copy — it is the current version, and you hold it (UC), clean.
- If colleagues are reading copies, you make one more and read alongside them (SC). The master file is already current.
- If a colleague had it out with handwritten edits (UD), you do not take their marked-up original and inherit the job of filing it. Instead, their edits are entered into the master file (writeback to memory), and everyone — including you — now reads clean copies of the updated master (SC). The edits are safely in the system of record; nobody is left holding the only annotated original.
The point is where the edits end up: in the master file, not in a stray photocopy someone might toss. ReadClean makes sure the latest data lands in memory, so the clean copies it hands out are safe to discard.
7. Engineering Diagram — ReadClean with a dirty holder
Read top to bottom: request, a cleaning snoop, dirty data returned, writeback to memory, then a clean completion. RN1 went UD → SC; RN0 installed SC; memory now current. The extra beat versus ReadShared is the writeback — that is what makes the copies clean.
8. The Cases by Holder State
The flow's middle depends on the holder, but the requester always ends clean.
| Holder state | Home action | Snoop | Writeback | Requester ends |
|---|---|---|---|---|
| Uncached (I) | fetch from memory | none | no | UC |
| Shared (SC) | join sharers | none | no | SC |
| Unique Clean (UC) | downgrade holder | SnpClean | no | SC |
| Unique Dirty (UD) | downgrade + clean | SnpClean | yes | SC |
The rule to carry: the requester ends UC only when it is the sole holder (uncached), and SC whenever it coexists with a holder — but never UD. A writeback appears only in the UD row, and it is the defining beat: turning a dirty holder into clean copies requires cleaning memory. Every other row leaves memory already current.
9. Clean Means Memory Is Current — the writeback obligation
The property to isolate is what "clean" actually promises.
- Clean is a claim about memory. A clean copy (SC/UC) asserts that memory holds the same value. That is what makes a clean line safe to evict silently — nothing is lost, because memory has it.
- So cleaning a dirty line demands a writeback. If ReadClean downgrades a dirty holder to a clean state, it must first put that dirty data into memory. Otherwise the "clean" flag is a lie — the latest value is not in memory.
- The requester inherits no dirty duty. Because the data went to memory, the requester's copy is genuinely clean; it will never have to write anything back. That is the whole point of ReadClean.
- Contrast the siblings. ReadUnique forwards dirty (requester ends UD, owns the writeback duty). ReadClean cleans dirty (requester ends SC, owes nothing). Same dirty holder, opposite destination for the data.
The point to carry:
A clean line is a promise that memory is current. ReadClean's job is to keep that promise while handing out clean copies — so when it encounters dirty data, it must write that data back to memory, not merely relabel the copies as clean. Skip the writeback and you manufacture clean-looking copies of data memory does not have; the next silent eviction throws the only current value away. Cleanliness is earned by the writeback, not asserted by the flag.
10. Flow Walkthrough — ReadClean, dirty holder
RN0 (in I) issues a ReadClean for a line homed at HN; RN1 holds it in UD.
- REQ. RN0 sends ReadClean, allocating a tracker. It wants a clean copy — no dirty duty.
- Directory lookup. HN sees RN1 in UD. The dirty data must be cleaned — captured and written to memory — and RN1 downgraded, but not made to forward dirty responsibility to RN0.
- SnpClean. HN sends SnpClean to RN1. RN1 returns its dirty data (SnpRespData) and downgrades UD → SC.
- Writeback + CompData. HN writes the data back to memory, making it current, and returns a clean CompData to RN0, which installs SC. The directory lists RN0 and RN1 in SC; memory holds the latest value.
- CompAck. RN0 closes with CompAck.
End state: two clean co-sharers, memory current, and no cache holding dirty responsibility. Compare 8.2: the same dirty holder under ReadUnique would forward the dirty data to RN0 as UD — here it went to memory instead.
11. RTL / Hardware View — ReadClean outcome logic
The ReadClean flow reduces to a small function of the holder state: snoop a unique holder, write back if it was dirty, and end the requester clean. Representative — the requester is never UD, and a dirty holder triggers a writeback.
// Representative ReadClean outcome logic (educational).
// Fetch a CLEAN copy. A unique holder is downgraded (SnpClean). A DIRTY holder's
// data is WRITTEN BACK to memory -- cleaned, not forwarded -- so the requester
// ends clean and owes no writeback. req_final is never UD.
module chi_readclean_outcome (
input logic [2:0] holder_state, // INV, SC, UC, UD
output logic do_snoop, // SnpClean to a unique holder?
output logic writeback, // dirty data cleaned to memory?
output logic [2:0] holder_final, // holder's state after the flow
output logic [2:0] req_final // requester's state after the flow (never UD)
);
localparam logic [2:0] INV = 3'd0, SC = 3'd3, UC = 3'd1, UD = 3'd2;
logic held, is_unique, is_dirty;
assign held = (holder_state != INV);
assign is_unique = (holder_state == UC) || (holder_state == UD);
assign is_dirty = (holder_state == UD);
// Snoop only a unique holder; shared/uncached needs none.
assign do_snoop = is_unique;
// A dirty holder is CLEANED: its data is written back to memory.
assign writeback = is_dirty;
// A unique holder downgrades to SC; shared/uncached is left as-is.
assign holder_final = is_unique ? SC : holder_state;
// Requester ends CLEAN: UC if it is the sole holder (uncached), else SC. Never UD.
assign req_final = held ? SC : UC;
endmoduleThe same behavior in Verilog-2001:
// Representative ReadClean outcome logic (Verilog-2001).
module chi_readclean_outcome (
input [2:0] holder_state,
output do_snoop,
output writeback,
output [2:0] holder_final,
output [2:0] req_final
);
localparam INV = 3'd0, SC = 3'd3, UC = 3'd1, UD = 3'd2;
wire held = (holder_state != INV);
wire is_unique = (holder_state == UC) || (holder_state == UD);
wire is_dirty = (holder_state == UD);
assign do_snoop = is_unique;
assign writeback = is_dirty;
assign holder_final = is_unique ? SC : holder_state;
assign req_final = held ? SC : UC;
endmoduleAnd in VHDL:
-- Representative ReadClean outcome logic (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity chi_readclean_outcome is
port (
holder_state : in std_logic_vector(2 downto 0);
do_snoop : out std_logic;
writeback : out std_logic;
holder_final : out std_logic_vector(2 downto 0);
req_final : out std_logic_vector(2 downto 0)
);
end entity;
architecture rtl of chi_readclean_outcome is
constant INV : std_logic_vector(2 downto 0) := "000";
constant SC : std_logic_vector(2 downto 0) := "011";
constant UC : std_logic_vector(2 downto 0) := "001";
constant UD : std_logic_vector(2 downto 0) := "010";
signal held, is_unique, is_dirty : boolean;
begin
held <= (holder_state /= INV);
is_unique <= (holder_state = UC) or (holder_state = UD);
is_dirty <= (holder_state = UD);
do_snoop <= '1' when is_unique else '0';
writeback <= '1' when is_dirty else '0';
holder_final <= SC when is_unique else holder_state;
req_final <= SC when held else UC;
end architecture;All three write back on a dirty holder (writeback = is_dirty) and end the requester clean (req_final is SC or UC, never UD). That writeback is the cleanliness obligation, and the DebugLab shows what dropping it costs.
12. Verification View — requester ends clean, dirty is written back
The properties that define ReadClean: the requester never ends dirty, and a dirty holder is cleaned to memory.
// Bind to chi_readclean_outcome.
// 1. The requester ends CLEAN — SC or UC, never dirty (UD) and never invalid.
property p_req_clean;
@(*) (req_final == 3'd3 /*SC*/) || (req_final == 3'd1 /*UC*/);
endproperty
// 2. A dirty holder MUST be written back — cleaned to memory, not left uncleaned.
property p_dirty_writeback;
@(*) (holder_state == 3'd2 /*UD*/) |-> writeback;
endproperty
// 3. Any holder ends clean (SC) or as it was — never left in a dirty state after the flow.
property p_holder_clean;
@(*) (holder_final != 3'd2 /*UD*/);
endpropertyThe system point, beyond the checks:
ReadClean's correctness is a conservation law for dirty data. A modified value must exist somewhere durable at all times — either in a cache that will write it back, or in memory. ReadClean chooses memory: it takes the requester out of the dirty-responsibility business by putting the data where it is always safe. So the writeback is not optional bookkeeping; it is the step that conserves the value as the flow strips the "dirty" flag off every copy. Omit it and the value exists only in caches that believe they may discard it — the flow has quietly created a leak. Clean copies are only sound if memory backs them.
- What it proves: the requester ends clean, and a dirty holder triggers a writeback.
- What it does not prove: the writeback actually reaches memory — that is the home/memory path (Chapter 7.2).
- Bug signature: a dirty holder cleaned to SC with no writeback — the latest value stranded in disposable copies.
13. Testbench — every holder-state case
Drives each starting holder state and checks the snoop, writeback, and resulting states — especially that the requester is never dirty and a dirty holder is written back.
module tb_chi_readclean_outcome;
logic [2:0] holder_state, holder_final, req_final;
logic do_snoop, writeback;
int errors = 0;
localparam INV = 3'd0, UC = 3'd1, UD = 3'd2, SC = 3'd3;
chi_readclean_outcome dut (.*);
task automatic check(input logic [2:0] hs, input logic exp_snoop, exp_wb,
input logic [2:0] exp_hf, exp_rf, input string name);
holder_state = hs; #1;
if (do_snoop !== exp_snoop || writeback !== exp_wb ||
holder_final !== exp_hf || req_final !== exp_rf || req_final === UD) begin
errors++; $display("FAIL %s: snoop=%0b wb=%0b hf=%0d rf=%0d",
name, do_snoop, writeback, holder_final, req_final);
end else $display("PASS %s: snoop=%0b wb=%0b hf=%0d rf=%0d",
name, do_snoop, writeback, holder_final, req_final);
endtask
initial begin
check(INV, 1'b0, 1'b0, INV, UC, "uncached -> memory, req UC");
check(SC, 1'b0, 1'b0, SC, SC, "shared -> join, req SC");
check(UC, 1'b1, 1'b0, SC, SC, "uniqueCln -> downgrade, req SC");
check(UD, 1'b1, 1'b1, SC, SC, "uniqueDty -> clean + wb, req SC");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS uncached -> memory, req UC: snoop=0 wb=0 hf=0 rf=1
PASS shared -> join, req SC: snoop=0 wb=0 hf=3 rf=3
PASS uniqueCln -> downgrade, req SC: snoop=1 wb=0 hf=3 rf=3
PASS uniqueDty -> clean + wb, req SC: snoop=1 wb=1 hf=3 rf=3
ALL TESTS PASSED14. DebugLab — cleaning a dirty holder without the writeback
Cleaning a dirty holder without the writeback
READCLEAN DOWNGRADES DIRTY HOLDER WITHOUT WRITEBACK -> LOST UPDATEData written by one core is silently lost — a later read returns a stale value — with no error. It happens only for lines that were dirty before a ReadClean, and only after the clean copies are evicted; keep the copies resident and the data survives.
A dirty line became clean copies, but memory was never updated:
before: RN1 = UD (value = NEW), memory = OLD
RN0 issues ReadClean -> HN SnpClean to RN1
RN1: UD -> SC (marked clean), returns NEW
HN installs RN0 = SC (marked clean, value NEW)
HN does NOT write NEW back to memory <-- missing writeback
state: RN0 = SC(NEW), RN1 = SC(NEW), memory = OLD
later: both SC copies evicted SILENTLY (clean -> no writeback)
-> NEW is gone, memory still OLD -> lost updateBoth caches held the only copy of NEW while believing it was safely in memory.
The flow marked the copies clean (SC) without performing the writeback that makes clean true. From that point memory was stale relative to the SC copies, so those copies held data that clean semantics allow to be discarded.
A clean copy asserts memory is current; ReadClean must make that assertion true. When it turns a dirty holder into clean copies, the dirty data has to go into memory via a writeback — otherwise the latest value lives only in caches that may silently drop it. This is a conservation failure: the dirty value was neither kept dirty nor written back, so nothing is obliged to preserve it. It differs from dropping data at the snoop-response level (Chapter 6.5): here the data is returned, but the flow forgets to land it in memory.
Write the dirty data back to memory whenever ReadClean downgrades a dirty holder — exactly the writeback = is_dirty term in the outcome logic. Only after the data is in memory may the copies be marked clean. Then a clean flag is honest: memory holds the value, and evicting a clean copy loses nothing.
15. Common Mistakes
- Cleaning without writeback. Assumption: relabeling to SC is enough. Bug: lost update (the DebugLab). Prevention: write dirty data back to memory.
- Forwarding dirty to the requester. Assumption: ReadClean is like ReadUnique. Bug: requester wrongly ends UD, owes a writeback. Prevention: ReadClean cleans; requester ends clean.
- Ending the requester dirty. Assumption: any read state is fine. Bug: unexpected dirty responsibility. Prevention: ReadClean ends SC/UC, never UD.
- Invalidating a holder. Assumption: clean means sole. Bug: needless loss of a sharer. Prevention: SnpClean downgrades; sharers may coexist.
- Snooping when uncached or shared. Assumption: every case snoops. Bug: needless snoops. Prevention: snoop only unique holders.
- Forgetting CompAck. Assumption: data ends the read. Bug: transaction never closes (Chapter 7.1). Prevention: close with CompAck.
16. Engineering Checklist
- End the requester clean — UC if sole, SC if coexisting — never UD.
- Snoop a unique holder with SnpClean — downgrade, not invalidate.
- Write dirty data back to memory whenever a dirty holder is cleaned.
- Mark copies clean only after memory holds the value.
- Do not snoop when the line is uncached or already shared.
- Close with CompAck; ReadClean grants no write permission and no dirty duty.
17. Key Takeaways
- ReadClean fetches a clean copy without taking on dirty responsibility — it sits between ReadShared and ReadUnique.
- The requester ends clean — SC when coexisting, UC when sole — and never UD.
- A dirty holder is cleaned: its data is written back to memory via SnpClean, not forwarded.
- After the flow, memory is current and every remaining copy genuinely matches it.
- Marking copies clean without the writeback strands the latest value in disposable copies — a lost update.
- Clean means memory is current; earn it with the writeback. The model here is representative.
18. Quick Revision
ReadClean flow. ReadClean reads a clean copy without dirty responsibility — it sits between ReadShared (share) and ReadUnique (own). The requester ends clean: UC if it is the sole holder (uncached), SC if it coexists with a holder — but never UD. Cases by holder: uncached → memory fetch, UC; shared → join, SC; unique clean → SnpClean downgrade to SC, requester SC; unique dirty → SnpClean, the holder returns its dirty data and downgrades to SC, and the home writes the data back to memory, requester installs a clean SC. The defining obligation: turning a dirty holder into clean copies requires a writeback, because a clean flag promises memory is current. Skip it and the latest value sits only in caches marked clean, discarded silently on eviction — a lost update. ReadClean cleans dirty data to memory where ReadUnique forwards it to the requester. End clean, write dirty back, earn the clean flag. Representative model; 8.4 is ReadNotSharedDirty.
Coming Next
Chapter 8.4 — ReadNotSharedDirty Flow. ReadClean kept memory current; ReadNotSharedDirty is the specialist for a subtler case — a requester that will accept a shared copy but must not inherit the responsibility for shared-dirty data. Chapter 8.4 walks this variant: what "not shared dirty" means as an end state, how it differs from ReadShared and ReadClean, and the narrow but real situations where it is the correct read to issue — completing the family of read flows before Module 8 turns to writes.