feat: add capture sketch, Vidor image tool and jitter analysis script

This commit is contained in:
randogoth 2026-10-25 12:30:12 +02:00
parent 0332d8eb33
commit d51a6c0ca8
20 changed files with 2085 additions and 0 deletions

View file

@ -0,0 +1,13 @@
// STRIX FPGA image, written to the FPGA's flash by the Vidor bootloader after upload.
// bitstream.h and signature.h are generated by `devbox run image`.
#include <VidorBoot.h>
__attribute__((used, section(".fpga_bitstream_signature")))
const unsigned char fpga_signature[4096] = {
#include "signature.h"
};
__attribute__((used, section(".fpga_bitstream")))
const unsigned char fpga_bitstream[] = {
#include "bitstream.h"
};

View file

@ -0,0 +1,141 @@
// STRIX raw capture tool: boots the STRIX FPGA image and serves raw ring-oscillator
// captures over USB serial for jitter characterization (see tools/capture.py).
//
// Commands (one per line):
// id print the FPGA identification
// cap <source> <decimation> capture a burst and send it as "DATA <n>\n" + n raw bytes
// source: 0 tap A, 1 enhanced A, 2 enhanced B, 3 pair XOR
#include <SPI.h>
#include <VidorBoot.h>
#include "wiring_private.h"
// STRIX SPI link: SERCOM0 on its alternate pins A3 MOSI (PAD0), A4 SCK (PAD1), A6 MISO (PAD3).
SPIClassSAMD strixSpi(&sercom0, A6, A4, A3, SPI_PAD_0_SCK_1, SERCOM_RX_PAD_3);
const int STRIX_SS = A5;
const SPISettings STRIX_SPI_SETTINGS(8000000, MSBFIRST, SPI_MODE0);
enum : uint8_t {
REG_ID0 = 0x00,
REG_VERSION = 0x04,
REG_SOURCE = 0x08,
REG_DECIM_HIGH = 0x09,
REG_DECIM_LOW = 0x0A,
REG_CONTROL = 0x0B,
REG_STATUS = 0x0C,
REG_DATA = 0x10,
};
const uint8_t READ = 0x80;
const uint8_t CONTROL_START = 0x01;
const uint8_t STATUS_DONE = 0x02;
const size_t CAPTURE_BYTES = 32768;
const size_t CHUNK_BYTES = 512;
VidorUtils fpga;
void beginTransaction(uint8_t command) {
strixSpi.beginTransaction(STRIX_SPI_SETTINGS);
digitalWrite(STRIX_SS, LOW);
strixSpi.transfer(command);
}
void endTransaction() {
digitalWrite(STRIX_SS, HIGH);
strixSpi.endTransaction();
}
void writeRegister(uint8_t reg, uint8_t value) {
beginTransaction(reg);
strixSpi.transfer(value);
endTransaction();
}
uint8_t readRegister(uint8_t reg) {
beginTransaction(READ | reg);
uint8_t value = strixSpi.transfer(0);
endTransaction();
return value;
}
void printId() {
char id[5] = {0};
for (uint8_t i = 0; i < 4; i++) id[i] = readRegister(REG_ID0 + i);
Serial.print(id);
Serial.print(" v");
Serial.println(readRegister(REG_VERSION));
}
void capture(uint8_t source, uint16_t decimation) {
writeRegister(REG_SOURCE, source);
writeRegister(REG_DECIM_HIGH, decimation >> 8);
writeRegister(REG_DECIM_LOW, decimation & 0xFF);
writeRegister(REG_CONTROL, CONTROL_START);
unsigned long start = millis();
while (!(readRegister(REG_STATUS) & STATUS_DONE)) {
// 262,144 samples at 120 MHz take up to 143 s at the maximum decimation of 65535.
if (millis() - start > 180000) {
Serial.println("ERR timeout");
return;
}
}
Serial.print("DATA ");
Serial.println(CAPTURE_BYTES);
static uint8_t chunk[CHUNK_BYTES];
for (size_t sent = 0; sent < CAPTURE_BYTES; sent += CHUNK_BYTES) {
beginTransaction(READ | REG_DATA);
for (size_t i = 0; i < CHUNK_BYTES; i++) chunk[i] = strixSpi.transfer(0);
endTransaction();
Serial.write(chunk, CHUNK_BYTES);
}
}
void handleCommand(String line) {
line.trim();
if (line == "id") {
printId();
} else if (line.startsWith("cap ")) {
int space = line.indexOf(' ', 4);
if (space < 0) {
Serial.println("ERR usage: cap <source> <decimation>");
return;
}
long source = line.substring(4, space).toInt();
long decimation = line.substring(space + 1).toInt();
if (source < 0 || source > 3 || decimation < 1 || decimation > 65535) {
Serial.println("ERR source 0-3, decimation 1-65535");
return;
}
capture(source, decimation);
} else if (line.length() > 0) {
Serial.println("ERR unknown command");
}
}
void setup() {
Serial.begin(115200);
while (!Serial);
// Starts the 48 MHz FPGA clock and switches the FPGA from Arduino's loader to our image.
if (!fpga.begin(true)) {
Serial.println("ERR FPGA boot failed");
}
delay(100); // configuration and PLL lock
pinMode(STRIX_SS, OUTPUT);
digitalWrite(STRIX_SS, HIGH);
strixSpi.begin();
// SPIClass::begin() applies the pins' default mux; A3-A6 need SERCOM0's alternate mux.
pinPeripheral(A3, PIO_SERCOM_ALT);
pinPeripheral(A4, PIO_SERCOM_ALT);
pinPeripheral(A6, PIO_SERCOM_ALT);
printId();
}
void loop() {
if (Serial.available()) handleCommand(Serial.readStringUntil('\n'));
}