feat: add two-path bias amplifier with random walk and majority vote stages (FPGA v6)

This commit is contained in:
randogoth 2026-10-25 14:59:27 +02:00
parent 5305d66102
commit 8541cce59c
11 changed files with 741 additions and 58 deletions

View file

@ -8,6 +8,7 @@
// cap start a capture with the current settings and send it as
// "DATA <n>\n" followed by n raw bytes
// stats print the latest statistics trial snapshot as hex
// amp print the latest bias amplifier trial snapshot as hex
#include <SPI.h>
#include <VidorBoot.h>
@ -25,6 +26,7 @@ enum : uint8_t {
REG_STATUS = 0x0C,
REG_DATA = 0x10,
REG_STATS = 0x40,
REG_AMP = 0x42,
};
const uint8_t READ = 0x80;
@ -32,7 +34,8 @@ const uint8_t CONTROL_START_CAPTURE = 0x01;
const uint8_t STATUS_CAPTURE_DONE = 0x02;
const size_t CAPTURE_BYTES = 32768;
const size_t CHUNK_BYTES = 512;
const size_t SNAPSHOT_BYTES = 72;
const size_t STATS_SNAPSHOT_BYTES = 72;
const size_t AMP_SNAPSHOT_BYTES = 44;
VidorUtils fpga;
@ -92,9 +95,9 @@ void capture() {
}
}
void printSnapshot() {
beginTransaction(READ | REG_STATS);
for (size_t i = 0; i < SNAPSHOT_BYTES; i++) {
void printSnapshot(uint8_t reg, size_t length) {
beginTransaction(READ | reg);
for (size_t i = 0; i < length; i++) {
uint8_t value = strixSpi.transfer(0);
if (value < 0x10) Serial.print('0');
Serial.print(value, HEX);
@ -131,7 +134,9 @@ void handleCommand(String line) {
} else if (command == "cap") {
capture();
} else if (command == "stats") {
printSnapshot();
printSnapshot(REG_STATS, STATS_SNAPSHOT_BYTES);
} else if (command == "amp") {
printSnapshot(REG_AMP, AMP_SNAPSHOT_BYTES);
} else if (line.length() > 0) {
Serial.println("ERR unknown or malformed command");
}

View file

@ -25,8 +25,10 @@
],
"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/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"
"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/bias_amplifier.v fpga/rtl/amp_path.v fpga/rtl/amp_stage.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",
"iverilog -g2005 -o /tmp/strix-sim/tb_amp_stage fpga/sim/tb_amp_stage.v fpga/rtl/amp_stage.v",
"vvp -n /tmp/strix-sim/tb_amp_stage"
],
"fpga": [
"cd fpga/src && quartus_sh --flow compile strix"

109
fpga/rtl/amp_path.v Normal file
View file

@ -0,0 +1,109 @@
// One amplifier path: the generator streams are merged into one stream, which then passes
// three stages in series. Merging before amplifying is equivalent to amplifying each stream
// with the same first stage and merging after (the random walk is distributive), and needs a
// third of the stages. Amplified bits are tallied per sub-trial and per trial.
module amp_path #(
parameter GENERATORS = 4
) (
input wire clk,
input wire restart,
input wire [GENERATORS-1:0] in_valid,
input wire [GENERATORS-1:0] in_bit,
input wire [2:0] stage_mode,
input wire [47:0] stage_param, // stage s at [16*s +: 16]
input wire sub_end, // pulse: close the current sub-trial
input wire [3:0] sub_index, // sub-trial being closed
input wire trial_end, // pulse: start a new trial (after the last sub_end)
output reg [31:0] trial_bits, // amplified bits in the current trial
output reg [31:0] trial_ones,
output reg [15:0] votes, // bit i: sub-trial i voted 1
output reg [15:0] voted, // bit i: sub-trial i had a majority
output reg overflow // a merge input arrived before the last was taken
);
// Merge: each generator's latest bit waits in its own slot; one slot is drained per clock,
// lowest index first. Generators deliver at most one bit per `temporal` (>= 1, default 5)
// clocks in lockstep, so with up to 4 generators the slots never overflow at temporal >= 4
// (flagged if they do).
reg [GENERATORS-1:0] pending, pending_bit, take;
reg merged_valid, merged_bit;
integer i;
always @(*) begin
take = {GENERATORS{1'b0}};
for (i = 0; i < GENERATORS; i = i + 1)
if (pending[i] && take == {GENERATORS{1'b0}}) take[i] = 1'b1;
end
always @(posedge clk) begin
merged_valid <= |take && !restart;
for (i = 0; i < GENERATORS; i = i + 1)
if (take[i]) merged_bit <= pending_bit[i];
if (restart) begin
pending <= {GENERATORS{1'b0}};
overflow <= 1'b0;
end else begin
for (i = 0; i < GENERATORS; i = i + 1) begin
if (in_valid[i]) begin
if (pending[i] && !take[i]) overflow <= 1'b1;
pending[i] <= 1'b1;
pending_bit[i] <= in_bit[i];
end else if (take[i]) begin
pending[i] <= 1'b0;
end
end
end
end
// Stages
wire first_valid, first_bit, second_valid, second_bit, out_valid, out_bit;
amp_stage first (
.clk(clk), .restart(restart), .mode(stage_mode[0]), .param(stage_param[15:0]),
.in_valid(merged_valid), .in_bit(merged_bit), .out_valid(first_valid), .out_bit(first_bit)
);
amp_stage second (
.clk(clk), .restart(restart), .mode(stage_mode[1]), .param(stage_param[31:16]),
.in_valid(first_valid), .in_bit(first_bit), .out_valid(second_valid), .out_bit(second_bit)
);
amp_stage third (
.clk(clk), .restart(restart), .mode(stage_mode[2]), .param(stage_param[47:32]),
.in_valid(second_valid), .in_bit(second_bit), .out_valid(out_valid), .out_bit(out_bit)
);
// Tallies. A bit arriving in a sub_end or trial_end cycle counts towards the next one, so
// the trial count and the sum of its sub-trial counts can differ by one bit.
reg [23:0] sub_bits, sub_ones;
always @(posedge clk) begin
if (restart || trial_end) begin
trial_bits <= out_valid ? 32'd1 : 32'd0;
trial_ones <= (out_valid && out_bit) ? 32'd1 : 32'd0;
votes <= 16'd0;
voted <= 16'd0;
end else if (out_valid) begin
trial_bits <= trial_bits + 32'd1;
trial_ones <= trial_ones + out_bit;
end
if (restart || sub_end) begin
sub_bits <= out_valid ? 24'd1 : 24'd0;
sub_ones <= (out_valid && out_bit) ? 24'd1 : 24'd0;
end else if (out_valid) begin
sub_bits <= sub_bits + 24'd1;
sub_ones <= sub_ones + out_bit;
end
if (!restart && sub_end && sub_bits != 0 && {sub_ones, 1'b0} != sub_bits) begin
voted[sub_index] <= 1'b1;
votes[sub_index] <= {sub_ones, 1'b0} > sub_bits;
end
end
endmodule

48
fpga/rtl/amp_stage.v Normal file
View file

@ -0,0 +1,48 @@
// One bias amplifier stage. Both modes track the walk position d = ones - zeros.
//
// Random walk (mode 0): reaching d = +param outputs a 1, d = -param outputs a 0, and the walk
// restarts at 0. Stages compose: bounds n1 then n2 amplify like a single bound n1*n2.
//
// Majority vote (mode 1): every param input bits produce one output bit, 1 if d > 0 (use odd
// windows to avoid ties, which output 0).
//
// param = 1 passes bits through unchanged in both modes.
module amp_stage (
input wire clk,
input wire restart,
input wire mode, // 0 random walk, 1 majority vote
input wire [15:0] param, // bound or window, at least 1
input wire in_valid,
input wire in_bit,
output reg out_valid,
output reg out_bit
);
reg signed [16:0] position;
reg [15:0] seen;
wire signed [16:0] next_position = position + (in_bit ? 17'sd1 : -17'sd1);
wire signed [16:0] bound = {1'b0, param};
wire walk_done = next_position == bound || next_position == -bound;
wire vote_done = seen == param - 16'd1;
wire done = mode ? vote_done : walk_done;
always @(posedge clk) begin
out_valid <= 1'b0;
if (restart) begin
position <= 17'sd0;
seen <= 16'd0;
end else if (in_valid) begin
if (done) begin
out_valid <= 1'b1;
out_bit <= next_position > 0;
position <= 17'sd0;
seen <= 16'd0;
end else begin
position <= next_position;
seen <= seen + 16'd1;
end
end
end
endmodule

186
fpga/rtl/bias_amplifier.v Normal file
View file

@ -0,0 +1,186 @@
// Bias amplifier with two paths fed by the selected healthy generators:
// path 0 (bias): the generator bits themselves;
// path 1 (autocorrelation): each bit XNOR the previous bit of the same generator, so
// positive lag-1 autocorrelation becomes an excess of ones.
// An optional von Neumann extractor per generator runs before both paths.
//
// Trials consist of `subtrials` sub-trials of `sub_ms` milliseconds. Each sub-trial votes with
// the majority of its amplified bits (no vote on a tie or without bits); the trial result is
// the majority of the votes.
//
// Snapshot layout (big-endian), latched at each trial end:
// 0-3 trial sequence number
// 4 bits 3:0 generators used during the whole trial, bit 4 von Neumann during the
// whole trial, bit 5 merge overflow since restart (should never be set)
// 5 sub-trials per trial
// 6 sub-trial length in ms
// 7 0
// 8+16p path p: amplified bits (4), ones (4), votes (2), voted (2), vote count (1),
// result (1: bit 0 trial bit, bit 1 trial valid), 0 (2)
// 40-43 trial sequence number again; a mismatch with bytes 0-3 means a torn read
module bias_amplifier #(
parameter GENERATORS = 4,
parameter TICK_CYCLES = 120000 // clk cycles per millisecond
) (
input wire clk,
input wire reset,
input wire [GENERATORS-1:0] gen_bit,
input wire gen_valid, // all generators deliver in lockstep
input wire [GENERATORS-1:0] sources, // selected and healthy generators
input wire von_neumann,
input wire [2:0] stage_mode,
input wire [47:0] stage_param,
input wire [7:0] sub_ms,
input wire [3:0] subtrials,
input wire restart,
input wire [7:0] read_index,
output reg [7:0] read_byte, // one cycle after read_index
output reg trial_end
);
localparam SNAPSHOT_BYTES = 44;
wire start_over = reset || restart;
// Von Neumann (01 -> 0, 10 -> 1) and autocorrelation conversion per generator
reg [GENERATORS-1:0] pair_first, have_first, previous_bit, has_previous;
wire [GENERATORS-1:0] vn_valid = von_neumann ? (gen_valid ? have_first & (gen_bit ^ pair_first) : {GENERATORS{1'b0}})
: {GENERATORS{gen_valid}};
wire [GENERATORS-1:0] vn_bit = von_neumann ? pair_first : gen_bit;
wire [GENERATORS-1:0] bias_valid = vn_valid & sources;
wire [GENERATORS-1:0] ac_valid = vn_valid & has_previous & sources;
wire [GENERATORS-1:0] ac_bit = ~(vn_bit ^ previous_bit);
always @(posedge clk) begin
if (start_over || !von_neumann) begin
have_first <= {GENERATORS{1'b0}};
end else if (gen_valid) begin
have_first <= ~have_first;
pair_first <= gen_bit;
end
if (start_over) begin
has_previous <= {GENERATORS{1'b0}};
end else begin
has_previous <= has_previous | vn_valid;
previous_bit <= (vn_valid & vn_bit) | (~vn_valid & previous_bit);
end
end
// Sub-trial and trial timing
reg [16:0] tick_count;
reg [7:0] ms_count;
reg [3:0] sub_index;
reg sub_end;
wire tick = (tick_count == TICK_CYCLES - 1);
always @(posedge clk) begin
sub_end <= 1'b0;
trial_end <= 1'b0;
if (start_over) begin
tick_count <= 17'd0;
ms_count <= 8'd0;
sub_index <= 4'd0;
end else begin
tick_count <= tick ? 17'd0 : tick_count + 17'd1;
if (tick) begin
if (ms_count + 8'd1 >= sub_ms) begin
ms_count <= 8'd0;
sub_end <= 1'b1;
end else begin
ms_count <= ms_count + 8'd1;
end
end
// trial_end follows the last sub_end by one cycle, once its vote is registered.
if (sub_end) begin
if (sub_index + 4'd1 >= subtrials) begin
sub_index <= 4'd0;
trial_end <= 1'b1;
end else begin
sub_index <= sub_index + 4'd1;
end
end
end
end
// Paths
wire [31:0] trial_bits [0:1];
wire [31:0] trial_ones [0:1];
wire [15:0] votes [0:1];
wire [15:0] voted [0:1];
wire [1:0] overflow;
amp_path #(.GENERATORS(GENERATORS)) bias_path (
.clk(clk), .restart(start_over), .in_valid(bias_valid), .in_bit(vn_bit),
.stage_mode(stage_mode), .stage_param(stage_param),
.sub_end(sub_end), .sub_index(sub_index), .trial_end(trial_end),
.trial_bits(trial_bits[0]), .trial_ones(trial_ones[0]),
.votes(votes[0]), .voted(voted[0]), .overflow(overflow[0])
);
amp_path #(.GENERATORS(GENERATORS)) autocorrelation_path (
.clk(clk), .restart(start_over), .in_valid(ac_valid), .in_bit(ac_bit),
.stage_mode(stage_mode), .stage_param(stage_param),
.sub_end(sub_end), .sub_index(sub_index), .trial_end(trial_end),
.trial_bits(trial_bits[1]), .trial_ones(trial_ones[1]),
.votes(votes[1]), .voted(voted[1]), .overflow(overflow[1])
);
// Snapshot
function [4:0] count_ones(input [15:0] value);
integer b;
begin
count_ones = 5'd0;
for (b = 0; b < 16; b = b + 1) count_ones = count_ones + value[b];
end
endfunction
function [127:0] path_bytes(input [31:0] bits, input [31:0] ones, input [15:0] vote, input [15:0] cast);
reg [4:0] cast_count, one_votes;
reg valid, result;
begin
cast_count = count_ones(cast);
one_votes = count_ones(vote & cast);
valid = cast_count != 0 && {one_votes, 1'b0} != {1'b0, cast_count};
result = {one_votes, 1'b0} > {1'b0, cast_count};
path_bytes = {bits, ones, vote, cast, 3'd0, cast_count, 6'd0, valid, result, 16'd0};
end
endfunction
reg [31:0] sequence_number;
reg [GENERATORS-1:0] sources_during;
reg von_neumann_during;
reg [7:0] snapshot [0:SNAPSHOT_BYTES-1]; // byte 0 first, indexed directly
wire [8*SNAPSHOT_BYTES-1:0] snapshot_next = {
sequence_number,
2'd0, |overflow, von_neumann_during & von_neumann, sources_during & sources,
4'd0, subtrials, sub_ms, 8'd0,
path_bytes(trial_bits[0], trial_ones[0], votes[0], voted[0]),
path_bytes(trial_bits[1], trial_ones[1], votes[1], voted[1]),
sequence_number
};
integer k;
always @(posedge clk) begin
if (start_over) begin
sequence_number <= 32'd0;
sources_during <= sources;
von_neumann_during <= von_neumann;
end else if (trial_end) begin
for (k = 0; k < SNAPSHOT_BYTES; k = k + 1)
snapshot[k] <= snapshot_next[8*(SNAPSHOT_BYTES-1-k) +: 8];
sequence_number <= sequence_number + 32'd1;
sources_during <= sources;
von_neumann_during <= von_neumann;
end else begin
sources_during <= sources_during & sources;
von_neumann_during <= von_neumann_during & von_neumann;
end
read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[read_index] : 8'h00;
end
endmodule

View file

@ -86,9 +86,10 @@ module stats_block #(
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;
// The snapshot is assembled as one flat byte string (byte 0 in the top bits) and latched
// into a byte array, so reading it is a plain byte multiplexer on the index.
reg [8*SNAPSHOT_BYTES-1:0] snapshot_next;
reg [7:0] snapshot [0:SNAPSHOT_BYTES-1]; // byte 0 first, indexed directly
integer k;
always @(*) begin
@ -100,7 +101,7 @@ module stats_block #(
snapshot_next[31:0] = sequence_number;
end
integer c;
integer c, b;
always @(posedge clk) begin
if (reset || restart) begin
sequence_number <= 32'd0;
@ -129,7 +130,8 @@ module stats_block #(
end
end
if (trial_end) begin
snapshot <= snapshot_next;
for (b = 0; b < SNAPSHOT_BYTES; b = b + 1)
snapshot[b] <= snapshot_next[8*(SNAPSHOT_BYTES-1-b) +: 8];
sequence_number <= sequence_number + 32'd1;
healthy_during <= healthy;
von_neumann_during <= von_neumann;
@ -143,6 +145,6 @@ module stats_block #(
// 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;
read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[read_index] : 8'h00;
endmodule

View file

@ -1,5 +1,6 @@
// 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.
// bias amplifier, 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
@ -18,7 +19,8 @@
//
// 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.
// stats_block.v) byte by byte, starting at byte 0 in every transaction. AMP does the same for
// the bias amplifier's trial snapshot (layout in bias_amplifier.v).
module strix_core #(
parameter CAPTURE_ADDR_WIDTH = 15,
parameter FREQ_GATE_BITS = 24,
@ -35,7 +37,7 @@ module strix_core #(
output wire capture_done
);
localparam [7:0] VERSION = 8'h05;
localparam [7:0] VERSION = 8'h06;
localparam GENERATORS = 4;
localparam [6:0] REG_ID0 = 7'h00, // "STRX"
@ -74,7 +76,15 @@ module strix_core #(
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
REG_STATS = 7'h40, // burst read of the statistics trial snapshot
REG_AMP = 7'h42, // burst read of the amplifier trial snapshot
REG_AMP_CTRL = 7'h50, // bit 0 von Neumann on the amplifier path;
// write bit 1: restart the amplifier, sequence 0
REG_AMP_SOURCES = 7'h51, // generators feeding the amplifier (healthy only)
REG_AMP_SUB_MS = 7'h52, // sub-trial length in ms
REG_AMP_SUBS = 7'h53, // sub-trials per trial, 1-15
REG_AMP_STAGE = 7'h54; // stage s at 0x54 + 3s: mode (0 random walk,
// 1 majority vote), parameter high, parameter low
// 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,
@ -146,6 +156,26 @@ module strix_core #(
stats_valid <= out_valid[0];
end
// Bias amplifier, fed from the same registered generator outputs
reg [GENERATORS-1:0] amp_sources;
reg amp_von_neumann, amp_restart;
reg [7:0] amp_sub_ms;
reg [3:0] amp_subtrials;
reg [2:0] amp_stage_mode;
reg [47:0] amp_stage_param;
reg [7:0] amp_index;
wire [7:0] amp_byte;
wire amp_trial_end;
bias_amplifier #(.GENERATORS(GENERATORS), .TICK_CYCLES(TICK_CYCLES)) amplifier (
.clk(clk), .reset(reset), .gen_bit(stats_bits[GENERATORS-1:0]), .gen_valid(stats_valid),
.sources(amp_sources & stats_healthy), .von_neumann(amp_von_neumann),
.stage_mode(amp_stage_mode), .stage_param(amp_stage_param),
.sub_ms(amp_sub_ms), .subtrials(amp_subtrials), .restart(amp_restart),
.read_index(amp_index), .read_byte(amp_byte), .trial_end(amp_trial_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),
@ -196,7 +226,7 @@ module strix_core #(
wire selected, rx_valid, rx_is_first, tx_loaded;
wire [7:0] rx_byte;
reg [7:0] tx_byte;
reg [7:0] tx_byte, tx_byte_next;
spi_slave spi (
.clk(clk), .reset(reset),
@ -233,47 +263,67 @@ module strix_core #(
always @(*) begin
case (reg_addr)
REG_ID0: tx_byte = "S";
REG_ID1: tx_byte = "T";
REG_ID2: tx_byte = "R";
REG_ID3: tx_byte = "X";
REG_VERSION: tx_byte = VERSION;
REG_FREQ_GATE: tx_byte = FREQ_GATE_BITS;
REG_GENERATORS: tx_byte = GENERATORS;
REG_RINGS: tx_byte = RINGS_PER_GEN;
REG_SOURCE_A: tx_byte = {4'd0, source_a};
REG_DECIM_HIGH: tx_byte = decimation[15:8];
REG_DECIM_LOW: tx_byte = decimation[7:0];
REG_STATUS: tx_byte = {4'd0, freq_valid, freq_busy, capture_done, capture_busy};
REG_SOURCE_B: tx_byte = {4'd0, source_b};
REG_MODE: tx_byte = {7'd0, pair_mode};
REG_DATA: tx_byte = capture_byte;
REG_RING_SELECT: tx_byte = ring_select;
REG_FREQ_COUNT3: tx_byte = freq_count[31:24];
REG_FREQ_COUNT2: tx_byte = freq_count[23:16];
REG_FREQ_COUNT1: tx_byte = freq_count[15:8];
REG_FREQ_COUNT0: tx_byte = freq_count[7:0];
REG_GEN_ENABLE: tx_byte = {4'd0, gen_enable};
REG_TEMPORAL: tx_byte = {4'd0, temporal};
REG_FREQ_GEN: tx_byte = {6'd0, freq_gen};
REG_HEALTHY: tx_byte = {4'd0, healthy};
REG_RCT_CUTOFF: tx_byte = rct_cutoff;
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);
REG_ID0: tx_byte_next = "S";
REG_ID1: tx_byte_next = "T";
REG_ID2: tx_byte_next = "R";
REG_ID3: tx_byte_next = "X";
REG_VERSION: tx_byte_next = VERSION;
REG_FREQ_GATE: tx_byte_next = FREQ_GATE_BITS;
REG_GENERATORS: tx_byte_next = GENERATORS;
REG_RINGS: tx_byte_next = RINGS_PER_GEN;
REG_SOURCE_A: tx_byte_next = {4'd0, source_a};
REG_DECIM_HIGH: tx_byte_next = decimation[15:8];
REG_DECIM_LOW: tx_byte_next = decimation[7:0];
REG_STATUS: tx_byte_next = {4'd0, freq_valid, freq_busy, capture_done, capture_busy};
REG_SOURCE_B: tx_byte_next = {4'd0, source_b};
REG_MODE: tx_byte_next = {7'd0, pair_mode};
REG_DATA: tx_byte_next = capture_byte;
REG_RING_SELECT: tx_byte_next = ring_select;
REG_FREQ_COUNT3: tx_byte_next = freq_count[31:24];
REG_FREQ_COUNT2: tx_byte_next = freq_count[23:16];
REG_FREQ_COUNT1: tx_byte_next = freq_count[15:8];
REG_FREQ_COUNT0: tx_byte_next = freq_count[7:0];
REG_GEN_ENABLE: tx_byte_next = {4'd0, gen_enable};
REG_TEMPORAL: tx_byte_next = {4'd0, temporal};
REG_FREQ_GEN: tx_byte_next = {6'd0, freq_gen};
REG_HEALTHY: tx_byte_next = {4'd0, healthy};
REG_RCT_CUTOFF: tx_byte_next = rct_cutoff;
REG_APT_HIGH: tx_byte_next = {5'd0, apt_cutoff[10:8]};
REG_APT_LOW: tx_byte_next = apt_cutoff[7:0];
REG_MAX_DEAD: tx_byte_next = max_dead;
REG_TRIAL_HIGH: tx_byte_next = trial_ms[15:8];
REG_TRIAL_LOW: tx_byte_next = trial_ms[7:0];
REG_STATS_CTRL: tx_byte_next = {7'd0, stats_von_neumann};
REG_STATS: tx_byte_next = stats_byte;
REG_AMP: tx_byte_next = amp_byte;
REG_AMP_CTRL: tx_byte_next = {7'd0, amp_von_neumann};
REG_AMP_SOURCES: tx_byte_next = {4'd0, amp_sources};
REG_AMP_SUB_MS: tx_byte_next = amp_sub_ms;
REG_AMP_SUBS: tx_byte_next = {4'd0, amp_subtrials};
7'h54: tx_byte_next = {7'd0, amp_stage_mode[0]};
7'h55: tx_byte_next = amp_stage_param[15:8];
7'h56: tx_byte_next = amp_stage_param[7:0];
7'h57: tx_byte_next = {7'd0, amp_stage_mode[1]};
7'h58: tx_byte_next = amp_stage_param[31:24];
7'h59: tx_byte_next = amp_stage_param[23:16];
7'h5A: tx_byte_next = {7'd0, amp_stage_mode[2]};
7'h5B: tx_byte_next = amp_stage_param[47:40];
7'h5C: tx_byte_next = amp_stage_param[39:32];
default: tx_byte_next = gen_register(reg_addr);
endcase
end
// Registered read-back: the next byte is needed only after half an SCK period, long after
// the command byte has set reg_addr, so a cycle of latency keeps this wide multiplexer off
// the SPI timing path.
always @(posedge clk) tx_byte <= tx_byte_next;
always @(posedge clk) begin
capture_start <= 1'b0;
freq_start <= 1'b0;
health_clear <= 1'b0;
stats_restart <= 1'b0;
amp_restart <= 1'b0;
if (reset) begin
gen_enable <= {GENERATORS{1'b1}};
temporal <= 4'd5;
@ -290,11 +340,23 @@ module strix_core #(
trial_ms <= 16'd200;
stats_von_neumann <= 1'b0;
stats_index <= 8'd0;
amp_sources <= {GENERATORS{1'b1}};
amp_von_neumann <= 1'b0;
amp_sub_ms <= 8'd40;
amp_subtrials <= 4'd5;
amp_stage_mode <= 3'b000;
amp_stage_param <= {16'd4, 16'd8, 16'd8}; // random walk 8 x 8 x 4 = bound 256
amp_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)
amp_index <= 8'd0;
else if (tx_loaded && is_read && reg_addr == REG_AMP)
amp_index <= amp_index + 8'd1;
if (!selected)
stats_index <= 8'd0;
else if (tx_loaded && is_read && reg_addr == REG_STATS)
@ -329,6 +391,22 @@ module strix_core #(
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_AMP_CTRL: begin
amp_von_neumann <= rx_byte[0];
amp_restart <= rx_byte[1];
end
REG_AMP_SOURCES: amp_sources <= rx_byte[GENERATORS-1:0];
REG_AMP_SUB_MS: if (rx_byte != 8'd0) amp_sub_ms <= rx_byte;
REG_AMP_SUBS: if (rx_byte >= 8'd1 && rx_byte <= 8'd15) amp_subtrials <= rx_byte[3:0];
7'h54: amp_stage_mode[0] <= rx_byte[0];
7'h55: amp_stage_param[15:8] <= rx_byte;
7'h56: amp_stage_param[7:0] <= rx_byte;
7'h57: amp_stage_mode[1] <= rx_byte[0];
7'h58: amp_stage_param[31:24] <= rx_byte;
7'h59: amp_stage_param[23:16] <= rx_byte;
7'h5A: amp_stage_mode[2] <= rx_byte[0];
7'h5B: amp_stage_param[47:40] <= rx_byte;
7'h5C: amp_stage_param[39:32] <= rx_byte;
REG_STATS_CTRL: begin
stats_von_neumann <= rx_byte[0];
stats_restart <= rx_byte[1];

97
fpga/sim/tb_amp_stage.v Normal file
View file

@ -0,0 +1,97 @@
// Testbench for amp_stage: random input bits against a reference model of both modes, plus a
// statistical check that a biased input comes out more biased.
`timescale 1ns / 1ps
module tb_amp_stage;
reg clk = 1'b0;
always #5 clk = ~clk;
reg restart = 1'b1, mode = 1'b0, in_valid = 1'b0, in_bit = 1'b0;
reg [15:0] param = 16'd1;
wire out_valid, out_bit;
amp_stage dut (
.clk(clk), .restart(restart), .mode(mode), .param(param),
.in_valid(in_valid), .in_bit(in_bit), .out_valid(out_valid), .out_bit(out_bit)
);
integer errors = 0;
// Reference model, advanced on the same edges as the stage
integer model_position, model_seen, model_ones, model_bound, dut_outputs, ones_out;
reg model_valid, model_bit;
always @(posedge clk) begin
model_valid = 1'b0;
if (restart) begin
model_position = 0; model_seen = 0; model_ones = 0;
end else if (in_valid) begin
if (mode == 1'b0) begin
model_bound = param; // signed copy: -param would wrap as unsigned
model_position = model_position + (in_bit ? 1 : -1);
if (model_position >= model_bound || model_position <= -model_bound) begin
model_valid = 1'b1; model_bit = model_position > 0; model_position = 0;
end
end else begin
model_seen = model_seen + 1; model_ones = model_ones + in_bit;
if (model_seen >= param) begin
model_valid = 1'b1; model_bit = 2 * model_ones > model_seen;
model_seen = 0; model_ones = 0;
end
end
end
end
// The model and the stage both update on the rising edge; compare in between.
always @(negedge clk) if (!restart) begin
if (out_valid !== model_valid || (out_valid && out_bit !== model_bit)) begin
if (errors < 5) $display("FAIL at %0t: out %b/%b, model %b/%b", $time, out_valid, out_bit,
model_valid, model_bit);
errors = errors + 1;
end
if (out_valid) begin
dut_outputs = dut_outputs + 1;
ones_out = ones_out + out_bit;
end
end
task run(input m, input [15:0] p, input integer cycles, input integer probability_permille);
integer i;
begin
@(negedge clk);
mode = m; param = p; restart = 1'b1;
@(negedge clk);
restart = 1'b0; dut_outputs = 0; ones_out = 0;
for (i = 0; i < cycles; i = i + 1) begin
in_valid = ({$random} % 3) != 0;
in_bit = ({$random} % 1000) < probability_permille;
@(negedge clk);
end
in_valid = 1'b0;
@(negedge clk);
end
endtask
initial begin
run(1'b0, 16'd1, 2000, 500);
run(1'b0, 16'd5, 20000, 500);
run(1'b0, 16'd16, 50000, 500);
run(1'b1, 16'd1, 2000, 500);
run(1'b1, 16'd7, 20000, 500);
run(1'b1, 16'd8, 20000, 500); // even window: ties output 0
// Amplification: 55% ones in, random walk bound 10 -> about 88% ones out
// (P = 1 / (1 + ((1-p)/p)^n) = 1 / (1 + (0.45/0.55)^10) = 0.88)
run(1'b0, 16'd10, 400000, 550);
if (dut_outputs < 500 || ones_out * 100 < dut_outputs * 83 || ones_out * 100 > dut_outputs * 93) begin
$display("FAIL amplification: %0d of %0d outputs are 1", ones_out, dut_outputs);
errors = errors + 1;
end
if (errors == 0) $display("PASS (amplified %0d/%0d ones)", ones_out, dut_outputs);
else $display("%0d error(s)", errors);
$finish;
end
endmodule

View file

@ -1,7 +1,7 @@
// Testbench for strix_core: register access, generator XOR logic checked against the sampled
// ring outputs, captures read back across transactions, generator enable, the frequency
// 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.
// counter, the health monitors with injected faults, the statistics snapshots against a
// reference model, and the bias amplifier's bit counts in several configurations. Rings are replaced by the SIM model in ring_osc.v.
`timescale 1ns / 1ps
module tb_strix_core;
@ -17,10 +17,16 @@ module tb_strix_core;
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_HEALTHY = 7'h20, REG_HEALTH_CTRL = 7'h21, REG_TRIAL_LOW = 7'h27,
REG_STATS_CTRL = 7'h28, REG_STATS = 7'h40;
REG_STATS_CTRL = 7'h28, REG_STATS = 7'h40, REG_AMP = 7'h42,
REG_AMP_CTRL = 7'h50, REG_AMP_SOURCES = 7'h51, REG_AMP_SUB_MS = 7'h52,
REG_AMP_SUBS = 7'h53, REG_AMP_STAGE = 7'h54;
localparam AMP_SNAPSHOT_BYTES = 44;
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
// Amplifier trials in the tests: 3 sub-trials of 2 "ms" -> 9000 clk cycles -> 1800 output
// bits per generator at temporal 5
localparam AMP_GEN_BITS = 3 * 2 * TICK_CYCLES / 5;
// Per-generator health registers
function [6:0] gen_reg(input integer generator, input integer index);
@ -219,6 +225,43 @@ module tb_strix_core;
end
endtask
reg [7:0] amp [0:AMP_SNAPSHOT_BYTES-1];
function [31:0] amp_word(input integer index);
amp_word = {amp[index], amp[index+1], amp[index+2], amp[index+3]};
endfunction
// Configures the amplifier (stage bounds or windows, random walk), restarts it, waits for its
// second trial and reads that trial's snapshot.
task amp_trial(input [3:0] sources, input von_neumann, input [15:0] p0, input [15:0] p1, input [15:0] p2);
reg [7:0] dummy;
integer i;
begin
write_reg(REG_AMP_SOURCES, sources);
write_reg(REG_AMP_STAGE + 1, p0 >> 8); write_reg(REG_AMP_STAGE + 2, p0);
write_reg(REG_AMP_STAGE + 4, p1 >> 8); write_reg(REG_AMP_STAGE + 5, p1);
write_reg(REG_AMP_STAGE + 7, p2 >> 8); write_reg(REG_AMP_STAGE + 8, p2);
write_reg(REG_AMP_CTRL, {6'd0, 1'b1, von_neumann}); // restart
@(posedge dut.amp_trial_end); @(posedge dut.amp_trial_end); #200;
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, REG_AMP}, dummy);
for (i = 0; i < AMP_SNAPSHOT_BYTES; i = i + 1) spi_byte(8'h00, amp[i]);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
if (amp_word(0) !== amp_word(AMP_SNAPSHOT_BYTES - 4) || amp_word(0) !== 32'd1) begin
$display("FAIL amplifier snapshot sequence %0d/%0d", amp_word(0), amp_word(AMP_SNAPSHOT_BYTES - 4));
errors = errors + 1;
end
if (amp[4][5]) begin $display("FAIL amplifier merge overflow"); errors = errors + 1; end
end
endtask
task expect_range(input integer got, input integer low, input integer high, input [8*24-1:0] what);
if (got < low || got > high) begin
$display("FAIL %0s: %0d not in %0d..%0d", what, got, low, high);
errors = errors + 1;
end
endtask
task wait_trial_end;
begin
@(posedge dut.trial_end);
@ -237,7 +280,7 @@ module tb_strix_core;
// Identification
read_reg(REG_ID0, v); expect(v, "S", "ID0");
read_reg(REG_ID3, v); expect(v, "X", "ID3");
read_reg(REG_VERSION, v); expect(v, 8'h05, "VERSION");
read_reg(REG_VERSION, v); expect(v, 8'h06, "VERSION");
read_reg(REG_GENERATORS, v); expect(v, 8'd4, "GENERATORS");
read_reg(REG_RINGS, v); expect(v, RINGS, "RINGS");
read_reg(REG_TEMPORAL, v); expect(v, 8'd5, "TEMPORAL");
@ -358,6 +401,34 @@ module tb_strix_core;
end
write_reg(REG_STATS_CTRL, 8'h00);
// Bias amplifier
write_reg(REG_AMP_SUB_MS, 8'd2);
write_reg(REG_AMP_SUBS, 8'd3);
// Passthrough (all bounds 1): every generator bit appears once on the bias path
amp_trial(4'hF, 1'b0, 16'd1, 16'd1, 16'd1);
expect(amp[4] & 8'h0F, 8'h0F, "amp sources");
expect(amp[5], 8'd3, "amp sub-trials");
expect_range(amp_word(8), 4 * AMP_GEN_BITS - 8, 4 * AMP_GEN_BITS + 8, "amp passthrough bits");
expect_range(amp_word(24), 4 * AMP_GEN_BITS - 8, 4 * AMP_GEN_BITS + 8, "amp autocorr bits");
expect_range(amp_word(12), 4 * AMP_GEN_BITS / 3, 4 * AMP_GEN_BITS * 2 / 3, "amp passthrough ones");
if (amp[20] == 0) begin $display("FAIL amplifier cast no votes"); errors = errors + 1; end
// One source only
amp_trial(4'h1, 1'b0, 16'd1, 16'd1, 16'd1);
expect(amp[4] & 8'h0F, 8'h01, "amp single source");
expect_range(amp_word(8), AMP_GEN_BITS - 2, AMP_GEN_BITS + 2, "amp single-source bits");
// Random walk bound 2 in stage 1: about a quarter of the bits (4 steps per output)
amp_trial(4'hF, 1'b0, 16'd2, 16'd1, 16'd1);
expect_range(amp_word(8), 4 * AMP_GEN_BITS / 6, 4 * AMP_GEN_BITS / 3, "amp bound-2 bits");
// Von Neumann keeps about a quarter of the bits
amp_trial(4'hF, 1'b1, 16'd1, 16'd1, 16'd1);
expect(amp[4] & 8'h10, 8'h10, "amp von Neumann flag");
expect_range(amp_word(8), 4 * AMP_GEN_BITS / 6, 4 * AMP_GEN_BITS / 3, "amp von Neumann bits");
write_reg(REG_AMP_CTRL, 8'h00);
if (xor_checks < 100) begin
$display("FAIL only %0d temporal XOR checks ran", xor_checks);
errors = errors + 1;

View file

@ -63,6 +63,9 @@ 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/health_monitor.v
set_global_assignment -name VERILOG_FILE ../rtl/stats_block.v
set_global_assignment -name VERILOG_FILE ../rtl/bias_amplifier.v
set_global_assignment -name VERILOG_FILE ../rtl/amp_path.v
set_global_assignment -name VERILOG_FILE ../rtl/amp_stage.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/raw_capture.v

View file

@ -8,6 +8,7 @@ Subcommands:
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)
stats follow the live statistics trial by trial, with rolling and cumulative windows
amp configure the bias amplifier and follow its trials (baseline without intention)
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
@ -46,6 +47,8 @@ REG_HEALTHY, REG_HEALTH_CTRL, REG_RCT_CUTOFF, REG_APT_HIGH, REG_APT_LOW, REG_MAX
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"]
REG_AMP_CTRL, REG_AMP_SOURCES, REG_AMP_SUB_MS, REG_AMP_SUBS, REG_AMP_STAGE = 0x50, 0x51, 0x52, 0x53, 0x54
PATH_NAMES = ["bias", "autocorr"]
CONTROL_START_FREQ = 0x04
STATUS_FREQ_VALID = 0x08
@ -79,8 +82,8 @@ class Board:
while self.serial.readline():
pass
self.serial.timeout = 200
if not identity.startswith("STRX v5"):
sys.exit(f"unexpected board identity: {identity!r} (needs the v5 FPGA image)")
if not identity.startswith("STRX v6"):
sys.exit(f"unexpected board identity: {identity!r} (needs the v6 FPGA image)")
self.generators = self.read(REG_GENERATORS)
self.rings = self.read(REG_RINGS)
print(f"board: {identity}, {self.generators} generators x {self.rings} rings")
@ -137,6 +140,22 @@ def read_snapshot(board: "Board") -> dict | None:
"trial_ms": raw[6] << 8 | raw[7], "channels": channels}
def read_amp_snapshot(board: "Board") -> dict | None:
"""Latest amplifier trial snapshot (layout in fpga/rtl/bias_amplifier.v), or None if torn."""
raw = bytes.fromhex(board.command("amp"))
word = lambda i: int.from_bytes(raw[i:i + 4], "big")
if word(0) != word(len(raw) - 4):
return None
paths = []
for p in range(2):
base = 8 + 16 * p
paths.append({"bits": word(base), "ones": word(base + 4),
"votes": raw[base + 8] << 8 | raw[base + 9], "voted": raw[base + 10] << 8 | raw[base + 11],
"result": raw[base + 13] & 1, "valid": bool(raw[base + 13] & 2)})
return {"sequence": word(0), "sources": raw[4] & 0x0F, "von_neumann": bool(raw[4] & 0x10),
"overflow": bool(raw[4] & 0x20), "subtrials": raw[5], "sub_ms": raw[6], "paths": paths}
def cached_capture(board: Board | None, path: Path, *args) -> bytes | None:
if board:
path.write_bytes(board.capture(*args))
@ -400,6 +419,60 @@ def run_stats(board: Board | None, args) -> None:
f" {int(np.sum(np.abs(z) > 3)):>5d} {ac.mean():>15.3f} {ac.std():>5.2f}")
def run_amp(board: Board | None, args) -> None:
if board is None:
sys.exit("amp needs the board")
if len(args.stages) != 3:
sys.exit("--stages needs three values")
mode = 1 if args.method == "vote" else 0
for s, param in enumerate(args.stages):
board.write(REG_AMP_STAGE + 3 * s, mode)
board.write(REG_AMP_STAGE + 3 * s + 1, param >> 8)
board.write(REG_AMP_STAGE + 3 * s + 2, param & 0xFF)
board.write(REG_AMP_SOURCES, args.sources)
board.write(REG_AMP_SUB_MS, args.sub_ms)
board.write(REG_AMP_SUBS, args.subtrials)
board.write(REG_AMP_CTRL, (1 if args.von_neumann else 0) | 2) # bit 1 restarts
while (snapshot := read_amp_snapshot(board)) is None or snapshot["sequence"] > 1:
time.sleep(args.sub_ms * args.subtrials / 4000)
total = math.prod(args.stages)
print(f"{args.method}, stages {' x '.join(map(str, args.stages))} (total {total}), sources {args.sources:#x}, "
f"{args.subtrials} x {args.sub_ms} ms sub-trials, von Neumann {'on' if args.von_neumann else 'off'}")
print(f"{'trial':>5} {'src':>3} " + " ".join(f"{n + ' bits':>13} {'z':>6} {'votes':>{args.subtrials}} {'res':>3}"
for n in PATH_NAMES))
results = [[] for _ in PATH_NAMES]
bit_z = [[] for _ in PATH_NAMES]
last_sequence = -1
while len(results[0]) < args.trials:
snapshot = read_amp_snapshot(board)
if snapshot is None or snapshot["sequence"] == last_sequence:
time.sleep(args.sub_ms * args.subtrials / 4000)
continue
last_sequence = snapshot["sequence"]
if snapshot["overflow"]:
print("warning: merge overflow reported")
cells = []
for p, path in enumerate(snapshot["paths"]):
z, _ = z_scores(path["bits"], path["ones"], 0)
votes = "".join(("1" if path["votes"] >> i & 1 else "0") if path["voted"] >> i & 1 else "."
for i in range(snapshot["subtrials"]))
result = str(path["result"]) if path["valid"] else "-"
if path["valid"]:
results[p].append(path["result"])
bit_z[p].append(z)
cells.append(f"{path['bits']:>13d} {z:>6.2f} {votes:>{args.subtrials}} {result:>3}")
print(f"{snapshot['sequence']:>5} {snapshot['sources']:>3x} " + " ".join(cells))
print("\nbaseline: amplified-bit z per trial should have sd about 1; trial results about 50% ones\n")
print(f"{'path':<9} {'trials':>6} {'ones':>5} {'rate':>6} {'z':>6} {'bit z mean':>10} {'sd':>5}")
for p, name in enumerate(PATH_NAMES):
n, ones = len(results[p]), sum(results[p])
z = (2 * ones - n) / math.sqrt(n) if n else float("nan")
bz = np.array(bit_z[p])
print(f"{name:<9} {n:>6} {ones:>5} {ones / max(n, 1):>6.3f} {z:>6.2f} {np.nanmean(bz):>10.3f} {np.nanstd(bz):>5.2f}")
def run_pairs(board: Board | None, args) -> None:
generators = board.generators if board else 4
rng = np.random.default_rng(0)
@ -454,6 +527,15 @@ def main() -> None:
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")
amp = sub.add_parser("amp")
amp.add_argument("--trials", type=int, default=50)
amp.add_argument("--method", choices=["walk", "vote"], default="walk")
amp.add_argument("--stages", type=int, nargs="+", default=[8, 8, 4],
help="bound (walk) or window (vote) of the three stages")
amp.add_argument("--sources", type=lambda s: int(s, 0), default=0xF, help="generator mask")
amp.add_argument("--sub-ms", type=int, default=40)
amp.add_argument("--subtrials", type=int, default=5)
amp.add_argument("--von-neumann", action="store_true")
health = sub.add_parser("health")
health.add_argument("--clear", action="store_true", help="clear failures and event counters first")
pairs = sub.add_parser("pairs")
@ -464,7 +546,7 @@ def main() -> None:
args.out.mkdir(parents=True, exist_ok=True)
board = None if args.analyze_only else Board(args.port)
handlers = {"freq": run_freq, "rings": run_rings, "sweep": run_sweep,
"output": run_output, "health": run_health, "stats": run_stats, "pairs": run_pairs}
"output": run_output, "health": run_health, "stats": run_stats, "amp": run_amp, "pairs": run_pairs}
handlers[args.command](board, args)