AMBA CHI · Module 7 · CHI Transaction Model
The Snoop Transaction
Reads, writes, and atomics are launched by a requester; the snoop runs the other way — the home launches it, reaching into a cache to fetch, invalidate, or downgrade a line on another transaction's behalf. A snoop rarely stands alone: it nests inside a read or write. The opcode forces a specific end state — SnpShared downgrades the holder to Shared Clean, SnpUnique invalidates it, SnpClean keeps a clean copy — and the node answers with a plain response, or the line's data when it holds it dirty. The forced state must be exact: answer a SnpUnique by merely downgrading, and a stale sharer survives beside the new owner. Representative model, not the specification.
Intermediate16 min readAMBA CHISnoopSnpUniqueCoherencyInvalidation
Module 7 · Chapter 7.4 · CHI Transaction Model
Project thread — 7.1–7.3 were requester-initiated: read, write, atomic. This chapter turns the arrow around — the home initiates the snoop, and it nests inside those flows. 7.5 zooms out to the transaction lifecycle.
1. Learning Outcomes
By the end of this chapter you should be able to:
- State that the snoop is home-initiated — the home reaches into a cache on another transaction's behalf.
- Explain how a snoop nests inside a read or write flow.
- Match each snoop opcode to the state it forces: SnpShared → SC, SnpUnique → I, SnpClean → clean.
- Distinguish the two responses — SnpResp (no data) and SnpRespData (returns the line).
- Diagnose why a SnpUnique must invalidate, not merely downgrade, the snooped copy.
- Implement a representative snooped-node snoop handler in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
Coherence is enforced by snoops. When one core wants a line another core holds, it is the snoop that makes the other core hand it over, share it, or drop it. Every cache-to-cache transfer, every invalidation that lets a writer proceed, is a snoop. If you build or verify a coherent cache, a CPU cluster, or an interconnect, the snoop transaction is where correctness is won or lost.
It is also the transaction with an external actor doing the work. A read or write acts on the requester's own tracker; a snoop reaches into someone else's cache and forces a state change there. Get that forced state slightly wrong — downgrade where you should invalidate — and you do not crash; you quietly leave two caches disagreeing about who owns the line. Understanding snoops means understanding the one operation that changes a cache the CPU did not ask to change.
3. Key Terms
4. Previous Chapter Connection
Chapters 7.1–7.3 were all launched by a requester: the read pulled data in, the write pushed it out, the atomic did both at the home. In each, when the directory showed another cache held the line, the home did something you saw only in passing — it snooped that holder.
This chapter makes that step the subject. The snoop is issued by the home, on the SNP channel (Chapter 6.5), targeting a specific cache. It is how the read of a dirty line gets its data, and how a ReadUnique or WriteUnique clears other copies out of the way. So the snoop is not a fifth kind of standalone operation so much as the mechanism the other transactions rely on — and it has its own opcodes, responses, and, above all, its own rule about the state it must leave behind.
5. Core Concept — the home reaches into a cache
A snoop transaction is the home node acting on a cache it does not own, to satisfy some other transaction. It has a compact shape.
- Home issues the snoop. Resolving a requester's read or write, the home consults its directory, finds a holder, and sends a SNP with a snoop opcode — SnpShared, SnpUnique, SnpClean, SnpOnce — targeting that holder by NodeID.
- The snooped node acts. The holder applies the state change the opcode demands: SnpShared downgrades it to SC; SnpUnique invalidates it to I; SnpClean leaves a clean shared copy; SnpOnce leaves its state unchanged.
- The snooped node responds. It returns a SnpResp on RSP reporting the outcome — or, if it holds the line dirty (or the snoop set RetToSrc), a SnpRespData on DAT that returns the line so the dirty data is not lost (Chapter 6.5).
- The home uses the result. The home forwards data to the original requester (as CompData), writes dirty data back to memory, and updates its directory. The snoop's outcome feeds the transaction it was nested in.
The synthesis:
A snoop is the home reaching into a cache. The opcode names the state it forces — share, invalidate, or clean — and the snooped node must land in exactly that state and return the line if it was dirty. The snoop rarely stands alone: it is the step inside a read or write where the home makes another cache yield. Direction distinguishes it — every other transaction flows from a requester; the snoop flows from the home into a holder.
6. Engineering Mental Model — the librarian recalling a book
Return to the library (Chapter 7.1), but watch the librarian act on another reader.
- Reader A asks for a book that Reader B currently holds. The librarian does not wait — the librarian recalls it from B (the snoop).
- The kind of recall matters. "Share your notes" leaves B a copy but marks it read-only (SnpShared → SC). "I need it back entirely — surrender your copy" makes B give it up completely (SnpUnique → I). "Confirm your copy is clean and keep it" leaves B a pristine copy (SnpClean).
- If B had written in the margins (a dirty line), B must hand those annotated pages back, not just acknowledge the recall (SnpRespData, not SnpResp) — otherwise the edits are lost.
- The librarian then gives A the book and updates the catalog.
The crucial part is the kind of recall. Tell B to surrender the book, but B only marks it read-only and keeps it — now two readers think they have valid copies, and when A writes in it, B is reading stale pages.
7. Engineering Diagram — a snoop nested in a ReadUnique
Read top to bottom: the requester asks for unique ownership, the home snoops the holder into Invalid and collects its dirty data, then forwards the line. The snoop is the middle — home-initiated, inside the read.
8. Snoop Opcodes and the State They Force
Each snoop opcode demands a specific end state in the snooped node.
| Snoop opcode | Forces snooped node to | Returns data when |
|---|---|---|
| SnpShared | SC (downgrade; may keep a shared copy) | held dirty |
| SnpClean | SC (clean shared copy retained) | held dirty |
| SnpUnique | I (invalidate — surrender the line) | held dirty |
| SnpCleanInvalid | I (invalidate, ensure memory clean) | held dirty |
| SnpOnce | unchanged (snapshot only) | on request |
| SnpMakeInvalid | I (invalidate, no data needed) | never |
The rule to carry: the opcode is the required outcome. A Shared snoop lets the holder keep a read-only copy; a Unique snoop forces the holder to I so the requester can own the line exclusively; a Clean snoop keeps the holder clean but shared. Whether data comes back depends on dirtiness, not on the opcode alone — a dirty line must be returned so its data survives (Chapter 6.5).
9. With Data or Without — the two snoop responses
A snoop always produces a response; whether it carries data is the variable.
- SnpResp — no data. The snooped node applies the state change and reports the outcome on RSP. Used when the line was clean (memory already has the data) or the copy was already Invalid. The home learns the new state; no data moves.
- SnpRespData — the line comes back. When the snooped node held the line dirty (UD / SD), or the snoop requested the data (RetToSrc), it returns the line on DAT as SnpRespData. This is how dirty data reaches the requester and memory without being lost.
- The home's job either way. The home records the snooped node's new state in its directory, forwards any data to the requester, and writes dirty data back to memory as needed.
The point to carry:
Two things come out of a snoop: an outcome (always) and, conditionally, data. The outcome updates the directory; the data preserves a dirty line. Confuse them — return the outcome but not the dirty data — and you lose the only up-to-date copy (Chapter 6.5). Land in the wrong state — downgrade where the opcode said invalidate — and you leave two caches disagreeing. A correct snoop gets both the state and the data right.
10. Transaction Walkthrough — a SnpUnique inside a ReadUnique
RN0 issues a ReadUnique (it intends to write) on a line homed at HN; RN1 holds it in UD (unique, dirty).
- REQ. RN0 sends ReadUnique to HN, allocating TxnID 11. It wants the line exclusively.
- Directory lookup. HN sees RN1 holds the line in UD. For RN0 to own it uniquely, every other copy must go — HN must invalidate RN1.
- SNP. HN sends SnpUnique to RN1. RN1 must land in I.
- SnpRespData. RN1 was dirty, so it returns the line as SnpRespData and transitions UD → I — surrendering the copy entirely.
- CompData. HN forwards CompData to RN0 (which installs the line in UD, ready to write), and writes the dirty data back / updates memory as the home requires. Its directory now shows RN0 unique, RN1 invalid.
- CompAck. RN0 closes with CompAck.
The essential move is step 4: RN1 goes to I, not to SC. RN0 now holds the line uniquely, and no stale copy remains anywhere. Downgrade RN1 to SC instead and the next section's bug appears.
11. RTL / Hardware View — a snooped-node snoop handler
The snooped node's core decision is combinational: given the snoop opcode and its current cache state, compute the next state and whether it must return data. Representative.
// Representative snooped-node snoop handler (educational).
// Given the snoop opcode and current cache state, decide the NEXT state and
// whether the line must be returned. SnpUnique -> INVALIDATE (INV); SnpShared /
// SnpClean -> downgrade to SC. A dirty line (UD/SD) is returned so data survives.
module chi_snoop_handler (
input logic [1:0] snp_type, // SNP_SHARED, SNP_UNIQUE, SNP_CLEAN
input logic [2:0] cur_state, // INV, UC, UD, SC, SD
output logic [2:0] next_state,
output logic send_data, // return the line (SnpRespData) vs SnpResp
output logic resp_is_data // response rides DAT (data) not just RSP
);
localparam logic [1:0] SNP_SHARED = 2'd0, SNP_UNIQUE = 2'd1, SNP_CLEAN = 2'd2;
localparam logic [2:0] INV = 3'd0, UC = 3'd1, UD = 3'd2, SC = 3'd3, SD = 3'd4;
logic dirty, held;
assign dirty = (cur_state == UD) || (cur_state == SD);
assign held = (cur_state != INV);
always_comb begin
// Not holding the line: nothing to give up, no data.
if (!held) begin
next_state = INV;
send_data = 1'b0;
end else begin
// SnpUnique must INVALIDATE; other snoops downgrade to Shared Clean.
next_state = (snp_type == SNP_UNIQUE) ? INV : SC;
// A dirty line is returned so its data is not lost.
send_data = dirty;
end
end
assign resp_is_data = send_data; // data -> SnpRespData on DAT, else SnpResp on RSP
endmoduleThe same behavior in Verilog-2001:
// Representative snooped-node snoop handler (Verilog-2001).
module chi_snoop_handler (
input [1:0] snp_type,
input [2:0] cur_state,
output reg [2:0] next_state,
output reg send_data,
output resp_is_data
);
localparam SNP_SHARED = 2'd0, SNP_UNIQUE = 2'd1, SNP_CLEAN = 2'd2;
localparam INV = 3'd0, UC = 3'd1, UD = 3'd2, SC = 3'd3, SD = 3'd4;
wire dirty = (cur_state == UD) || (cur_state == SD);
wire held = (cur_state != INV);
always @* begin
if (!held) begin
next_state = INV;
send_data = 1'b0;
end else begin
next_state = (snp_type == SNP_UNIQUE) ? INV : SC;
send_data = dirty;
end
end
assign resp_is_data = send_data;
endmoduleAnd in VHDL:
-- Representative snooped-node snoop handler (VHDL).
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity chi_snoop_handler is
port (
snp_type : in std_logic_vector(1 downto 0);
cur_state : in std_logic_vector(2 downto 0);
next_state : out std_logic_vector(2 downto 0);
send_data : out std_logic;
resp_is_data : out std_logic
);
end entity;
architecture rtl of chi_snoop_handler is
constant SNP_UNIQUE : std_logic_vector(1 downto 0) := "01";
constant INV : std_logic_vector(2 downto 0) := "000";
constant UD : std_logic_vector(2 downto 0) := "010";
constant SC : std_logic_vector(2 downto 0) := "011";
constant SD : std_logic_vector(2 downto 0) := "100";
signal dirty, held : boolean;
signal sd_i : std_logic;
begin
dirty <= (cur_state = UD) or (cur_state = SD);
held <= (cur_state /= INV);
next_state <= INV when not held else
INV when snp_type = SNP_UNIQUE else
SC;
sd_i <= '1' when (held and dirty) else '0';
send_data <= sd_i;
resp_is_data <= sd_i;
end architecture;All three send a SnpUnique's holder to INV and every other snoop's holder to SC, returning data only when the line was dirty. The DebugLab shows what a single wrong next-state does.
12. Verification View — SnpUnique invalidates, dirty returns data
The two properties that keep a snoop correct: a Unique snoop must invalidate a held copy, and a dirty line must be returned.
// Bind to chi_snoop_handler.
// 1. A SnpUnique on a held line must leave it INVALID — never merely downgraded.
property p_unique_invalidates;
@(*) (snp_type == 2'd1 /*SNP_UNIQUE*/ && cur_state != 3'd0 /*INV*/)
|-> (next_state == 3'd0 /*INV*/);
endproperty
// 2. A dirty held line (UD or SD) must return its data on the snoop.
property p_dirty_returns_data;
@(*) ((cur_state == 3'd2 /*UD*/) || (cur_state == 3'd4 /*SD*/)) |-> send_data;
endpropertyThe system point, beyond the checks:
A snoop carries an obligation into someone else's cache, and the obligation has two parts. The state part: land in exactly the state the opcode demands — a SnpUnique that leaves the holder in SC has not done its job, because the requester was promised unique ownership and a sharer still exists. The data part: if the copy was dirty, return it, or the newest value dies with the invalidation. Coherence is the invariant that at most one writer and no stale readers exist for a line; a snoop is precisely the operation that re-establishes that invariant, so a snoop that lands in the wrong state breaks the very thing it was sent to enforce.
- What it proves: SnpUnique invalidates a held copy, and dirty lines return their data.
- What it does not prove: the home's directory update or forwarding — those are the home's concern.
- Bug signature: a SnpUnique leaving the holder in SC — a stale sharer beside a supposed unique owner.
13. Testbench — the snoop outcomes
Checks the forced state and the data decision across the key cases.
module tb_chi_snoop_handler;
logic [1:0] snp_type; logic [2:0] cur_state, next_state;
logic send_data, resp_is_data;
int errors = 0;
localparam SNP_SHARED = 2'd0, SNP_UNIQUE = 2'd1, SNP_CLEAN = 2'd2;
localparam INV = 3'd0, UC = 3'd1, UD = 3'd2, SC = 3'd3, SD = 3'd4;
chi_snoop_handler dut (.*);
task automatic check(input logic [1:0] sn, input logic [2:0] cs,
input logic [2:0] exp_ns, input logic exp_sd, input string name);
snp_type = sn; cur_state = cs; #1;
if (next_state !== exp_ns || send_data !== exp_sd) begin
errors++; $display("FAIL %s: next=%0d data=%0b", name, next_state, send_data);
end else $display("PASS %s: next=%0d data=%0b", name, next_state, send_data);
endtask
initial begin
check(SNP_UNIQUE, UD, INV, 1'b1, "SnpUnique on UD -> INV, data");
check(SNP_UNIQUE, SC, INV, 1'b0, "SnpUnique on SC -> INV, no data");
check(SNP_SHARED, UD, SC, 1'b1, "SnpShared on UD -> SC, data");
check(SNP_SHARED, SC, SC, 1'b0, "SnpShared on SC -> SC, no data");
check(SNP_CLEAN, SD, SC, 1'b1, "SnpClean on SD -> SC, data");
check(SNP_UNIQUE, INV, INV, 1'b0, "SnpUnique on INV -> INV, no data");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS SnpUnique on UD -> INV, data
PASS SnpUnique on SC -> INV, no data
PASS SnpShared on UD -> SC, data
PASS SnpShared on SC -> SC, no data
PASS SnpClean on SD -> SC, data
PASS SnpUnique on INV -> INV, no data
ALL TESTS PASSED14. DebugLab — a SnpUnique that downgrades instead of invalidating
A SnpUnique that downgrades instead of invalidating
SNPUNIQUE DOWNGRADES TO SC INSTEAD OF INVALIDATING -> STALE SHARERA core reads stale data — a value another core has already overwritten — with no error and no bus anomaly. It only happens after a ReadUnique or WriteUnique to a line that core was sharing. Data content on the wire always looks correct.
After a SnpUnique, the snooped node is still valid:
before: RN0 = I, RN1 = UD (holds line, dirty)
RN0 issues ReadUnique -> HN sends SnpUnique to RN1
RN1 returns SnpRespData (dirty data OK) but sets state = SC <-- wrong
after: RN0 = UD (unique owner), RN1 = SC (still valid!)
RN0 writes X -> memory/RN0 have X, RN1 still reads old valueThe dirty data was returned correctly — but RN1 kept a readable copy it should have surrendered, so two caches now disagree.
The snoop handler mapped SnpUnique to a downgrade to SC, the same next-state it uses for SnpShared, rather than to INV. From that point the home believed RN0 was the unique owner while RN1 still held a valid line.
The snoop opcode names a required end state, and SnpUnique's is Invalid. Unique ownership means no other copy exists; the whole point of the snoop was to clear RN1 out so RN0 could own the line exclusively and write it. Downgrading to SC preserves a sharer, violating the single-writer invariant. Returning the dirty data was necessary but not sufficient — the state was wrong, and coherence lives in the state.
Map SnpUnique (and SnpCleanInvalid, SnpMakeInvalid) to INV — the snooped node surrenders the line entirely. The handler's (snp_type == SNP_UNIQUE) ? INV : SC encodes exactly this split: invalidate for Unique, downgrade for Shared/Clean. Get the data and the state right; a snoop that returns data but lands in the wrong state still breaks coherence.
15. Common Mistakes
- SnpUnique that downgrades. Assumption: any state loss suffices. Bug: stale sharer (the DebugLab). Prevention: SnpUnique → INV.
- Dropping dirty data on a snoop. Assumption: the response alone is enough. Bug: lost dirty data (Chapter 6.5). Prevention: return SnpRespData when dirty.
- Treating the snoop as requester-initiated. Assumption: caches snoop each other. Bug: mis-modeled flow. Prevention: the home initiates snoops.
- Ignoring the nesting. Assumption: a snoop stands alone. Bug: forgetting to forward CompData to the real requester. Prevention: the snoop feeds the read/write it is inside.
- Returning data on a clean snoop needlessly. Assumption: always send data. Bug: wasted bandwidth. Prevention: SnpResp (no data) when the line is clean.
- Forgetting the directory update. Assumption: the snoop ends at the holder. Bug: stale directory. Prevention: the home records the snooped node's new state.
16. Engineering Checklist
- Recognize the snoop as home-initiated, targeting a specific holder.
- Apply the exact end state the opcode demands — SnpUnique → INV, SnpShared/Clean → SC.
- Return the line as SnpRespData when it was dirty (or RetToSrc); else SnpResp.
- Feed the result into the nesting read/write — forward data, write back dirty.
- Update the home directory with the snooped node's new state.
- Never leave a stale sharer after a Unique snoop.
17. Key Takeaways
- The snoop is the one transaction the home initiates, reaching into a cache to fetch, invalidate, or downgrade a line.
- It nests inside reads and writes — the step where the home resolves against a holder.
- The opcode forces a state: SnpShared → SC, SnpUnique → I, SnpClean → clean-shared.
- The response is SnpResp (no data) or SnpRespData (returns the line, when dirty or RetToSrc).
- A SnpUnique must invalidate — downgrading leaves a stale sharer and breaks coherence.
- Get both the state and the data right; the model here is representative.
18. Quick Revision
The snoop transaction. The snoop is home-initiated — the home reaches into a cache on another transaction's behalf, so it nests inside the reads and writes of 7.1–7.2. The home sends a SNP opcode that forces a specific end state in the snooped node: SnpShared and SnpClean downgrade it to SC; SnpUnique (and SnpCleanInvalid, SnpMakeInvalid) invalidate it to I; SnpOnce leaves it unchanged. The node answers with SnpResp (RSP, no data) when the line was clean, or SnpRespData (DAT, returning the line) when it was dirty or the snoop set RetToSrc — so dirty data is never lost. Two outputs matter: the state (updates the directory and preserves coherence) and the data (preserves the dirty value). The rule that bites: SnpUnique must land in I — downgrade it to SC and a stale sharer survives beside the new unique owner, a silent coherence violation. Get the state and the data both right. Representative model; 7.5 covers the transaction lifecycle.
Coming Next
Chapter 7.5 — Transaction Lifecycle. You have now traced all four transaction kinds — read, write, atomic, snoop — as flows of packets. Chapter 7.5 lifts to the transaction as an object with a lifetime: dispatch, in-flight, response, retirement. It shows what the requester and home each track for every outstanding transaction, when a transaction is truly done, and how the tracker that owns it is allocated and freed — the bookkeeping that keeps many transactions in flight at once without confusing one for another.