From 5c69d826a2bf15bcad4b3470cb9ca642238f7ae9 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 25 Oct 2026 16:13:41 +0200 Subject: [PATCH] fix: hand GPIO0 to the WiFi module, count only healthy generators, shorten amplifier stage path (FPGA v9) --- .../libraries/StrixFPGA/src/StrixRegisters.h | 7 ++--- fpga/rtl/amp_stage.v | 27 +++++++++++++------ fpga/rtl/stats_block.v | 17 +++++++----- fpga/rtl/strix.v | 11 +++++--- fpga/rtl/strix_core.v | 22 ++++++++++----- fpga/sim/tb_strix_core.v | 4 +-- tools/jitter.py | 4 +-- 7 files changed, 61 insertions(+), 31 deletions(-) diff --git a/arduino/libraries/StrixFPGA/src/StrixRegisters.h b/arduino/libraries/StrixFPGA/src/StrixRegisters.h index 54d6e5b..3a795bb 100644 --- a/arduino/libraries/StrixFPGA/src/StrixRegisters.h +++ b/arduino/libraries/StrixFPGA/src/StrixRegisters.h @@ -1,4 +1,4 @@ -// STRIX FPGA register map (FPGA version 8). Mirrors the definitions in fpga/rtl/strix_core.v, +// STRIX FPGA register map (FPGA version 9). Mirrors the definitions in fpga/rtl/strix_core.v, // which also documents the SPI protocol and snapshot layouts. #pragma once @@ -6,7 +6,7 @@ namespace strix { -const uint8_t FPGA_VERSION = 8; +const uint8_t FPGA_VERSION = 9; const uint8_t READ = 0x80; // command bit 7: read enum Register : uint8_t { @@ -42,7 +42,7 @@ enum Register : uint8_t { REG_LEVEL_LOW = 0x2B, REG_DROP_HIGH = 0x2C, REG_DROP_LOW = 0x2D, - REG_NINA_CTRL = 0x2E, // bit 0: WiFi module may run + REG_NINA_CTRL = 0x2E, // bit 0: WiFi module may run; bit 1: GPIO0 driven by the module REG_GEN_HEALTH = 0x30, // + 4g: flags, RCT events, APT events, dead rings REG_STATS = 0x40, REG_AMP = 0x42, @@ -61,6 +61,7 @@ const uint8_t STATS_VON_NEUMANN = 0x01, STATS_RESTART = 0x02; const uint8_t STREAM_VON_NEUMANN = 0x08, STREAM_FLUSH = 0x80, STREAM_COMBINED = 4; const uint8_t AMP_VON_NEUMANN = 0x01, AMP_RESTART = 0x02; const uint8_t HEALTH_CLEAR = 0x01; +const uint8_t NINA_ENABLE = 0x01, NINA_GPIO0_FROM_MODULE = 0x02; // Per-generator health flags (REG_GEN_HEALTH + 4g) const uint8_t FLAG_RCT_FAILED = 0x01, FLAG_APT_FAILED = 0x02, FLAG_TOO_MANY_DEAD = 0x04, diff --git a/fpga/rtl/amp_stage.v b/fpga/rtl/amp_stage.v index 75bf152..a233733 100644 --- a/fpga/rtl/amp_stage.v +++ b/fpga/rtl/amp_stage.v @@ -6,7 +6,8 @@ // Majority vote (mode 1): every param input bits produce one output bit, 1 if d > 0 (use odd // windows to avoid ties, which output 0). // -// param = 1 passes bits through unchanged in both modes. +// param = 1 passes bits through unchanged in both modes. The end conditions compare the current +// position and count against registered limits, keeping the adder off the decision path. module amp_stage ( input wire clk, input wire restart, @@ -21,11 +22,21 @@ module amp_stage ( reg signed [16:0] position; reg [15:0] seen; - wire signed [16:0] next_position = position + (in_bit ? 17'sd1 : -17'sd1); - wire signed [16:0] bound = {1'b0, param}; - wire walk_done = next_position == bound || next_position == -bound; - wire vote_done = seen == param - 16'd1; - wire done = mode ? vote_done : walk_done; + // Limits derived from param, registered (param changes only between runs, with a restart). + reg signed [16:0] upper_last, lower_last; // +(param - 1) and -(param - 1) + reg [15:0] window_last; // param - 1 + always @(posedge clk) begin + upper_last <= {1'b0, param} - 17'sd1; + lower_last <= 17'sd1 - {1'b0, param}; + window_last <= param - 16'd1; + end + + // A step reaches the bound when the walk already stands one short of it in that direction. + wire walk_done = in_bit ? position == upper_last : position == lower_last; + wire vote_done = seen == window_last; + wire done = mode ? vote_done : walk_done; + // Majority after this bit: d + 1 > 0 for a one, d - 1 > 0 for a zero. + wire vote_one = in_bit ? !position[16] : (!position[16] && position > 17'sd1); always @(posedge clk) begin out_valid <= 1'b0; @@ -35,11 +46,11 @@ module amp_stage ( end else if (in_valid) begin if (done) begin out_valid <= 1'b1; - out_bit <= next_position > 0; + out_bit <= mode ? vote_one : in_bit; position <= 17'sd0; seen <= 16'd0; end else begin - position <= next_position; + position <= position + (in_bit ? 17'sd1 : -17'sd1); seen <= seen + 16'd1; end end diff --git a/fpga/rtl/stats_block.v b/fpga/rtl/stats_block.v index 909be2c..4d9e482 100644 --- a/fpga/rtl/stats_block.v +++ b/fpga/rtl/stats_block.v @@ -23,7 +23,7 @@ module stats_block #( input wire clk, input wire reset, input wire [CHANNELS-1:0] bits, - input wire valid, // new bits on all channels this cycle + input wire [CHANNELS-1:0] valid, // new bit on each channel this cycle input wire von_neumann, input wire [15:0] trial_ms, input wire restart, // start a new trial now, sequence back to 0 @@ -61,17 +61,20 @@ module stats_block #( // Von Neumann extraction per channel reg [CHANNELS-1:0] pair_first, have_first; - wire [CHANNELS-1:0] vn_valid = have_first & (bits ^ pair_first); - wire [CHANNELS-1:0] count_valid = von_neumann ? (valid ? vn_valid : {CHANNELS{1'b0}}) - : {CHANNELS{valid}}; + wire [CHANNELS-1:0] vn_valid = valid & have_first & (bits ^ pair_first); + wire [CHANNELS-1:0] count_valid = von_neumann ? vn_valid : valid; wire [CHANNELS-1:0] count_bit = von_neumann ? pair_first : bits; + integer v; always @(posedge clk) begin if (reset || !von_neumann) begin have_first <= {CHANNELS{1'b0}}; - end else if (valid) begin - have_first <= ~have_first; - pair_first <= bits; + end else begin + for (v = 0; v < CHANNELS; v = v + 1) + if (valid[v]) begin + have_first[v] <= ~have_first[v]; + pair_first[v] <= bits[v]; + end end end diff --git a/fpga/rtl/strix.v b/fpga/rtl/strix.v index 32f6973..e967dd1 100644 --- a/fpga/rtl/strix.v +++ b/fpga/rtl/strix.v @@ -34,7 +34,7 @@ module strix ( // NINA pass-through: the SAMD21 talks to the WiFi module as if wired directly. - wire nina_enable; // from strix_core + wire nina_enable, nina_gpio0_in; // from strix_core wire sam_nina_mosi = bMKR_D[8]; // SERCOM1 wire sam_nina_sck = bMKR_D[9]; @@ -46,7 +46,9 @@ module strix ( assign bWM_PIO36 = sam_nina_mosi; assign bWM_PIO29 = sam_nina_sck; assign bWM_PIO28 = sam_nina_ss; - assign bWM_PIO27 = sam_nina_gpio0; + // GPIO0 is bidirectional on the module: the SAMD21 drives it to select the boot mode, the + // running module drives it as its data-ready signal. Only one side drives at a time. + assign bWM_PIO27 = nina_gpio0_in ? 1'bz : sam_nina_gpio0; // The module stays in reset until the firmware enables it: its boot current on top of the // ring oscillators can brown out the board, and it is not needed without WiFi. assign oWM_RESET = nina_enable ? sam_nina_resetn : 1'b0; @@ -57,6 +59,7 @@ module strix ( wire nina_miso = bWM_PIO1; // to D10 wire nina_ack = bWM_PIO7; // to D4 wire nina_uart_tx = iWM_TX; // to D13 + wire nina_gpio0 = bWM_PIO27; // to D6 while the module drives it // STRIX SPI link on SAMD21 SERCOM0 alternate pins: A3 MOSI, A4 SCK, A5 SS, A6 MISO. @@ -68,7 +71,8 @@ module strix ( strix_core core ( .clk(clk), .reset(reset), .spi_sck(strix_sck), .spi_mosi(strix_mosi), .spi_ss_n(strix_ss_n), - .spi_miso(strix_miso), .capture_done(capture_done), .nina_enable(nina_enable) + .spi_miso(strix_miso), .capture_done(capture_done), .nina_enable(nina_enable), + .nina_gpio0_in(nina_gpio0_in) ); assign oSAM_INT = capture_done; @@ -79,6 +83,7 @@ module strix ( generate for (d = 0; d < 15; d = d + 1) begin : header_d assign bMKR_D[d] = (d == 4) ? nina_ack : + (d == 6) ? (nina_gpio0_in ? nina_gpio0 : 1'bz) : (d == 10) ? nina_miso : (d == 13) ? nina_uart_tx : 1'bz; end diff --git a/fpga/rtl/strix_core.v b/fpga/rtl/strix_core.v index bb931e1..aff31f4 100644 --- a/fpga/rtl/strix_core.v +++ b/fpga/rtl/strix_core.v @@ -37,10 +37,11 @@ module strix_core #( input wire spi_ss_n, output wire spi_miso, output wire capture_done, - output reg nina_enable // releases the WiFi module from reset (see REG_NINA_CTRL) + output reg nina_enable, // releases the WiFi module from reset (see REG_NINA_CTRL) + output reg nina_gpio0_in // GPIO0 is driven by the module (to the SAMD21), not to it ); - localparam [7:0] VERSION = 8'h08; + localparam [7:0] VERSION = 8'h09; localparam GENERATORS = 4; localparam [6:0] REG_ID0 = 7'h00, // "STRX" @@ -85,7 +86,10 @@ module strix_core #( REG_LEVEL_LOW = 7'h2B, REG_DROP_HIGH = 7'h2C, // bytes dropped because the FIFO was full REG_DROP_LOW = 7'h2D, - REG_NINA_CTRL = 7'h2E, // bit 0: WiFi module may run (otherwise held in reset) + REG_NINA_CTRL = 7'h2E, // bit 0: WiFi module may run (otherwise held in reset); + // bit 1: GPIO0 direction, 0 SAMD21 -> module (boot + // mode selection), 1 module -> SAMD21 (its data-ready + // signal once running) 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 @@ -208,7 +212,9 @@ module strix_core #( ); stats_block #(.CHANNELS(GENERATORS + 1), .TICK_CYCLES(TICK_CYCLES)) stats ( - .clk(clk), .reset(reset), .bits(stats_bits), .valid(stats_valid), + // Only healthy generators are counted; the combined channel while any is healthy. + .clk(clk), .reset(reset), .bits(stats_bits), + .valid({GENERATORS + 1{stats_valid}} & {|stats_healthy, stats_healthy}), .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) @@ -329,7 +335,7 @@ module strix_core #( 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_NINA_CTRL: tx_byte_next = {7'd0, nina_enable}; + REG_NINA_CTRL: tx_byte_next = {6'd0, nina_gpio0_in, nina_enable}; 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; @@ -407,6 +413,7 @@ module strix_core #( amp_index <= 8'd0; stream_source <= 3'd4; nina_enable <= 1'b0; + nina_gpio0_in <= 1'b0; stream_von_neumann <= 1'b0; is_read <= 1'b0; reg_addr <= 7'd0; @@ -454,7 +461,10 @@ 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_NINA_CTRL: nina_enable <= rx_byte[0]; + REG_NINA_CTRL: begin + nina_enable <= rx_byte[0]; + nina_gpio0_in <= rx_byte[1]; + end REG_STREAM_CTRL: begin if (rx_byte[2:0] <= 3'd4) stream_source <= rx_byte[2:0]; stream_von_neumann <= rx_byte[3]; diff --git a/fpga/sim/tb_strix_core.v b/fpga/sim/tb_strix_core.v index 9880aa3..dd1bde7 100644 --- a/fpga/sim/tb_strix_core.v +++ b/fpga/sim/tb_strix_core.v @@ -108,7 +108,7 @@ module tb_strix_core; model_latched_ones <= model_ones; model_latched_agree <= model_agree; end - if (dut.stats.valid) begin + if (dut.stats.valid[0]) 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) @@ -282,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'h08, "VERSION"); + read_reg(REG_VERSION, v); expect(v, 8'h09, "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"); diff --git a/tools/jitter.py b/tools/jitter.py index 6fb4333..f0ee04e 100644 --- a/tools/jitter.py +++ b/tools/jitter.py @@ -82,8 +82,8 @@ class Board: while self.serial.readline(): pass self.serial.timeout = 200 - if not identity.startswith("STRX v8"): - sys.exit(f"unexpected board identity: {identity!r} (needs the v8 FPGA image)") + if not identity.startswith("STRX v9"): + sys.exit(f"unexpected board identity: {identity!r} (needs the v9 FPGA image)") self.generators = self.read(REG_GENERATORS) self.rings = self.read(REG_RINGS) print(f"board: {identity}, {self.generators} generators x {self.rings} rings")