feat: add per-second GCP-format trials with history, watch and HTTP access (FPGA v10)

This commit is contained in:
randogoth 2026-10-25 18:23:18 +02:00
parent d821b77d3b
commit 6bee626b7f
12 changed files with 475 additions and 29 deletions

131
fpga/rtl/gcp_block.v Normal file
View file

@ -0,0 +1,131 @@
// Per-second trials in the format of Global Consciousness Project (GCP) style network analyses,
// running alongside the other outputs. Every TRIAL_MS milliseconds (one second) each generator
// yields:
// - the sum of the first SAMPLE_BITS bits of the second, the GCP trial (expected 100, sd 7.07);
// - the sum of all its bits during the second, for a full-rate z-score.
// Both use XOR-masked bits: bit i of the second is inverted when i is odd, so a constant bias
// cancels. All generators deliver in lockstep, so one bit count serves them all; generators that
// were not healthy for the whole second are flagged in the snapshot, not excluded from counting.
//
// Snapshot layout (big-endian), latched at the end of each second:
// 0-3 sequence number, starting at 1 (0: no second completed yet)
// 4 generators healthy during the whole second (bit g)
// 5 SAMPLE_BITS
// 6-7 0
// 8-11 bits per generator during the second
// 12+5g generator g: sample sum (1), masked ones during the second (4)
// 32-35 sequence number again; a mismatch with bytes 0-3 means a torn read
module gcp_block #(
parameter GENERATORS = 4,
parameter SAMPLE_BITS = 200,
parameter TRIAL_MS = 1000,
parameter TICK_CYCLES = 120000 // clk cycles per millisecond
) (
input wire clk,
input wire reset,
input wire [GENERATORS-1:0] gen_bit,
input wire gen_valid,
input wire [GENERATORS-1:0] healthy,
input wire [7:0] read_index,
output reg [7:0] read_byte, // one cycle after read_index
output reg trial_end // pulse: a new snapshot is available
);
localparam SNAPSHOT_BYTES = 12 + 5 * GENERATORS + 4;
localparam [7:0] SAMPLE_SIZE = SAMPLE_BITS;
// Trial timing
reg [16:0] tick_count;
reg [15:0] ms_count;
wire tick = (tick_count == TICK_CYCLES - 1);
always @(posedge clk) begin
trial_end <= 1'b0;
if (reset) begin
tick_count <= 17'd0;
ms_count <= 16'd0;
end else begin
tick_count <= tick ? 17'd0 : tick_count + 17'd1;
if (tick && ms_count == TRIAL_MS - 1) begin
ms_count <= 16'd0;
trial_end <= 1'b1;
end else if (tick) begin
ms_count <= ms_count + 16'd1;
end
end
end
// Counters. As in stats_block, the bit arriving in the trial_end cycle opens the next second.
reg [31:0] bit_count;
reg [7:0] sampled; // bits taken into the sample sums this second
reg odd; // the next bit has an odd index in the second
reg [7:0] sample_sum [0:GENERATORS-1];
reg [31:0] masked_ones [0:GENERATORS-1];
reg [31:0] sequence_number;
reg [GENERATORS-1:0] healthy_during;
wire mask = trial_end ? 1'b0 : odd;
wire in_sample = trial_end || sampled < SAMPLE_BITS;
wire [GENERATORS-1:0] masked = gen_bit ^ {GENERATORS{mask}};
reg [8*SNAPSHOT_BYTES-1:0] snapshot_next;
reg [7:0] snapshot [0:SNAPSHOT_BYTES-1];
integer k;
always @(*) begin
snapshot_next = {SNAPSHOT_BYTES{8'h00}};
snapshot_next[8*SNAPSHOT_BYTES-1 -: 96] = {sequence_number,
{8-GENERATORS{1'b0}}, healthy_during & healthy,
SAMPLE_SIZE, 16'd0, bit_count};
for (k = 0; k < GENERATORS; k = k + 1)
snapshot_next[8*SNAPSHOT_BYTES-1-96-40*k -: 40] = {sample_sum[k], masked_ones[k]};
snapshot_next[31:0] = sequence_number;
end
integer g, b;
always @(posedge clk) begin
if (reset) begin
bit_count <= 32'd0;
sampled <= 8'd0;
odd <= 1'b0;
sequence_number <= 32'd1;
healthy_during <= {GENERATORS{1'b1}};
for (g = 0; g < GENERATORS; g = g + 1) begin
sample_sum[g] <= 8'd0;
masked_ones[g] <= 32'd0;
end
end else begin
if (gen_valid) begin
bit_count <= (trial_end ? 32'd0 : bit_count) + 32'd1;
sampled <= in_sample ? (trial_end ? 8'd0 : sampled) + 8'd1 : sampled;
odd <= ~mask;
for (g = 0; g < GENERATORS; g = g + 1) begin
sample_sum[g] <= (trial_end ? 8'd0 : sample_sum[g]) + (in_sample && masked[g]);
masked_ones[g] <= (trial_end ? 32'd0 : masked_ones[g]) + masked[g];
end
end else if (trial_end) begin
bit_count <= 32'd0;
sampled <= 8'd0;
odd <= 1'b0;
for (g = 0; g < GENERATORS; g = g + 1) begin
sample_sum[g] <= 8'd0;
masked_ones[g] <= 32'd0;
end
end
if (trial_end) begin
for (b = 0; b < SNAPSHOT_BYTES; b = b + 1)
snapshot[b] <= snapshot_next[8*(SNAPSHOT_BYTES-1-b) +: 8];
sequence_number <= sequence_number + 32'd1;
healthy_during <= healthy;
end else begin
healthy_during <= healthy_during & healthy;
end
end
end
always @(posedge clk)
read_byte <= (read_index < SNAPSHOT_BYTES) ? snapshot[read_index] : 8'h00;
endmodule

View file

@ -1,5 +1,5 @@
// Board-independent STRIX logic: entropy generators with health monitors, entropy byte
// stream, live statistics, bias amplifier, source selection, raw capture, frequency counter
// stream, live statistics, bias amplifier, per-second GCP-style trials, 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,
@ -20,7 +20,8 @@
// 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). Reading STREAM returns
// the bias amplifier's trial snapshot (layout in bias_amplifier.v), and GCP for the per-second
// trial snapshot (layout in gcp_block.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 #(
@ -28,7 +29,8 @@ module strix_core #(
parameter FREQ_GATE_BITS = 24,
parameter RINGS_PER_GEN = 90,
parameter DEAD_WINDOW_BITS = 16,
parameter TICK_CYCLES = 120000 // clk cycles per millisecond
parameter TICK_CYCLES = 120000, // clk cycles per millisecond
parameter GCP_TRIAL_MS = 1000
) (
input wire clk,
input wire reset,
@ -41,7 +43,7 @@ module strix_core #(
output reg nina_gpio0_in // GPIO0 is driven by the module (to the SAMD21), not to it
);
localparam [7:0] VERSION = 8'h09;
localparam [7:0] VERSION = 8'h0A;
localparam GENERATORS = 4;
localparam [6:0] REG_ID0 = 7'h00, // "STRX"
@ -93,6 +95,7 @@ module strix_core #(
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_GCP = 7'h46, // burst read of the per-second trial snapshot
REG_AMP_CTRL = 7'h50, // bit 0 von Neumann on the amplifier path;
// write bit 1: restart the amplifier, sequence 0
REG_AMP_SOURCES = 7'h51, // generators feeding the amplifier (healthy only)
@ -220,6 +223,18 @@ module strix_core #(
.trial_end(trial_end)
);
// Per-second GCP-style trials, fed from the same registered generator outputs
reg [7:0] gcp_index;
wire [7:0] gcp_byte;
wire gcp_trial_end;
gcp_block #(.GENERATORS(GENERATORS), .TRIAL_MS(GCP_TRIAL_MS), .TICK_CYCLES(TICK_CYCLES)) gcp (
.clk(clk), .reset(reset), .gen_bit(stats_bits[GENERATORS-1:0]), .gen_valid(stats_valid),
.healthy(stats_healthy), .read_index(gcp_index), .read_byte(gcp_byte),
.trial_end(gcp_trial_end)
);
// Source selection
reg [3:0] source_a, source_b;
@ -367,6 +382,7 @@ module strix_core #(
REG_DATA: burst_byte = capture_byte;
REG_STATS: burst_byte = stats_byte;
REG_AMP: burst_byte = amp_byte;
REG_GCP: burst_byte = gcp_byte;
REG_STREAM: burst_byte = stream_available ? stream_head : 8'h00;
default: begin
burst_byte = 8'h00;
@ -411,6 +427,7 @@ 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;
gcp_index <= 8'd0;
stream_source <= 3'd4;
nina_enable <= 1'b0;
nina_gpio0_in <= 1'b0;
@ -425,6 +442,11 @@ module strix_core #(
else if (tx_loaded && is_read && reg_addr == REG_AMP)
amp_index <= amp_index + 8'd1;
if (!selected)
gcp_index <= 8'd0;
else if (tx_loaded && is_read && reg_addr == REG_GCP)
gcp_index <= gcp_index + 8'd1;
if (!selected)
stats_index <= 8'd0;
else if (tx_loaded && is_read && reg_addr == REG_STATS)