ND120_PF_CAPTURE¶
Source: Verilog/DELILAH-CPU/CGA/circuit/ND120_PF_CAPTURE.v
Hierarchy: not instantiated by any of the 9 build tops (elaborated by yosys).
Module hierarchy - All modules

Schematic¶
Drawn from the Verilog: no build top uses this module, so it was elaborated from its own file with no defines and default parameters. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).
Description¶
ND120 - FIRST PAGE-FAULT FREEZE REGISTER WHAT THIS IS FOR On the Tang, SINTRAN halts in ERRFATAL reporting IIC 3 (page fault). Nobody knows whether that page fault is REAL. This block captures the inputs to the trap logic at the exact TCLK edge that latched the vector, then LOCKS, so the evidence survives the rest of the boot and can be read out afterwards. It answers one question with no inference: PT valid, permission bits genuinely clear -> a REAL page fault PT all zero while the page-table RAM is mid-read -> SPURIOUS PT valid and GRANTING -> something other than PGF raised the trap WHY A FREEZE REGISTER AND NOT A TRACE BUFFER The event is one-shot and self-announcing: the machine halts moments after it. One frozen sample of the right cycle is worth more than a rolling window that may not be centred on it, and it costs ~80 flip- flops instead of BSRAM this design has none to spare. THE TIMING SUBTLETY THAT DECIDES WHETHER THIS BLOCK IS USEFUL AT ALL TVEC_3_0 is REGISTERED on TCLK. So by the time TVEC reads "page fault", the inputs that caused it have already moved on. Sampling the inputs at the same moment TVEC is observed would capture the WRONG CYCLE and produce a confident, wrong answer. Therefore: the inputs are snapshotted on EVERY capturing edge into a one-deep "previous edge" register, and when TVEC then shows a page fault, it is that PREVIOUS snapshot which is frozen. The testbench CGA_PF_CAPTURE_tb.v exists specifically to prove this lands on the right cycle - do not trust the block without it. LATCH MODE vs FF MODE In FPGA_FF_MODE the vector flip-flops are clocked by sysclk qualified with a TCLK_EN pulse. In the default build they are clocked by TCLK directly. This block follows whichever is in use, so the captured cycle means the same thing in both. Verilog/DELILAH-CPU/CGA/circuit/ND120_PF_CAPTURE.v 21-AUG-2026 Ronny Hansen
Parameters¶
| Parameter | Default |
|---|---|
CNTW |
24 |
PF_VECTOR |
1 |
ROT_LOG2 |
12 |
ARM_LOG2 |
30 |
CENSUS_LOG2 |
29 |
MATCH_PAGE_ONLY |
1 |
MATCH_PAGE |
6'o60 |
MATCH_ANY |
0 |
MATCH_LA_19_10 |
10'o760 |
Verilog source¶
Verilog/DELILAH-CPU/CGA/circuit/ND120_PF_CAPTURE.v on GitHub.
Show the Verilog of ND120_PF_CAPTURE (492 lines)
/*****************************************************************************
** ND120 - FIRST PAGE-FAULT FREEZE REGISTER **
** **
** WHAT THIS IS FOR **
** On the Tang, SINTRAN halts in ERRFATAL reporting IIC 3 (page fault). **
** Nobody knows whether that page fault is REAL. This block captures the **
** inputs to the trap logic at the exact TCLK edge that latched the **
** vector, then LOCKS, so the evidence survives the rest of the boot and **
** can be read out afterwards. **
** **
** It answers one question with no inference: **
** PT valid, permission bits genuinely clear -> a REAL page fault **
** PT all zero while the page-table RAM is **
** mid-read -> SPURIOUS **
** PT valid and GRANTING -> something other than **
** PGF raised the trap **
** **
** WHY A FREEZE REGISTER AND NOT A TRACE BUFFER **
** The event is one-shot and self-announcing: the machine halts moments **
** after it. One frozen sample of the right cycle is worth more than a **
** rolling window that may not be centred on it, and it costs ~80 flip- **
** flops instead of BSRAM this design has none to spare. **
** **
** THE TIMING SUBTLETY THAT DECIDES WHETHER THIS BLOCK IS USEFUL AT ALL **
** TVEC_3_0 is REGISTERED on TCLK. So by the time TVEC reads "page fault", **
** the inputs that caused it have already moved on. Sampling the inputs **
** at the same moment TVEC is observed would capture the WRONG CYCLE and **
** produce a confident, wrong answer. **
** Therefore: the inputs are snapshotted on EVERY capturing edge into a **
** one-deep "previous edge" register, and when TVEC then shows a page **
** fault, it is that PREVIOUS snapshot which is frozen. The testbench **
** CGA_PF_CAPTURE_tb.v exists specifically to prove this lands on the **
** right cycle - do not trust the block without it. **
** **
** LATCH MODE vs FF MODE **
** In FPGA_FF_MODE the vector flip-flops are clocked by sysclk qualified **
** with a TCLK_EN pulse. In the default build they are clocked by TCLK **
** directly. This block follows whichever is in use, so the captured **
** cycle means the same thing in both. **
** **
** Verilog/DELILAH-CPU/CGA/circuit/ND120_PF_CAPTURE.v **
** 21-AUG-2026 **
** Ronny Hansen **
*****************************************************************************/
`default_nettype none
// EVERY state register below carries an explicit power-up value. `clear` is
// tied to 0 in the real instantiation, so nothing else ever initialises them,
// and an undefined power-up `captured` or `armed` means the block reports a
// frozen fault the instant the FPGA configures. Measured 21-AUG-2026: the
// board dumped a "captured" word with all-zero contents before the CPU had
// even booted, and the dumper then held the console TX pin permanently.
module ND120_PF_CAPTURE #(
parameter integer CNTW = 24, //! cycle-counter width
parameter integer PF_VECTOR = 1, //! TVEC value that means "page fault"
parameter integer ROT_LOG2 = 12, //! slice period 2^N sysclk (rotates only once captured)
parameter integer ARM_LOG2 = 30, //! ignore faults for the first 2^N sysclk
//! Only freeze a fault whose logical page matches.
//!
//! WARNING ON THE ENCODING (established 21-AUG-2026, two independent ways):
//! LA[19:16] is the page-table RAM INDEX, and its TOP TWO BITS are the
//! COMPLEMENTS of their PCR source bits (CGA_MAC_LA1025.v:219-359, via the
//! schematic's dedicated PCR_14_13_10_9_N bus). The microcode un-inverts
//! them for software: TRA PGS XORs 0o1400 ("% 16 PT MODUS / INVERT 2 BITS",
//! WCS 001001). So the SOFTWARE table number is raw XOR 0o1400 :
//! raw LA[19:10] = 0o1360 <-> software PNUMB = 0o760
//! SINTRAN's WNDN5 (ND-500 window) is the SOFTWARE value 0o760, which is
//! RAW 0o1360. Match on the RAW value here. Decoding the raw index as if
//! it were the software table is what produced a spurious "page table 13 /
//! X5DPT" reading - do not repeat it.
//! Start the readout rotation this many sysclk after arming EVEN IF
//! nothing was captured, so the CENSUS can still be read out. Without
//! this a run that captures nothing produces no dump at all - measured
//! 21-AUG-2026: the machine halted, the census had the answer, and it was
//! unreachable because rotation waited on a capture that never happened.
parameter integer CENSUS_LOG2 = 29,
//! Match only the PAGE field (PNUMB[5:0]) and ignore the page-table
//! field. The fatal branch needs page 60 octal; both candidate tables
//! (7 = DPIT and 13 = X5DPT) carry that same page number, so matching the
//! page alone catches the fault and lets the hardware REPORT which table
//! it really was - instead of assuming one.
parameter integer MATCH_PAGE_ONLY = 1,
parameter [5:0] MATCH_PAGE = 6'o60,
parameter integer MATCH_ANY = 0,
parameter [9:0] MATCH_LA_19_10 = 10'o760,
//! 23-AUG-2026, the zero-read question: freeze on the first ARMED
//! committed access (VACC high at the capturing edge) to the matched page
//! whose PT permit bits are ALL CLEAR - i.e. the first access that MUST
//! page-fault - instead of on the fault vector. The frozen TVEC and the
//! census then say whether the trap actually followed:
//! freeze fires, census/last_la never show a fault there -> the trap
//! path failed in vivo (timing/qualifier);
//! freeze NEVER fires although the CPU executed zeros from that page ->
//! the entry was GRANTING, the zeros came from a legally-mapped wrong
//! physical page (map-RAM contents);
//! freeze fires and the fault follows -> that access traps fine, look
//! elsewhere.
parameter integer MATCH_ON_NOPERM_ACCESS = 0,
parameter integer HAS_PTRAM_STROBES = 0 //! 1 only when the page-table RAM
//! strobes are really connected.
//! Reported in the readout so an
//! unrouted 0 can never be read as
//! "the RAM was not driving".
) (
input wire sysclk,
input wire clear, //! clears the freeze so a second run can capture again
input wire tclk, //! trap clock - the vector register's clock
input wire tclk_en, //! FPGA_FF_MODE enable pulse; tie 0 in latch mode
// ---- the signals in question, as seen by CGA_TRAP -----------------------
input wire [ 6:0] pt_15_9, //! page-table entry bits 15..9; [6]=WPM [5]=RPM [4]=FPM
input wire vacc, //! MMU-translated access in progress (active high)
input wire [13:0] la_23_10, //! logical address - PGS is a copy of la[21:10]
input wire [ 3:0] tvec_3_0, //! the latched trap vector
input wire pviol,
input wire restr,
input wire ptram_cs_n, //! page-table RAM chip select (active low)
input wire ptram_oe_n, //! page-table RAM output enable (active low)
input wire epgs, //! EPGS asserted = the microcode is READING PGS
// ---- frozen evidence ----------------------------------------------------
output reg captured = 1'b0, //! 1 = a page fault has been frozen
output reg [ 6:0] c_pt_15_9 = 7'd0,
output reg c_vacc = 1'b0,
output reg [13:0] c_la_23_10 = 14'd0,
output reg [ 3:0] c_tvec_3_0 = 4'd0,
output reg c_pviol = 1'b0,
output reg c_restr = 1'b0,
output reg c_ptram_cs_n = 1'b1,
output reg c_ptram_oe_n = 1'b1,
output reg [ 6:0] c_pt_prev = 7'd0, //! PT one capturing edge BEFORE the fault
output reg [ 6:0] c_pt_next = 7'd0, //! PT one capturing edge AFTER the fault
output reg c_next_valid = 1'b0,
//! CENSUS - these keep updating and are NOT frozen. They answer "did any
//! page fault happen at all, and where", which a targeted freeze alone
//! cannot: a silent run is ambiguous between "no such fault" and "the
//! trigger missed it".
output reg [ 7:0] n_faults = 8'd0, //! page-fault vector transitions since arming
output reg [13:0] last_la = 14'd0, //! LA of the most recent one, ANY address
//! PHASE 4b - the decisive pair. pgs_shadow mirrors the real PGS register's
//! rule ("load LA_21_10 whenever VACC is high"), and c_pgs_at_read freezes
//! it at the first EPGS after the fault. If c_pgs_at_read differs from
//! c_la_23_10 then PGS WAS OVERWRITTEN between the fault and the handler
//! reading it - which is the whole question.
//! LA one capturing edge BEFORE the fault. THE DECISIVE PAIR: the
//! page-table RAM is a synchronous BRAM, so PT_15_9 at the trap edge
//! belongs to the address presented ONE CYCLE EARLIER. If c_la_prev
//! differs from c_la_23_10, the entry the trap judged and the address it
//! reported belong to DIFFERENT ACCESSES.
output reg [13:0] c_la_prev = 14'd0,
//! PGS value AT the first handler read (EPGS) after the freeze - the
//! Phase-4b overwrite detector. In the readout word since 23-AUG-2026:
//! low 10 bits at frozen[56:47], high 4 bits at frozen[75:72]; valid
//! only when frozen[46] (c_pgs_valid) is set.
output reg [13:0] c_pgs_at_read = 14'd0,
output reg c_pgs_valid = 1'b0,
output reg [CNTW-1:0] c_cycle = {CNTW{1'b0}},
output wire ptram_strobes_valid, //! 0 = the two RAM bits mean NOTHING
//! ---- READOUT ----------------------------------------------------------
//! The frozen evidence is 56 bits but the only wire out of the CGA is 16
//! bits wide (XMIC_DBG_15_0). The machine HALTS moments after the fault, so
//! there is unlimited time: the word is emitted as four 14-bit slices with
//! a 2-bit slice index in the top bits, rotating slowly enough for the
//! console dump to catch each one.
//! readout[15:14] = slice index 0..3
//! readout[13:0] = that slice of the frozen word
output wire [15:0] readout_15_0,
//! ---- LIVE EVENT PULSES (23-AUG, Phase 1b ordering) ---------------------
//! One sysclk pulse per matching event, NOT gated by `captured`, so a ring
//! outside this module can record EVERY occurrence and place it in time
//! against the page-table write stream.
//! evt_noperm = a committed access at the matched page whose entry grants
//! nothing (PT[6:4] == 000)
//! evt_fault = a page-fault vector latched for the matched page
output wire evt_noperm,
output wire evt_fault,
//! ---- ANY-ADDRESS FAULT STREAM (23-AUG, run 11) -------------------------
//! One pulse per page-fault vector transition at ANY address, with the
//! snapshot that produced it. A ring outside this module records the
//! stream and the trigger stops it at the ERRFATAL printer, so the last
//! records ARE the fault that halts the machine.
output wire evt_any,
output wire [9:0] evt_any_la_19_10,
output wire [6:0] evt_any_pt_15_9
);
assign ptram_strobes_valid = (HAS_PTRAM_STROBES != 0);
// ---- ARMING ---------------------------------------------------------------
// Do NOT freeze anything until the machine is actually running.
//
// Measured 21-AUG-2026: without this the block froze during reset / the WCS
// microcode load, the top's dump triggered at power-up, and `dump_fin`
// latched the TX pin away from the console PERMANENTLY - so the boot command
// could never be typed and the real fault could never happen. Twice.
//
// 2^30 sysclk is roughly 40 s at 27 MHz, which is past the microcode load and
// long before the fault we are hunting (25+ minutes in).
reg [ARM_LOG2-1:0] arm_cnt = {ARM_LOG2{1'b0}};
reg armed = 1'b0;
always @(posedge sysclk) begin
if (clear) begin
arm_cnt <= {ARM_LOG2{1'b0}};
armed <= 1'b0;
end else if (!armed) begin
arm_cnt <= arm_cnt + 1'b1;
if (arm_cnt == {ARM_LOG2{1'b1}}) armed <= 1'b1;
end
end
// ---- census release: let the readout rotate even with no capture --------
reg [CENSUS_LOG2-1:0] census_cnt = {CENSUS_LOG2{1'b0}};
reg census_ready = 1'b0;
always @(posedge sysclk) begin
if (clear) begin
census_cnt <= {CENSUS_LOG2{1'b0}};
census_ready <= 1'b0;
end else if (armed && !census_ready) begin
census_cnt <= census_cnt + 1'b1;
if (census_cnt == {CENSUS_LOG2{1'b1}}) census_ready <= 1'b1;
end
end
// ---- free-running cycle counter, so the event can be placed in the boot --
reg [CNTW-1:0] cycle = {CNTW{1'b0}};
always @(posedge sysclk) begin
if (clear) cycle <= {CNTW{1'b0}};
else cycle <= cycle + 1'b1;
end
// ---- the capturing edge --------------------------------------------------
// Default build: the vector register is clocked by TCLK itself, so the
// capturing moment is a TCLK rising edge.
// FPGA_FF_MODE: it is a sysclk edge qualified by TCLK_EN.
reg tclk_d = 1'b0;
always @(posedge sysclk) tclk_d <= tclk;
wire tclk_rise = tclk & ~tclk_d;
`ifdef FPGA_FF_MODE
wire capture_edge = tclk_en;
`else
wire capture_edge = tclk_rise;
`endif
// ---- one-deep snapshot of the inputs AT that edge ------------------------
// This is the whole point: TVEC is observed one edge later than the inputs
// that produced it.
reg [ 6:0] pre_pt = 7'd0;
reg pre_vacc = 1'b0;
reg [13:0] pre_la = 14'd0;
reg pre_pviol = 1'b0;
reg pre_restr = 1'b0;
reg pre_cs_n = 1'b1;
reg pre_oe_n = 1'b1;
reg [CNTW-1:0] pre_cycle = {CNTW{1'b0}};
reg pre_valid = 1'b0; // a snapshot exists (guards the very first edge)
reg [ 6:0] prev_pt = 7'd0; // PT at the edge BEFORE the snapshot
always @(posedge sysclk) begin
if (clear) begin
pre_valid <= 1'b0;
end else if (capture_edge) begin
prev_pt <= pre_pt; // one edge older than the snapshot
prev_la <= pre_la;
pre_pt <= pt_15_9;
pre_vacc <= vacc;
pre_la <= la_23_10;
pre_pviol <= pviol;
pre_restr <= restr;
pre_cs_n <= ptram_cs_n;
pre_oe_n <= ptram_oe_n;
pre_cycle <= cycle;
pre_valid <= 1'b1;
end
end
// ---- PGS SHADOW: same rule as the real PGS register --------------------
// CGA_IDBCTL_PGSREG loads LA_21_10 on every cycle VACC is high (VACCN is the
// load enable, D tied back to Q so it holds when VACC is low). Mirror that
// here so the probe sees what the handler will see, WITHOUT touching
// CGA_IDBCTL.
reg [13:0] prev_la = 14'd0;
reg [13:0] pgs_shadow = 14'd0;
always @(posedge sysclk) begin
if (clear) begin
c_pgs_valid <= 1'b0;
c_pgs_at_read <= 14'd0;
pgs_shadow <= 14'd0;
end else begin
if (vacc) pgs_shadow <= la_23_10; // the real PGS rule: load while VACC high
if (epgs && captured && !c_pgs_valid) begin
c_pgs_valid <= 1'b1; // did the handler read PGS at all
c_pgs_at_read <= pgs_shadow; // what it actually saw
end
end
end
// ---- CENSUS: count every page-fault vector transition, any address ------
// Independent of the targeted freeze. A run that captures nothing but
// reports n_faults>0 tells us the faults happened at OTHER addresses; a run
// reporting n_faults==0 tells us the trap logic never saw one at all.
reg pf_any_d = 1'b0;
wire pf_any = pf_level;
always @(posedge sysclk) begin
if (clear) begin
pf_any_d <= 1'b0;
n_faults <= 8'd0;
last_la <= 14'd0;
end else begin
pf_any_d <= pf_any;
if (armed && pf_any && !pf_any_d && pre_valid) begin
if (n_faults != 8'hFF) n_faults <= n_faults + 1'b1; // saturate
last_la <= pre_la;
end
end
end
// ---- freeze on the FIRST page-fault vector, then lock --------------------
// TRIGGER ON THE TRANSITION INTO THE PAGE-FAULT VECTOR, NOT THE LEVEL.
//
// TVEC is registered and HOLDS its value between capturing edges. Triggering
// on the level meant that if the arm window expired while TVEC was already
// sitting at 1 from an earlier fault, the block froze whatever snapshot was
// current - an unrelated later cycle. Measured on silicon 21-AUG-2026: the
// captured word said PVIOL=1 with VACC=0, which is physically impossible
// (every protect term is ANDed with VACC), and that inconsistency is what
// exposed it. Only a fresh 0->1 transition is a new fault.
reg pf_hit_d = 1'b0;
wire pf_level = (tvec_3_0 == PF_VECTOR[3:0]);
// LA_23_10 is LA[23:10]; SINTRAN's PNUMB is LA[19:10] = the low 10 bits.
wire [9:0] la_pnumb = pre_la[9:0];
wire la_match = (MATCH_ANY != 0)
|| ((MATCH_PAGE_ONLY != 0) ? (la_pnumb[5:0] == MATCH_PAGE)
: (la_pnumb == MATCH_LA_19_10));
// Edge-detect the CONJUNCTION (fault vector AND address match), not the
// vector alone. TVEC HOLDS between capturing edges, so with back-to-back
// faults it can already be sitting at the page-fault value when the
// interesting address finally appears - and a vector-only edge detector
// then never fires. Measured 21-AUG-2026: a targeted run saw the ERRFATAL
// halt on the console with ZERO captures, and this is one of the two
// explanations that had to be separated.
wire pf_hit = pf_level & la_match;
always @(posedge sysclk) pf_hit_d <= pf_hit;
wire pf_now = pf_hit & ~pf_hit_d;
// ---- 23-AUG: the no-permit committed-access trigger (see parameter note).
// Uses the SNAPSHOT (pre_*) values so the frozen record and the trigger
// judge the same capturing edge. Edge-detected like pf_now so a condition
// that persists across edges freezes only once.
reg acc_hit_d = 1'b0;
wire acc_hit = pre_valid & pre_vacc & la_match & (pre_pt[6:4] == 3'b000);
always @(posedge sysclk) acc_hit_d <= acc_hit;
wire acc_now = acc_hit & ~acc_hit_d;
wire freeze_now = (MATCH_ON_NOPERM_ACCESS != 0) ? acc_now : pf_now;
// Live pulses out (see the port comment): the ring in the FPGA top uses these
// to order accesses against page-table writes. Unconditional on `captured`.
assign evt_noperm = acc_now;
assign evt_fault = pf_now;
// Any-address fault stream (see the port comment). pre_* is the snapshot the
// fault was judged on, so the address and the entry match the vector.
assign evt_any = pf_any & ~pf_any_d & pre_valid;
assign evt_any_la_19_10 = pre_la[9:0];
assign evt_any_pt_15_9 = pre_pt;
always @(posedge sysclk) begin
if (clear) begin
captured <= 1'b0;
c_pt_15_9 <= 7'd0;
c_vacc <= 1'b0;
c_la_23_10 <= 14'd0;
c_tvec_3_0 <= 4'd0;
c_pviol <= 1'b0;
c_restr <= 1'b0;
c_ptram_cs_n <= 1'b1;
c_ptram_oe_n <= 1'b1;
c_cycle <= {CNTW{1'b0}};
c_pt_prev <= 7'd0;
c_pt_next <= 7'd0;
c_next_valid <= 1'b0;
end else if (armed && !captured && freeze_now && pre_valid) begin
// freeze the snapshot taken AT the capturing edge, not the inputs now
captured <= 1'b1;
c_pt_15_9 <= pre_pt;
c_vacc <= pre_vacc;
c_la_23_10 <= pre_la;
c_tvec_3_0 <= tvec_3_0;
c_pviol <= pre_pviol;
c_restr <= pre_restr;
c_ptram_cs_n <= pre_cs_n;
c_ptram_oe_n <= pre_oe_n;
c_cycle <= pre_cycle;
c_pt_prev <= prev_pt;
c_la_prev <= prev_la;
end else if (captured && !c_next_valid && capture_edge) begin
// THE DISCRIMINATOR. One capturing edge after the freeze, sample PT
// again. If it was zero at the faulting edge but is non-zero now, the
// page-table entry simply ARRIVED LATE and the trap decoded an idle bus.
// If it is still zero, the entry really is unmapped and the fault is
// real. This needs no signal that is not already inside the CGA, which
// is why it replaced routing the page-table RAM strobes up through four
// levels of hierarchy.
c_pt_next <= pt_15_9;
c_next_valid <= 1'b1;
end
// no else: once captured, it holds until `clear`. That lock is the point -
// the FIRST fault is the interesting one; later ones are consequences.
end
// ---- the frozen word, assembled once so the layout is stated in ONE place
// and the reader cannot drift from the writer.
// [ 6: 0] PT_15_9 [21: 8] LA_23_10 [26] PVIOL [30] captured
// [ 7] VACC [25:22] TVEC_3_0 [27] RESTR [31] strobes_valid
// [28] ptram CS_n [29] ptram OE_n [55:32] cycle counter
wire [103:0] frozen = {
2'd0, // [103:102] pad
census_ready, // [101]
armed, // [100] <- WITHOUT this, n_faults==0 is ambiguous
// between "no fault" and "never armed"
last_la, // [99:86]
n_faults, // [85:78]
2'd0, // [77:76] pad
c_pgs_at_read[13:10], // [75:72] <- 23-AUG: PGS at the handler's read,
c_next_valid, // [71] high bits (low bits at [56:47]);
c_pt_next, // [70:64] valid only when [46] c_pgs_valid=1
c_pt_prev, // [63:57]
c_pgs_at_read[9:0], // [56:47] <- 23-AUG: PGS at the handler's read, low bits
c_pgs_valid, // [46]
c_la_prev, // [45:32] <- pairs with c_pt_prev
ptram_strobes_valid, // [31]
captured, // [30]
c_ptram_oe_n, // [29]
c_ptram_cs_n, // [28]
c_restr, // [27]
c_pviol, // [26]
c_tvec_3_0, // [25:22]
c_la_23_10, // [21:8]
c_vacc, // [7]
c_pt_15_9 // [6:0]
};
reg [ROT_LOG2-1:0] rot = {ROT_LOG2{1'b0}};
reg [2:0] slice_idx = 3'd0;
always @(posedge sysclk) begin
// ROTATE ONLY AFTER A FAULT IS FROZEN. The board's ring samples this
// window over time; if it rotates before the capture, the ring mixes
// PRE- and POST-capture slices and the reconstructed word is a blend of
// two states. Measured on silicon 21-AUG-2026: a dump decoded to
// captured=0 AND c_next_valid=1, which the RTL cannot produce. Holding
// slice 0 until `captured` means every slice above 0 comes from the
// frozen, immutable word.
if (clear || !(captured || census_ready)) begin
rot <= {ROT_LOG2{1'b0}};
slice_idx <= 3'd0;
end else begin
rot <= rot + 1'b1;
if (rot == {ROT_LOG2{1'b1}})
slice_idx <= (slice_idx == 3'd7) ? 3'd0 : slice_idx + 1'b1;
end
end
// 6 slices x 13 bits = 78 bits, which covers the 78-bit word. The index is
// 3 bits so a reader can always tell which slice it is looking at.
reg [12:0] slice_data;
always @(*) begin
case (slice_idx)
3'd0: slice_data = frozen[12:0];
3'd1: slice_data = frozen[25:13];
3'd2: slice_data = frozen[38:26];
3'd3: slice_data = frozen[51:39];
3'd4: slice_data = frozen[64:52];
3'd5: slice_data = frozen[77:65];
3'd6: slice_data = frozen[90:78];
default: slice_data = frozen[103:91];
endcase
end
assign readout_15_0 = {slice_idx, slice_data};
endmodule
`default_nettype wire