A trace shows the past; a hook lets you inspect and change the present. When C1 has waited for its grant for a million cycles, you want to stop H, read its pending entries and the caches' states, and decide which message is missing. Every hook below has to be designed in, because none of it can be added once the chip exists.
Hook | What it does | What it costs or risks |
|---|---|---|
Freeze and dump pending entries | Stop H accepting new work, then read every pending entry: requester, request, acks needed and received, data valid, queued next request. | Read paths into the pending table; a freeze must stop admission without dropping in-flight responses. |
Single-step the home | Process one message per debug command, logging each decision. | A gate on H's scheduler; slow, so only for small reproductions. |
Read directory entries through a debug port | Return the directory record for a given line: U, S{C0,C1}, exclusive(C0). | A read mux into the directory array, arbitrated with normal lookups. |
Read cache line state | Return a cache's state for a line, including MSHR and writeback entries. | Same at each cache; must include transient states, not just the array. |
Force a line state | Write a directory or cache state directly. | Can violate single-writer, multiple-reader at once; allowed only on a frozen, quiescent system, and only for experiments. |
Disable optimisations (chicken bits) | Turn off a feature such as silent upgrades, forwarding or a fast path. | A configuration bit per feature, and a verified fallback path. |
Inject delays | Hold one message class, optionally for one address range, for a set number of cycles. | A small counter per channel; must only delay, never drop or reorder within the channel. |
Chicken bits are the bridge from debug to workaround: if disabling forwarding makes a failure disappear, the bit both localises the bug and may ship as the fix. That use is the subject of the next topic, Post-Silicon Regression and Guardrails.
Use the write miss from S{C0} in Write Miss and Ownership, labelled A. A test hangs: C1 never completes its store. A pending-entry timeout at H fires and freezes H. The dump reads (illustrative values):
Source | Value read | What it shows |
|---|---|---|
H pending entry for X | S{C0}, pend GetM C1, acks 0/1 | H sent Inv for A and is still waiting for C0's ack. |
C1 line state for X | IM^D / -, MSHR for A | C1 is waiting for Data, as expected. |
C0 line state for X | I / - | C0 has processed the Inv: its copy is gone. |
C0 response output queue | 1 entry: Inv-Ack(X), A, waiting for a credit | The ack exists but cannot leave C0. |
Response channel credits, C0 to H | 0 of 2 | The credits were never returned. |
No message reached H after the freeze, so H's entry is unchanged in the dump. The diagnosis is not a protocol error: C0 did the right thing, and the ack is stuck behind a credit-return bug on the response channel. Without the queue and credit dump, the evidence would have been 'C0 is I, H waits for an ack', which looks like a lost message. The hang triage lesson turns this into a general method.
A delay injector holds a message on one channel until a counter reaches a programmed value, for addresses that match a mask. Because the course model assumes no ordering between message classes, delaying one class only produces orders the protocol must already handle. On a real design this holds only for message classes the protocol specification does not order relative to each other; if the spec orders a group of channels, delay the whole group together, and keep the delay below any protocol timeout. Within those limits, any failure it exposes is a real bug, not an artefact. The fragment below is a complete, lint-clean module; settings should be changed only while the channel is idle.
// Debug delay injector for one message class on a valid/ready channel.
// When enabled and the line address matches, each message is held for
// dbg_delay cycles before it is offered downstream. Disabled: pass-through.
module delay_inject #(
parameter int AW = 42,
parameter int CW = 8
) (
input logic clk,
input logic rst_n,
input logic dbg_en,
input logic [AW-1:0] dbg_addr_match,
input logic [AW-1:0] dbg_addr_mask,
input logic [CW-1:0] dbg_delay,
input logic in_valid,
input logic [AW-1:0] in_addr,
output logic in_ready,
output logic out_valid,
output logic [AW-1:0] out_addr,
input logic out_ready
);
logic [CW-1:0] count;
logic hit;
assign hit = dbg_en && ((in_addr & dbg_addr_mask) == (dbg_addr_match & dbg_addr_mask));
assign out_addr = in_addr;
assign out_valid = in_valid && (!hit || count == dbg_delay);
assign in_ready = out_ready && (!hit || count == dbg_delay);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0;
else if (out_valid && out_ready) count <= '0;
else if (in_valid && hit && count != dbg_delay) count <= count + 1'b1;
end
endmoduleThe delay is bounded, so it cannot by itself cause a hang, and it stalls the channel rather than dropping or reordering within it. It does hold up later messages on the same channel, including ones to other lines: that is acceptable in debug, but it is why the address mask matters.
Area and timing: each hook is a mux or a counter, small alone but present on every controller. Read paths into the directory and pending tables compete with normal lookups and must not lengthen the critical path.
Verification: hooks are logic too. A freeze that drops a response, or a force that is reachable in normal operation, is a bug in its own right. Hooks need their own tests, and a check that they are inert when disabled.
Security: a dump of pending entries, data buffers or cache lines can contain another process's or another security domain's data. Debug access must be authenticated and gated by the system's debug authorisation, and should return metadata (states, ids, counts) by default; data contents only where the debug policy allows it. Force and delay hooks must be unreachable without that authorisation, since they can break isolation or create denial of service. Addresses and timing in traces are also sensitive, because they can reveal secret-dependent access patterns. In production, the device lifecycle state or fuses disable or restrict every hook.
Forcing M creates write permission that H did not grant. If another cache still holds S, or a grant is in flight, two caches can now disagree about X, which violates single-writer, multiple-reader. On a quiescent system you can make the forced state consistent with the directory and the other caches; on a running one you are injecting a coherence bug, and any failure you then see is your own.