feat: add 16-ring bank, sampled XOR, pair capture and frequency counter (FPGA v2)

This commit is contained in:
randogoth 2026-10-25 12:56:54 +02:00
parent d51a6c0ca8
commit 504b00752a
12 changed files with 654 additions and 347 deletions

View file

@ -1,34 +0,0 @@
// One generator: two rings of different lengths whose enhanced outputs are XORed, plus
// the intermediate signals exposed for jitter characterization. All four are sampled
// every clock.
module entropy_gen #(
parameter LENGTH_A = 12,
parameter LENGTH_B = 10
) (
input wire clk,
input wire enable,
output wire [3:0] samples // {pair XOR, enhanced B, enhanced A, single tap of A}
);
wire ring_a_tap, ring_a_enhanced, ring_b_tap, ring_b_enhanced;
ring_osc #(.LENGTH(LENGTH_A)) ring_a (
.enable(enable), .first_tap(ring_a_tap), .enhanced(ring_a_enhanced)
);
ring_osc #(.LENGTH(LENGTH_B)) ring_b (
.enable(enable), .first_tap(ring_b_tap), .enhanced(ring_b_enhanced)
);
(* keep *) wire pair_xor = ring_a_enhanced ^ ring_b_enhanced;
// sample_raw catches the asynchronous ring signals and may go metastable;
// sample_sync resolves it. The SDC file cuts timing into sample_raw.
reg [3:0] sample_raw, sample_sync;
always @(posedge clk) begin
sample_raw <= {pair_xor, ring_b_enhanced, ring_a_enhanced, ring_a_tap};
sample_sync <= sample_raw;
end
assign samples = sample_sync;
endmodule

47
fpga/rtl/freq_counter.v Normal file
View file

@ -0,0 +1,47 @@
// Measures a ring frequency: an 8-bit prescaler clocked by the ring itself divides it by
// 256, and its top bit's rising edges are counted in the clk domain over a gate of
// 2^GATE_BITS clk cycles. frequency = count * 256 * f_clk / 2^GATE_BITS.
module freq_counter #(
parameter GATE_BITS = 24 // 2^24 cycles at 120 MHz = 140 ms
) (
input wire clk,
input wire reset,
input wire signal, // asynchronous signal to measure (used as a clock)
input wire start,
output reg busy,
output reg valid,
output reg [31:0] count
);
reg [7:0] prescaler = 8'd0;
always @(posedge signal) prescaler <= prescaler + 8'd1;
// The SDC file cuts timing from prescaler into this synchronizer.
reg [2:0] divided_history;
always @(posedge clk) divided_history <= {divided_history[1:0], prescaler[7]};
wire divided_rising = (divided_history[2:1] == 2'b01);
reg [GATE_BITS-1:0] gate;
reg [31:0] edges;
always @(posedge clk) begin
if (reset) begin
busy <= 1'b0;
valid <= 1'b0;
end else if (start) begin
busy <= 1'b1;
valid <= 1'b0;
gate <= {GATE_BITS{1'b0}};
edges <= 32'd0;
end else if (busy) begin
gate <= gate + 1'b1;
if (divided_rising) edges <= edges + 32'd1;
if (&gate) begin
busy <= 1'b0;
valid <= 1'b1;
count <= edges + divided_rising;
end
end
end
endmodule

View file

@ -1,12 +1,15 @@
// Captures one burst of sampled bits into block RAM for offline analysis. Keeps every
// decimation-th sample of the selected source and packs them MSB first into bytes.
// decimation-th sample and packs it MSB first into bytes: one bit per sample from
// source A, or in pair mode two bits per sample (A then B) so both sources are recorded
// at the same instants.
module raw_capture #(
parameter ADDR_WIDTH = 15 // 2^15 bytes = 256 Kbit per burst
) (
input wire clk,
input wire reset,
input wire [3:0] samples,
input wire [1:0] source,
input wire bit_a,
input wire bit_b,
input wire pair_mode,
input wire [15:0] decimation, // 0 and 1 both mean every sample
input wire start,
output reg busy,
@ -18,8 +21,8 @@ module raw_capture #(
reg [7:0] buffer [0:(1 << ADDR_WIDTH) - 1];
reg [15:0] sample_count;
reg [2:0] bit_count;
reg [6:0] partial_byte;
reg [3:0] bit_count;
reg [7:0] partial_byte;
reg [ADDR_WIDTH-1:0] next_addr;
reg write_enable;
reg [ADDR_WIDTH-1:0] write_addr;
@ -27,7 +30,9 @@ module raw_capture #(
wire keep_sample = (sample_count == 16'd0);
wire count_wraps = (decimation <= 16'd1) || (sample_count == decimation - 16'd1);
wire new_bit = samples[source];
wire [7:0] shifted = pair_mode ? {partial_byte[5:0], bit_a, bit_b} : {partial_byte[6:0], bit_a};
wire [3:0] new_count = bit_count + (pair_mode ? 4'd2 : 4'd1);
always @(posedge clk) begin
write_enable <= 1'b0;
@ -38,22 +43,24 @@ module raw_capture #(
busy <= 1'b1;
done <= 1'b0;
sample_count <= 16'd0;
bit_count <= 3'd0;
bit_count <= 4'd0;
next_addr <= {ADDR_WIDTH{1'b0}};
end else if (busy) begin
sample_count <= count_wraps ? 16'd0 : sample_count + 16'd1;
if (keep_sample) begin
partial_byte <= {partial_byte[5:0], new_bit};
bit_count <= bit_count + 3'd1;
if (bit_count == 3'd7) begin
partial_byte <= shifted;
if (new_count == 4'd8) begin
bit_count <= 4'd0;
write_enable <= 1'b1;
write_addr <= next_addr;
write_data <= {partial_byte, new_bit};
write_data <= shifted;
next_addr <= next_addr + 1'b1;
if (&next_addr) begin
busy <= 1'b0;
done <= 1'b1;
end
end else begin
bit_count <= new_count;
end
end
end

36
fpga/rtl/ring_bank.v Normal file
View file

@ -0,0 +1,36 @@
// A bank of ring oscillators. Every ring's single tap and enhanced output is synchronized
// into the clk domain separately, so all combining happens after sampling. Ring lengths
// cycle through 9, 12 and 15 stages so neighbouring rings run at different frequencies.
module ring_bank #(
parameter RINGS = 16
) (
input wire clk,
input wire enable,
output wire [RINGS-1:0] enhanced, // synchronized enhanced outputs
output wire [RINGS-1:0] first_tap, // synchronized single taps
output wire [RINGS-1:0] raw_tap // unsynchronized single taps, for frequency counting
);
wire [RINGS-1:0] ring_enhanced;
genvar i;
generate
for (i = 0; i < RINGS; i = i + 1) begin : ring
ring_osc #(.LENGTH(9 + 3 * (i % 3))) osc (
.enable(enable), .first_tap(raw_tap[i]), .enhanced(ring_enhanced[i])
);
end
endgenerate
// sample_raw catches the asynchronous ring signals and may go metastable;
// sample_sync resolves it. The SDC file cuts timing into sample_raw.
reg [2*RINGS-1:0] sample_raw, sample_sync;
always @(posedge clk) begin
sample_raw <= {raw_tap, ring_enhanced};
sample_sync <= sample_raw;
end
assign enhanced = sample_sync[RINGS-1:0];
assign first_tap = sample_sync[2*RINGS-1:RINGS];
endmodule

View file

@ -1,12 +1,17 @@
// Board-independent STRIX logic: entropy generator, raw capture and the SPI register
// interface to the SAMD21.
// Board-independent STRIX logic: ring bank, 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 (6-bit codes): 0x00-0x0F enhanced output of ring i, 0x10-0x1F single
// tap of ring i, 0x20-0x2F XOR of the enhanced outputs of rings 0..n-1 for n = 1..16.
// All sources are taken after sampling.
module strix_core #(
parameter CAPTURE_ADDR_WIDTH = 15
parameter CAPTURE_ADDR_WIDTH = 15,
parameter FREQ_GATE_BITS = 24
) (
input wire clk,
input wire reset,
@ -17,27 +22,62 @@ module strix_core #(
output wire capture_done
);
localparam [7:0] VERSION = 8'h01;
localparam [7:0] VERSION = 8'h02;
localparam RINGS = 16;
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_SOURCE = 7'h08, // 0 tap A, 1 enhanced A, 2 enhanced B, 3 pair
REG_DECIM_HIGH = 7'h09,
REG_DECIM_LOW = 7'h0A,
REG_CONTROL = 7'h0B, // write: bit 0 start capture, bit 1 rewind DATA
REG_STATUS = 7'h0C, // bit 0 busy, bit 1 done
REG_DATA = 7'h10;
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_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_RING_COUNT = 7'h0F,
REG_DATA = 7'h10,
REG_FREQ_SELECT = 7'h11, // ring to measure
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;
// Entropy and capture
// Ring bank and source selection
wire [3:0] samples;
wire [RINGS-1:0] enhanced, first_tap, raw_tap;
entropy_gen generator (.clk(clk), .enable(1'b1), .samples(samples));
ring_bank #(.RINGS(RINGS)) bank (
.clk(clk), .enable(1'b1),
.enhanced(enhanced), .first_tap(first_tap), .raw_tap(raw_tap)
);
reg [1:0] source;
reg [RINGS-1:0] xor_prefix_comb, xor_prefix;
integer j;
always @(*) begin
xor_prefix_comb[0] = enhanced[0];
for (j = 1; j < RINGS; j = j + 1)
xor_prefix_comb[j] = xor_prefix_comb[j-1] ^ enhanced[j];
end
reg [5:0] source_a, source_b;
reg bit_a, bit_b;
wire [63:0] all_sources = {16'd0, xor_prefix, first_tap, enhanced};
always @(posedge clk) begin
xor_prefix <= xor_prefix_comb;
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;
@ -45,12 +85,24 @@ module strix_core #(
wire [7:0] capture_byte;
raw_capture #(.ADDR_WIDTH(CAPTURE_ADDR_WIDTH)) capture (
.clk(clk), .reset(reset), .samples(samples), .source(source),
.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 [3:0] freq_select;
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(raw_tap[freq_select]), .start(freq_start),
.busy(freq_busy), .valid(freq_valid), .count(freq_count)
);
// SPI
wire selected, rx_valid, rx_is_first, tx_loaded;
@ -76,25 +128,38 @@ module strix_core #(
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_SOURCE: tx_byte = {6'd0, source};
REG_DECIM_HIGH: tx_byte = decimation[15:8];
REG_DECIM_LOW: tx_byte = decimation[7:0];
REG_STATUS: tx_byte = {6'd0, capture_done, capture_busy};
REG_DATA: tx_byte = capture_byte;
default: tx_byte = 8'h00;
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_SOURCE_A: tx_byte = {2'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 = {2'd0, source_b};
REG_MODE: tx_byte = {7'd0, pair_mode};
REG_RING_COUNT: tx_byte = RINGS;
REG_DATA: tx_byte = capture_byte;
REG_FREQ_SELECT: tx_byte = {4'd0, freq_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];
default: tx_byte = 8'h00;
endcase
end
always @(posedge clk) begin
capture_start <= 1'b0;
freq_start <= 1'b0;
if (reset) begin
source <= 2'd3;
source_a <= 6'h00;
source_b <= 6'h01;
pair_mode <= 1'b0;
decimation <= 16'd1;
freq_select <= 4'd0;
is_read <= 1'b0;
reg_addr <= 7'd0;
bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
@ -113,12 +178,16 @@ module strix_core #(
bytes_read <= bytes_read + 1'b1;
end else if (!is_read) begin
case (reg_addr)
REG_SOURCE: source <= rx_byte[1:0];
REG_DECIM_HIGH: decimation[15:8] <= rx_byte;
REG_DECIM_LOW: decimation[7:0] <= rx_byte;
REG_SOURCE_A: source_a <= rx_byte[5:0];
REG_SOURCE_B: source_b <= rx_byte[5: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_FREQ_SELECT: freq_select <= rx_byte[3:0];
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

View file

@ -1,21 +1,27 @@
// Testbench for strix_core: SPI register access and a small raw capture read back in
// two transactions. Rings are replaced by the SIM model in ring_osc.v.
// Testbench for strix_core: SPI register access, single and pair captures read back across
// transactions, and the frequency counter. Rings are replaced by the SIM model in ring_osc.v.
`timescale 1ns / 1ps
module tb_strix_core;
localparam CAPTURE_ADDR_WIDTH = 6; // 64-byte buffer keeps the simulation short
localparam CAPTURE_BYTES = 1 << CAPTURE_ADDR_WIDTH;
localparam SCK_HALF = 62.5; // 8 MHz SCK
localparam SCK_HALF = 62.5; // 8 MHz SCK
localparam [6:0] REG_ID0 = 7'h00, REG_ID3 = 7'h03, REG_VERSION = 7'h04,
REG_SOURCE_A = 7'h08, REG_DECIM_LOW = 7'h0A, REG_CONTROL = 7'h0B,
REG_STATUS = 7'h0C, REG_SOURCE_B = 7'h0D, REG_MODE = 7'h0E,
REG_RING_COUNT = 7'h0F, REG_DATA = 7'h10, REG_FREQ_SELECT = 7'h11,
REG_FREQ_COUNT0 = 7'h15;
reg clk = 1'b0;
always #4.1667 clk = ~clk; // 120 MHz
always #4.1667 clk = ~clk; // 120 MHz
reg reset = 1'b1;
reg sck = 1'b0, mosi = 1'b0, ss_n = 1'b1;
wire miso, done;
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH)) dut (
strix_core #(.CAPTURE_ADDR_WIDTH(CAPTURE_ADDR_WIDTH), .FREQ_GATE_BITS(14)) dut (
.clk(clk), .reset(reset),
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
.capture_done(done)
@ -55,12 +61,56 @@ module tb_strix_core;
end
endtask
task expect(input [7:0] got, input [7:0] want, input [8*16-1:0] what);
if (got !== want) begin
$display("FAIL %0s: got %02x, expected %02x", what, got, want);
errors = errors + 1;
end
endtask
task run_capture;
reg [7:0] status;
integer polls;
begin
write_reg(REG_CONTROL, 8'h01);
polls = 0;
read_reg(REG_STATUS, status);
while (status[1] !== 1'b1 && polls < 1000) begin
read_reg(REG_STATUS, status);
polls = polls + 1;
end
if (status[1] !== 1'b1 || done !== 1'b1) begin
$display("FAIL capture did not finish");
errors = errors + 1;
end
end
endtask
// Captured bytes must be defined and vary, otherwise the read-back checks prove nothing.
task check_buffer_contents;
integer i, changes;
begin
changes = 0;
for (i = 0; i < CAPTURE_BYTES; i = i + 1) begin
if (^dut.capture.buffer[i] === 1'bx) begin
$display("FAIL buffer[%0d] undefined", i);
errors = errors + 1;
end
if (i > 0 && dut.capture.buffer[i] !== dut.capture.buffer[i - 1]) changes = changes + 1;
end
if (changes == 0) begin
$display("FAIL capture buffer is constant");
errors = errors + 1;
end
end
endtask
task read_data(input integer first, input integer count);
reg [7:0] dummy, value;
integer i;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, 7'h10}, dummy);
spi_byte({1'b1, REG_DATA}, dummy);
for (i = 0; i < count; i = i + 1) begin
spi_byte(8'h00, value);
if (value !== dut.capture.buffer[first + i]) begin
@ -72,65 +122,65 @@ module tb_strix_core;
end
endtask
task expect(input [7:0] got, input [7:0] want, input [8*16-1:0] what);
if (got !== want) begin
$display("FAIL %0s: got %02x, expected %02x", what, got, want);
errors = errors + 1;
end
endtask
reg [7:0] v;
integer n;
integer i, polls;
initial begin
#100 reset = 1'b0;
#200;
read_reg(7'h00, v); expect(v, "S", "ID0");
read_reg(7'h03, v); expect(v, "X", "ID3");
read_reg(7'h04, v); expect(v, 8'h01, "VERSION");
// 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'h02, "VERSION");
read_reg(REG_RING_COUNT, v); expect(v, 8'd16, "RING_COUNT");
write_reg(7'h08, 8'h01);
write_reg(7'h09, 8'h00);
write_reg(7'h0A, 8'h03);
read_reg(7'h08, v); expect(v, 8'h01, "SRC");
read_reg(7'h0A, v); expect(v, 8'h03, "DECIM_L");
write_reg(7'h0B, 8'h01);
n = 0;
read_reg(7'h0C, v);
while (v[1] !== 1'b1 && n < 1000) begin
read_reg(7'h0C, v);
n = n + 1;
end
expect(v, 8'h02, "STATUS");
if (done !== 1'b1) begin
$display("FAIL capture_done not set");
errors = errors + 1;
end
// Captured bytes must be defined and vary, otherwise the comparisons below prove nothing.
n = 0;
for (v = 0; v < CAPTURE_BYTES - 1; v = v + 1) begin
if (^dut.capture.buffer[v] === 1'bx) begin
$display("FAIL mem[%0d] undefined", v);
errors = errors + 1;
end
if (dut.capture.buffer[v] !== dut.capture.buffer[v + 1]) n = n + 1;
end
if (n == 0) begin
$display("FAIL capture buffer is constant");
errors = errors + 1;
end
// Read the buffer in two transactions; the second must continue where the first stopped.
// Single capture of the XOR of rings 0-1, every 3rd sample, read in two transactions
write_reg(REG_SOURCE_A, 8'h21);
write_reg(REG_DECIM_LOW, 8'h03);
read_reg(REG_SOURCE_A, v); expect(v, 8'h21, "SOURCE_A");
run_capture;
check_buffer_contents;
read_data(0, CAPTURE_BYTES / 2);
read_data(CAPTURE_BYTES / 2, CAPTURE_BYTES / 2);
// Rewind and read the first bytes again.
write_reg(7'h0B, 8'h02);
write_reg(REG_CONTROL, 8'h02); // rewind
read_data(0, 4);
// Pair capture with A = B: every bit pair must be 00 or 11
write_reg(REG_SOURCE_A, 8'h10);
write_reg(REG_SOURCE_B, 8'h10);
write_reg(REG_MODE, 8'h01);
write_reg(REG_DECIM_LOW, 8'h01);
run_capture;
check_buffer_contents;
for (i = 0; i < 4 * CAPTURE_BYTES; i = i + 1) begin
v = dut.capture.buffer[i / 4] >> (2 * (i % 4));
if (v[1] !== v[0]) begin
$display("FAIL pair %0d differs", i);
errors = errors + 1;
end
end
read_data(0, CAPTURE_BYTES);
// Frequency counter on ring 3
write_reg(REG_FREQ_SELECT, 8'h03);
write_reg(REG_CONTROL, 8'h04);
polls = 0;
read_reg(REG_STATUS, v);
while (v[3] !== 1'b1 && polls < 1000) begin
read_reg(REG_STATUS, v);
polls = polls + 1;
end
if (v[3] !== 1'b1) begin
$display("FAIL frequency measurement did not finish");
errors = errors + 1;
end
read_reg(REG_FREQ_COUNT0, v);
if (v == 8'd0 || dut.counter.count === 32'bx) begin
$display("FAIL frequency count is %0d", dut.counter.count);
errors = errors + 1;
end
if (errors == 0) $display("PASS");
else $display("%0d error(s)", errors);
$finish;

View file

@ -60,7 +60,8 @@ set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_
set_global_assignment -name VERILOG_FILE ../rtl/strix.v
set_global_assignment -name VERILOG_FILE ../rtl/sys_pll.v
set_global_assignment -name VERILOG_FILE ../rtl/strix_core.v
set_global_assignment -name VERILOG_FILE ../rtl/entropy_gen.v
set_global_assignment -name VERILOG_FILE ../rtl/ring_bank.v
set_global_assignment -name VERILOG_FILE ../rtl/freq_counter.v
set_global_assignment -name VERILOG_FILE ../rtl/ring_osc.v
set_global_assignment -name VERILOG_FILE ../rtl/raw_capture.v
set_global_assignment -name VERILOG_FILE ../rtl/spi_slave.v

View file

@ -3,7 +3,7 @@ derive_pll_clocks
derive_clock_uncertainty
# Ring oscillator outputs are asynchronous by design; the first sampling stage absorbs them.
set_false_path -to [get_registers {*entropy_gen:generator|sample_raw[*]}]
set_false_path -to [get_registers {*ring_bank:bank|sample_raw[*]}]
# SPI inputs are synchronized in spi_slave; MISO timing relies on the half-SCK margin.
set_false_path -from [get_ports {bMKR_A[*]}]
@ -12,3 +12,6 @@ set_false_path -to [get_ports {oSAM_INT}]
# NINA pass-through is purely combinational between pins.
set_false_path -from [get_ports {bMKR_D[*] bWM_PIO* iWM_TX}] -to [get_ports {bMKR_D[*] bWM_PIO* oWM_RX oWM_RESET}]
# The frequency counter's prescaler runs on a ring output; its top bit is synchronized.
set_false_path -from [get_registers {*freq_counter:counter|prescaler[*]}]