This lesson has one job: make sure no error disappears between the memory and the software that must handle it. A corrected error nobody counts hides a dying SRAM until a double error arrives. An uncorrectable error that returns as ordinary data corrupts the DMA stream silently. The reporting path has three branches, one per severity, and each must end somewhere software or hardware acts on.
Correctable errors increment a saturating 8-bit counter, and a sticky per-bank status bit records which bank corrected; only when the count reaches a threshold (a NanoSoC teaching value, 255 by default) does a maintenance interrupt fire so software can retire a sick line. Uncorrectable errors on a CPU fetch trap the CPU: the interconnect answers the read with SLVERR so the fault looks like any other slave refusal, and a sticky status bit names the bank. Uncorrectable errors on DMA data cannot trap a fetcher, and NanoSoC has no way to mark one word as bad: the 32-bit stream carries only data, valid, ready and last, with no poison sideband. What happens instead is that the SBR read engine sees the SLVERR response on the AXI read, sets STATUS.ERR and raises irq_sbr. The driver must therefore treat SBR ERR as an instruction to discard the whole destination buffer, and it must recover the DMA, which stays BUSY waiting for words that will never arrive, by a reset or by a shorter retry. Where a stream does carry a poison bit, the protocol is Poison and Containment and the response codes are Error Responses. The aggregator below, a compiling teaching model, collects per-bank flags with write-1-to-clear status. Its status registers are set-wins: if a new event and a software clear land in the same cycle, the event survives, which is the behaviour the interrupts section teaches as the default. Clear-wins would lose the event.
// NanoSoC teaching values: error aggregator skeleton.
// One flag pair per bank, one combined interrupt, write-1-to-clear status.
// Correctable: sticky bank flag plus a saturating counter; the interrupt
// fires only when the counter reaches THRESH. Uncorrectable: sticky flag,
// interrupt at once. Status clears are write-1-to-clear and SET-WINS.
module nano_err_agg #(
parameter int THRESH = 255
) (
input logic clk,
input logic rst_n,
input logic [1:0] corr_i,
input logic [1:0] uncorr_i,
input logic [1:0] status_w1c,
input logic cnt_clr,
output logic [1:0] status_corr,
output logic [1:0] status_uncorr,
output logic [7:0] corr_count,
output logic irq
);
logic [8:0] sum;
assign sum = {1'b0, cnt_clr ? 8'h00 : corr_count} + corr_i[0] + corr_i[1];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
status_corr <= 2'h0;
status_uncorr <= 2'h0;
corr_count <= 8'h00;
end else begin
status_corr <= (status_corr & ~status_w1c) | corr_i;
status_uncorr <= (status_uncorr & ~status_w1c) | uncorr_i;
corr_count <= sum[8] ? 8'hFF : sum[7:0];
end
end
assign irq = (corr_count >= 8'(THRESH)) | (|status_uncorr);
endmoduleThresholds turn counts into policy, and the counter width and the threshold must be chosen together from the upset rate, not copied from another project. The method: software reviews and clears the counter once per review period; the threshold is the baseline number of corrections expected in one period times a margin, so the interrupt fires only when the bank is worse than normal; and the counter must be wide enough to reach the threshold without saturating first. The script applies it with illustrative values (a 7-day review period and a margin of 4) and shows the default 255 is only right for one band of rates: for a very quiet bank it never fires within a year, and for a busy one the 8-bit counter saturates below the threshold that policy needs. The second part reproduces the same-cycle collision of a new event and a clear under both update styles.
import math
REVIEW_DAYS, MARGIN, COUNTER_MAX = 7, 4, 255
def policy(baseline_per_day):
thresh = max(1, math.ceil(baseline_per_day * REVIEW_DAYS * MARGIN))
return thresh, thresh <= COUNTER_MAX
for baseline in (0.5, 5, 9, 10, 100):
thresh, fits = policy(baseline)
days_to_default = 255 / baseline
print(f"baseline {baseline}/day: threshold {thresh}, fits 8-bit counter: {fits}, default 255 fires after {days_to_default:.1f} days at baseline")
assert policy(0.5) == (14, True)
assert policy(9) == (252, True)
assert policy(10) == (280, False), "an 8-bit counter saturates at 255, below the policy threshold"
assert 255 / 0.5 > 365, "default threshold never fires within a year on a quiet bank"
def update(status, event, w1c, set_wins):
return (status & ~w1c) | event if set_wins else (status | event) & ~w1c
assert update(0b1, 0b1, 0b1, set_wins=True) == 0b1, "set-wins keeps the new event"
assert update(0b1, 0b1, 0b1, set_wins=False) == 0b0, "clear-wins loses it"
print("PASS: threshold = baseline x period x margin; 255 only suits baselines up to 9 per day; set-wins keeps a colliding event")Every error output of every protected memory ends at a counter, an interrupt or a fabric error response; no error signal dangles, and status updates are set-wins.
The CPU-fetch path returns SLVERR with the faulting address logged, matching the decode error table's secure rows.
The DMA-data path ends at SBR STATUS.ERR and irq_sbr, the driver discards the destination buffer on ERR, and the recovery of the stalled DMA is written down.
Inject one correctable error and confirm the counter moves with no interrupt; inject up to the threshold and confirm the maintenance interrupt; inject a correctable error in the same cycle as a status clear and confirm the flag stays set; inject an uncorrectable on a CPU fetch and confirm SLVERR plus the sticky bank flag; inject one on DMA data and confirm the SBR sees SLVERR, sets STATUS.ERR and raises irq_sbr while the DMA stays BUSY. If an injected fault vanishes, the reporting enable for that path is still at reset.
The correction path works but the counting tap does not: the corr_i line to the aggregator is tied off or unconnected. Correction without counting is the failure mode that hides a dying bank.