strix/fpga/sim/tb_strix_core.v

472 lines
20 KiB
Verilog

// 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, the statistics snapshots against a
// reference model, the bias amplifier's bit counts in several configurations, and the entropy
// byte stream against the FIFO contents. 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_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_HEALTHY = 7'h20, REG_HEALTH_CTRL = 7'h21, REG_TRIAL_LOW = 7'h27,
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, REG_STREAM_CTRL = 7'h29,
REG_LEVEL_HIGH = 7'h2A, REG_LEVEL_LOW = 7'h2B, REG_STREAM = 7'h44;
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);
gen_reg = 7'h30 + 4 * generator + index;
endfunction
localparam FLAGS = 0, RCT_EVENTS = 1, APT_EVENTS = 2, DEAD_RINGS = 3;
reg clk = 1'b0;
always #4.1667 clk = ~clk; // 120 MHz
reg reset = 1'b1;
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), .RINGS_PER_GEN(RINGS),
.DEAD_WINDOW_BITS(6), .TICK_CYCLES(TICK_CYCLES)) dut (
.clk(clk), .reset(reset),
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
.capture_done(done)
);
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
// Reference model of statistics channel 0, mirroring stats_block without von Neumann:
// counts restart at each trial end, and the bit arriving in that cycle opens the new trial.
reg [31:0] model_bits = 0, model_ones = 0, model_agree = 0;
reg [31:0] model_latched_bits, model_latched_ones, model_latched_agree;
reg model_previous, model_has_previous = 1'b0;
always @(posedge clk) begin
if (dut.stats_restart) begin
model_bits <= 0; model_ones <= 0; model_agree <= 0; model_has_previous <= 1'b0;
end else begin
if (dut.trial_end) begin
model_latched_bits <= model_bits;
model_latched_ones <= model_ones;
model_latched_agree <= model_agree;
end
if (dut.stats.valid) begin
model_bits <= (dut.trial_end ? 0 : model_bits) + 1;
model_ones <= (dut.trial_end ? 0 : model_ones) + dut.stats.bits[0];
model_agree <= (dut.trial_end ? 0 : model_agree)
+ (model_has_previous && dut.stats.bits[0] == model_previous);
model_previous <= dut.stats.bits[0];
model_has_previous <= 1'b1;
end else if (dut.trial_end) begin
model_bits <= 0; model_ones <= 0; model_agree <= 0;
end
end
end
reg [7:0] snapshot [0:SNAPSHOT_BYTES-1];
function [31:0] snapshot_word(input integer index);
snapshot_word = {snapshot[index], snapshot[index+1], snapshot[index+2], snapshot[index+3]};
endfunction
task spi_byte(input [7:0] out, output [7:0] in);
integer i;
begin
for (i = 7; i >= 0; i = i - 1) begin
mosi = out[i];
#SCK_HALF sck = 1'b1;
in[i] = miso;
#SCK_HALF sck = 1'b0;
end
end
endtask
task write_reg(input [6:0] addr, input [7:0] value);
reg [7:0] dummy;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b0, addr}, dummy);
spi_byte(value, dummy);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
end
endtask
task read_reg(input [6:0] addr, output [7:0] value);
reg [7:0] dummy;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, addr}, dummy);
spi_byte(8'h00, value);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
end
endtask
task expect(input [7:0] got, input [7:0] want, input [8*16-1:0] what);
if (got !== want) begin
$display("FAIL %0s: got %02x, expected %02x", what, got, want);
errors = errors + 1;
end
endtask
task run_capture;
reg [7:0] status;
integer polls;
begin
write_reg(REG_CONTROL, 8'h01);
polls = 0;
read_reg(REG_STATUS, status);
while (status[1] !== 1'b1 && polls < 1000) begin
read_reg(REG_STATUS, status);
polls = polls + 1;
end
if (status[1] !== 1'b1 || done !== 1'b1) begin
$display("FAIL capture did not finish");
errors = errors + 1;
end
end
endtask
// 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
if (^dut.capture.buffer[i] === 1'bx) begin
$display("FAIL buffer[%0d] undefined", i);
errors = errors + 1;
end
if (i > 0 && dut.capture.buffer[i] !== dut.capture.buffer[i - 1]) changes = changes + 1;
end
end
endtask
task read_data(input integer first, input integer count);
reg [7:0] dummy, value;
integer i;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, REG_DATA}, dummy);
for (i = 0; i < count; i = i + 1) begin
spi_byte(8'h00, value);
if (value !== dut.capture.buffer[first + i]) begin
$display("FAIL data[%0d]: got %02x, expected %02x", first + i, value, dut.capture.buffer[first + i]);
errors = errors + 1;
end
end
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
end
endtask
task read_snapshot;
reg [7:0] dummy;
integer i;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, REG_STATS}, dummy);
for (i = 0; i < SNAPSHOT_BYTES; i = i + 1) spi_byte(8'h00, snapshot[i]);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
end
endtask
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);
#200;
end
endtask
reg [7:0] v;
integer changes, polls;
reg [31:0] sequence_seen;
initial begin
#100 reset = 1'b0;
#200;
// 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'h07, "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");
// 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;
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);
// 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;
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 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);
while (v[3] !== 1'b1 && polls < 1000) begin
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
read_reg(REG_FREQ_COUNT0, v);
if (v == 8'd0) begin $display("FAIL frequency count is zero"); errors = errors + 1; end
// Health: all generators healthy once a dead-ring sweep (90 x 64 cycles) has completed
#100000;
read_reg(REG_HEALTHY, v); expect(v, 8'h0F, "HEALTHY");
read_reg(gen_reg(0, FLAGS), v); expect(v, 8'h38, "FLAGS g0"); // healthy, swept, active
read_reg(gen_reg(0, DEAD_RINGS), v); expect(v, 8'd0, "DEAD g0");
// A dead ring is counted but tolerated
force dut.gen[1].generator.raw_enhanced[7] = 1'b0;
#120000;
read_reg(gen_reg(1, DEAD_RINGS), v); expect(v, 8'd1, "DEAD g1");
read_reg(REG_HEALTHY, v); expect(v, 8'h0F, "HEALTHY dead ring");
release dut.gen[1].generator.raw_enhanced[7];
// A stuck spatial stream fails both NIST tests
force dut.gen[2].generator.level4 = 1'b0;
#20000;
release dut.gen[2].generator.level4;
read_reg(REG_HEALTHY, v); expect(v, 8'h0B, "HEALTHY stuck");
read_reg(gen_reg(2, FLAGS), v); expect(v & 8'h03, 8'h03, "FLAGS g2 failed");
read_reg(gen_reg(2, RCT_EVENTS), v);
if (v == 8'd0) begin $display("FAIL no RCT events"); errors = errors + 1; end
read_reg(gen_reg(2, APT_EVENTS), v);
if (v == 8'd0) begin $display("FAIL no APT events"); errors = errors + 1; end
// Clear restores health without restarting the sweep
write_reg(REG_HEALTH_CTRL, 8'h01);
read_reg(REG_HEALTHY, v); expect(v, 8'h0F, "HEALTHY cleared");
read_reg(gen_reg(2, RCT_EVENTS), v); expect(v, 8'd0, "RCT events cleared");
// A disabled generator is not healthy
write_reg(REG_GEN_ENABLE, 8'h07);
read_reg(REG_HEALTHY, v); expect(v, 8'h07, "HEALTHY disabled");
read_reg(gen_reg(3, FLAGS), v); expect(v, 8'h00, "FLAGS g3 disabled");
write_reg(REG_GEN_ENABLE, 8'h0F);
// Statistics: 10 "ms" trials, temporal 5 -> TRIAL_BITS bits per channel per trial
write_reg(REG_TEMPORAL, 8'd5);
write_reg(REG_TRIAL_LOW, 8'd10);
write_reg(REG_STATS_CTRL, 8'h02); // restart
wait_trial_end;
wait_trial_end;
read_snapshot;
if (snapshot_word(0) !== snapshot_word(SNAPSHOT_BYTES - 4)) begin
$display("FAIL torn snapshot"); errors = errors + 1;
end
expect(snapshot_word(0), 8'd1, "trial sequence");
expect(snapshot[4], 8'h0F, "trial healthy");
expect(snapshot[7], 8'd10, "trial ms");
if (snapshot_word(8) !== model_latched_bits || snapshot_word(12) !== model_latched_ones
|| snapshot_word(16) !== model_latched_agree) begin
$display("FAIL channel 0: %0d/%0d/%0d, model %0d/%0d/%0d", snapshot_word(8), snapshot_word(12),
snapshot_word(16), model_latched_bits, model_latched_ones, model_latched_agree);
errors = errors + 1;
end
if (snapshot_word(8) < TRIAL_BITS - 1 || snapshot_word(8) > TRIAL_BITS + 1) begin
$display("FAIL channel 0 bit count %0d, expected about %0d", snapshot_word(8), TRIAL_BITS);
errors = errors + 1;
end
if (snapshot_word(8 + 48) !== snapshot_word(8)) begin
$display("FAIL combined channel bit count differs"); errors = errors + 1;
end
if (snapshot_word(12) == 0 || snapshot_word(12) == snapshot_word(8)) begin
$display("FAIL channel 0 ones count %0d is degenerate", snapshot_word(12)); errors = errors + 1;
end
// Von Neumann keeps about a quarter of the bits
write_reg(REG_STATS_CTRL, 8'h03);
wait_trial_end;
wait_trial_end;
read_snapshot;
expect(snapshot[5], 8'h01, "trial von Neumann");
if (snapshot_word(8) < TRIAL_BITS / 10 || snapshot_word(8) > TRIAL_BITS * 45 / 100) begin
$display("FAIL von Neumann bit count %0d", snapshot_word(8)); errors = errors + 1;
end
write_reg(REG_STATS_CTRL, 8'h00);
// 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);
// Entropy stream: combined channel, flushed, then 16 bytes read in one burst
write_reg(REG_STREAM_CTRL, 8'h84);
#20000;
read_reg(REG_LEVEL_LOW, v);
expect_range(v, 20, 255, "stream level");
begin : stream_read
reg [7:0] expected [0:15];
reg [12:0] start_ptr;
reg [7:0] dummy, value;
integer i;
start_ptr = dut.stream.read_ptr;
for (i = 0; i < 16; i = i + 1) expected[i] = dut.stream.buffer[(start_ptr + i) % 4096];
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, REG_STREAM}, dummy);
for (i = 0; i < 16; i = i + 1) begin
spi_byte(8'h00, value);
if (value !== expected[i]) begin
$display("FAIL stream byte %0d: got %02x, expected %02x", i, value, expected[i]);
errors = errors + 1;
end
end
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
expect(dut.stream.read_ptr - start_ptr, 8'd16, "stream bytes removed");
end
write_reg(REG_STREAM_CTRL, 8'h84);
read_reg(REG_LEVEL_LOW, v);
expect_range(v, 0, 15, "stream level after flush");
if (xor_checks < 100) begin
$display("FAIL only %0d temporal XOR checks ran", xor_checks);
errors = errors + 1;
end
if (errors == 0) $display("PASS (%0d temporal XOR checks)", xor_checks);
else $display("%0d error(s)", errors);
$finish;
end
endmodule