Skip to content

AMBA CHI · Module 16 · CHI RTL Design Thinking

Snoop Processing

The home launches snoops; this chapter is the RN-F pipeline that processes them, and its crux is that a snoop hitting a dirty line must return the data, not just an acknowledgment. The subtle stage is forming the response, whose type depends on whether the line is dirty. A clean line yields a state-only acknowledgment; a dirty line, newer than memory, being invalidated or downgraded means this node holds the only up-to-date copy, so its response must carry the data before the copy is gone. The failure to avoid is invalidating the dirty line but sending a data-less acknowledgment, so the only copy is lost and memory keeps the stale value. Representative model, not the specification.

Advanced16 min readAMBA CHISnoopRN-FSnpRespDataDirty

Module 16 · Chapter 16.2 · CHI RTL Design Thinking

Project thread — 16.1 was the HN request pipeline. 16.2 is the RN-F snoop pipeline; 16.3 is directory management.

1. Learning Outcomes

By the end of this chapter you should be able to:

  • Name the stages of an RN-F's snoop pipeline — accept, lookup, form response, update state.
  • Explain that the response type depends on whether the line is dirty.
  • State that a dirty line being invalidated/downgraded must return data (SnpRespData).
  • Describe the line's state transition under each snoop type.
  • Diagnose the lost dirty data from a snoop response that omits the data.
  • Implement a representative snoop-response model in SystemVerilog, Verilog-2001, and VHDL.

2. Why Should I Learn This?

The home decides coherence, but the caches hold the data — so when the home snoops, the RN-F's pipeline is what actually surrenders or shares a line. Getting its stages right (accept, look up, respond, update) is the mechanics; getting the response content right is the correctness. A snoop response is not just "yes, I invalidated it" — for a dirty line, it must also carry the data, because that cache holds the only up-to-date copy.

This is where a plausible-looking pipeline silently loses data. A snoop FSM that correctly updates the cache state — invalidating the dirty line as the snoop demands — but sends a plain acknowledgment with no data leaves the home holding a receipt for data it never received. The only current copy is gone (invalidated), and memory has the stale value — silent corruption. The response type must be a function of the line's dirty status: dirty-and-removed means data-bearing. This chapter is the snoop pipeline and the one decision — attach the data — that keeps a dirty snoop from destroying the newest value.

3. Key Terms

4. Previous Chapter Connection

This chapter is the snooped side of the coherence action the home launches in Chapter 16.1. It implements the snoop types of Chapter 9.2 and the snoop responses of Chapter 9.3 — in the RN-F's RTL. The pass-dirty idea (Chapter 9.3) and the dirty-data-transfer of Chapter 9.5 are exactly what the response-formation stage must honor.

It also connects to Chapter 13.6 from the opposite side. There, the bug was a recipient installing forwarded dirty data as clean — losing the dirty status on the receive side. Here, the bug is the snooped node failing to emit the dirty data at all — losing it on the send side. Both destroy the only up-to-date copy, but at different points in the transfer. Together they bracket the dirty-data path: the sender must attach the data (16.2), and the receiver must honor its dirty status (13.6). This chapter is the sender's obligation, in RTL.

5. Core Concept — a dirty snoop must return the data

The RN-F's snoop pipeline accepts, looks up, forms a response, and updates state — and the response is data-bearing whenever the snooped line is dirty and removed/downgraded.

  • The stages. Accept the snoop (SNP credit) → cache lookup for the line → determine its stateform the response (type + data?) → update the cache state per the snoop type → send.
  • Clean line → acknowledgment. If the line is clean (matches memory), the response carries only the resulting state (SnpResp) — the home/memory already has the data.
  • Dirty line → data. If the line is dirty (UD/SD — newer than memory) and the snoop invalidates or downgrades it, this cache holds the only current copy, so the response must carry the data (SnpRespData).
  • Type follows dirtiness. The response-formation stage must select the type from the line's dirty status, not send a fixed SnpResp regardless.

The synthesis:

An RN-F processes a snoop by accepting it, looking up the line, forming a response, and updating the cache state. The response type depends on the line's dirty status: a clean line yields a plain SnpResp; a dirty line being invalidated/downgraded yields a SnpRespData that carries the data, because this cache holds the only up-to-date copy. Sending a plain SnpResp for a dirty line destroys the newest value.

6. Engineering Mental Model — returning a borrowed, annotated book

Think of being asked to return a book you borrowed from a library (the home).

  • If you never wrote in it (a clean line), you just hand it back — or even say "I have the same edition, take yours." The library already has the content. A simple acknowledgment suffices.
  • But if you annotated it heavily (a dirty line) — your notes are the only copy of that thinking, newer than anything the library has — then returning the book means physically handing over the annotated pages, not just saying "yes, I'll stop using it."
  • If you tell the librarian "done with it" (SnpResp) and then shred your annotated copy (invalidate) without giving them the pages (no data), your annotations are gone forever. The library keeps the un-annotated original (stale memory), unaware anything was lost.

The annotated pages are the dirty data; handing them over is SnpRespData. Saying "done" while shredding the only annotated copy is the bug — the response must carry the pages when the copy is the only current one.

7. Engineering Diagram — the snoop pipeline

The RN-F's snoop-processing pipeline. An incoming snoop is accepted against a credit, the line looked up in the cache, its state determined, and a response formed whose type depends on whether the line is dirty: a clean line yields a state-only SnpResp, a dirty line being removed yields a data-bearing SnpRespData. The cache state is then updated per the snoop type and the response sent.Accept snoopSNP creditCache lookupstate + dirty?Form responsedirty → attach dataUpdate stateper snoop typeSendSnpResp /SnpRespDatalookupstate, dirtytype chosenrespond12
Figure 1 — the RN-F's snoop-processing pipeline. An incoming snoop is accepted against a credit, the line is looked up in the cache, its state determined, and a response formed whose type depends on whether the line is dirty: a clean line yields a state-only SnpResp, a dirty line being removed yields a data-bearing SnpRespData. The cache state is then updated per the snoop type and the response sent.

The form response stage is the critical one: it reads the line's dirty status and attaches the data when the line is dirty and being removed. The rest is mechanical. The DebugLab lets the pipeline update state correctly but form the wrong (data-less) response.

8. Engineering Diagram — the snooped line's state transitions

The snooped line's state transitions and the response each produces. A unique-dirty line hit by an invalidating snoop returns its data and goes invalid; a shared-dirty line hit by a shared snoop returns its data and becomes shared-clean; a shared-clean line hit by a shared snoop returns a state-only response and stays shared-clean. Data is returned exactly when the line was dirty.UD(dirty)SD(dirty)SC(clean)ISnpUnique → SnpRespData, ISnpUnique → SnpRespData, ISnpUnique →SnpRespData,…SnpShared → SnpRespData, SCSnpShared → SnpRespData, SCSnpShared →SnpRespData,…SnpShared → SnpResp (no data)SnpShared → SnpResp (no data)SnpShared →SnpResp (no…
Figure 2 — the snooped line's state transitions and the response each produces. A unique-dirty line hit by an invalidating snoop returns its data and goes invalid; a shared-dirty line hit by a shared snoop returns its data and becomes shared-clean; a shared-clean line hit by a shared snoop returns a state-only response and stays shared-clean. Data is returned exactly when the line was dirty.

Data is returned on exactly the dirty transitions — UD and SD give SnpRespData; the clean SC gives a plain SnpResp. The response type is a function of the pre-snoop dirty status. Sending SnpResp on the UD/SD transitions is the bug.

9. Why the Response Must Carry the Data

The reasoning behind data-bearing responses.

  • A dirty line is the only current copy. UD/SD means this cache's value is newer than memory — no one else, including memory, has the up-to-date data (Chapter 10.5).
  • An invalidating snoop removes it. A snoop that invalidates (or downgrades away the dirty state) destroys this copy — after it, the cache no longer holds the dirty value.
  • So the data must go with the response. The only way the up-to-date value survives is if the response carries it to the home before (or as) the copy is removed — SnpRespData.
  • A data-less response loses it. SnpResp acknowledges the state change but delivers no data, so the home invalidates the dirty copy and captures nothing — the value is gone, memory is stale.

The point to carry:

The snoop-response type is where state and data must be kept consistent, and the pipeline makes it easy to get one right while getting the other wrong. Updating the cache state (invalidate the line) and forming the response (attach data?) are separate RTL stages, so a design can flawlessly transition UD → I in the state logic while the response logic emits a fixed SnpResp — two stages that individually look correct but together lose data. The invariant that couples them is: if the state transition removes the last dirty copy, the response must carry that copy's data. This is a conservation law — the dirty value cannot simply vanish; it must be moved (into the response) before it is destroyed (by the state update). The bug is a failure to conserve: the destroy half runs, the move half does not. Correct response formation must therefore read the same dirty status the state update reads, and derive the response type from it — the two stages must agree, because they are two halves of one atomic hand-off of the dirty value.

10. Processing a Snoop — dirty vs clean

Two snoops to the same RN-F, one hitting a dirty line, one clean.

  1. SnpUnique to a UD line. The RN-F looks up the line: UD — dirty, the only current copy. The snoop invalidates it.
  2. Form SnpRespData. Because the line is dirty and being invalidated, the response is SnpRespData — carrying the data. The home captures the newest value.
  3. Update to I. The cache transitions UD → I. The dirty copy is gone from the cache, but its data was delivered in the response. Nothing lost.
  4. SnpShared to an SC line. A different line is looked up: SC — shared-clean, matches memory. The snoop keeps it shared.
  5. Form SnpResp (no data). Because the line is clean, a plain SnpResp suffices — the home/memory already has the data. The line stays SC.

The dirty snoop returned data; the clean snoop returned only state — each correct for its line. The DebugLab sends the step-5 (data-less) response for the step-1 (dirty) case, losing the value.

11. RTL / Hardware View — snoop-response formation

The response is data-bearing when the snooped line is dirty and removed/downgraded; the new state follows the snoop type. Representative.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative snoop-response formation (educational).
// The response type is a function of the line's DIRTY status: a dirty line being
// invalidated or downgraded must return its data (SnpRespData) because this cache holds
// the ONLY up-to-date copy. A clean line returns a state-only SnpResp. Sending SnpResp
// for a dirty removed line LOSES the newest value.
typedef enum logic [1:0] { ST_I, ST_SC, ST_SD, ST_UD } state_e;
 
module chi_snp_response (
  input  state_e cur_state,        // the snooped line's current state
  input  logic   snp_invalidates,  // snoop removes the line (e.g. SnpUnique)
  input  logic   snp_downgrades,   // snoop downgrades dirty->clean (e.g. SnpShared on SD)
  output logic   resp_is_data,     // SnpRespData (carry data) vs SnpResp
  output state_e new_state
);
  logic line_dirty;
  assign line_dirty = (cur_state == ST_UD) || (cur_state == ST_SD);
 
  always_comb begin
    // Data MUST be returned when a dirty line is removed or downgraded away from dirty.
    resp_is_data = line_dirty && (snp_invalidates || snp_downgrades);
 
    // Resulting state per the snoop action.
    if      (snp_invalidates) new_state = ST_I;                        // removed
    else if (snp_downgrades)  new_state = ST_SC;                       // dirty -> clean-shared
    else                      new_state = (cur_state == ST_UD) ? ST_SD : cur_state;
  end
endmodule

The same behavior in Verilog-2001:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative snoop-response formation (Verilog-2001).
module chi_snp_response (
  input  [1:0] cur_state,          // 0=I 1=SC 2=SD 3=UD
  input        snp_invalidates, snp_downgrades,
  output       resp_is_data,
  output reg [1:0] new_state
);
  localparam ST_I=2'd0, ST_SC=2'd1, ST_SD=2'd2, ST_UD=2'd3;
  wire line_dirty = (cur_state == ST_UD) || (cur_state == ST_SD);
  assign resp_is_data = line_dirty & (snp_invalidates | snp_downgrades);
  always @* begin
    if      (snp_invalidates) new_state = ST_I;
    else if (snp_downgrades)  new_state = ST_SC;
    else                      new_state = (cur_state == ST_UD) ? ST_SD : cur_state;
  end
endmodule

And in VHDL:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Representative snoop-response formation (VHDL).
library ieee;
use ieee.std_logic_1164.all;
 
entity chi_snp_response is
  port (
    cur_state       : in  std_logic_vector(1 downto 0);  -- 00=I 01=SC 10=SD 11=UD
    snp_invalidates : in  std_logic;
    snp_downgrades  : in  std_logic;
    resp_is_data    : out std_logic;
    new_state       : out std_logic_vector(1 downto 0)
  );
end entity;
 
architecture rtl of chi_snp_response is
  constant ST_I  : std_logic_vector(1 downto 0) := "00";
  constant ST_SC : std_logic_vector(1 downto 0) := "01";
  constant ST_SD : std_logic_vector(1 downto 0) := "10";
  constant ST_UD : std_logic_vector(1 downto 0) := "11";
  signal line_dirty : std_logic;
begin
  line_dirty   <= '1' when (cur_state = ST_UD or cur_state = ST_SD) else '0';
  resp_is_data <= line_dirty and (snp_invalidates or snp_downgrades);
 
  process (cur_state, snp_invalidates, snp_downgrades)
  begin
    if snp_invalidates = '1' then
      new_state <= ST_I;
    elsif snp_downgrades = '1' then
      new_state <= ST_SC;
    elsif cur_state = ST_UD then
      new_state <= ST_SD;
    else
      new_state <= cur_state;
    end if;
  end process;
end architecture;

All three set resp_is_data from the line's dirty status ANDed with a removing/downgrading snoop — so a dirty line always returns its data. The DebugLab ties resp_is_data to 0 (always SnpResp) regardless of line_dirty.

12. Verification View — a dirty removed line returns data

The properties couple the response to dirtiness: dirty-and-removed implies data.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind to chi_snp_response.
// 1. A dirty line that is invalidated or downgraded MUST return data.
property p_dirty_returns_data;
  @(*) ((cur_state == ST_UD || cur_state == ST_SD) && (snp_invalidates || snp_downgrades))
       |-> resp_is_data;
endproperty
 
// 2. A clean line never needs to return data on a snoop.
property p_clean_no_data;
  @(*) ((cur_state == ST_SC || cur_state == ST_I)) |-> !resp_is_data;
endproperty
 
// 3. An invalidating snoop always ends in I (the copy is removed).
property p_invalidate_goes_I;
  @(*) snp_invalidates |-> (new_state == ST_I);
endproperty

The system point, beyond the checks:

p_dirty_returns_data is a data-conservation property, and it is the kind of property that a directed happy-path test will usually pass by accident and a coverage gap will hide. Most snoops in a typical stream hit clean lines (shared read data), where a data-less response is exactly right — so a pipeline that always sends data-less responses works for the overwhelming majority of snoops and fails only on the rare dirty-invalidating case. That rarity is precisely what makes the bug dangerous: it survives directed testing, ships, and manifests as an occasional silently-stale location under write-sharing. The verification lesson is that the property must be checked on the dirty transitions specifically, with coverage that forces a dirty line to be snooped-invalidated — you cannot rely on random traffic to hit it often enough. This generalizes across the whole module: the RTL bugs that matter are the ones on the low-probability correctness edges (a dirty snoop, a same-line race, a full tracker), and they must be targeted by verification, not left to chance, because the happy path masks them.

  • What it proves: a dirty removed/downgraded line returns data; a clean line does not.
  • What it does not prove: the data payload is the correct bytes — that is the cache datapath.
  • Bug signature: resp_is_data low while the line is dirty and being removed.

13. Testbench — a dirty invalidating snoop must return data

Snoops a dirty and a clean line and checks data is returned exactly for the dirty one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_chi_snp_response;
  logic [1:0] cur_state;
  logic snp_invalidates, snp_downgrades;
  logic resp_is_data;
  logic [1:0] new_state;
  localparam ST_I=2'd0, ST_SC=2'd1, ST_SD=2'd2, ST_UD=2'd3;
  int errors = 0;
 
  chi_snp_response dut (.*);
 
  task check(input [1:0] st, input logic inv, dg, input logic exp_data, input string nm);
    begin
      cur_state = st; snp_invalidates = inv; snp_downgrades = dg; #1;
      if (resp_is_data !== exp_data) begin errors++; $display("FAIL %s: resp_is_data=%0b exp=%0b", nm, resp_is_data, exp_data); end
      else $display("PASS %s: resp_is_data=%0b new_state=%0d", nm, resp_is_data, new_state);
    end
  endtask
 
  initial begin
    // UD line invalidated -> MUST return data, go I.
    check(ST_UD, 1, 0, 1'b1, "UD SnpUnique -> data + I");
    if (new_state !== ST_I) begin errors++; $display("FAIL UD did not go I"); end
    // SD line downgraded -> MUST return data, go SC.
    check(ST_SD, 0, 1, 1'b1, "SD SnpShared -> data + SC");
    // SC line snooped shared -> NO data.
    check(ST_SC, 0, 0, 1'b0, "SC SnpShared -> no data");
    // The critical bug case: a UD line must NOT respond data-less.
    cur_state = ST_UD; snp_invalidates = 1; snp_downgrades = 0; #1;
    if (!resp_is_data) begin errors++; $display("FAIL dirty line responded WITHOUT data -> data lost!"); end
    else $display("PASS dirty invalidating snoop carries data");
 
    if (errors == 0) $display("ALL TESTS PASSED");
    else             $display("%0d FAILURE(S)", errors);
    $finish;
  end
endmodule

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS UD SnpUnique -> data + I: resp_is_data=1 new_state=0
PASS SD SnpShared -> data + SC: resp_is_data=1 new_state=1
PASS SC SnpShared -> no data: resp_is_data=0 new_state=1
PASS dirty invalidating snoop carries data
ALL TESTS PASSED

14. DebugLab — a snoop response that omits the dirty data

1

A snoop response that omits the dirty data

SNOOP RESPONSE OMITS DIRTY DATA -> ONLY UP-TO-DATE COPY INVALIDATED AND LOST -> SILENT CORRUPTION
Symptom

Silent data corruption on write-shared lines — a core reads a line and gets a stale value after another core had modified it and was later snooped. No hang, no error. It correlates with lines that were dirty in one cache and then snooped by another core's request; clean-shared lines are unaffected.

Evidence

The dirty line was invalidated but its data never sent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RN-F holds line X = UD (dirty, only current copy, value = NEW)
home sends SnpUnique(X) on behalf of another requester
pipeline: state update UD -> I  (correct)   |   response: SnpResp, NO data  (BUG)
  -> home receives "invalidated" ack but NO data
  -> the only copy of NEW is now gone (X = I in the cache)
  -> memory still holds OLD -> requester gets OLD -> NEW lost
correct: line dirty + invalidated -> SnpRespData carrying NEW -> home captures NEW

The state machine did its job; the response formation dropped the data.

First Divergence

The response-formation stage emitted a data-less SnpResp for a dirty invalidated line, instead of a data-bearing SnpRespData. From that point the dirty value had no path to the home.

Root Cause

A dirty line is the only up-to-date copy, so a snoop that removes or downgrades it must return the data with the response; a data-less response destroys the value. The state update (invalidate) and the response formation (attach data?) are separate RTL stages, and correctness requires them to agree: if the state transition removes the last dirty copy, the response must carry that copy's data — a conservation law that the dirty value be moved into the response before it is destroyed by the state update. A fixed data-less SnpResp breaks the conservation: the destroy half runs, the move half does not. This is the send-side counterpart of Chapter 13.6's receive-side bug (installing dirty data as clean); together they bracket the dirty-data path. It hides in testing because most snoops hit clean lines, where a data-less response is correct.

Fix

Make the response type a function of the line's dirty status: emit a data-bearing SnpRespData carrying the data whenever a dirty line is invalidated or downgraded away from dirty, as the response model does — so the newest value reaches the home before the copy is removed. Read the same dirty status the state update reads; the two stages must agree.

15. Common Mistakes

  • Always sending SnpResp. Assumption: state ack is enough. Bug: dirty data lost (the DebugLab). Prevention: data-bearing when dirty.
  • Deriving response type from the wrong signal. Assumption: snoop type alone decides. Bug: dirty status ignored. Prevention: read the line's dirtiness.
  • Updating state before capturing data. Assumption: any order works. Bug: data gone before it is read. Prevention: read data as/before removing.
  • Treating downgrade as data-free. Assumption: only invalidation needs data. Bug: SD → SC loses the dirty value. Prevention: downgrade-from-dirty carries data too.
  • Testing only clean lines. Assumption: random traffic covers it. Bug: the dirty case never hit. Prevention: force dirty-invalidating snoops.
  • Confusing with the recipient side. Assumption: 13.6 covers it. Bug: send vs receive conflated. Prevention: 16.2 emits data; 13.6 honors it.

16. Engineering Checklist

  • Stage the pipeline: accept → lookupform response → update state → send.
  • Determine the line's dirty status at lookup.
  • Emit SnpRespData (carry data) when a dirty line is invalidated/downgraded.
  • Emit plain SnpResp for a clean line.
  • Ensure the state update and response formation agree on dirtiness.
  • Cover the dirty-invalidating snoop case in verification.

17. Key Takeaways

  • An RN-F snoop pipeline: accept → lookup → form response → update state → send.
  • The response type depends on the line's dirty status.
  • A dirty line invalidated/downgraded must return data (SnpRespData).
  • A clean line returns only state (SnpResp).
  • A data-less response for a dirty line destroys the only current copy.
  • State update and response formation must agree; the model here is representative.

18. Quick Revision

Snoop processing. An RN-F (fully-coherent request node) processes a snoop through an RTL pipeline: accept it (SNP credit) → cache lookup for the line → determine its stateform the responseupdate the cache state per the snoop type → send. The correctness crux is response formation, whose type depends on the line's dirty status. A clean line (matches memory) yields a plain SnpResp carrying only the resulting state — the home already has the data. A dirty line (UD/SD — newer than memory) being invalidated or downgraded means this cache holds the only up-to-date copy, so the response must be a data-bearing SnpRespData that carries the value, delivering it to the home before the copy is removed. The failure to avoid: a pipeline that updates the state correctly (UD → I) but always emits a data-less SnpResp, so on a dirty invalidating snoop the home gets an acknowledgment but no data — the only current copy is destroyed and memory keeps the stale value (silent corruption). The state-update and response-formation stages must agree on dirtiness — a conservation law: move the dirty value into the response before destroying it. This hides in testing because most snoops hit clean lines. It is the send-side counterpart of Chapter 13.6's receive-side bug. Representative model; 16.3 covers directory management.

Coming Next

Chapter 16.3 — Directory Management. The home's snoops and updates all run against the directory; managing it in RTL has its own hazard. Chapter 16.3 covers directory management — the RTL for lookup, update, and eviction, and why evicting a directory entry to make room for a new line must back-invalidate the evicted line's sharers first, or a cache silently retains a line the directory no longer tracks — a coherence hole.