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
|
|
@ -7,6 +7,7 @@
|
||||||
// w <register> <value> write a register, prints "OK"
|
// w <register> <value> write a register, prints "OK"
|
||||||
// cap start a capture with the current settings and send it as
|
// cap start a capture with the current settings and send it as
|
||||||
// "DATA <n>\n" followed by n raw bytes
|
// "DATA <n>\n" followed by n raw bytes
|
||||||
|
// stats print the latest statistics trial snapshot as hex
|
||||||
|
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <VidorBoot.h>
|
#include <VidorBoot.h>
|
||||||
|
|
@ -23,6 +24,7 @@ enum : uint8_t {
|
||||||
REG_CONTROL = 0x0B,
|
REG_CONTROL = 0x0B,
|
||||||
REG_STATUS = 0x0C,
|
REG_STATUS = 0x0C,
|
||||||
REG_DATA = 0x10,
|
REG_DATA = 0x10,
|
||||||
|
REG_STATS = 0x40,
|
||||||
};
|
};
|
||||||
|
|
||||||
const uint8_t READ = 0x80;
|
const uint8_t READ = 0x80;
|
||||||
|
|
@ -30,6 +32,7 @@ const uint8_t CONTROL_START_CAPTURE = 0x01;
|
||||||
const uint8_t STATUS_CAPTURE_DONE = 0x02;
|
const uint8_t STATUS_CAPTURE_DONE = 0x02;
|
||||||
const size_t CAPTURE_BYTES = 32768;
|
const size_t CAPTURE_BYTES = 32768;
|
||||||
const size_t CHUNK_BYTES = 512;
|
const size_t CHUNK_BYTES = 512;
|
||||||
|
const size_t SNAPSHOT_BYTES = 72;
|
||||||
|
|
||||||
VidorUtils fpga;
|
VidorUtils fpga;
|
||||||
|
|
||||||
|
|
@ -89,6 +92,17 @@ void capture() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void printSnapshot() {
|
||||||
|
beginTransaction(READ | REG_STATS);
|
||||||
|
for (size_t i = 0; i < SNAPSHOT_BYTES; i++) {
|
||||||
|
uint8_t value = strixSpi.transfer(0);
|
||||||
|
if (value < 0x10) Serial.print('0');
|
||||||
|
Serial.print(value, HEX);
|
||||||
|
}
|
||||||
|
endTransaction();
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
// Parses "<a>" or "<a> <b>" after the command word; returns the number of values found.
|
// Parses "<a>" or "<a> <b>" after the command word; returns the number of values found.
|
||||||
int parseArgs(const String &args, long &a, long &b) {
|
int parseArgs(const String &args, long &a, long &b) {
|
||||||
int space = args.indexOf(' ');
|
int space = args.indexOf(' ');
|
||||||
|
|
@ -116,6 +130,8 @@ void handleCommand(String line) {
|
||||||
Serial.println("OK");
|
Serial.println("OK");
|
||||||
} else if (command == "cap") {
|
} else if (command == "cap") {
|
||||||
capture();
|
capture();
|
||||||
|
} else if (command == "stats") {
|
||||||
|
printSnapshot();
|
||||||
} else if (line.length() > 0) {
|
} else if (line.length() > 0) {
|
||||||
Serial.println("ERR unknown or malformed command");
|
Serial.println("ERR unknown or malformed command");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
],
|
],
|
||||||
"sim": [
|
"sim": [
|
||||||
"mkdir -p /tmp/strix-sim",
|
"mkdir -p /tmp/strix-sim",
|
||||||
"iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_generator.v fpga/rtl/health_monitor.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v",
|
"iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_generator.v fpga/rtl/health_monitor.v fpga/rtl/stats_block.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v",
|
||||||
"vvp -n /tmp/strix-sim/tb_strix_core"
|
"vvp -n /tmp/strix-sim/tb_strix_core"
|
||||||
],
|
],
|
||||||
"fpga": [
|
"fpga": [
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ module entropy_generator #(
|
||||||
input wire [3:0] temporal, // consecutive bits XORed per output bit, 1-8
|
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
|
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_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 spatial_bit, // XOR of all rings, one per clock
|
||||||
output wire [RINGS-1:0] ring_samples, // synchronized enhanced outputs, for health tests
|
output wire [RINGS-1:0] ring_samples, // synchronized enhanced outputs, for health tests
|
||||||
output reg ring_enhanced,
|
output reg ring_enhanced,
|
||||||
|
|
@ -72,6 +73,7 @@ module entropy_generator #(
|
||||||
wire last = (phase >= temporal - 4'd1) || (temporal == 4'd0);
|
wire last = (phase >= temporal - 4'd1) || (temporal == 4'd0);
|
||||||
|
|
||||||
always @(posedge clk) begin
|
always @(posedge clk) begin
|
||||||
|
out_valid <= last;
|
||||||
if (last) begin
|
if (last) begin
|
||||||
out_bit <= accumulator ^ spatial_bit;
|
out_bit <= accumulator ^ spatial_bit;
|
||||||
accumulator <= 1'b0;
|
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,
|
// Board-independent STRIX logic: entropy generators with health monitors, live statistics,
|
||||||
// raw capture, frequency counter and the SPI register interface to the SAMD21.
|
// 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,
|
// 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
|
// 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
|
// 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
|
// 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.
|
// 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 #(
|
module strix_core #(
|
||||||
parameter CAPTURE_ADDR_WIDTH = 15,
|
parameter CAPTURE_ADDR_WIDTH = 15,
|
||||||
parameter FREQ_GATE_BITS = 24,
|
parameter FREQ_GATE_BITS = 24,
|
||||||
parameter RINGS_PER_GEN = 90,
|
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 clk,
|
||||||
input wire reset,
|
input wire reset,
|
||||||
|
|
@ -30,7 +35,7 @@ module strix_core #(
|
||||||
output wire capture_done
|
output wire capture_done
|
||||||
);
|
);
|
||||||
|
|
||||||
localparam [7:0] VERSION = 8'h04;
|
localparam [7:0] VERSION = 8'h05;
|
||||||
localparam GENERATORS = 4;
|
localparam GENERATORS = 4;
|
||||||
|
|
||||||
localparam [6:0] REG_ID0 = 7'h00, // "STRX"
|
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_RCT_CUTOFF = 7'h22, // repetition count test cutoff (run length)
|
||||||
REG_APT_HIGH = 7'h23, // adaptive proportion test cutoff (per 1024 bits)
|
REG_APT_HIGH = 7'h23, // adaptive proportion test cutoff (per 1024 bits)
|
||||||
REG_APT_LOW = 7'h24,
|
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
|
// 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,
|
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 [GENERATORS-1:0] gen_enable;
|
||||||
reg [3:0] temporal;
|
reg [3:0] temporal;
|
||||||
reg [7:0] ring_select;
|
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 health_clear;
|
||||||
reg [7:0] rct_cutoff, max_dead;
|
reg [7:0] rct_cutoff, max_dead;
|
||||||
|
|
@ -96,7 +106,7 @@ module strix_core #(
|
||||||
|
|
||||||
entropy_generator #(.RINGS(RINGS_PER_GEN)) generator (
|
entropy_generator #(.RINGS(RINGS_PER_GEN)) generator (
|
||||||
.clk(clk), .enable(gen_enable[g]), .temporal(temporal), .ring_select(ring_select),
|
.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_enhanced(ring_enhanced[g]), .ring_tap(ring_tap[g]),
|
||||||
.ring_raw_tap(ring_raw_tap[g]), .ring_samples(ring_samples)
|
.ring_raw_tap(ring_raw_tap[g]), .ring_samples(ring_samples)
|
||||||
);
|
);
|
||||||
|
|
@ -116,6 +126,33 @@ module strix_core #(
|
||||||
end
|
end
|
||||||
endgenerate
|
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
|
// Source selection
|
||||||
|
|
||||||
reg [3:0] source_a, source_b;
|
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_HIGH: tx_byte = {5'd0, apt_cutoff[10:8]};
|
||||||
REG_APT_LOW: tx_byte = apt_cutoff[7:0];
|
REG_APT_LOW: tx_byte = apt_cutoff[7:0];
|
||||||
REG_MAX_DEAD: tx_byte = max_dead;
|
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);
|
default: tx_byte = gen_register(reg_addr);
|
||||||
endcase
|
endcase
|
||||||
end
|
end
|
||||||
|
|
@ -232,6 +273,7 @@ module strix_core #(
|
||||||
capture_start <= 1'b0;
|
capture_start <= 1'b0;
|
||||||
freq_start <= 1'b0;
|
freq_start <= 1'b0;
|
||||||
health_clear <= 1'b0;
|
health_clear <= 1'b0;
|
||||||
|
stats_restart <= 1'b0;
|
||||||
if (reset) begin
|
if (reset) begin
|
||||||
gen_enable <= {GENERATORS{1'b1}};
|
gen_enable <= {GENERATORS{1'b1}};
|
||||||
temporal <= 4'd5;
|
temporal <= 4'd5;
|
||||||
|
|
@ -245,11 +287,19 @@ module strix_core #(
|
||||||
apt_cutoff <= 11'd824; // same, 1024-bit window
|
apt_cutoff <= 11'd824; // same, 1024-bit window
|
||||||
max_dead <= 8'd8; // measured sizing tolerates about 17
|
max_dead <= 8'd8; // measured sizing tolerates about 17
|
||||||
health_clear <= 1'b1;
|
health_clear <= 1'b1;
|
||||||
|
trial_ms <= 16'd200;
|
||||||
|
stats_von_neumann <= 1'b0;
|
||||||
|
stats_index <= 8'd0;
|
||||||
is_read <= 1'b0;
|
is_read <= 1'b0;
|
||||||
reg_addr <= 7'd0;
|
reg_addr <= 7'd0;
|
||||||
bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
||||||
prefetch_addr <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
prefetch_addr <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
||||||
end else begin
|
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)
|
if (!selected)
|
||||||
prefetch_addr <= bytes_read;
|
prefetch_addr <= bytes_read;
|
||||||
else if (tx_loaded && reading_data)
|
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_HIGH: apt_cutoff[10:8] <= rx_byte[2:0];
|
||||||
REG_APT_LOW: apt_cutoff[7:0] <= rx_byte;
|
REG_APT_LOW: apt_cutoff[7:0] <= rx_byte;
|
||||||
REG_MAX_DEAD: max_dead <= 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
|
REG_CONTROL: begin
|
||||||
if (rx_byte[0]) capture_start <= 1'b1;
|
if (rx_byte[0]) capture_start <= 1'b1;
|
||||||
if (rx_byte[0] || rx_byte[1]) bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
if (rx_byte[0] || rx_byte[1]) bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Testbench for strix_core: register access, generator XOR logic checked against the sampled
|
// Testbench for strix_core: register access, generator XOR logic checked against the sampled
|
||||||
// ring outputs, captures read back across transactions, generator enable, the frequency
|
// ring outputs, captures read back across transactions, generator enable, the frequency
|
||||||
// counter, and the health monitors with injected faults. Rings are replaced by the SIM model in ring_osc.v.
|
// counter, the health monitors with injected faults, and the statistics snapshots against a
|
||||||
|
// reference model. Rings are replaced by the SIM model in ring_osc.v.
|
||||||
`timescale 1ns / 1ps
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
module tb_strix_core;
|
module tb_strix_core;
|
||||||
|
|
@ -15,7 +16,11 @@ module tb_strix_core;
|
||||||
REG_DECIM_LOW = 7'h0A, REG_CONTROL = 7'h0B, REG_STATUS = 7'h0C,
|
REG_DECIM_LOW = 7'h0A, REG_CONTROL = 7'h0B, REG_STATUS = 7'h0C,
|
||||||
REG_DATA = 7'h10, REG_RING_SELECT = 7'h11, REG_FREQ_COUNT0 = 7'h15,
|
REG_DATA = 7'h10, REG_RING_SELECT = 7'h11, REG_FREQ_COUNT0 = 7'h15,
|
||||||
REG_GEN_ENABLE = 7'h16, REG_TEMPORAL = 7'h17, REG_FREQ_GEN = 7'h18,
|
REG_GEN_ENABLE = 7'h16, REG_TEMPORAL = 7'h17, REG_FREQ_GEN = 7'h18,
|
||||||
REG_HEALTHY = 7'h20, REG_HEALTH_CTRL = 7'h21;
|
REG_HEALTHY = 7'h20, REG_HEALTH_CTRL = 7'h21, REG_TRIAL_LOW = 7'h27,
|
||||||
|
REG_STATS_CTRL = 7'h28, REG_STATS = 7'h40;
|
||||||
|
localparam SNAPSHOT_BYTES = 72;
|
||||||
|
localparam TICK_CYCLES = 1500; // 1 "ms" = 12.5 us: a 10 "ms" trial outlasts a 72-byte read
|
||||||
|
localparam TRIAL_BITS = 10 * TICK_CYCLES / 5; // 10 "ms" trials at temporal 5
|
||||||
|
|
||||||
// Per-generator health registers
|
// Per-generator health registers
|
||||||
function [6:0] gen_reg(input integer generator, input integer index);
|
function [6:0] gen_reg(input integer generator, input integer index);
|
||||||
|
|
@ -31,7 +36,7 @@ module tb_strix_core;
|
||||||
wire miso, done;
|
wire miso, done;
|
||||||
|
|
||||||
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH), .FREQ_GATE_BITS(14), .RINGS_PER_GEN(RINGS),
|
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH), .FREQ_GATE_BITS(14), .RINGS_PER_GEN(RINGS),
|
||||||
.DEAD_WINDOW_BITS(6)) dut (
|
.DEAD_WINDOW_BITS(6), .TICK_CYCLES(TICK_CYCLES)) dut (
|
||||||
.clk(clk), .reset(reset),
|
.clk(clk), .reset(reset),
|
||||||
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
|
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
|
||||||
.capture_done(done)
|
.capture_done(done)
|
||||||
|
|
@ -80,6 +85,40 @@ module tb_strix_core;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// Reference model of statistics channel 0, mirroring stats_block without von Neumann:
|
||||||
|
// counts restart at each trial end, and the bit arriving in that cycle opens the new trial.
|
||||||
|
reg [31:0] model_bits = 0, model_ones = 0, model_agree = 0;
|
||||||
|
reg [31:0] model_latched_bits, model_latched_ones, model_latched_agree;
|
||||||
|
reg model_previous, model_has_previous = 1'b0;
|
||||||
|
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (dut.stats_restart) begin
|
||||||
|
model_bits <= 0; model_ones <= 0; model_agree <= 0; model_has_previous <= 1'b0;
|
||||||
|
end else begin
|
||||||
|
if (dut.trial_end) begin
|
||||||
|
model_latched_bits <= model_bits;
|
||||||
|
model_latched_ones <= model_ones;
|
||||||
|
model_latched_agree <= model_agree;
|
||||||
|
end
|
||||||
|
if (dut.stats.valid) begin
|
||||||
|
model_bits <= (dut.trial_end ? 0 : model_bits) + 1;
|
||||||
|
model_ones <= (dut.trial_end ? 0 : model_ones) + dut.stats.bits[0];
|
||||||
|
model_agree <= (dut.trial_end ? 0 : model_agree)
|
||||||
|
+ (model_has_previous && dut.stats.bits[0] == model_previous);
|
||||||
|
model_previous <= dut.stats.bits[0];
|
||||||
|
model_has_previous <= 1'b1;
|
||||||
|
end else if (dut.trial_end) begin
|
||||||
|
model_bits <= 0; model_ones <= 0; model_agree <= 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
reg [7:0] snapshot [0:SNAPSHOT_BYTES-1];
|
||||||
|
|
||||||
|
function [31:0] snapshot_word(input integer index);
|
||||||
|
snapshot_word = {snapshot[index], snapshot[index+1], snapshot[index+2], snapshot[index+3]};
|
||||||
|
endfunction
|
||||||
|
|
||||||
task spi_byte(input [7:0] out, output [7:0] in);
|
task spi_byte(input [7:0] out, output [7:0] in);
|
||||||
integer i;
|
integer i;
|
||||||
begin
|
begin
|
||||||
|
|
@ -169,8 +208,27 @@ module tb_strix_core;
|
||||||
end
|
end
|
||||||
endtask
|
endtask
|
||||||
|
|
||||||
reg [7:0] v;
|
task read_snapshot;
|
||||||
integer changes, polls;
|
reg [7:0] dummy;
|
||||||
|
integer i;
|
||||||
|
begin
|
||||||
|
ss_n = 1'b0; #SCK_HALF;
|
||||||
|
spi_byte({1'b1, REG_STATS}, dummy);
|
||||||
|
for (i = 0; i < SNAPSHOT_BYTES; i = i + 1) spi_byte(8'h00, snapshot[i]);
|
||||||
|
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task wait_trial_end;
|
||||||
|
begin
|
||||||
|
@(posedge dut.trial_end);
|
||||||
|
#200;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
reg [7:0] v;
|
||||||
|
integer changes, polls;
|
||||||
|
reg [31:0] sequence_seen;
|
||||||
|
|
||||||
initial begin
|
initial begin
|
||||||
#100 reset = 1'b0;
|
#100 reset = 1'b0;
|
||||||
|
|
@ -179,7 +237,7 @@ module tb_strix_core;
|
||||||
// Identification
|
// Identification
|
||||||
read_reg(REG_ID0, v); expect(v, "S", "ID0");
|
read_reg(REG_ID0, v); expect(v, "S", "ID0");
|
||||||
read_reg(REG_ID3, v); expect(v, "X", "ID3");
|
read_reg(REG_ID3, v); expect(v, "X", "ID3");
|
||||||
read_reg(REG_VERSION, v); expect(v, 8'h04, "VERSION");
|
read_reg(REG_VERSION, v); expect(v, 8'h05, "VERSION");
|
||||||
read_reg(REG_GENERATORS, v); expect(v, 8'd4, "GENERATORS");
|
read_reg(REG_GENERATORS, v); expect(v, 8'd4, "GENERATORS");
|
||||||
read_reg(REG_RINGS, v); expect(v, RINGS, "RINGS");
|
read_reg(REG_RINGS, v); expect(v, RINGS, "RINGS");
|
||||||
read_reg(REG_TEMPORAL, v); expect(v, 8'd5, "TEMPORAL");
|
read_reg(REG_TEMPORAL, v); expect(v, 8'd5, "TEMPORAL");
|
||||||
|
|
@ -259,6 +317,47 @@ module tb_strix_core;
|
||||||
read_reg(gen_reg(3, FLAGS), v); expect(v, 8'h00, "FLAGS g3 disabled");
|
read_reg(gen_reg(3, FLAGS), v); expect(v, 8'h00, "FLAGS g3 disabled");
|
||||||
write_reg(REG_GEN_ENABLE, 8'h0F);
|
write_reg(REG_GEN_ENABLE, 8'h0F);
|
||||||
|
|
||||||
|
// Statistics: 10 "ms" trials, temporal 5 -> TRIAL_BITS bits per channel per trial
|
||||||
|
write_reg(REG_TEMPORAL, 8'd5);
|
||||||
|
write_reg(REG_TRIAL_LOW, 8'd10);
|
||||||
|
write_reg(REG_STATS_CTRL, 8'h02); // restart
|
||||||
|
wait_trial_end;
|
||||||
|
wait_trial_end;
|
||||||
|
read_snapshot;
|
||||||
|
if (snapshot_word(0) !== snapshot_word(SNAPSHOT_BYTES - 4)) begin
|
||||||
|
$display("FAIL torn snapshot"); errors = errors + 1;
|
||||||
|
end
|
||||||
|
expect(snapshot_word(0), 8'd1, "trial sequence");
|
||||||
|
expect(snapshot[4], 8'h0F, "trial healthy");
|
||||||
|
expect(snapshot[7], 8'd10, "trial ms");
|
||||||
|
if (snapshot_word(8) !== model_latched_bits || snapshot_word(12) !== model_latched_ones
|
||||||
|
|| snapshot_word(16) !== model_latched_agree) begin
|
||||||
|
$display("FAIL channel 0: %0d/%0d/%0d, model %0d/%0d/%0d", snapshot_word(8), snapshot_word(12),
|
||||||
|
snapshot_word(16), model_latched_bits, model_latched_ones, model_latched_agree);
|
||||||
|
errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (snapshot_word(8) < TRIAL_BITS - 1 || snapshot_word(8) > TRIAL_BITS + 1) begin
|
||||||
|
$display("FAIL channel 0 bit count %0d, expected about %0d", snapshot_word(8), TRIAL_BITS);
|
||||||
|
errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (snapshot_word(8 + 48) !== snapshot_word(8)) begin
|
||||||
|
$display("FAIL combined channel bit count differs"); errors = errors + 1;
|
||||||
|
end
|
||||||
|
if (snapshot_word(12) == 0 || snapshot_word(12) == snapshot_word(8)) begin
|
||||||
|
$display("FAIL channel 0 ones count %0d is degenerate", snapshot_word(12)); errors = errors + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Von Neumann keeps about a quarter of the bits
|
||||||
|
write_reg(REG_STATS_CTRL, 8'h03);
|
||||||
|
wait_trial_end;
|
||||||
|
wait_trial_end;
|
||||||
|
read_snapshot;
|
||||||
|
expect(snapshot[5], 8'h01, "trial von Neumann");
|
||||||
|
if (snapshot_word(8) < TRIAL_BITS / 10 || snapshot_word(8) > TRIAL_BITS * 45 / 100) begin
|
||||||
|
$display("FAIL von Neumann bit count %0d", snapshot_word(8)); errors = errors + 1;
|
||||||
|
end
|
||||||
|
write_reg(REG_STATS_CTRL, 8'h00);
|
||||||
|
|
||||||
if (xor_checks < 100) begin
|
if (xor_checks < 100) begin
|
||||||
$display("FAIL only %0d temporal XOR checks ran", xor_checks);
|
$display("FAIL only %0d temporal XOR checks ran", xor_checks);
|
||||||
errors = errors + 1;
|
errors = errors + 1;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ set_global_assignment -name VERILOG_FILE ../rtl/sys_pll.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/strix_core.v
|
set_global_assignment -name VERILOG_FILE ../rtl/strix_core.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/entropy_generator.v
|
set_global_assignment -name VERILOG_FILE ../rtl/entropy_generator.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/health_monitor.v
|
set_global_assignment -name VERILOG_FILE ../rtl/health_monitor.v
|
||||||
|
set_global_assignment -name VERILOG_FILE ../rtl/stats_block.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/freq_counter.v
|
set_global_assignment -name VERILOG_FILE ../rtl/freq_counter.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/ring_osc.v
|
set_global_assignment -name VERILOG_FILE ../rtl/ring_osc.v
|
||||||
set_global_assignment -name VERILOG_FILE ../rtl/raw_capture.v
|
set_global_assignment -name VERILOG_FILE ../rtl/raw_capture.v
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Subcommands:
|
||||||
sweep capture sources at increasing decimation k and fit the accumulated jitter
|
sweep capture sources at increasing decimation k and fit the accumulated jitter
|
||||||
output run the onod3000 test suite on every generator's processed output (256 KB each)
|
output run the onod3000 test suite on every generator's processed output (256 KB each)
|
||||||
health show the health monitors (NIST repetition count / adaptive proportion, dead rings)
|
health show the health monitors (NIST repetition count / adaptive proportion, dead rings)
|
||||||
|
stats follow the live statistics trial by trial, with rolling and cumulative windows
|
||||||
pairs capture the same ring index in two generators simultaneously and test for correlation
|
pairs capture the same ring index in two generators simultaneously and test for correlation
|
||||||
|
|
||||||
Entropy per bit is estimated from conditional block entropies and converted to a
|
Entropy per bit is estimated from conditional block entropies and converted to a
|
||||||
|
|
@ -43,6 +44,8 @@ REG_RING_SELECT, REG_FREQ_COUNT3 = 0x11, 0x12
|
||||||
REG_GEN_ENABLE, REG_TEMPORAL, REG_FREQ_GEN = 0x16, 0x17, 0x18
|
REG_GEN_ENABLE, REG_TEMPORAL, REG_FREQ_GEN = 0x16, 0x17, 0x18
|
||||||
REG_HEALTHY, REG_HEALTH_CTRL, REG_RCT_CUTOFF, REG_APT_HIGH, REG_APT_LOW, REG_MAX_DEAD = 0x20, 0x21, 0x22, 0x23, 0x24, 0x25
|
REG_HEALTHY, REG_HEALTH_CTRL, REG_RCT_CUTOFF, REG_APT_HIGH, REG_APT_LOW, REG_MAX_DEAD = 0x20, 0x21, 0x22, 0x23, 0x24, 0x25
|
||||||
REG_GEN_HEALTH = 0x30 # + 4g: flags, RCT events, APT events, dead rings
|
REG_GEN_HEALTH = 0x30 # + 4g: flags, RCT events, APT events, dead rings
|
||||||
|
REG_TRIAL_HIGH, REG_TRIAL_LOW, REG_STATS_CTRL = 0x26, 0x27, 0x28
|
||||||
|
CHANNEL_NAMES = ["g0", "g1", "g2", "g3", "combined"]
|
||||||
CONTROL_START_FREQ = 0x04
|
CONTROL_START_FREQ = 0x04
|
||||||
STATUS_FREQ_VALID = 0x08
|
STATUS_FREQ_VALID = 0x08
|
||||||
|
|
||||||
|
|
@ -76,8 +79,8 @@ class Board:
|
||||||
while self.serial.readline():
|
while self.serial.readline():
|
||||||
pass
|
pass
|
||||||
self.serial.timeout = 200
|
self.serial.timeout = 200
|
||||||
if not identity.startswith("STRX v4"):
|
if not identity.startswith("STRX v5"):
|
||||||
sys.exit(f"unexpected board identity: {identity!r} (needs the v4 FPGA image)")
|
sys.exit(f"unexpected board identity: {identity!r} (needs the v5 FPGA image)")
|
||||||
self.generators = self.read(REG_GENERATORS)
|
self.generators = self.read(REG_GENERATORS)
|
||||||
self.rings = self.read(REG_RINGS)
|
self.rings = self.read(REG_RINGS)
|
||||||
print(f"board: {identity}, {self.generators} generators x {self.rings} rings")
|
print(f"board: {identity}, {self.generators} generators x {self.rings} rings")
|
||||||
|
|
@ -123,6 +126,17 @@ class Board:
|
||||||
return count * 256 / gate_seconds
|
return count * 256 / gate_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def read_snapshot(board: "Board") -> dict | None:
|
||||||
|
"""Latest trial snapshot (layout in fpga/rtl/stats_block.v), or None if the read was torn."""
|
||||||
|
raw = bytes.fromhex(board.command("stats"))
|
||||||
|
word = lambda i: int.from_bytes(raw[i:i + 4], "big")
|
||||||
|
if word(0) != word(len(raw) - 4):
|
||||||
|
return None
|
||||||
|
channels = [(word(8 + 12 * c), word(12 + 12 * c), word(16 + 12 * c)) for c in range((len(raw) - 12) // 12)]
|
||||||
|
return {"sequence": word(0), "healthy": raw[4], "von_neumann": bool(raw[5] & 1),
|
||||||
|
"trial_ms": raw[6] << 8 | raw[7], "channels": channels}
|
||||||
|
|
||||||
|
|
||||||
def cached_capture(board: Board | None, path: Path, *args) -> bytes | None:
|
def cached_capture(board: Board | None, path: Path, *args) -> bytes | None:
|
||||||
if board:
|
if board:
|
||||||
path.write_bytes(board.capture(*args))
|
path.write_bytes(board.capture(*args))
|
||||||
|
|
@ -317,6 +331,75 @@ def run_health(board: Board | None, args) -> None:
|
||||||
f"{rct:>4} {apt_events:>4} {dead:>4}")
|
f"{rct:>4} {apt_events:>4} {dead:>4}")
|
||||||
|
|
||||||
|
|
||||||
|
def z_scores(bits: int, ones: int, agreements: int) -> tuple[float, float]:
|
||||||
|
"""Bias z = (2*ones - n)/sqrt(n); lag-1 autocorrelation z = (2*agreements - n)/sqrt(n)."""
|
||||||
|
if bits == 0:
|
||||||
|
return float("nan"), float("nan")
|
||||||
|
root = math.sqrt(bits)
|
||||||
|
return (2 * ones - bits) / root, (2 * agreements - bits) / root
|
||||||
|
|
||||||
|
|
||||||
|
def run_stats(board: Board | None, args) -> None:
|
||||||
|
if board is None:
|
||||||
|
sys.exit("stats needs the board")
|
||||||
|
board.write(REG_TRIAL_HIGH, args.trial_ms >> 8)
|
||||||
|
board.write(REG_TRIAL_LOW, args.trial_ms & 0xFF)
|
||||||
|
board.write(REG_STATS_CTRL, (1 if args.von_neumann else 0) | 2) # bit 1 restarts trials
|
||||||
|
# The snapshot still holds the last trial from before the restart until the first new
|
||||||
|
# trial ends; wait for the sequence to start over.
|
||||||
|
while (snapshot := read_snapshot(board)) is None or snapshot["sequence"] > 1:
|
||||||
|
time.sleep(args.trial_ms / 4000)
|
||||||
|
|
||||||
|
count = len(CHANNEL_NAMES)
|
||||||
|
totals = np.zeros((count, 3), dtype=np.int64)
|
||||||
|
recent: list[np.ndarray] = []
|
||||||
|
trial_z = [[] for _ in range(count)]
|
||||||
|
trial_ac = [[] for _ in range(count)]
|
||||||
|
last_sequence, missed, torn = -1, 0, 0
|
||||||
|
|
||||||
|
print(f"trials of {args.trial_ms} ms, von Neumann {'on' if args.von_neumann else 'off'}, "
|
||||||
|
f"rolling window {args.rolling} trials")
|
||||||
|
print(f"{'trial':>5} {'healthy':>7} " + " ".join(f"{n + ' z':>11}" for n in CHANNEL_NAMES)
|
||||||
|
+ f" {'rolling':>8} {'walk':>9}")
|
||||||
|
while len(trial_z[0]) < args.trials:
|
||||||
|
snapshot = read_snapshot(board)
|
||||||
|
if snapshot is None:
|
||||||
|
torn += 1
|
||||||
|
continue
|
||||||
|
sequence = snapshot["sequence"]
|
||||||
|
if sequence == last_sequence:
|
||||||
|
time.sleep(args.trial_ms / 4000)
|
||||||
|
continue
|
||||||
|
if last_sequence >= 0 and sequence != last_sequence + 1:
|
||||||
|
missed += sequence - last_sequence - 1
|
||||||
|
last_sequence = sequence
|
||||||
|
|
||||||
|
counts = np.array(snapshot["channels"], dtype=np.int64)
|
||||||
|
totals += counts
|
||||||
|
recent = (recent + [counts])[-args.rolling:]
|
||||||
|
rolling = np.sum(recent, axis=0)
|
||||||
|
zs = []
|
||||||
|
for c in range(count):
|
||||||
|
z, ac = z_scores(*counts[c])
|
||||||
|
trial_z[c].append(z)
|
||||||
|
trial_ac[c].append(ac)
|
||||||
|
zs.append(z)
|
||||||
|
rolling_z, _ = z_scores(*rolling[count - 1])
|
||||||
|
walk = 2 * totals[count - 1][1] - totals[count - 1][0]
|
||||||
|
print(f"{sequence:>5} {snapshot['healthy']:>7x} " + " ".join(f"{z:>11.2f}" for z in zs)
|
||||||
|
+ f" {rolling_z:>8.2f} {walk:>9d}")
|
||||||
|
|
||||||
|
print(f"\n{len(trial_z[0])} trials, {missed} missed, {torn} torn reads (retried)")
|
||||||
|
print("baseline: trial z-scores should have mean about 0 and standard deviation about 1\n")
|
||||||
|
print(f"{'channel':<9} {'bits':>12} {'cum. z':>7} {'trial z mean':>12} {'sd':>5} {'|z|>3':>5}"
|
||||||
|
f" {'autocorr z mean':>15} {'sd':>5}")
|
||||||
|
for c in range(count):
|
||||||
|
z_total, _ = z_scores(*totals[c])
|
||||||
|
z, ac = np.array(trial_z[c]), np.array(trial_ac[c])
|
||||||
|
print(f"{CHANNEL_NAMES[c]:<9} {totals[c][0]:>12d} {z_total:>7.2f} {z.mean():>12.3f} {z.std():>5.2f}"
|
||||||
|
f" {int(np.sum(np.abs(z) > 3)):>5d} {ac.mean():>15.3f} {ac.std():>5.2f}")
|
||||||
|
|
||||||
|
|
||||||
def run_pairs(board: Board | None, args) -> None:
|
def run_pairs(board: Board | None, args) -> None:
|
||||||
generators = board.generators if board else 4
|
generators = board.generators if board else 4
|
||||||
rng = np.random.default_rng(0)
|
rng = np.random.default_rng(0)
|
||||||
|
|
@ -366,6 +449,11 @@ def main() -> None:
|
||||||
output.add_argument("--bursts", type=int, default=8, help="32 KB captures per generator")
|
output.add_argument("--bursts", type=int, default=8, help="32 KB captures per generator")
|
||||||
output.add_argument("--onod", type=Path, default=Path(".build/onod3000/release/onod3000"))
|
output.add_argument("--onod", type=Path, default=Path(".build/onod3000/release/onod3000"))
|
||||||
output.add_argument("--verbose", action="store_true", help="print the full onod3000 report")
|
output.add_argument("--verbose", action="store_true", help="print the full onod3000 report")
|
||||||
|
stats = sub.add_parser("stats")
|
||||||
|
stats.add_argument("--trials", type=int, default=100)
|
||||||
|
stats.add_argument("--trial-ms", type=int, default=200)
|
||||||
|
stats.add_argument("--rolling", type=int, default=10, help="trials in the rolling window")
|
||||||
|
stats.add_argument("--von-neumann", action="store_true")
|
||||||
health = sub.add_parser("health")
|
health = sub.add_parser("health")
|
||||||
health.add_argument("--clear", action="store_true", help="clear failures and event counters first")
|
health.add_argument("--clear", action="store_true", help="clear failures and event counters first")
|
||||||
pairs = sub.add_parser("pairs")
|
pairs = sub.add_parser("pairs")
|
||||||
|
|
@ -376,7 +464,7 @@ def main() -> None:
|
||||||
args.out.mkdir(parents=True, exist_ok=True)
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
board = None if args.analyze_only else Board(args.port)
|
board = None if args.analyze_only else Board(args.port)
|
||||||
handlers = {"freq": run_freq, "rings": run_rings, "sweep": run_sweep,
|
handlers = {"freq": run_freq, "rings": run_rings, "sweep": run_sweep,
|
||||||
"output": run_output, "health": run_health, "pairs": run_pairs}
|
"output": run_output, "health": run_health, "stats": run_stats, "pairs": run_pairs}
|
||||||
handlers[args.command](board, args)
|
handlers[args.command](board, args)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue