feat: add four 90-ring entropy generators with spatial and temporal XOR (FPGA v3)

This commit is contained in:
randogoth 2026-10-25 13:23:27 +02:00
parent 504b00752a
commit 585e755c66
9 changed files with 389 additions and 217 deletions

View file

@ -1,18 +1,20 @@
// Testbench for strix_core: SPI register access, single and pair captures read back across
// transactions, and the frequency counter. Rings are replaced by the SIM model in ring_osc.v.
// Testbench for strix_core: register access, generator XOR logic checked against the sampled
// ring outputs, captures read back across transactions, generator enable, and the frequency
// counter. Rings are replaced by the SIM model in ring_osc.v.
`timescale 1ns / 1ps
module tb_strix_core;
localparam CAPTURE_ADDR_WIDTH = 6; // 64-byte buffer keeps the simulation short
localparam CAPTURE_BYTES = 1 << CAPTURE_ADDR_WIDTH;
localparam RINGS = 90;
localparam SCK_HALF = 62.5; // 8 MHz SCK
localparam [6:0] REG_ID0 = 7'h00, REG_ID3 = 7'h03, REG_VERSION = 7'h04,
REG_SOURCE_A = 7'h08, REG_DECIM_LOW = 7'h0A, REG_CONTROL = 7'h0B,
REG_STATUS = 7'h0C, REG_SOURCE_B = 7'h0D, REG_MODE = 7'h0E,
REG_RING_COUNT = 7'h0F, REG_DATA = 7'h10, REG_FREQ_SELECT = 7'h11,
REG_FREQ_COUNT0 = 7'h15;
REG_GENERATORS = 7'h06, REG_RINGS = 7'h07, REG_SOURCE_A = 7'h08,
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_GEN_ENABLE = 7'h16, REG_TEMPORAL = 7'h17, REG_FREQ_GEN = 7'h18;
reg clk = 1'b0;
always #4.1667 clk = ~clk; // 120 MHz
@ -21,7 +23,7 @@ module tb_strix_core;
reg sck = 1'b0, mosi = 1'b0, ss_n = 1'b1;
wire miso, done;
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH), .FREQ_GATE_BITS(14)) dut (
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH), .FREQ_GATE_BITS(14), .RINGS_PER_GEN(RINGS)) dut (
.clk(clk), .reset(reset),
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
.capture_done(done)
@ -29,6 +31,47 @@ module tb_strix_core;
integer errors = 0;
// Generator 0 XOR checks, running throughout the simulation. The spatial XOR has four
// pipeline stages; the temporal XOR covers the last `temporal` spatial bits.
reg [3:0] spatial_expected;
reg [7:0] spatial_history;
reg [3:0] previous_temporal = 4'd0;
integer windows_to_skip = 0;
integer xor_checks = 0;
always @(posedge clk) if (!reset) begin
spatial_expected <= {spatial_expected[2:0], ^dut.gen[0].generator.sample_sync};
spatial_history <= {spatial_history[6:0], dut.gen[0].generator.spatial_bit};
end
always @(posedge clk) if (!reset) begin : check_xor
reg expected_out;
integer t;
#1;
if (dut.gen[0].generator.spatial_bit !== spatial_expected[3] && $time > 1000) begin
$display("FAIL spatial XOR mismatch at %0t", $time);
errors = errors + 1;
end
// A window that spans a change of the temporal setting mixes both lengths; skip it.
if (dut.temporal != previous_temporal) begin
previous_temporal = dut.temporal;
windows_to_skip = 2;
end
// Just after an output update, out_bit must equal the XOR of the window that ended at it.
if (dut.gen[0].generator.phase == 4'd0 && windows_to_skip > 0) begin
windows_to_skip = windows_to_skip - 1;
end else if (dut.gen[0].generator.phase == 4'd0 && $time > 1000) begin
expected_out = 1'b0;
for (t = 0; t < dut.temporal; t = t + 1) expected_out = expected_out ^ spatial_history[t];
if (dut.gen[0].generator.out_bit !== expected_out) begin
$display("FAIL temporal XOR mismatch at %0t", $time);
errors = errors + 1;
end
xor_checks = xor_checks + 1;
end
end
task spi_byte(input [7:0] out, output [7:0] in);
integer i;
begin
@ -86,9 +129,9 @@ module tb_strix_core;
end
endtask
// Captured bytes must be defined and vary, otherwise the read-back checks prove nothing.
task check_buffer_contents;
integer i, changes;
// Counts distinct neighbouring bytes and flags undefined ones.
task buffer_changes(output integer changes);
integer i;
begin
changes = 0;
for (i = 0; i < CAPTURE_BYTES; i = i + 1) begin
@ -98,10 +141,6 @@ module tb_strix_core;
end
if (i > 0 && dut.capture.buffer[i] !== dut.capture.buffer[i - 1]) changes = changes + 1;
end
if (changes == 0) begin
$display("FAIL capture buffer is constant");
errors = errors + 1;
end
end
endtask
@ -123,7 +162,7 @@ module tb_strix_core;
endtask
reg [7:0] v;
integer i, polls;
integer changes, polls;
initial begin
#100 reset = 1'b0;
@ -132,38 +171,40 @@ 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'h02, "VERSION");
read_reg(REG_RING_COUNT, v); expect(v, 8'd16, "RING_COUNT");
read_reg(REG_VERSION, v); expect(v, 8'h03, "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");
// Single capture of the XOR of rings 0-1, every 3rd sample, read in two transactions
write_reg(REG_SOURCE_A, 8'h21);
write_reg(REG_DECIM_LOW, 8'h03);
read_reg(REG_SOURCE_A, v); expect(v, 8'h21, "SOURCE_A");
// Out-of-range settings are ignored
write_reg(REG_TEMPORAL, 8'd9); read_reg(REG_TEMPORAL, v); expect(v, 8'd5, "TEMPORAL 9");
write_reg(REG_RING_SELECT, RINGS); read_reg(REG_RING_SELECT, v); expect(v, 8'd0, "RING_SELECT");
// Processed output of generator 1 at its native rate, read in two transactions
write_reg(REG_TEMPORAL, 8'd3);
write_reg(REG_SOURCE_A, 8'h01);
write_reg(REG_DECIM_LOW, 8'd3);
run_capture;
check_buffer_contents;
buffer_changes(changes);
if (changes == 0) begin $display("FAIL processed capture is constant"); errors = errors + 1; end
read_data(0, CAPTURE_BYTES / 2);
read_data(CAPTURE_BYTES / 2, CAPTURE_BYTES / 2);
write_reg(REG_CONTROL, 8'h02); // rewind
read_data(0, 4);
// Pair capture with A = B: every bit pair must be 00 or 11
write_reg(REG_SOURCE_A, 8'h10);
write_reg(REG_SOURCE_B, 8'h10);
write_reg(REG_MODE, 8'h01);
write_reg(REG_DECIM_LOW, 8'h01);
// A disabled generator's spatial XOR is constant (all rings stop high)
write_reg(REG_GEN_ENABLE, 8'h0E);
write_reg(REG_SOURCE_A, 8'h04);
write_reg(REG_DECIM_LOW, 8'd1);
#500;
run_capture;
check_buffer_contents;
for (i = 0; i < 4 * CAPTURE_BYTES; i = i + 1) begin
v = dut.capture.buffer[i / 4] >> (2 * (i % 4));
if (v[1] !== v[0]) begin
$display("FAIL pair %0d differs", i);
errors = errors + 1;
end
end
read_data(0, CAPTURE_BYTES);
buffer_changes(changes);
if (changes != 0) begin $display("FAIL disabled generator still varies"); errors = errors + 1; end
write_reg(REG_GEN_ENABLE, 8'h0F);
// Frequency counter on ring 3
write_reg(REG_FREQ_SELECT, 8'h03);
// Frequency of generator 2, ring 5
write_reg(REG_FREQ_GEN, 8'd2);
write_reg(REG_RING_SELECT, 8'd5);
write_reg(REG_CONTROL, 8'h04);
polls = 0;
read_reg(REG_STATUS, v);
@ -171,17 +212,16 @@ module tb_strix_core;
read_reg(REG_STATUS, v);
polls = polls + 1;
end
if (v[3] !== 1'b1) begin
$display("FAIL frequency measurement did not finish");
errors = errors + 1;
end
if (v[3] !== 1'b1) begin $display("FAIL frequency measurement did not finish"); errors = errors + 1; end
read_reg(REG_FREQ_COUNT0, v);
if (v == 8'd0 || dut.counter.count === 32'bx) begin
$display("FAIL frequency count is %0d", dut.counter.count);
if (v == 8'd0) begin $display("FAIL frequency count is zero"); errors = errors + 1; end
if (xor_checks < 100) begin
$display("FAIL only %0d temporal XOR checks ran", xor_checks);
errors = errors + 1;
end
if (errors == 0) $display("PASS");
if (errors == 0) $display("PASS (%0d temporal XOR checks)", xor_checks);
else $display("%0d error(s)", errors);
$finish;
end