// 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. // // 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 // byte(s) in that register. A read returns the register in every following byte; DATA // advances through the capture buffer with each byte, continuing across transactions. // // Capture sources, for generator g = 0..3: // 0x00+g processed output (spatial XOR, then temporal XOR; capture with decimation = TEMPORAL) // 0x04+g spatial XOR of all rings, every clock // 0x08+g enhanced output of ring RING_SELECT // 0x0C+g single tap of ring RING_SELECT // // Health: a generator is healthy when it is enabled, its monitor has completed a dead-ring // sweep, neither NIST test has failed since the last clear, and at most MAX_DEAD_RINGS rings // are dead. Per-generator registers at 0x30 + 4g: FLAGS, RCT_EVENTS, APT_EVENTS, DEAD_RINGS. // // 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. module strix_core #( parameter CAPTURE_ADDR_WIDTH = 15, parameter FREQ_GATE_BITS = 24, parameter RINGS_PER_GEN = 90, parameter DEAD_WINDOW_BITS = 16, parameter TICK_CYCLES = 120000 // clk cycles per millisecond ) ( input wire clk, input wire reset, input wire spi_sck, input wire spi_mosi, input wire spi_ss_n, output wire spi_miso, output wire capture_done ); localparam [7:0] VERSION = 8'h05; localparam GENERATORS = 4; localparam [6:0] REG_ID0 = 7'h00, // "STRX" REG_ID1 = 7'h01, REG_ID2 = 7'h02, REG_ID3 = 7'h03, REG_VERSION = 7'h04, REG_FREQ_GATE = 7'h05, // read-only: gate is 2^value clk cycles REG_GENERATORS = 7'h06, // read-only REG_RINGS = 7'h07, // read-only: rings per generator REG_SOURCE_A = 7'h08, REG_DECIM_HIGH = 7'h09, REG_DECIM_LOW = 7'h0A, REG_CONTROL = 7'h0B, // write: bit 0 start capture, bit 1 rewind DATA, // bit 2 start frequency measurement REG_STATUS = 7'h0C, // bit 0 capture busy, bit 1 capture done, // bit 2 frequency busy, bit 3 frequency valid REG_SOURCE_B = 7'h0D, REG_MODE = 7'h0E, // bit 0 pair capture (A and B per sample) REG_DATA = 7'h10, REG_RING_SELECT = 7'h11, // ring for the characterization sources and frequency REG_FREQ_COUNT3 = 7'h12, // prescaled edge count, most significant byte first REG_FREQ_COUNT2 = 7'h13, REG_FREQ_COUNT1 = 7'h14, REG_FREQ_COUNT0 = 7'h15, REG_GEN_ENABLE = 7'h16, // bit g enables generator g REG_TEMPORAL = 7'h17, // consecutive bits XORed per output bit, 1-8 REG_FREQ_GEN = 7'h18, // generator for the frequency measurement REG_HEALTHY = 7'h20, // read-only: bit g = generator g healthy REG_HEALTH_CTRL = 7'h21, // write: bit 0 clears failures and event counters REG_RCT_CUTOFF = 7'h22, // repetition count test cutoff (run length) REG_APT_HIGH = 7'h23, // adaptive proportion test cutoff (per 1024 bits) REG_APT_LOW = 7'h24, REG_MAX_DEAD = 7'h25, // dead rings tolerated per generator REG_TRIAL_HIGH = 7'h26, // trial length in ms 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 // 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, // 3 active, 4 sweep done, 5 healthy GEN_RCT_EVENTS = 2'd1, GEN_APT_EVENTS = 2'd2, GEN_DEAD_RINGS = 2'd3; // Generators reg [GENERATORS-1:0] gen_enable; reg [3:0] temporal; reg [7:0] ring_select; wire [GENERATORS-1:0] out_bit, out_valid, spatial_bit, ring_enhanced, ring_tap, ring_raw_tap; reg health_clear; reg [7:0] rct_cutoff, max_dead; reg [10:0] apt_cutoff; wire [GENERATORS-1:0] rct_failed, apt_failed, sweep_done, too_many_dead, healthy; wire [7:0] rct_events [0:GENERATORS-1]; wire [7:0] apt_events [0:GENERATORS-1]; wire [7:0] dead_rings [0:GENERATORS-1]; genvar g; generate for (g = 0; g < GENERATORS; g = g + 1) begin : gen wire [RINGS_PER_GEN-1:0] ring_samples; wire active = gen_enable[g] && !reset; entropy_generator #(.RINGS(RINGS_PER_GEN)) generator ( .clk(clk), .enable(gen_enable[g]), .temporal(temporal), .ring_select(ring_select), .out_bit(out_bit[g]), .out_valid(out_valid[g]), .spatial_bit(spatial_bit[g]), .ring_enhanced(ring_enhanced[g]), .ring_tap(ring_tap[g]), .ring_raw_tap(ring_raw_tap[g]), .ring_samples(ring_samples) ); health_monitor #(.RINGS(RINGS_PER_GEN), .DEAD_WINDOW_BITS(DEAD_WINDOW_BITS)) monitor ( .clk(clk), .active(active), .clear(health_clear), .sample_bit(spatial_bit[g]), .ring_samples(ring_samples), .rct_cutoff(rct_cutoff), .apt_cutoff(apt_cutoff), .rct_failed(rct_failed[g]), .apt_failed(apt_failed[g]), .rct_events(rct_events[g]), .apt_events(apt_events[g]), .dead_rings(dead_rings[g]), .sweep_done(sweep_done[g]) ); assign too_many_dead[g] = sweep_done[g] && dead_rings[g] > max_dead; assign healthy[g] = active && sweep_done[g] && !rct_failed[g] && !apt_failed[g] && !too_many_dead[g]; end endgenerate // Statistics. All generators share the temporal setting and run in lockstep, so // generator 0's valid strobe times every channel. reg [15:0] trial_ms; reg stats_von_neumann, stats_restart; reg [7:0] stats_index; wire [7:0] stats_byte; wire trial_end; // Registered inputs keep the health logic and the combining XOR off the counter paths; // every channel is delayed by the same cycle, so they stay aligned. reg [GENERATORS:0] stats_bits; reg stats_valid; reg [GENERATORS-1:0] stats_healthy; always @(posedge clk) begin stats_healthy <= healthy; stats_bits <= {^(out_bit & healthy), out_bit}; stats_valid <= out_valid[0]; 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), .healthy(stats_healthy), .read_index(stats_index), .read_byte(stats_byte), .trial_end(trial_end) ); // Source selection reg [3:0] source_a, source_b; reg bit_a, bit_b; wire [15:0] all_sources = {ring_tap, ring_enhanced, spatial_bit, out_bit}; always @(posedge clk) begin bit_a <= all_sources[source_a]; bit_b <= all_sources[source_b]; end // Capture reg pair_mode; reg [15:0] decimation; reg capture_start; wire capture_busy; reg [CAPTURE_ADDR_WIDTH-1:0] prefetch_addr; wire [7:0] capture_byte; raw_capture #(.ADDR_WIDTH(CAPTURE_ADDR_WIDTH)) capture ( .clk(clk), .reset(reset), .bit_a(bit_a), .bit_b(bit_b), .pair_mode(pair_mode), .decimation(decimation), .start(capture_start), .busy(capture_busy), .done(capture_done), .read_addr(prefetch_addr), .read_data(capture_byte) ); // Frequency counter reg [1:0] freq_gen; reg freq_start; wire freq_busy, freq_valid; wire [31:0] freq_count; freq_counter #(.GATE_BITS(FREQ_GATE_BITS)) counter ( .clk(clk), .reset(reset), .signal(ring_raw_tap[freq_gen]), .start(freq_start), .busy(freq_busy), .valid(freq_valid), .count(freq_count) ); // SPI wire selected, rx_valid, rx_is_first, tx_loaded; wire [7:0] rx_byte; reg [7:0] tx_byte; spi_slave spi ( .clk(clk), .reset(reset), .sck(spi_sck), .mosi(spi_mosi), .ss_n(spi_ss_n), .miso(spi_miso), .selected(selected), .rx_valid(rx_valid), .rx_byte(rx_byte), .rx_is_first(rx_is_first), .tx_byte(tx_byte), .tx_loaded(tx_loaded) ); reg is_read; reg [6:0] reg_addr; // DATA bytes are prefetched one ahead: prefetch_addr moves when a byte is loaded for // shifting, bytes_read only when it has been fully clocked out. Between transactions // prefetch_addr rewinds to bytes_read so the trailing prefetch is not lost. reg [CAPTURE_ADDR_WIDTH-1:0] bytes_read; wire reading_data = is_read && reg_addr == REG_DATA; function [7:0] gen_register(input [6:0] addr); reg [1:0] n; begin n = addr[3:2]; if (addr[6:4] != 3'b011) gen_register = 8'h00; else case (addr[1:0]) GEN_FLAGS: gen_register = {2'd0, healthy[n], sweep_done[n], gen_enable[n], too_many_dead[n], apt_failed[n], rct_failed[n]}; GEN_RCT_EVENTS: gen_register = rct_events[n]; GEN_APT_EVENTS: gen_register = apt_events[n]; default: gen_register = sweep_done[n] ? dead_rings[n] : 8'h00; endcase end endfunction 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); endcase end always @(posedge clk) begin capture_start <= 1'b0; freq_start <= 1'b0; health_clear <= 1'b0; stats_restart <= 1'b0; if (reset) begin gen_enable <= {GENERATORS{1'b1}}; temporal <= 4'd5; ring_select <= 8'd0; source_a <= 4'h0; source_b <= 4'h1; pair_mode <= 1'b0; decimation <= 16'd5; freq_gen <= 2'd0; rct_cutoff <= 8'd81; // 0.5 bits/bit claimed, alpha 2^-40 apt_cutoff <= 11'd824; // same, 1024-bit window max_dead <= 8'd8; // measured sizing tolerates about 17 health_clear <= 1'b1; trial_ms <= 16'd200; stats_von_neumann <= 1'b0; stats_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) stats_index <= 8'd0; else if (tx_loaded && is_read && reg_addr == REG_STATS) stats_index <= stats_index + 8'd1; if (!selected) prefetch_addr <= bytes_read; else if (tx_loaded && reading_data) prefetch_addr <= prefetch_addr + 1'b1; if (rx_valid) begin if (rx_is_first) begin is_read <= rx_byte[7]; reg_addr <= rx_byte[6:0]; end else if (reading_data) begin bytes_read <= bytes_read + 1'b1; end else if (!is_read) begin case (reg_addr) REG_SOURCE_A: source_a <= rx_byte[3:0]; REG_SOURCE_B: source_b <= rx_byte[3:0]; REG_MODE: pair_mode <= rx_byte[0]; REG_DECIM_HIGH: decimation[15:8] <= rx_byte; REG_DECIM_LOW: decimation[7:0] <= rx_byte; REG_RING_SELECT: if (rx_byte < RINGS_PER_GEN) ring_select <= rx_byte; REG_GEN_ENABLE: gen_enable <= rx_byte[GENERATORS-1:0]; REG_TEMPORAL: if (rx_byte >= 8'd1 && rx_byte <= 8'd8) temporal <= rx_byte[3:0]; REG_FREQ_GEN: freq_gen <= rx_byte[1:0]; REG_HEALTH_CTRL: health_clear <= rx_byte[0]; REG_RCT_CUTOFF: if (rx_byte >= 8'd2) rct_cutoff <= rx_byte; REG_APT_HIGH: apt_cutoff[10:8] <= rx_byte[2:0]; REG_APT_LOW: apt_cutoff[7:0] <= rx_byte; 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_STATS_CTRL: begin stats_von_neumann <= rx_byte[0]; stats_restart <= rx_byte[1]; end REG_CONTROL: begin if (rx_byte[0]) capture_start <= 1'b1; if (rx_byte[0] || rx_byte[1]) bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}}; if (rx_byte[2]) freq_start <= 1'b1; end default: ; endcase end end end end endmodule