fix: hand GPIO0 to the WiFi module, count only healthy generators, shorten amplifier stage path (FPGA v9)

This commit is contained in:
randogoth 2026-10-25 16:13:41 +02:00
parent 2a96333aef
commit 5c69d826a2
7 changed files with 61 additions and 31 deletions

View file

@ -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,

View file

@ -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;
// 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

View file

@ -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

View file

@ -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

View file

@ -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];

View file

@ -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");

View file

@ -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")