272 lines
11 KiB
Verilog
272 lines
11 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, and the health monitors with injected faults. 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;
|
|
|
|
// 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)) 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
|
|
|
|
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
|
|
|
|
reg [7:0] v;
|
|
integer changes, polls;
|
|
|
|
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'h04, "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);
|
|
|
|
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
|