diff --git a/fpga/rtl/amp_path.v b/fpga/rtl/amp_path.v index 517e36c..5ac1641 100644 --- a/fpga/rtl/amp_path.v +++ b/fpga/rtl/amp_path.v @@ -18,6 +18,8 @@ module amp_path #( 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 [4:0] vote_count, // sub-trials that voted (registered from voted) + output reg [4:0] one_votes, // of those, voted 1 output reg overflow // a merge input arrived before the last was taken ); @@ -81,6 +83,14 @@ module amp_path #( reg [23:0] sub_bits, sub_ones; + 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 + always @(posedge clk) begin if (restart || trial_end) begin trial_bits <= out_valid ? 32'd1 : 32'd0; @@ -100,6 +110,10 @@ module amp_path #( sub_ones <= sub_ones + out_bit; end + // Registered counts, ready one cycle after the last vote of a trial + vote_count <= count_ones(voted); + one_votes <= count_ones(votes & voted); + 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; diff --git a/fpga/rtl/bias_amplifier.v b/fpga/rtl/bias_amplifier.v index baef0f5..75dd95c 100644 --- a/fpga/rtl/bias_amplifier.v +++ b/fpga/rtl/bias_amplifier.v @@ -72,12 +72,13 @@ module bias_amplifier #( reg [16:0] tick_count; reg [7:0] ms_count; reg [3:0] sub_index; - reg sub_end; + reg sub_end, trial_due; wire tick = (tick_count == TICK_CYCLES - 1); always @(posedge clk) begin sub_end <= 1'b0; - trial_end <= 1'b0; + trial_due <= 1'b0; + trial_end <= trial_due; if (start_over) begin tick_count <= 17'd0; ms_count <= 8'd0; @@ -92,11 +93,12 @@ module bias_amplifier #( ms_count <= ms_count + 8'd1; end end - // trial_end follows the last sub_end by one cycle, once its vote is registered. + // trial_end follows the last sub_end by two cycles, once its vote and the vote + // counts are registered. if (sub_end) begin if (sub_index + 4'd1 >= subtrials) begin sub_index <= 4'd0; - trial_end <= 1'b1; + trial_due <= 1'b1; end else begin sub_index <= sub_index + 4'd1; end @@ -110,6 +112,8 @@ module bias_amplifier #( wire [31:0] trial_ones [0:1]; wire [15:0] votes [0:1]; wire [15:0] voted [0:1]; + wire [4:0] vote_count [0:1]; + wire [4:0] one_votes [0:1]; wire [1:0] overflow; amp_path #(.GENERATORS(GENERATORS)) bias_path ( @@ -117,7 +121,8 @@ module bias_amplifier #( .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]) + .votes(votes[0]), .voted(voted[0]), .vote_count(vote_count[0]), .one_votes(one_votes[0]), + .overflow(overflow[0]) ); amp_path #(.GENERATORS(GENERATORS)) autocorrelation_path ( @@ -125,27 +130,18 @@ module bias_amplifier #( .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]) + .votes(votes[1]), .voted(voted[1]), .vote_count(vote_count[1]), .one_votes(one_votes[1]), + .overflow(overflow[1]) ); // Snapshot - function [4:0] count_ones(input [15:0] value); - integer b; + function [127:0] path_bytes(input [31:0] bits, input [31:0] ones, input [15:0] vote, input [15:0] cast, + input [4:0] cast_count, input [4:0] ones_cast); + reg valid, result; 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}; + valid = cast_count != 0 && {ones_cast, 1'b0} != {1'b0, cast_count}; + result = {ones_cast, 1'b0} > {1'b0, cast_count}; path_bytes = {bits, ones, vote, cast, 3'd0, cast_count, 6'd0, valid, result, 16'd0}; end endfunction @@ -159,8 +155,8 @@ module bias_amplifier #( 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]), + path_bytes(trial_bits[0], trial_ones[0], votes[0], voted[0], vote_count[0], one_votes[0]), + path_bytes(trial_bits[1], trial_ones[1], votes[1], voted[1], vote_count[1], one_votes[1]), sequence_number }; diff --git a/fpga/rtl/entropy_stream.v b/fpga/rtl/entropy_stream.v new file mode 100644 index 0000000..4fffa4f --- /dev/null +++ b/fpga/rtl/entropy_stream.v @@ -0,0 +1,89 @@ +// Byte stream of entropy for the SAMD21: bits from the selected channel (optionally through a +// von Neumann extractor) are packed MSB first into bytes and queued in a FIFO. When the FIFO +// is full, new bytes are dropped and counted; bits are never reused. +// +// Reading: `head` is the oldest byte, valid while `available`; `pop` removes it. A popped +// byte's successor appears on `head` two cycles later. `level` and `available` are registered +// and lag by a cycle, which only ever understates what can be read. +module entropy_stream #( + parameter ADDR_WIDTH = 12 // 4096-byte FIFO +) ( + input wire clk, + input wire reset, + input wire flush, + input wire bit_in, + input wire valid, + input wire von_neumann, + input wire pop, + output reg [7:0] head, + output reg available, + output reg [ADDR_WIDTH:0] level, + output reg [15:0] dropped // bytes dropped since flush, saturating +); + + // Von Neumann (01 -> 0, 10 -> 1) + + reg pair_first, have_first; + wire vn_valid = von_neumann ? (valid && have_first && bit_in != pair_first) : valid; + wire vn_bit = von_neumann ? pair_first : bit_in; + + always @(posedge clk) begin + if (reset || flush || !von_neumann) begin + have_first <= 1'b0; + end else if (valid) begin + have_first <= ~have_first; + pair_first <= bit_in; + end + end + + // Packing + + reg [6:0] partial; + reg [2:0] bit_count; + reg byte_ready; + reg [7:0] byte_out; + + always @(posedge clk) begin + byte_ready <= 1'b0; + if (reset || flush) begin + bit_count <= 3'd0; + end else if (vn_valid) begin + partial <= {partial[5:0], vn_bit}; + bit_count <= bit_count + 3'd1; + if (bit_count == 3'd7) begin + byte_ready <= 1'b1; + byte_out <= {partial, vn_bit}; + end + end + end + + // FIFO + + reg [7:0] buffer [0:(1 << ADDR_WIDTH) - 1]; + reg [ADDR_WIDTH:0] write_ptr, read_ptr; + + wire [ADDR_WIDTH:0] fill = write_ptr - read_ptr; + wire full = fill[ADDR_WIDTH]; + + always @(posedge clk) begin + if (reset || flush) begin + write_ptr <= {(ADDR_WIDTH + 1){1'b0}}; + read_ptr <= {(ADDR_WIDTH + 1){1'b0}}; + dropped <= 16'd0; + end else begin + if (byte_ready) begin + if (!full) begin + buffer[write_ptr[ADDR_WIDTH-1:0]] <= byte_out; + write_ptr <= write_ptr + 1'b1; + end else if (dropped != 16'hFFFF) begin + dropped <= dropped + 16'd1; + end + end + if (pop && fill != 0) read_ptr <= read_ptr + 1'b1; + end + head <= buffer[read_ptr[ADDR_WIDTH-1:0]]; + level <= fill; + available <= fill != 0 && !(pop && fill == 1); + end + +endmodule diff --git a/fpga/rtl/strix_core.v b/fpga/rtl/strix_core.v index d89f5b0..2957977 100644 --- a/fpga/rtl/strix_core.v +++ b/fpga/rtl/strix_core.v @@ -1,6 +1,6 @@ -// Board-independent STRIX logic: entropy generators with health monitors, live statistics, -// bias amplifier, source selection, raw capture, frequency counter and the SPI register -// interface to the SAMD21. +// Board-independent STRIX logic: entropy generators with health monitors, entropy byte +// stream, live statistics, 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 @@ -20,7 +20,9 @@ // 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. AMP does the same for -// the bias amplifier's trial snapshot (layout in bias_amplifier.v). +// the bias amplifier's trial snapshot (layout in bias_amplifier.v). Reading STREAM returns +// entropy bytes from the stream FIFO, removing each byte once it has been clocked out; read +// at most STREAM_LEVEL bytes (an empty FIFO returns 0x00). module strix_core #( parameter CAPTURE_ADDR_WIDTH = 15, parameter FREQ_GATE_BITS = 24, @@ -37,7 +39,7 @@ module strix_core #( output wire capture_done ); - localparam [7:0] VERSION = 8'h06; + localparam [7:0] VERSION = 8'h07; localparam GENERATORS = 4; localparam [6:0] REG_ID0 = 7'h00, // "STRX" @@ -76,7 +78,14 @@ 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_STREAM_CTRL = 7'h29, // bits 2:0 source (0-3 generator, 4 combined), + // bit 3 von Neumann; write bit 7: flush + REG_LEVEL_HIGH = 7'h2A, // bytes waiting in the stream FIFO + REG_LEVEL_LOW = 7'h2B, + REG_DROP_HIGH = 7'h2C, // bytes dropped because the FIFO was full + REG_DROP_LOW = 7'h2D, REG_STATS = 7'h40, // burst read of the statistics trial snapshot + REG_STREAM = 7'h44, // burst read of entropy bytes 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 @@ -156,6 +165,26 @@ module strix_core #( stats_valid <= out_valid[0]; end + // Entropy byte stream, fed from the same registered outputs. A generator that is not + // healthy (or no healthy generator, for the combined channel) feeds nothing. + + reg [2:0] stream_source; + reg stream_von_neumann, stream_flush, stream_pop; + wire [7:0] stream_head; + wire stream_available; + wire [12:0] stream_level; + wire [15:0] stream_dropped; + wire stream_source_healthy = stream_source == 3'd4 ? |stats_healthy + : stats_healthy[stream_source[1:0]]; + + entropy_stream #(.ADDR_WIDTH(12)) stream ( + .clk(clk), .reset(reset), .flush(stream_flush), + .bit_in(stats_bits[stream_source]), .valid(stats_valid && stream_source_healthy), + .von_neumann(stream_von_neumann), .pop(stream_pop), + .head(stream_head), .available(stream_available), .level(stream_level), + .dropped(stream_dropped) + ); + // Bias amplifier, fed from the same registered generator outputs reg [GENERATORS-1:0] amp_sources; @@ -277,7 +306,6 @@ module strix_core #( 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]; @@ -294,8 +322,11 @@ module strix_core #( 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_STREAM_CTRL: tx_byte_next = {4'd0, stream_von_neumann, stream_source}; + REG_LEVEL_HIGH: tx_byte_next = {3'd0, stream_level[12:8]}; + REG_LEVEL_LOW: tx_byte_next = stream_level[7:0]; + REG_DROP_HIGH: tx_byte_next = stream_dropped[15:8]; + REG_DROP_LOW: tx_byte_next = stream_dropped[7:0]; 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; @@ -313,10 +344,32 @@ module strix_core #( 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; + // Registered read-back. The next byte is needed only half an SCK period after the previous + // one completes. Plain registers are static, so their wide multiplexer gets an extra + // pipeline stage; burst registers, whose bytes change while they are read, take a short + // path. + reg [7:0] register_byte; + reg [7:0] burst_byte; + reg is_burst; + + always @(*) begin + is_burst = 1'b1; + case (reg_addr) + REG_DATA: burst_byte = capture_byte; + REG_STATS: burst_byte = stats_byte; + REG_AMP: burst_byte = amp_byte; + REG_STREAM: burst_byte = stream_available ? stream_head : 8'h00; + default: begin + burst_byte = 8'h00; + is_burst = 1'b0; + end + endcase + end + + always @(posedge clk) begin + register_byte <= tx_byte_next; + tx_byte <= is_burst ? burst_byte : register_byte; + end always @(posedge clk) begin capture_start <= 1'b0; @@ -324,6 +377,8 @@ module strix_core #( health_clear <= 1'b0; stats_restart <= 1'b0; amp_restart <= 1'b0; + stream_flush <= 1'b0; + stream_pop <= 1'b0; if (reset) begin gen_enable <= {GENERATORS{1'b1}}; temporal <= 4'd5; @@ -347,6 +402,8 @@ module strix_core #( 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; + stream_source <= 3'd4; + stream_von_neumann <= 1'b0; is_read <= 1'b0; reg_addr <= 7'd0; bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}}; @@ -373,6 +430,8 @@ module strix_core #( reg_addr <= rx_byte[6:0]; end else if (reading_data) begin bytes_read <= bytes_read + 1'b1; + end else if (is_read && reg_addr == REG_STREAM) begin + stream_pop <= 1'b1; // the byte has been clocked out end else if (!is_read) begin case (reg_addr) REG_SOURCE_A: source_a <= rx_byte[3:0]; @@ -391,6 +450,11 @@ 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_STREAM_CTRL: begin + if (rx_byte[2:0] <= 3'd4) stream_source <= rx_byte[2:0]; + stream_von_neumann <= rx_byte[3]; + stream_flush <= rx_byte[7]; + end REG_AMP_CTRL: begin amp_von_neumann <= rx_byte[0]; amp_restart <= rx_byte[1]; diff --git a/fpga/sim/tb_strix_core.v b/fpga/sim/tb_strix_core.v index e5e548a..8c97901 100644 --- a/fpga/sim/tb_strix_core.v +++ b/fpga/sim/tb_strix_core.v @@ -1,7 +1,8 @@ // 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, and the bias amplifier's bit counts in several configurations. Rings are replaced by the SIM model in ring_osc.v. +// 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; @@ -19,7 +20,8 @@ module tb_strix_core; 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_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 @@ -280,7 +282,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'h06, "VERSION"); + 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"); @@ -429,6 +431,34 @@ module tb_strix_core; 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; diff --git a/fpga/src/strix.qsf b/fpga/src/strix.qsf index 5732530..a0aaa9b 100644 --- a/fpga/src/strix.qsf +++ b/fpga/src/strix.qsf @@ -63,6 +63,7 @@ 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/entropy_stream.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