AMBA CHI · Module 8 · Request Flows
WriteBack Flow
WriteUnique wrote a line the requester did not own; WriteBack is the opposite — a cache evicting a line it already holds dirty, returning the data to memory. It is the natural end of a line's residence in a cache. The flow uses the write handshake — request, DBID grant, the dirty data as CopyBackWrData — and the cache ends Invalid. Two things make it critical: it applies only to a dirty owned line (a clean line needs no writeback), and the evicting cache holds the only copy of that dirty data, so it must keep the line until the writeback is sent and accepted — drop it early and the modification is gone. Representative model, not the specification.
Intermediate15 min readAMBA CHIWriteBackEvictionDirtyCopyBack
Module 8 · Chapter 8.6 · Request Flows
Project thread — 8.5 wrote a line you did not own (WriteUnique). This chapter evicts a line you do own dirty (WriteBack). 8.7 is Evict — dropping a clean line, which needs no writeback.
1. Learning Outcomes
By the end of this chapter you should be able to:
- State that WriteBack evicts a line the requester owns dirty (UD/SD), returning its data to memory.
- Trace the flow — REQ → DBID grant → CopyBackWrData → done — with the cache ending Invalid.
- Explain why WriteBack applies only to a dirty line, not a clean one.
- State the retention rule: hold the dirty line until the writeback data is sent and accepted.
- Diagnose why invalidating the line early loses the modified data.
- Implement a representative WriteBack FSM in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
Caches are finite, so lines constantly get evicted to make room. When an evicted line is dirty — it holds the only up-to-date value — it cannot simply be dropped; its data must be written back to memory first. WriteBack is that operation, and it runs constantly in any real system. Every cache-capacity conflict, every context switch that floods the cache, ends in a wave of WriteBacks.
It is marked critical for a blunt reason: a WriteBack is the point where a cache gives up the only copy of modified data. Everything about the flow is arranged to make sure that data lands in memory before the cache lets go. Miss the retention rule — invalidate the line before the data is safely handed off — and you lose a write with no error, no trace, just a stale value in memory. Understanding WriteBack is understanding how a cache safely relinquishes what only it holds.
3. Key Terms
4. Previous Chapter Connection
Chapter 8.5's WriteUnique wrote a line the requester did not own — a write-through, leaving the requester with no cached copy. WriteBack is the mirror on the ownership axis: the requester does own the line, holds it dirty, and is now giving it up.
The mechanics reuse what you already have: the write handshake of Chapter 7.2 — request, DBID grant, WriteData, done — and the grant-before-data rule. What is new is the reason and the end state: WriteBack exists to evict, so the cache transitions to Invalid, and the data it sends is the dirty line it owned. No snoop is needed — the requester is the dirty owner, so no other coherent copy conflicts. It is the write that ends a line's life in a cache.
5. Core Concept — evict a dirty owned line to memory
A WriteBack returns a dirty line the requester owns to memory and frees the cache entry. It is a write, so it uses the DBID handshake, and it ends with the cache Invalid.
- Request. The cache needs to evict a line it holds in UD (or is the SD owner). It sends WriteBackFull (or WriteBackPtl) to the home. This is valid only because the line is dirty — a clean line would use Evict (Chapter 8.7).
- Grant. The home returns a DBID (via CompDBIDResp) — the cache may send the data only after this (Chapter 7.2).
- Send the dirty data. The cache sends CopyBackWrData, tagged with the DBID — the dirty line's data. Only now, as the data leaves and is accepted, does the cache transition the line to I.
- Home writes memory. The home writes the data back to memory. The line is now clean in memory and gone from the cache. No snoop was needed.
The synthesis:
WriteBack evicts a dirty owned line to memory. It uses the write handshake — REQ → DBID → CopyBackWrData — and ends the cache in I. It is valid only for a dirty line, and the cache must retain that line until the data is sent and accepted, because it holds the only copy. No snoop is involved; the requester is the dirty owner giving the line up.
6. Engineering Mental Model — return the annotated book, file your edits
You have been keeping the office's reference book with your handwritten edits (a dirty line you own), and now you need your desk space back.
- You cannot just toss it — your edits are the only up-to-date version. You must return it to the records office so your changes are filed (the writeback).
- You request a return slot (the DBID grant), then hand over the annotated book (CopyBackWrData). Only once the office has taken it do you clear it off your desk (transition to Invalid).
- If the book had no edits (a clean copy), you would not bother filing it — you would simply recycle your copy (that is Evict, next chapter).
The danger is clearing your desk before the office has the book in hand. Shred your annotated copy the instant you decide to return it, and if the handover falls through, your edits are gone — and they existed nowhere else. Hold the book until it is safely received.
7. Engineering Diagram — a WriteBackFull
Read top to bottom: request, the DBID grant, the dirty data (as the line goes to I), then the write to memory. The cache held the line until the data left; no snoop was required.
8. WriteBack versus WriteUnique versus Evict
Three neighbouring operations, distinguished by ownership and dirtiness.
| Operation | Requester owns line? | Line dirty? | Sends data? | Cache ends |
|---|---|---|---|---|
| WriteUnique (8.5) | no | writes new data | yes (new) | I (write-through) |
| WriteBack (8.6) | yes | yes | yes (the dirty line) | I |
| Evict (8.7) | yes | no (clean) | no | I |
The rule to carry: WriteBack is the dirty-eviction path. You write back because the line is dirty — its data must reach memory before the entry is freed. WriteUnique writes a line you never owned; Evict drops a clean line you own, sending no data (memory already has it). Choosing WriteBack for a clean line wastes bandwidth; choosing Evict for a dirty line loses data.
9. Hold the Dirty Line Until Handoff — the retention rule
The critical rule of WriteBack deserves its own section, because it is where dirty data is most easily lost.
- The cache holds the only copy. A dirty line's latest value exists only in the owning cache — memory is stale. Until the writeback lands, nothing else has it.
- Retain until sent and accepted. The cache must keep the line valid — holding its dirty data — until the CopyBackWrData is sent and accepted by the home. Only then may it transition to I and reuse the entry.
- No early invalidation. Marking the line Invalid, or reusing the entry for a new line, before the data is handed off overwrites or discards the only copy of the modification.
- Grant-before-data still applies. As with any write, the data goes out only after the DBID (Chapter 7.2) — so the retention window spans from the request through the accepted data.
The point to carry:
A WriteBack is a handoff of the only copy of modified data. The entire flow is arranged so the cache does not let go until memory has it: request, wait for the grant, send the data, and only on acceptance invalidate. The line's dirty data must survive the whole window — treat it like a baton that cannot be dropped until the next runner's hand is closed around it. Invalidate a beat too early and the modification exists nowhere.
10. Flow Walkthrough — WriteBackFull evicting UD
RN0 holds a line in UD and must evict it to make room; the line is homed at HN, backed by memory (SN).
- REQ. RN0 needs the entry. Because the line is dirty, it sends WriteBackFull — a clean line would use Evict instead. The line stays valid in RN0's cache.
- Grant. HN returns CompDBIDResp with a DBID — RN0 may now send the data. RN0 still holds the dirty line.
- CopyBackWrData. RN0 sends the dirty line's data, tagged with the DBID. As the data is sent and accepted, RN0 transitions the line UD → I and frees the entry.
- Write memory. HN writes the data to memory. The value RN0 modified is now safely in memory; RN0's cache no longer holds it.
End state: memory holds the latest value, RN0 is I, the entry is free for a new line. The retention was the crux: RN0 kept the dirty line through steps 1–3 and dropped it only when the data was accepted. Invalidate at step 1 and the DebugLab's data loss follows.
11. RTL / Hardware View — a WriteBack FSM
The requester's WriteBack is a small machine: issue the request (only if dirty), wait for the DBID, send the dirty data, and invalidate the line only once the data is accepted. Representative and sequential.
// Representative requester-side WriteBack FSM (educational).
// Evict a DIRTY owned line: REQ (only if dirty) -> WAIT for DBID -> send
// CopyBackWrData -> invalidate ONLY after the data is accepted. holds_dirty keeps
// the line valid through the whole handoff, so the only copy is never dropped early.
module chi_writeback_txn (
input logic clk, rst_n,
input logic evict, // this cache wants to evict the line
input logic is_dirty, // line is UD/SD -> WriteBack applies (else Evict)
input logic dbid_valid, // DBID grant arrived
input logic [3:0] dbid_in, // the granted DBID
input logic data_accepted, // home accepted the CopyBackWrData
output logic req_valid, // issue WriteBack (only for a dirty line)
output logic data_valid, // drive CopyBackWrData (tagged dbid_q)
output logic [3:0] data_dbid,
output logic holds_dirty, // line still valid (dirty retained)
output logic invalidated // line dropped to I (after data accepted)
);
typedef enum logic [1:0] { IDLE, WAIT_DBID, SEND_DATA } state_t;
state_t state, next;
logic [3:0] dbid_q;
always_comb begin
next = state; req_valid = 1'b0; data_valid = 1'b0;
case (state)
IDLE: if (evict && is_dirty) begin req_valid = 1'b1; next = WAIT_DBID; end
WAIT_DBID: if (dbid_valid) next = SEND_DATA;
SEND_DATA: begin data_valid = 1'b1; if (data_accepted) next = IDLE; end
endcase
end
assign data_dbid = dbid_q;
assign holds_dirty = (state != IDLE); // retain until handoff done
assign invalidated = (state == SEND_DATA) && data_accepted; // drop to I only after accept
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) dbid_q <= '0;
else if (state == WAIT_DBID && dbid_valid) dbid_q <= dbid_in;
endmoduleThe same behavior in Verilog-2001:
// Representative requester-side WriteBack FSM (Verilog-2001).
module chi_writeback_txn (
input clk, rst_n, evict, is_dirty, dbid_valid, data_accepted,
input [3:0] dbid_in,
output req_valid, data_valid, holds_dirty, invalidated,
output [3:0] data_dbid
);
localparam IDLE = 2'd0, WAIT_DBID = 2'd1, SEND_DATA = 2'd2;
reg [1:0] state, next;
reg [3:0] dbid_q;
always @* begin
next = state;
case (state)
IDLE: next = (evict && is_dirty) ? WAIT_DBID : IDLE;
WAIT_DBID: next = dbid_valid ? SEND_DATA : WAIT_DBID;
SEND_DATA: next = data_accepted ? IDLE : SEND_DATA;
default: next = IDLE;
endcase
end
assign req_valid = (state == IDLE) && evict && is_dirty;
assign data_valid = (state == SEND_DATA);
assign data_dbid = dbid_q;
assign holds_dirty = (state != IDLE);
assign invalidated = (state == SEND_DATA) && data_accepted;
always @(posedge clk or negedge rst_n)
if (!rst_n) dbid_q <= 4'd0;
else if (state == WAIT_DBID && dbid_valid) dbid_q <= dbid_in;
endmoduleAnd in VHDL:
-- Representative requester-side WriteBack FSM (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity chi_writeback_txn is
port (
clk, rst_n : in std_logic;
evict : in std_logic;
is_dirty : in std_logic;
dbid_valid : in std_logic;
dbid_in : in std_logic_vector(3 downto 0);
data_accepted : in std_logic;
req_valid : out std_logic;
data_valid : out std_logic;
data_dbid : out std_logic_vector(3 downto 0);
holds_dirty : out std_logic;
invalidated : out std_logic
);
end entity;
architecture rtl of chi_writeback_txn is
type state_t is (IDLE, WAIT_DBID, SEND_DATA);
signal state : state_t := IDLE;
signal dbid_q : std_logic_vector(3 downto 0) := (others => '0');
begin
process (clk, rst_n)
begin
if rst_n = '0' then
state <= IDLE;
dbid_q <= (others => '0');
elsif rising_edge(clk) then
case state is
when IDLE => if evict = '1' and is_dirty = '1' then state <= WAIT_DBID; end if;
when WAIT_DBID => if dbid_valid = '1' then
dbid_q <= dbid_in;
state <= SEND_DATA;
end if;
when SEND_DATA => if data_accepted = '1' then state <= IDLE; end if;
end case;
end if;
end process;
req_valid <= '1' when (state = IDLE and evict = '1' and is_dirty = '1') else '0';
data_valid <= '1' when state = SEND_DATA else '0';
data_dbid <= dbid_q;
holds_dirty <= '0' when state = IDLE else '1';
invalidated <= '1' when (state = SEND_DATA and data_accepted = '1') else '0';
end architecture;All three issue the WriteBack only when the line is dirty, keep holds_dirty asserted through the handoff, and set invalidated only on acceptance — so the dirty line is never dropped early. The DebugLab shows the data loss when it is.
12. Verification View — dirty until accepted, and only for dirty lines
The properties that keep a WriteBack safe: it is issued only for dirty lines, and the line is retained until the data is accepted.
// Bind to chi_writeback_txn.
// 1. WriteBack is issued only for a DIRTY line — a clean line uses Evict instead.
property p_only_dirty;
@(posedge clk) disable iff (!rst_n)
req_valid |-> is_dirty;
endproperty
// 2. The dirty line is retained until the writeback data is accepted — the line
// is invalidated only after acceptance, never before.
property p_hold_until_accepted;
@(posedge clk) disable iff (!rst_n)
($fell(holds_dirty)) |-> $past(data_accepted);
endproperty
// 3. Data goes out only after the DBID grant (grant-before-data).
property p_data_after_dbid;
@(posedge clk) disable iff (!rst_n)
$rose(data_valid) |-> $past(dbid_valid);
endpropertyThe system point, beyond the checks:
A WriteBack is the one moment a cache voluntarily surrenders the sole copy of modified data, so the flow's correctness is entirely about never having a gap where that data exists nowhere. The line must be dirty to justify the writeback; the data must go out under a grant; and — the crux — the line must stay valid until the data is accepted, so at every instant the modification lives either in the cache or in the home's accepted buffer, never in the void between. The transition to Invalid is the last step, gated on acceptance, precisely because it is the point of no return: after it, the cache has nothing, and only a completed handoff makes that safe.
- What it proves: WriteBack is dirty-only, data follows the DBID, and the line is held until accepted.
- What it does not prove: the home actually wrote memory — that is the home/memory path (Chapter 7.2).
- Bug signature: the line invalidated before
data_accepted— the only copy of the modification dropped.
13. Testbench — retain through the handoff; skip clean lines
Evicts a dirty line and checks the line is held until acceptance, then confirms a clean line issues no WriteBack.
module tb_chi_writeback_txn;
logic clk = 0, rst_n = 0;
logic evict = 0, is_dirty = 0, dbid_valid = 0, data_accepted = 0;
logic [3:0] dbid_in = 4'd0;
logic req_valid, data_valid, holds_dirty, invalidated;
logic [3:0] data_dbid;
int errors = 0;
chi_writeback_txn dut (.*);
always #5 clk = ~clk;
initial begin
@(posedge clk) rst_n = 1;
// Clean line: no WriteBack.
@(posedge clk) begin evict = 1; is_dirty = 0; end
@(posedge clk) if (req_valid) begin errors++; $display("FAIL WriteBack issued for clean line"); end
else $display("PASS clean line: no WriteBack");
evict = 0;
// Dirty line: WriteBack issued, line held.
@(posedge clk) begin evict = 1; is_dirty = 1; end
@(posedge clk) begin evict = 0; is_dirty = 1; end
if (!holds_dirty) begin errors++; $display("FAIL dirty line not held"); end
else $display("PASS dirty line: WriteBack issued, holds dirty");
// Grant DBID.
@(posedge clk) begin dbid_valid = 1; dbid_in = 4'd5; end
@(posedge clk) dbid_valid = 0;
// In SEND_DATA, before acceptance: still held, not invalidated.
if (!data_valid || !holds_dirty || invalidated) begin
errors++; $display("FAIL not holding dirty before acceptance"); end
else $display("PASS holds dirty until data accepted");
// Accept the data -> invalidate.
@(posedge clk) data_accepted = 1;
if (!invalidated) begin errors++; $display("FAIL not invalidated on acceptance"); end
else $display("PASS invalidated after data accepted");
@(posedge clk) data_accepted = 0;
if (holds_dirty) begin errors++; $display("FAIL still holding after handoff"); end
else $display("PASS line released after handoff");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS clean line: no WriteBack
PASS dirty line: WriteBack issued, holds dirty
PASS holds dirty until data accepted
PASS invalidated after data accepted
PASS line released after handoff
ALL TESTS PASSED14. DebugLab — invalidating the line before the writeback is accepted
Invalidating the line before the writeback is accepted
ENTRY INVALIDATED/REUSED BEFORE WRITEBACK ACCEPTED -> DIRTY DATA LOSTUnder heavy cache pressure, writes are silently lost — a later read returns a stale value for a line that was definitely modified. It only happens when evictions and fills are rapid; a lightly loaded cache never shows it.
The entry was reused before the writeback landed:
RN0 line L = UD (value = NEW), memory = OLD
RN0 issues WriteBack(L) -> and immediately marks L's entry Invalid
RN0 reuses the entry for incoming line M -> writes M's data over NEW
later: CopyBackWrData for L is sent -> carries M's data or garbage, not NEW
memory = OLD (or M's data), NEW is gone -> lost writeThe dirty data NEW was overwritten in the cache before the writeback carried it out.
The cache transitioned the line to Invalid (and reused the entry) on issuing the WriteBack, not on the writeback data being accepted. From that point the only copy of the modification sat in an entry free to be overwritten.
A WriteBack surrenders the only copy of dirty data, so the line must be retained until the handoff completes. The modification lives solely in the owning cache until memory has it; freeing the entry before the CopyBackWrData is sent and accepted opens a window where a new fill overwrites it. This is a retention failure specific to eviction — distinct from the send-side grant-before-data ordering (Chapter 7.2): the data path was fine; the cache simply let go of the line too soon.
Keep the dirty line valid — holds_dirty asserted, the entry not reused — until the CopyBackWrData is accepted, then transition to I and free the entry. The FSM invalidates only in SEND_DATA on data_accepted, so the modification is always either in the cache or in the home's accepted buffer, never in a reusable-but-unwritten entry.
15. Common Mistakes
- Invalidating before acceptance. Assumption: issuing the WriteBack frees the entry. Bug: dirty data lost (the DebugLab). Prevention: retain until data accepted.
- WriteBack for a clean line. Assumption: all evictions write back. Bug: wasted bandwidth. Prevention: clean lines use Evict (Chapter 8.7).
- Sending data before the DBID. Assumption: data follows the request. Bug: dropped write (Chapter 7.2). Prevention: wait for the grant.
- Expecting a snoop. Assumption: the home snoops on a WriteBack. Bug: mis-modeled flow. Prevention: the owner writes back directly; no snoop.
- Not going to Invalid. Assumption: the cache keeps the line. Bug: stale copy after eviction. Prevention: WriteBack ends the cache in I.
- Reusing the entry mid-flight. Assumption: the entry is free once requested. Bug: overwritten dirty data. Prevention: reuse only after handoff.
16. Engineering Checklist
- Issue WriteBack only for a dirty owned line (UD/SD) — clean lines use Evict.
- Wait for the DBID grant before sending CopyBackWrData (Chapter 7.2).
- Retain the dirty line until the data is sent and accepted.
- Transition the line to I — and reuse the entry — only after acceptance.
- Expect no snoop — the requester is the dirty owner.
- Confirm memory holds the value before considering the line gone.
17. Key Takeaways
- WriteBack evicts a line the requester owns dirty (UD/SD), returning its data to memory; the cache ends I.
- The flow is the write handshake — REQ → DBID → CopyBackWrData — with no snoop (the owner writes back).
- It applies only to dirty lines; a clean line needs Evict, which sends no data.
- The cache holds the only copy of the dirty data, so it must retain the line until the writeback is accepted.
- Invalidating or reusing the entry early loses the modification — silent, with no error.
- Dirty-only, grant-before-data, retain-until-accepted; the model here is representative.
18. Quick Revision
WriteBack flow. WriteBack evicts a dirty owned line (UD, or the SD owner) back to memory, freeing the cache entry — the mirror of WriteUnique, which wrote a line you did not own. It uses the write handshake: REQ WriteBackFull → DBID grant (CompDBIDResp) → CopyBackWrData (the dirty data, tagged with the DBID) → the home writes memory. No snoop is needed, because the requester is the dirty owner. Two rules make it critical. It is valid only for a dirty line — a clean line uses Evict (8.7), sending no data. And the cache holds the only copy of the dirty data, so it must retain the line until the CopyBackWrData is sent and accepted, transitioning to I only then; invalidating or reusing the entry earlier overwrites the modification and loses the write silently. Dirty-only, grant-before-data, retain-until-accepted. Representative model; 8.7 is the Evict flow — dropping a clean line with no writeback.
Coming Next
Chapter 8.7 — Evict Flow. WriteBack returned a dirty line to memory; Evict is its lightweight sibling — dropping a clean line that needs no writeback, because memory already holds the value. Chapter 8.7 walks the flow: how a cache notifies the home it is dropping a clean line so the directory stays accurate, why no data moves, and when Evict rather than WriteBack is the correct way to free a cache entry — completing the eviction pair.