Skip to main content

logicaffeine_compile/codegen_sva/
scratchpad_rtl.rs

1//! scratchpad_rtl — the on-chip scratchpad subsystem in silicon.
2//!
3//! The handout claims "SECDED scratchpads" and "double-buffered: compute tile N while
4//! DMA streams tile N+1" (`CHIP_MEMORY_ANSWERS.md` §3, `MEMORY_EDGE_SPEC.md`). This
5//! module makes both real:
6//!   • a **SECDED(72,64) code** — single-error-CORRECT, double-error-DETECT — as a Rust
7//!     oracle (exhaustively tested over every single-bit and a sweep of double-bit
8//!     faults) and a generated-from-the-same-parity-structure Verilog encoder/decoder,
9//!     with the single-error-correction property **proven by our certified SAT** on a
10//!     tractable Hamming instance and verified bit-exact under iverilog on the full 64;
11//!   • a **double-buffer (ping-pong) controller** whose safety invariant — compute never
12//!     reads the bank the DMA is filling — is checkable by construction.
13//!
14//! The SECDED construction is the textbook Hamming-SEC (check bits at power-of-two
15//! codeword positions) plus one overall-parity bit for DED. Encoder and decoder are
16//! GENERATED from the same position→check-bit incidence, so they cannot diverge.
17
18/// Total codeword width (72) and data width (64) of the scratchpad ECC.
19pub const CW_BITS: usize = 72; // position 0 = overall parity; 1..=71 = Hamming(71,64)
20pub const DATA_BITS: usize = 64;
21const HAMMING_HI: usize = 71; // Hamming positions are 1..=71
22const CHECK_BITS: usize = 7; // powers of two 1,2,4,8,16,32,64 within 1..=71
23
24/// Is `pos` a power of two (a Hamming check-bit position)?
25fn is_check_pos(pos: usize) -> bool {
26    pos != 0 && (pos & (pos - 1)) == 0
27}
28
29/// The data-bit index carried at Hamming codeword position `pos` (non-power-of-two),
30/// or `None` if `pos` is a check position. Data bits fill non-power-of-two positions
31/// 1..=71 in order.
32fn data_index_at(pos: usize) -> Option<usize> {
33    if pos == 0 || is_check_pos(pos) {
34        return None;
35    }
36    let mut di = 0;
37    for p in 1..=HAMMING_HI {
38        if !is_check_pos(p) {
39            if p == pos {
40                return Some(di);
41            }
42            di += 1;
43        }
44    }
45    None
46}
47
48/// The decode verdict.
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum Decode {
51    /// Syndrome zero, parity even — clean.
52    NoError,
53    /// A single bit error at Hamming position `pos` (1..=71) — corrected.
54    Corrected(usize),
55    /// The overall-parity bit itself flipped (syndrome zero, parity odd) — corrected.
56    CorrectedParityBit,
57    /// Uncorrectable, DETECTED: a non-zero syndrome with even parity (two errors), or odd parity with a syndrome
58    /// naming no codeword position (an odd number of errors past one).
59    DoubleError,
60}
61
62/// SECDED encode: 64 data bits → a 72-bit codeword (`bit 0` = overall parity,
63/// bits 1..=71 = Hamming(71,64)).
64pub fn encode(data: u64) -> u128 {
65    let mut cw = [false; CW_BITS];
66    // Place data into non-power-of-two positions 1..=71.
67    for pos in 1..=HAMMING_HI {
68        if let Some(di) = data_index_at(pos) {
69            cw[pos] = (data >> di) & 1 == 1;
70        }
71    }
72    // Each check bit c = XOR of the data positions (bit i set) so that the parity over
73    // {positions with bit i set} is even.
74    for i in 0..CHECK_BITS {
75        let cp = 1usize << i;
76        let mut parity = false;
77        for pos in 1..=HAMMING_HI {
78            if pos != cp && (pos & cp) != 0 {
79                parity ^= cw[pos];
80            }
81        }
82        cw[cp] = parity;
83    }
84    // Overall parity over 1..=71.
85    let mut op = false;
86    for pos in 1..=HAMMING_HI {
87        op ^= cw[pos];
88    }
89    cw[0] = op;
90    pack(&cw)
91}
92
93/// SECDED decode: a (possibly corrupted) 72-bit codeword → (recovered data, verdict).
94pub fn decode(cw_bits: u128) -> (u64, Decode) {
95    let cw = unpack(cw_bits);
96    // Syndrome = which check equations fail.
97    let mut syndrome = 0usize;
98    for i in 0..CHECK_BITS {
99        let cp = 1usize << i;
100        let mut parity = false;
101        for pos in 1..=HAMMING_HI {
102            if pos & cp != 0 {
103                parity ^= cw[pos];
104            }
105        }
106        if parity {
107            syndrome |= cp;
108        }
109    }
110    // Overall parity across the WHOLE codeword (incl. bit 0) — odd ⇒ odd # of errors.
111    let odd_errors = (0..CW_BITS).fold(false, |acc, p| acc ^ cw[p]);
112
113    let mut fixed = cw;
114    let verdict = match (syndrome, odd_errors) {
115        (0, false) => Decode::NoError,
116        (0, true) => Decode::CorrectedParityBit, // only the overall bit flipped
117        (s, true) if s <= HAMMING_HI => {
118            fixed[s] ^= true; // single error at position s — correct it
119            Decode::Corrected(s)
120        }
121        _ => Decode::DoubleError, // syndrome set but parity even ⇒ 2 errors
122    };
123    let mut data = 0u64;
124    for pos in 1..=HAMMING_HI {
125        if let Some(di) = data_index_at(pos) {
126            if fixed[pos] {
127                data |= 1u64 << di;
128            }
129        }
130    }
131    (data, verdict)
132}
133
134fn pack(cw: &[bool; CW_BITS]) -> u128 {
135    let mut v = 0u128;
136    for (i, &b) in cw.iter().enumerate() {
137        if b {
138            v |= 1u128 << i;
139        }
140    }
141    v
142}
143
144fn unpack(v: u128) -> [bool; CW_BITS] {
145    let mut cw = [false; CW_BITS];
146    for (i, slot) in cw.iter_mut().enumerate() {
147        *slot = (v >> i) & 1 == 1;
148    }
149    cw
150}
151
152/// The data positions (0-indexed data bits) each check bit `i` XORs — the generator
153/// incidence shared by the Rust oracle and the emitted RTL.
154fn check_bit_terms(i: usize) -> Vec<usize> {
155    let cp = 1usize << i;
156    let mut terms = Vec::new();
157    for pos in 1..=HAMMING_HI {
158        if pos != cp && (pos & cp) != 0 {
159            if let Some(di) = data_index_at(pos) {
160                terms.push(di);
161            }
162        }
163    }
164    terms
165}
166
167/// A Verilog expression true iff the unsigned number on `bits` (LSB first) exceeds `c`, folded from the LSB up: past
168/// bit `i`, the number exceeds `c` iff bit `i` is set and the lower bits already exceed it, where `c` has a one there,
169/// or bit `i` is set or they do, where `c` has a zero. The fold keeps one gate per bit and drops the constant terms.
170fn exceeds(bits: &[String], c: usize) -> String {
171    if c >> bits.len() != 0 {
172        return "1'b0".to_string();
173    }
174    let mut acc: Option<String> = None;
175    for (i, b) in bits.iter().enumerate() {
176        acc = match (c >> i & 1 == 1, acc) {
177            (true, None) => None,
178            (true, Some(a)) => Some(format!("({b} & {a})")),
179            (false, None) => Some(b.clone()),
180            (false, Some(a)) => Some(format!("({b} | {a})")),
181        };
182    }
183    acc.unwrap_or_else(|| "1'b0".to_string())
184}
185
186/// Emit the SECDED(72,64) encoder + decoder/corrector as synthesizable Verilog,
187/// generated from the SAME incidence as [`encode`]/[`decode`], so they cannot diverge.
188/// Interface: `enc` takes 64-bit `din` → 72-bit `cw`; `dec` takes a 72-bit `cw_in` →
189/// 64-bit `dout` + `corrected` + `double_err`.
190pub fn emit_secded_verilog() -> String {
191    let mut w = String::new();
192    // Data-bit index at each Hamming position (for extract), and check-bit equations.
193    // ── encoder ──
194    w.push_str("// secded_enc — SECDED(72,64) encoder (Hamming SEC + overall parity).\n");
195    w.push_str("// GENERATED from the same incidence as codegen_sva::scratchpad_rtl::encode.\n");
196    w.push_str("module secded_enc(input [63:0] din, output [71:0] cw);\n");
197    // Assign data bits to their Hamming positions.
198    for pos in 1..=HAMMING_HI {
199        if let Some(di) = data_index_at(pos) {
200            w.push_str(&format!("  wire p{pos} = din[{di}];\n"));
201        }
202    }
203    // Every parity is one reduction over its support, which synthesis maps as a balanced tree; a chained `a ^ b ^ …`
204    // stays as deep as it is written.
205    let reduce = |terms: &[usize]| {
206        let t: Vec<String> = terms.iter().map(|d| format!("din[{d}]")).collect();
207        format!("^{{{}}}", t.join(", "))
208    };
209    for i in 0..CHECK_BITS {
210        w.push_str(&format!("  wire p{} = {};\n", 1usize << i, reduce(&check_bit_terms(i))));
211    }
212    // The overall parity over positions 1..=71, expanded to the data bits: each data bit counts once for its own
213    // position and once more for every check bit that covers it, so it is in the parity iff that count is odd.
214    let mut count = [1usize; DATA_BITS];
215    for i in 0..CHECK_BITS {
216        for d in check_bit_terms(i) {
217            count[d] += 1;
218        }
219    }
220    let overall: Vec<usize> = (0..DATA_BITS).filter(|&d| count[d] % 2 == 1).collect();
221    w.push_str(&format!("  wire p0 = {};\n", reduce(&overall)));
222    // Pack cw[0..71].
223    w.push_str("  assign cw = {");
224    for pos in (0..=HAMMING_HI).rev() {
225        w.push_str(&format!("p{pos}"));
226        if pos != 0 {
227            w.push_str(", ");
228        }
229    }
230    w.push_str("};\nendmodule\n\n");
231
232    // ── decoder / corrector ──
233    w.push_str("// secded_dec — SECDED(72,64) decoder: corrects any single-bit error,\n");
234    w.push_str("// detects any double-bit error (double_err), recovers the 64 data bits.\n");
235    w.push_str("module secded_dec(input [71:0] cw_in, output [63:0] dout,\n");
236    w.push_str("                  output corrected, output double_err);\n");
237    // Unpack positions.
238    for pos in 0..=HAMMING_HI {
239        w.push_str(&format!("  wire q{pos} = cw_in[{pos}];\n"));
240    }
241    // Syndrome bits: recompute each check equation over ALL positions with bit i set.
242    for i in 0..CHECK_BITS {
243        let cp = 1usize << i;
244        let terms: Vec<String> = (1..=HAMMING_HI)
245            .filter(|p| p & cp != 0)
246            .map(|p| format!("q{p}"))
247            .collect();
248        w.push_str(&format!("  wire s{i} = ^{{{}}};\n", terms.join(", ")));
249    }
250    w.push_str(&format!(
251        "  wire [{hi}:0] syndrome = {{{bits}}};\n",
252        hi = CHECK_BITS - 1,
253        bits = (0..CHECK_BITS)
254            .rev()
255            .map(|i| format!("s{i}"))
256            .collect::<Vec<_>>()
257            .join(", ")
258    ));
259    // Overall parity across all 72 bits.
260    w.push_str("  wire odd_err = ^cw_in;\n");
261    w.push_str("  wire syn_nz = |syndrome;\n");
262    // An odd error is correctable only where the syndrome names a codeword position (zero names the overall parity
263    // bit); one past the codeword is an odd number of errors past one, uncorrectable like every even one.
264    let syndrome_bits: Vec<String> = (0..CHECK_BITS).map(|i| format!("s{i}")).collect();
265    w.push_str(&format!(
266        "  wire beyond = {};\n",
267        exceeds(&syndrome_bits, HAMMING_HI)
268    ));
269    w.push_str("  assign corrected = odd_err & ~beyond;\n");
270    w.push_str("  assign double_err = syn_nz & (~odd_err | beyond);\n");
271    // Corrected value at each data position: flip qpos iff syndrome == pos and odd_err.
272    for pos in 1..=HAMMING_HI {
273        if let Some(di) = data_index_at(pos) {
274            w.push_str(&format!(
275                "  wire f{di} = q{pos} ^ (odd_err & (syndrome == {pos})); // pos {pos}\n"
276            ));
277        }
278    }
279    w.push_str("  assign dout = {");
280    for di in (0..DATA_BITS).rev() {
281        w.push_str(&format!("f{di}"));
282        if di != 0 {
283            w.push_str(", ");
284        }
285    }
286    w.push_str("};\nendmodule\n");
287    w
288}
289
290/// Emit the ping-pong double-buffer controller: `swap` toggles which bank compute reads
291/// (`read_bank`) vs the DMA fills (`fill_bank` = ~read_bank). The safety invariant
292/// `read_bank != fill_bank` holds by construction (they are complements).
293pub fn emit_double_buffer_verilog() -> String {
294    r#"// double_buffer — ping-pong scratchpad bank select. Compute reads read_bank while
295// the DMA fills fill_bank; a swap flips both. read_bank != fill_bank by construction.
296module double_buffer(input clk, input rst, input swap,
297                     output reg read_bank, output fill_bank);
298  assign fill_bank = ~read_bank;
299  always @(posedge clk) begin
300    if (rst) read_bank <= 1'b0;
301    else if (swap) read_bank <= ~read_bank;
302  end
303endmodule
304"#
305    .to_string()
306}
307
308/// Emit a self-checking testbench: encode `din`, inject a fault pattern from
309/// `secded_vec.hex` (each line: `din  flip_mask  expect_dout  expect_corrected
310/// expect_double`), decode, and compare. Prints PASS/FAIL.
311pub fn emit_secded_testbench() -> String {
312    r#"// tb_secded — SECDED(72,64) encode -> inject fault -> decode, vs the oracle.
313`timescale 1ns/1ps
314module tb_secded;
315  reg [63:0] din, edout; reg [71:0] mask; reg ecorr, edbl;
316  integer fd, r, errs = 0, n = 0;
317  wire [71:0] cw; wire [63:0] dout; wire corrected, double_err;
318  secded_enc enc(.din(din), .cw(cw));
319  wire [71:0] cw_corrupt = cw ^ mask;
320  secded_dec dec(.cw_in(cw_corrupt), .dout(dout), .corrected(corrected), .double_err(double_err));
321  initial begin
322    fd = $fopen("secded_vec.hex", "r");
323    while (!$feof(fd)) begin
324      r = $fscanf(fd, "%h %h %h %b %b\n", din, mask, edout, ecorr, edbl);
325      if (r == 5) begin
326        #1;
327        if (double_err !== edbl) begin errs=errs+1; $display("FAIL row %0d: double_err %b!=%b", n, double_err, edbl); end
328        else if (!edbl && dout !== edout) begin errs=errs+1; $display("FAIL row %0d: data %h!=%h", n, dout, edout); end
329        n = n + 1;
330      end
331    end
332    if (errs == 0) $display("PASS: %0d SECDED fault cases match the oracle (SEC + DED)", n);
333    $finish;
334  end
335endmodule
336"#
337    .to_string()
338}
339
340#[cfg(test)]
341#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/scratchpad_rtl/tests.rs"]
342mod tests;