feat: add live statistics with per-trial snapshots and von Neumann option (FPGA v5)
This commit is contained in:
parent
67bd7eec58
commit
5305d66102
8 changed files with 427 additions and 17 deletions
|
|
@ -15,6 +15,7 @@ module entropy_generator #(
|
|||
input wire [3:0] temporal, // consecutive bits XORed per output bit, 1-8
|
||||
input wire [7:0] ring_select, // ring exposed on the characterization outputs
|
||||
output reg out_bit, // processed output, held for `temporal` clocks
|
||||
output reg out_valid, // pulse: out_bit has just been updated
|
||||
output wire spatial_bit, // XOR of all rings, one per clock
|
||||
output wire [RINGS-1:0] ring_samples, // synchronized enhanced outputs, for health tests
|
||||
output reg ring_enhanced,
|
||||
|
|
@ -72,6 +73,7 @@ module entropy_generator #(
|
|||
wire last = (phase >= temporal - 4'd1) || (temporal == 4'd0);
|
||||
|
||||
always @(posedge clk) begin
|
||||
out_valid <= last;
|
||||
if (last) begin
|
||||
out_bit <= accumulator ^ spatial_bit;
|
||||
accumulator <= 1'b0;
|
||||
|
|
|
|||
148
fpga/rtl/stats_block.v
Normal file
148
fpga/rtl/stats_block.v
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Live statistics: back-to-back trials of `trial_ms` milliseconds. During each trial every
|
||||
// channel counts its bits, ones, and agreements (a bit equal to the bit before it, which may
|
||||
// be the last bit of the previous trial, so a trial has as many comparisons as bits). At the
|
||||
// end of a trial the counts are latched into a snapshot and the counters restart in the same
|
||||
// cycle, so no bit is lost between trials. Bias, z-scores, random walk and lag-1
|
||||
// autocorrelation all follow from the three counts; rolling and cumulative windows are built
|
||||
// from consecutive snapshots outside the FPGA.
|
||||
//
|
||||
// With von Neumann enabled, each channel's bits pass through a von Neumann extractor (pairs
|
||||
// 01 -> 0, 10 -> 1, 00 and 11 dropped) before counting.
|
||||
//
|
||||
// Snapshot layout (big-endian), read one byte at a time by index:
|
||||
// 0-3 trial sequence number
|
||||
// 4 healthy generators during the whole trial (AND over the trial)
|
||||
// 5 bit 0: von Neumann was enabled for the whole trial
|
||||
// 6-7 trial length in ms
|
||||
// 8+12c channel c: bits (4), ones (4), agreements (4)
|
||||
// 8+12C trial sequence number again; a mismatch with bytes 0-3 means a torn read
|
||||
module stats_block #(
|
||||
parameter CHANNELS = 5,
|
||||
parameter TICK_CYCLES = 120000 // clk cycles per millisecond
|
||||
) (
|
||||
input wire clk,
|
||||
input wire reset,
|
||||
input wire [CHANNELS-1:0] bits,
|
||||
input wire valid, // new bits on all channels this cycle
|
||||
input wire von_neumann,
|
||||
input wire [15:0] trial_ms,
|
||||
input wire restart, // start a new trial now, sequence back to 0
|
||||
input wire [3:0] healthy,
|
||||
input wire [7:0] read_index,
|
||||
output reg [7:0] read_byte, // one cycle after read_index
|
||||
output reg trial_end // pulse: a new snapshot is available
|
||||
);
|
||||
|
||||
localparam SNAPSHOT_BYTES = 12 + 12 * CHANNELS;
|
||||
|
||||
// Trial timing
|
||||
|
||||
reg [16:0] tick_count;
|
||||
reg [15:0] ms_count;
|
||||
wire tick = (tick_count == TICK_CYCLES - 1);
|
||||
wire trial_due = tick && (ms_count + 16'd1 >= trial_ms);
|
||||
|
||||
always @(posedge clk) begin
|
||||
trial_end <= 1'b0;
|
||||
if (reset || restart) begin
|
||||
tick_count <= 17'd0;
|
||||
ms_count <= 16'd0;
|
||||
end else begin
|
||||
tick_count <= tick ? 17'd0 : tick_count + 17'd1;
|
||||
if (trial_due) begin
|
||||
ms_count <= 16'd0;
|
||||
trial_end <= 1'b1;
|
||||
end else if (tick) begin
|
||||
ms_count <= ms_count + 16'd1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Von Neumann extraction per channel
|
||||
|
||||
reg [CHANNELS-1:0] pair_first, have_first;
|
||||
wire [CHANNELS-1:0] vn_valid = have_first & (bits ^ pair_first);
|
||||
wire [CHANNELS-1:0] count_valid = von_neumann ? (valid ? vn_valid : {CHANNELS{1'b0}})
|
||||
: {CHANNELS{valid}};
|
||||
wire [CHANNELS-1:0] count_bit = von_neumann ? pair_first : bits;
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (reset || !von_neumann) begin
|
||||
have_first <= {CHANNELS{1'b0}};
|
||||
end else if (valid) begin
|
||||
have_first <= ~have_first;
|
||||
pair_first <= bits;
|
||||
end
|
||||
end
|
||||
|
||||
// Counters
|
||||
|
||||
reg [31:0] bit_count [0:CHANNELS-1];
|
||||
reg [31:0] one_count [0:CHANNELS-1];
|
||||
reg [31:0] agree_count [0:CHANNELS-1];
|
||||
reg [CHANNELS-1:0] previous_bit, has_previous;
|
||||
|
||||
reg [31:0] sequence_number;
|
||||
reg [3:0] healthy_during;
|
||||
reg von_neumann_during;
|
||||
|
||||
// The snapshot is latched as one flat byte string (byte 0 in the top bits), so reading it
|
||||
// is a plain byte multiplexer.
|
||||
reg [8*SNAPSHOT_BYTES-1:0] snapshot, snapshot_next;
|
||||
|
||||
integer k;
|
||||
always @(*) begin
|
||||
snapshot_next = {SNAPSHOT_BYTES{8'h00}};
|
||||
snapshot_next[8*SNAPSHOT_BYTES-1 -: 64] = {sequence_number, 4'd0, healthy_during & healthy,
|
||||
7'd0, von_neumann_during & von_neumann, trial_ms};
|
||||
for (k = 0; k < CHANNELS; k = k + 1)
|
||||
snapshot_next[8*SNAPSHOT_BYTES-1-64-96*k -: 96] = {bit_count[k], one_count[k], agree_count[k]};
|
||||
snapshot_next[31:0] = sequence_number;
|
||||
end
|
||||
|
||||
integer c;
|
||||
always @(posedge clk) begin
|
||||
if (reset || restart) begin
|
||||
sequence_number <= 32'd0;
|
||||
healthy_during <= 4'hF;
|
||||
von_neumann_during <= von_neumann;
|
||||
for (c = 0; c < CHANNELS; c = c + 1) begin
|
||||
bit_count[c] <= 32'd0;
|
||||
one_count[c] <= 32'd0;
|
||||
agree_count[c] <= 32'd0;
|
||||
end
|
||||
has_previous <= {CHANNELS{1'b0}};
|
||||
end else begin
|
||||
for (c = 0; c < CHANNELS; c = c + 1) begin
|
||||
// The bit arriving in the trial's last cycle belongs to the next trial.
|
||||
if (count_valid[c]) begin
|
||||
bit_count[c] <= (trial_end ? 32'd0 : bit_count[c]) + 32'd1;
|
||||
one_count[c] <= (trial_end ? 32'd0 : one_count[c]) + count_bit[c];
|
||||
agree_count[c] <= (trial_end ? 32'd0 : agree_count[c])
|
||||
+ (has_previous[c] && count_bit[c] == previous_bit[c]);
|
||||
previous_bit[c] <= count_bit[c];
|
||||
has_previous[c] <= 1'b1;
|
||||
end else if (trial_end) begin
|
||||
bit_count[c] <= 32'd0;
|
||||
one_count[c] <= 32'd0;
|
||||
agree_count[c] <= 32'd0;
|
||||
end
|
||||
end
|
||||
if (trial_end) begin
|
||||
snapshot <= snapshot_next;
|
||||
sequence_number <= sequence_number + 32'd1;
|
||||
healthy_during <= healthy;
|
||||
von_neumann_during <= von_neumann;
|
||||
end else begin
|
||||
healthy_during <= healthy_during & healthy;
|
||||
von_neumann_during <= von_neumann_during & von_neumann;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// Snapshot read-out, registered: the byte for read_index appears one cycle later.
|
||||
|
||||
always @(posedge clk)
|
||||
read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[8*(SNAPSHOT_BYTES-1-read_index) +: 8] : 8'h00;
|
||||
|
||||
endmodule
|
||||
|
|
@ -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}};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue