feat: add two-path bias amplifier with random walk and majority vote stages (FPGA v6)
This commit is contained in:
parent
5305d66102
commit
8541cce59c
11 changed files with 741 additions and 58 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
109
fpga/rtl/amp_path.v
Normal 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
48
fpga/rtl/amp_stage.v
Normal 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
186
fpga/rtl/bias_amplifier.v
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
97
fpga/sim/tb_amp_stage.v
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue