feat: add FPGA raw jitter capture design with SPI register interface

This commit is contained in:
randogoth 2026-10-25 12:25:25 +02:00
parent 3ee8a5c3db
commit 0332d8eb33
13 changed files with 936 additions and 2 deletions

3
.gitignore vendored
View file

@ -17,3 +17,6 @@ fpga/**/simulation/
# Devbox # Devbox
.devbox/ .devbox/
# Python virtualenv created by devbox
.venv/

View file

@ -1,6 +1,13 @@
{ {
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json",
"packages": ["arduino-cli@latest", "path:./nix#quartus"], "packages": [
"arduino-cli@latest",
"path:./nix#quartus",
"python312@latest",
"python312Packages.pyserial@latest",
"python312Packages.numpy@latest",
"iverilog@latest"
],
"shell": { "shell": {
"init_hook": [ "init_hook": [
"echo 'Welcome to devbox!' > /dev/null" "echo 'Welcome to devbox!' > /dev/null"
@ -14,6 +21,11 @@
"build": [ "build": [
"arduino-cli compile --fqbn arduino:samd:mkrvidor4000 arduino/strix" "arduino-cli compile --fqbn arduino:samd:mkrvidor4000 arduino/strix"
], ],
"sim": [
"mkdir -p /tmp/strix-sim",
"iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_gen.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v",
"vvp -n /tmp/strix-sim/tb_strix_core"
],
"fpga": [ "fpga": [
"cd fpga/src && quartus_sh --flow compile strix" "cd fpga/src && quartus_sh --flow compile strix"
] ]

View file

@ -52,6 +52,219 @@
"github:NixOS/nixpkgs/nixpkgs-unstable": { "github:NixOS/nixpkgs/nixpkgs-unstable": {
"last_modified": "2026-09-29T03:29:29Z", "last_modified": "2026-09-29T03:29:29Z",
"resolved": "github:NixOS/nixpkgs/b6c8664de9b6cc07fe5666a29f91884ba81197c4?lastModified=1790652569&narHash=sha256-641r7xrlSOHYoktL9%2FaArPIYNgZJ9eM5URzSEOtPJDY%3D" "resolved": "github:NixOS/nixpkgs/b6c8664de9b6cc07fe5666a29f91884ba81197c4?lastModified=1790652569&narHash=sha256-641r7xrlSOHYoktL9%2FaArPIYNgZJ9eM5URzSEOtPJDY%3D"
},
"iverilog@latest": {
"last_modified": "2026-10-02T22:55:44Z",
"resolved": "github:NixOS/nixpkgs/55ba7f49ef2962b42cbd126522b7df5f95037679#iverilog",
"source": "devbox-search",
"version": "13.0",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/p2jawqjs75q5y5mzmz97mzsy41fnpk3z-iverilog-13.0",
"default": true
}
],
"store_path": "/nix/store/p2jawqjs75q5y5mzmz97mzsy41fnpk3z-iverilog-13.0"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/z850am8r4302c3l5vscpylb7gnrd01am-iverilog-13.0",
"default": true
}
],
"store_path": "/nix/store/z850am8r4302c3l5vscpylb7gnrd01am-iverilog-13.0"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/d66hg1vhaqd0ij8iznymb79s4h4ach7c-iverilog-13.0",
"default": true
}
],
"store_path": "/nix/store/d66hg1vhaqd0ij8iznymb79s4h4ach7c-iverilog-13.0"
}
}
},
"python312@latest": {
"last_modified": "2026-10-02T22:55:44Z",
"plugin_version": "0.0.4",
"resolved": "github:NixOS/nixpkgs/55ba7f49ef2962b42cbd126522b7df5f95037679#python312",
"source": "devbox-search",
"version": "3.12.15",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/wki0ba66wcjwx0yxn36bgr08jgcn9bdj-python3-3.12.15",
"default": true
}
],
"store_path": "/nix/store/wki0ba66wcjwx0yxn36bgr08jgcn9bdj-python3-3.12.15"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/pan3ykbb4wj84c6lhy2zb27pifyd48af-python3-3.12.15",
"default": true
},
{
"name": "debug",
"path": "/nix/store/q5h1455f2swvmwpqkiswrl946fgs2h24-python3-3.12.15-debug"
}
],
"store_path": "/nix/store/pan3ykbb4wj84c6lhy2zb27pifyd48af-python3-3.12.15"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/v7wvwm1bpm6kzlqgw33m98arm7sw40ln-python3-3.12.15",
"default": true
},
{
"name": "debug",
"path": "/nix/store/8965kbzqdvqmjy5zs4lxvy09g1qy9szm-python3-3.12.15-debug"
}
],
"store_path": "/nix/store/v7wvwm1bpm6kzlqgw33m98arm7sw40ln-python3-3.12.15"
}
}
},
"python312Packages.numpy@latest": {
"last_modified": "2025-11-23T21:50:36Z",
"resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#python312Packages.numpy",
"source": "devbox-search",
"version": "2.3.4",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/x2ij7c58vq7cw1j5fvxfcm6r7ixanrwl-python3.12-numpy-2.3.4",
"default": true
},
{
"name": "dist",
"path": "/nix/store/n62ay9ig7glbx774kz1kx958pmlh60l2-python3.12-numpy-2.3.4-dist"
}
],
"store_path": "/nix/store/x2ij7c58vq7cw1j5fvxfcm6r7ixanrwl-python3.12-numpy-2.3.4"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/awrd1xis641s1mqbc2qqah9aadf3dr5a-python3.12-numpy-2.3.4",
"default": true
},
{
"name": "dist",
"path": "/nix/store/i0xbq8jvfbdw1i6spmr99ar07xq02yzl-python3.12-numpy-2.3.4-dist"
}
],
"store_path": "/nix/store/awrd1xis641s1mqbc2qqah9aadf3dr5a-python3.12-numpy-2.3.4"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/9q4xvx19yv07xm65ifzkwpn4cd6g33i8-python3.12-numpy-2.3.4",
"default": true
},
{
"name": "dist",
"path": "/nix/store/0l5qc7i7pdrpycijb1ys607vx8h7661b-python3.12-numpy-2.3.4-dist"
}
],
"store_path": "/nix/store/9q4xvx19yv07xm65ifzkwpn4cd6g33i8-python3.12-numpy-2.3.4"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/0cgbqmgzzzhxmqsdaxa2hqz5d50a28cp-python3.12-numpy-2.3.4",
"default": true
},
{
"name": "dist",
"path": "/nix/store/0kqkffxakw86i90a7zpfjicxmhzzds83-python3.12-numpy-2.3.4-dist"
}
],
"store_path": "/nix/store/0cgbqmgzzzhxmqsdaxa2hqz5d50a28cp-python3.12-numpy-2.3.4"
}
}
},
"python312Packages.pyserial@latest": {
"last_modified": "2025-11-23T21:50:36Z",
"resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#python312Packages.pyserial",
"source": "devbox-search",
"version": "3.5",
"systems": {
"aarch64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/69ldajx5m2cas9k3p7nr5d8923sgdi6y-python3.12-pyserial-3.5",
"default": true
},
{
"name": "dist",
"path": "/nix/store/zzrgf4i785a0zl6p33syzmk9h1h1ngs3-python3.12-pyserial-3.5-dist"
}
],
"store_path": "/nix/store/69ldajx5m2cas9k3p7nr5d8923sgdi6y-python3.12-pyserial-3.5"
},
"aarch64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/v2yf4ssrygrh14s2n3hfq8w10h64jr6b-python3.12-pyserial-3.5",
"default": true
},
{
"name": "dist",
"path": "/nix/store/i0siiab84gysw5bpjyy7qr052w2psss0-python3.12-pyserial-3.5-dist"
}
],
"store_path": "/nix/store/v2yf4ssrygrh14s2n3hfq8w10h64jr6b-python3.12-pyserial-3.5"
},
"x86_64-darwin": {
"outputs": [
{
"name": "out",
"path": "/nix/store/h12lybacn8cfcxdwwlb5n5z1c1lcqpc4-python3.12-pyserial-3.5",
"default": true
},
{
"name": "dist",
"path": "/nix/store/ajdpw1zb0lwjd1dmyjdf2nxxg0vpbrhd-python3.12-pyserial-3.5-dist"
}
],
"store_path": "/nix/store/h12lybacn8cfcxdwwlb5n5z1c1lcqpc4-python3.12-pyserial-3.5"
},
"x86_64-linux": {
"outputs": [
{
"name": "out",
"path": "/nix/store/j90zi1mxv9dvqmsjqicnaafm22bp3qca-python3.12-pyserial-3.5",
"default": true
},
{
"name": "dist",
"path": "/nix/store/2l5xicsrirfp0v8bawhsva1856c4zakd-python3.12-pyserial-3.5-dist"
}
],
"store_path": "/nix/store/j90zi1mxv9dvqmsjqicnaafm22bp3qca-python3.12-pyserial-3.5"
}
}
} }
} }
} }

34
fpga/rtl/entropy_gen.v Normal file
View file

@ -0,0 +1,34 @@
// 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

67
fpga/rtl/raw_capture.v Normal file
View file

@ -0,0 +1,67 @@
// 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.
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 [15:0] decimation, // 0 and 1 both mean every sample
input wire start,
output reg busy,
output reg done,
input wire [ADDR_WIDTH-1:0] read_addr,
output reg [7:0] read_data
);
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 [ADDR_WIDTH-1:0] next_addr;
reg write_enable;
reg [ADDR_WIDTH-1:0] write_addr;
reg [7:0] write_data;
wire keep_sample = (sample_count == 16'd0);
wire count_wraps = (decimation <= 16'd1) || (sample_count == decimation - 16'd1);
wire new_bit = samples[source];
always @(posedge clk) begin
write_enable <= 1'b0;
if (reset) begin
busy <= 1'b0;
done <= 1'b0;
end else if (start) begin
busy <= 1'b1;
done <= 1'b0;
sample_count <= 16'd0;
bit_count <= 3'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
write_enable <= 1'b1;
write_addr <= next_addr;
write_data <= {partial_byte, new_bit};
next_addr <= next_addr + 1'b1;
if (&next_addr) begin
busy <= 1'b0;
done <= 1'b1;
end
end
end
end
end
always @(posedge clk) begin
if (write_enable) buffer[write_addr] <= write_data;
read_data <= buffer[read_addr];
end
endmodule

43
fpga/rtl/ring_osc.v Normal file
View file

@ -0,0 +1,43 @@
// Free-running ring oscillator built from LCELL buffers so synthesis keeps every stage.
// Three evenly spaced taps are XORed into an "enhanced" output at three times the ring
// frequency, which triples the chance that a sample lands on a jittery edge.
module ring_osc #(
parameter LENGTH = 12 // stages in the ring; exactly one of them inverts
) (
input wire enable, // low stops the ring (all stages settle high)
output wire first_tap,
output wire enhanced
);
`ifdef SIM
// Behavioural stand-in: independent taps toggling at random intervals, enough to
// exercise the synchronous logic downstream. It has no physical meaning.
reg [2:0] taps = 3'b000;
always begin
#(1 + {$random} % 7);
if (enable) taps[{$random} % 3] = ~taps[{$random} % 3];
else taps = 3'b111;
end
assign first_tap = taps[0];
assign enhanced = ^taps;
`else
// No keep attribute here: each lcell is already one logic cell, and keep on these
// nets makes Quartus insert a second cell per stage.
wire [LENGTH-1:0] stage;
lcell inverting_stage (.in(~(stage[LENGTH-1] & enable)), .out(stage[0]));
genvar i;
generate
for (i = 1; i < LENGTH; i = i + 1) begin : delay_stage
lcell buffer (.in(stage[i-1]), .out(stage[i]));
end
endgenerate
(* keep *) wire tap_xor = stage[0] ^ stage[LENGTH/3] ^ stage[(2*LENGTH)/3];
assign first_tap = stage[0];
assign enhanced = tap_xor;
`endif
endmodule

69
fpga/rtl/spi_slave.v Normal file
View file

@ -0,0 +1,69 @@
// SPI mode 0 slave, oversampled in the system clock domain (SCK must stay below about a
// tenth of clk). MISO changes a few clk cycles after each SCK falling edge, which leaves
// enough margin for the master's next rising edge at 8 MHz SCK with a 120 MHz clk.
module spi_slave (
input wire clk,
input wire reset,
input wire sck,
input wire mosi,
input wire ss_n,
output wire miso,
output wire selected, // ss_n asserted (synchronized)
output reg rx_valid, // pulse: rx_byte holds a complete byte
output reg [7:0] rx_byte,
output reg rx_is_first, // rx_byte is the first byte of the transaction
input wire [7:0] tx_byte, // sampled when the next byte starts shifting out
output reg tx_loaded // pulse: tx_byte was taken
);
reg [2:0] sck_history, ss_n_history;
reg [1:0] mosi_history;
always @(posedge clk) begin
sck_history <= {sck_history[1:0], sck};
ss_n_history <= {ss_n_history[1:0], ss_n};
mosi_history <= {mosi_history[0], mosi};
end
wire sck_rising = (sck_history[2:1] == 2'b01);
wire sck_falling = (sck_history[2:1] == 2'b10);
wire mosi_bit = mosi_history[1];
assign selected = ~ss_n_history[1];
reg [2:0] bit_count;
reg [6:0] rx_shift;
reg [7:0] tx_shift;
reg expecting_first;
assign miso = tx_shift[7];
always @(posedge clk) begin
rx_valid <= 1'b0;
tx_loaded <= 1'b0;
if (reset || !selected) begin
bit_count <= 3'd0;
expecting_first <= 1'b1;
tx_shift <= 8'h00; // MISO is don't-care during the command byte
end else begin
if (sck_rising) begin
rx_shift <= {rx_shift[5:0], mosi_bit};
bit_count <= bit_count + 3'd1;
if (bit_count == 3'd7) begin
rx_valid <= 1'b1;
rx_byte <= {rx_shift, mosi_bit};
rx_is_first <= expecting_first;
expecting_first <= 1'b0;
end
end
if (sck_falling) begin
if (bit_count == 3'd0) begin
tx_shift <= tx_byte;
tx_loaded <= 1'b1;
end else begin
tx_shift <= {tx_shift[6:0], 1'b0};
end
end
end
end
endmodule

85
fpga/rtl/strix.v Normal file
View file

@ -0,0 +1,85 @@
// STRIX top level for the Arduino MKR Vidor 4000. Port names match the Vidor constraints
// in strix.qsf; unused board pins are left out and stay tri-stated.
module strix (
input wire iCLK, // 48 MHz from the SAMD21 (GCLK0)
inout wire [14:0] bMKR_D, // header D0-D14, shared with the SAMD21
inout wire [6:0] bMKR_A, // header A0-A6, shared with the SAMD21
// NINA-W102 WiFi module
inout wire bWM_PIO1, // NINA MISO
inout wire bWM_PIO7, // NINA ACK (GPIO33)
inout wire bWM_PIO27, // NINA GPIO0
inout wire bWM_PIO28, // NINA SS (GPIO5)
inout wire bWM_PIO29, // NINA SCK
inout wire bWM_PIO36, // NINA MOSI
input wire iWM_TX,
inout wire oWM_RX,
inout wire oWM_RESET,
output wire oSAM_INT // interrupt line to the SAMD21
);
// Clock and reset
wire clk, pll_locked;
sys_pll pll (.inclk0(iCLK), .c0(clk), .locked(pll_locked));
reg [2:0] reset_pipe = 3'b111;
always @(posedge clk or negedge pll_locked)
if (!pll_locked) reset_pipe <= 3'b111;
else reset_pipe <= {reset_pipe[1:0], 1'b0};
wire reset = reset_pipe[2];
// NINA pass-through: the SAMD21 talks to the WiFi module as if wired directly.
wire sam_nina_mosi = bMKR_D[8]; // SERCOM1
wire sam_nina_sck = bMKR_D[9];
wire sam_nina_ss = bMKR_D[5];
wire sam_nina_gpio0 = bMKR_D[6];
wire sam_nina_resetn = bMKR_D[7];
wire sam_uart_tx = bMKR_D[14];
assign bWM_PIO36 = sam_nina_mosi;
assign bWM_PIO29 = sam_nina_sck;
assign bWM_PIO28 = sam_nina_ss;
assign bWM_PIO27 = sam_nina_gpio0;
assign oWM_RESET = sam_nina_resetn;
assign oWM_RX = sam_uart_tx;
assign bWM_PIO1 = 1'bz; // inputs from NINA
assign bWM_PIO7 = 1'bz;
wire nina_miso = bWM_PIO1; // to D10
wire nina_ack = bWM_PIO7; // to D4
wire nina_uart_tx = iWM_TX; // to D13
// STRIX SPI link on SAMD21 SERCOM0 alternate pins: A3 MOSI, A4 SCK, A5 SS, A6 MISO.
wire strix_mosi = bMKR_A[3];
wire strix_sck = bMKR_A[4];
wire strix_ss_n = bMKR_A[5];
wire strix_miso, capture_done;
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)
);
assign oSAM_INT = capture_done;
// Header pins driven by the FPGA; all others are inputs.
genvar d;
generate
for (d = 0; d < 15; d = d + 1) begin : header_d
assign bMKR_D[d] = (d == 4) ? nina_ack :
(d == 10) ? nina_miso :
(d == 13) ? nina_uart_tx : 1'bz;
end
endgenerate
assign bMKR_A = {strix_miso, 6'bzzzzzz};
endmodule

130
fpga/rtl/strix_core.v Normal file
View file

@ -0,0 +1,130 @@
// Board-independent STRIX logic: entropy generator, raw capture 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.
module strix_core #(
parameter CAPTURE_ADDR_WIDTH = 15
) (
input wire clk,
input wire reset,
input wire spi_sck,
input wire spi_mosi,
input wire spi_ss_n,
output wire spi_miso,
output wire capture_done
);
localparam [7:0] VERSION = 8'h01;
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;
// Entropy and capture
wire [3:0] samples;
entropy_gen generator (.clk(clk), .enable(1'b1), .samples(samples));
reg [1:0] source;
reg [15:0] decimation;
reg capture_start;
wire capture_busy;
reg [CAPTURE_ADDR_WIDTH-1:0] prefetch_addr;
wire [7:0] capture_byte;
raw_capture #(.ADDR_WIDTH(CAPTURE_ADDR_WIDTH)) capture (
.clk(clk), .reset(reset), .samples(samples), .source(source),
.decimation(decimation), .start(capture_start),
.busy(capture_busy), .done(capture_done),
.read_addr(prefetch_addr), .read_data(capture_byte)
);
// SPI
wire selected, rx_valid, rx_is_first, tx_loaded;
wire [7:0] rx_byte;
reg [7:0] tx_byte;
spi_slave spi (
.clk(clk), .reset(reset),
.sck(spi_sck), .mosi(spi_mosi), .ss_n(spi_ss_n), .miso(spi_miso),
.selected(selected), .rx_valid(rx_valid), .rx_byte(rx_byte),
.rx_is_first(rx_is_first), .tx_byte(tx_byte), .tx_loaded(tx_loaded)
);
reg is_read;
reg [6:0] reg_addr;
// DATA bytes are prefetched one ahead: prefetch_addr moves when a byte is loaded for
// shifting, bytes_read only when it has been fully clocked out. Between transactions
// prefetch_addr rewinds to bytes_read so the trailing prefetch is not lost.
reg [CAPTURE_ADDR_WIDTH-1:0] bytes_read;
wire reading_data = is_read && reg_addr == REG_DATA;
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;
endcase
end
always @(posedge clk) begin
capture_start <= 1'b0;
if (reset) begin
source <= 2'd3;
decimation <= 16'd1;
is_read <= 1'b0;
reg_addr <= 7'd0;
bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
prefetch_addr <= {CAPTURE_ADDR_WIDTH{1'b0}};
end else begin
if (!selected)
prefetch_addr <= bytes_read;
else if (tx_loaded && reading_data)
prefetch_addr <= prefetch_addr + 1'b1;
if (rx_valid) begin
if (rx_is_first) begin
is_read <= rx_byte[7];
reg_addr <= rx_byte[6:0];
end else if (reading_data) begin
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_CONTROL: begin
if (rx_byte[0]) capture_start <= 1'b1;
if (rx_byte[0] || rx_byte[1]) bytes_read <= {CAPTURE_ADDR_WIDTH{1'b0}};
end
default: ;
endcase
end
end
end
end
endmodule

100
fpga/rtl/sys_pll.v Normal file
View file

@ -0,0 +1,100 @@
// 48 MHz in, 120 MHz out (x5 / 2). Same altpll settings style as Arduino's Vidor
// SYSTEM_PLL, reduced to one output.
module sys_pll (
input wire inclk0,
output wire c0,
output wire locked
);
wire [4:0] clk;
altpll altpll_component (
.inclk ({1'b0, inclk0}),
.clk (clk),
.locked (locked),
.areset (1'b0),
.activeclock (),
.clkbad (),
.clkena ({6{1'b1}}),
.clkloss (),
.clkswitch (1'b0),
.configupdate (1'b0),
.enable0 (),
.enable1 (),
.extclk (),
.extclkena ({4{1'b1}}),
.fbin (1'b1),
.fbmimicbidir (),
.fbout (),
.fref (),
.icdrclk (),
.pfdena (1'b1),
.phasecounterselect ({4{1'b1}}),
.phasedone (),
.phasestep (1'b1),
.phaseupdown (1'b1),
.pllena (1'b1),
.scanaclr (1'b0),
.scanclk (1'b0),
.scanclkena (1'b1),
.scandata (1'b0),
.scandataout (),
.scandone (),
.scanread (1'b0),
.scanwrite (1'b0),
.sclkout0 (),
.sclkout1 (),
.vcooverrange (),
.vcounderrange ()
);
defparam
altpll_component.bandwidth_type = "AUTO",
altpll_component.clk0_divide_by = 2,
altpll_component.clk0_duty_cycle = 50,
altpll_component.clk0_multiply_by = 5,
altpll_component.clk0_phase_shift = "0",
altpll_component.compensate_clock = "CLK0",
altpll_component.inclk0_input_frequency = 20833,
altpll_component.intended_device_family = "Cyclone 10 LP",
altpll_component.lpm_hint = "CBX_MODULE_PREFIX=sys_pll",
altpll_component.lpm_type = "altpll",
altpll_component.operation_mode = "NORMAL",
altpll_component.pll_type = "AUTO",
altpll_component.port_activeclock = "PORT_UNUSED",
altpll_component.port_areset = "PORT_UNUSED",
altpll_component.port_clkbad0 = "PORT_UNUSED",
altpll_component.port_clkbad1 = "PORT_UNUSED",
altpll_component.port_clkloss = "PORT_UNUSED",
altpll_component.port_clkswitch = "PORT_UNUSED",
altpll_component.port_configupdate = "PORT_UNUSED",
altpll_component.port_fbin = "PORT_UNUSED",
altpll_component.port_inclk0 = "PORT_USED",
altpll_component.port_inclk1 = "PORT_UNUSED",
altpll_component.port_locked = "PORT_USED",
altpll_component.port_pfdena = "PORT_UNUSED",
altpll_component.port_phasecounterselect = "PORT_UNUSED",
altpll_component.port_phasedone = "PORT_UNUSED",
altpll_component.port_phasestep = "PORT_UNUSED",
altpll_component.port_phaseupdown = "PORT_UNUSED",
altpll_component.port_pllena = "PORT_UNUSED",
altpll_component.port_scanaclr = "PORT_UNUSED",
altpll_component.port_scanclk = "PORT_UNUSED",
altpll_component.port_scanclkena = "PORT_UNUSED",
altpll_component.port_scandata = "PORT_UNUSED",
altpll_component.port_scandataout = "PORT_UNUSED",
altpll_component.port_scandone = "PORT_UNUSED",
altpll_component.port_scanread = "PORT_UNUSED",
altpll_component.port_scanwrite = "PORT_UNUSED",
altpll_component.port_clk0 = "PORT_USED",
altpll_component.port_clk1 = "PORT_UNUSED",
altpll_component.port_clk2 = "PORT_UNUSED",
altpll_component.port_clk3 = "PORT_UNUSED",
altpll_component.port_clk4 = "PORT_UNUSED",
altpll_component.port_clk5 = "PORT_UNUSED",
altpll_component.self_reset_on_loss_lock = "OFF",
altpll_component.width_clock = 5;
assign c0 = clk[0];
endmodule

139
fpga/sim/tb_strix_core.v Normal file
View file

@ -0,0 +1,139 @@
// 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.
`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
reg clk = 1'b0;
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 (
.clk(clk), .reset(reset),
.spi_sck(sck), .spi_mosi(mosi), .spi_ss_n(ss_n), .spi_miso(miso),
.capture_done(done)
);
integer errors = 0;
task spi_byte(input [7:0] out, output [7:0] in);
integer i;
begin
for (i = 7; i >= 0; i = i - 1) begin
mosi = out[i];
#SCK_HALF sck = 1'b1;
in[i] = miso;
#SCK_HALF sck = 1'b0;
end
end
endtask
task write_reg(input [6:0] addr, input [7:0] value);
reg [7:0] dummy;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b0, addr}, dummy);
spi_byte(value, dummy);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
end
endtask
task read_reg(input [6:0] addr, output [7:0] value);
reg [7:0] dummy;
begin
ss_n = 1'b0; #SCK_HALF;
spi_byte({1'b1, addr}, dummy);
spi_byte(8'h00, value);
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
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);
for (i = 0; i < count; i = i + 1) begin
spi_byte(8'h00, value);
if (value !== dut.capture.buffer[first + i]) begin
$display("FAIL data[%0d]: got %02x, expected %02x", first + i, value, dut.capture.buffer[first + i]);
errors = errors + 1;
end
end
#SCK_HALF ss_n = 1'b1; #(4 * SCK_HALF);
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;
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");
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.
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);
read_data(0, 4);
if (errors == 0) $display("PASS");
else $display("%0d error(s)", errors);
$finish;
end
endmodule

View file

@ -55,6 +55,31 @@ set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_
set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_symbol set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_symbol
set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_signal_integrity set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_signal_integrity
set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_boundary_scan set_global_assignment -name EDA_GENERATE_FUNCTIONAL_NETLIST OFF -section_id eda_board_design_boundary_scan
# Sources and constraints
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_osc.v
set_global_assignment -name VERILOG_FILE ../rtl/raw_capture.v
set_global_assignment -name VERILOG_FILE ../rtl/spi_slave.v
set_global_assignment -name SDC_FILE strix.sdc
# Configuration settings matching Arduino's Vidor projects; the .ttf is the input for the
# flash image embedded in the SAMD21 sketch.
set_global_assignment -name STRATIXV_CONFIGURATION_SCHEME "PASSIVE SERIAL"
set_global_assignment -name USE_CONFIGURATION_DEVICE ON
set_global_assignment -name CYCLONEIII_CONFIGURATION_DEVICE EPCS16
set_global_assignment -name CRC_ERROR_OPEN_DRAIN OFF
set_global_assignment -name STRATIX_DEVICE_IO_STANDARD "3.3-V LVTTL"
set_global_assignment -name CYCLONEII_RESERVE_NCEO_AFTER_CONFIGURATION "USE AS REGULAR IO"
set_global_assignment -name ENABLE_CONFIGURATION_PINS OFF
set_global_assignment -name ENABLE_BOOT_SEL_PIN OFF
set_global_assignment -name CONFIGURATION_VCCIO_LEVEL AUTO
set_global_assignment -name ON_CHIP_BITSTREAM_DECOMPRESSION ON
set_global_assignment -name GENERATE_TTF_FILE ON
set_location_assignment PIN_E2 -to iCLK set_location_assignment PIN_E2 -to iCLK
set_location_assignment PIN_E1 -to iRESETn set_location_assignment PIN_E1 -to iRESETn
set_location_assignment PIN_E10 -to oSDRAM_ADDR[11] set_location_assignment PIN_E10 -to oSDRAM_ADDR[11]

14
fpga/src/strix.sdc Normal file
View file

@ -0,0 +1,14 @@
create_clock -name iCLK -period 20.833 [get_ports {iCLK}]
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[*]}]
# SPI inputs are synchronized in spi_slave; MISO timing relies on the half-SCK margin.
set_false_path -from [get_ports {bMKR_A[*]}]
set_false_path -to [get_ports {bMKR_A[*]}]
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}]