feat: add live statistics with per-trial snapshots and von Neumann option (FPGA v5)

This commit is contained in:
randogoth 2026-10-25 14:27:45 +02:00
parent 67bd7eec58
commit 5305d66102
8 changed files with 427 additions and 17 deletions

View file

@ -1,5 +1,5 @@
// Board-independent STRIX logic: entropy generators with health monitors, source selection,
// raw capture, frequency counter and the SPI register interface to the SAMD21.
// Board-independent STRIX logic: entropy generators with health monitors, live statistics,
// source selection, raw capture, frequency counter and the SPI register interface to the SAMD21.
//
// SPI protocol (mode 0, one transaction per ss_n low period): the first byte is a command,
// bit 7 = read (1) or write (0), bits 6:0 = register address. A write stores the following
@ -15,11 +15,16 @@
// Health: a generator is healthy when it is enabled, its monitor has completed a dead-ring
// sweep, neither NIST test has failed since the last clear, and at most MAX_DEAD_RINGS rings
// are dead. Per-generator registers at 0x30 + 4g: FLAGS, RCT_EVENTS, APT_EVENTS, DEAD_RINGS.
//
// Statistics channels 0-3 are the generators' processed outputs, channel 4 is the XOR of the
// healthy generators' outputs. Reading STATS returns the latest trial snapshot (layout in
// stats_block.v) byte by byte, starting at byte 0 in every transaction.
module strix_core #(
parameter CAPTURE_ADDR_WIDTH = 15,
parameter FREQ_GATE_BITS = 24,
parameter RINGS_PER_GEN = 90,
parameter DEAD_WINDOW_BITS = 16
parameter DEAD_WINDOW_BITS = 16,
parameter TICK_CYCLES = 120000 // clk cycles per millisecond
) (
input wire clk,
input wire reset,
@ -30,7 +35,7 @@ module strix_core #(
output wire capture_done
);
localparam [7:0] VERSION = 8'h04;
localparam [7:0] VERSION = 8'h05;
localparam GENERATORS = 4;
localparam [6:0] REG_ID0 = 7'h00, // "STRX"
@ -64,7 +69,12 @@ module strix_core #(
REG_RCT_CUTOFF = 7'h22, // repetition count test cutoff (run length)
REG_APT_HIGH = 7'h23, // adaptive proportion test cutoff (per 1024 bits)
REG_APT_LOW = 7'h24,
REG_MAX_DEAD = 7'h25; // dead rings tolerated per generator
REG_MAX_DEAD = 7'h25, // dead rings tolerated per generator
REG_TRIAL_HIGH = 7'h26, // trial length in ms
REG_TRIAL_LOW = 7'h27,
REG_STATS_CTRL = 7'h28, // bit 0 von Neumann on the statistics path;
// write bit 1: restart trials, sequence 0
REG_STATS = 7'h40; // burst read of the trial snapshot
// Per-generator health registers at 0x30 + 4g + index
localparam [1:0] GEN_FLAGS = 2'd0, // bit 0 RCT failed, 1 APT failed, 2 too many dead rings,
@ -78,7 +88,7 @@ module strix_core #(
reg [GENERATORS-1:0] gen_enable;
reg [3:0] temporal;
reg [7:0] ring_select;
wire [GENERATORS-1:0] out_bit, spatial_bit, ring_enhanced, ring_tap, ring_raw_tap;
wire [GENERATORS-1:0] out_bit, out_valid, spatial_bit, ring_enhanced, ring_tap, ring_raw_tap;
reg health_clear;
reg [7:0] rct_cutoff, max_dead;
@ -96,7 +106,7 @@ module strix_core #(
entropy_generator #(.RINGS(RINGS_PER_GEN)) generator (
.clk(clk), .enable(gen_enable[g]), .temporal(temporal), .ring_select(ring_select),
.out_bit(out_bit[g]), .spatial_bit(spatial_bit[g]),
.out_bit(out_bit[g]), .out_valid(out_valid[g]), .spatial_bit(spatial_bit[g]),
.ring_enhanced(ring_enhanced[g]), .ring_tap(ring_tap[g]),
.ring_raw_tap(ring_raw_tap[g]), .ring_samples(ring_samples)
);
@ -116,6 +126,33 @@ module strix_core #(
end
endgenerate
// Statistics. All generators share the temporal setting and run in lockstep, so
// generator 0's valid strobe times every channel.
reg [15:0] trial_ms;
reg stats_von_neumann, stats_restart;
reg [7:0] stats_index;
wire [7:0] stats_byte;
wire trial_end;
// Registered inputs keep the health logic and the combining XOR off the counter paths;
// every channel is delayed by the same cycle, so they stay aligned.
reg [GENERATORS:0] stats_bits;
reg stats_valid;
reg [GENERATORS-1:0] stats_healthy;
always @(posedge clk) begin
stats_healthy <= healthy;
stats_bits <= {^(out_bit & healthy), out_bit};
stats_valid <= out_valid[0];
end
stats_block #(.CHANNELS(GENERATORS + 1), .TICK_CYCLES(TICK_CYCLES)) stats (
.clk(clk), .reset(reset), .bits(stats_bits), .valid(stats_valid),
.von_neumann(stats_von_neumann), .trial_ms(trial_ms), .restart(stats_restart),
.healthy(stats_healthy), .read_index(stats_index), .read_byte(stats_byte),
.trial_end(trial_end)
);
// Source selection
reg [3:0] source_a, source_b;
@ -224,6 +261,10 @@ module strix_core #(
REG_APT_HIGH: tx_byte = {5'd0, apt_cutoff[10:8]};
REG_APT_LOW: tx_byte = apt_cutoff[7:0];
REG_MAX_DEAD: tx_byte = max_dead;
REG_TRIAL_HIGH: tx_byte = trial_ms[15:8];
REG_TRIAL_LOW: tx_byte = trial_ms[7:0];
REG_STATS_CTRL: tx_byte = {7'd0, stats_von_neumann};
REG_STATS: tx_byte = stats_byte;
default: tx_byte = gen_register(reg_addr);
endcase
end
@ -232,6 +273,7 @@ module strix_core #(
capture_start <= 1'b0;
freq_start <= 1'b0;
health_clear <= 1'b0;
stats_restart <= 1'b0;
if (reset) begin
gen_enable <= {GENERATORS{1'b1}};
temporal <= 4'd5;
@ -245,11 +287,19 @@ module strix_core #(
apt_cutoff <= 11'd824; // same, 1024-bit window
max_dead <= 8'd8; // measured sizing tolerates about 17
health_clear <= 1'b1;
trial_ms <= 16'd200;
stats_von_neumann <= 1'b0;
stats_index <= 8'd0;
is_read <= 1'b0;
reg_addr <= 7'd0;
bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
prefetch_addr <= {CAPTURE_ADDR_WIDTH{1'b0}};
end else begin
if (!selected)
stats_index <= 8'd0;
else if (tx_loaded && is_read && reg_addr == REG_STATS)
stats_index <= stats_index + 8'd1;
if (!selected)
prefetch_addr <= bytes_read;
else if (tx_loaded && reading_data)
@ -277,6 +327,12 @@ module strix_core #(
REG_APT_HIGH: apt_cutoff[10:8] <= rx_byte[2:0];
REG_APT_LOW: apt_cutoff[7:0] <= rx_byte;
REG_MAX_DEAD: max_dead <= rx_byte;
REG_TRIAL_HIGH: trial_ms[15:8] <= rx_byte;
REG_TRIAL_LOW: trial_ms[7:0] <= rx_byte;
REG_STATS_CTRL: begin
stats_von_neumann <= rx_byte[0];
stats_restart <= rx_byte[1];
end
REG_CONTROL: begin
if (rx_byte[0]) capture_start <= 1'b1;
if (rx_byte[0] || rx_byte[1]) bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};