feat: add device firmware with USB API, StrixFPGA library and API check tools

This commit is contained in:
randogoth 2026-10-25 15:29:36 +02:00
parent 7777384495
commit eaf8cd284a
14 changed files with 1037 additions and 253 deletions

View file

@ -0,0 +1,10 @@
name=StrixFPGA
version=0.1.0
author=STRIX
maintainer=STRIX
sentence=Boots the STRIX FPGA image on the MKR Vidor 4000 and provides SPI register access.
paragraph=Embeds the FPGA image generated by `devbox run image`.
category=Other
url=
architectures=samd
depends=VidorBoot

View file

@ -0,0 +1,66 @@
#include "StrixFPGA.h"
#include <SPI.h>
#include <VidorBoot.h>
#include "wiring_private.h"
namespace {
// SERCOM0 on its alternate pins: A3 MOSI (PAD0), A4 SCK (PAD1), A6 MISO (PAD3); A5 is SS.
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);
VidorUtils vidor;
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();
}
} // namespace
bool StrixFPGA::begin() {
// Starts the 48 MHz FPGA clock and switches the FPGA from Arduino's loader to our image.
if (!vidor.begin(true)) return false;
delay(100); // configuration and PLL lock
pinMode(STRIX_SS, OUTPUT);
digitalWrite(STRIX_SS, HIGH);
strixSpi.begin();
// SPIClassSAMD::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);
char id[4];
for (uint8_t i = 0; i < 4; i++) id[i] = read(strix::REG_ID0 + i);
version_ = read(strix::REG_VERSION);
return memcmp(id, "STRX", 4) == 0 && version_ == strix::FPGA_VERSION;
}
uint8_t StrixFPGA::read(uint8_t reg) {
beginTransaction(strix::READ | reg);
uint8_t value = strixSpi.transfer(0);
endTransaction();
return value;
}
void StrixFPGA::write(uint8_t reg, uint8_t value) {
beginTransaction(reg);
strixSpi.transfer(value);
endTransaction();
}
void StrixFPGA::readBurst(uint8_t reg, uint8_t *buffer, size_t length) {
beginTransaction(strix::READ | reg);
for (size_t i = 0; i < length; i++) buffer[i] = strixSpi.transfer(0);
endTransaction();
}

View file

@ -0,0 +1,29 @@
// Access to the STRIX FPGA on the MKR Vidor 4000: boots the embedded FPGA image and talks to
// it over the dedicated SPI link (SERCOM0 on A3 MOSI, A4 SCK, A5 SS, A6 MISO).
//
// The FPGA image (bitstream.h, signature.h) is generated by `devbox run image` and embedded by
// this library, so any sketch that includes it ships the image.
#pragma once
#include <Arduino.h>
#include "StrixRegisters.h"
class StrixFPGA {
public:
// Starts the FPGA clock, switches the FPGA to the STRIX image and sets up SPI. Returns false
// if the FPGA does not identify as STRIX with the expected version.
bool begin();
uint8_t read(uint8_t reg);
void write(uint8_t reg, uint8_t value);
// Reads `length` bytes in one transaction; for burst registers (DATA, STATS, AMP, STREAM)
// these are consecutive bytes, for others the same register repeated.
void readBurst(uint8_t reg, uint8_t *buffer, size_t length);
uint8_t version() const { return version_; }
private:
uint8_t version_ = 0;
};

View file

@ -0,0 +1,74 @@
// STRIX FPGA register map (FPGA version 7). Mirrors the definitions in fpga/rtl/strix_core.v,
// which also documents the SPI protocol and snapshot layouts.
#pragma once
#include <stdint.h>
namespace strix {
const uint8_t FPGA_VERSION = 7;
const uint8_t READ = 0x80; // command bit 7: read
enum Register : uint8_t {
REG_ID0 = 0x00, // "STRX" in 0x00-0x03
REG_VERSION = 0x04,
REG_FREQ_GATE = 0x05,
REG_GENERATORS = 0x06,
REG_RINGS = 0x07,
REG_SOURCE_A = 0x08,
REG_DECIM_HIGH = 0x09,
REG_DECIM_LOW = 0x0A,
REG_CONTROL = 0x0B,
REG_STATUS = 0x0C,
REG_SOURCE_B = 0x0D,
REG_MODE = 0x0E,
REG_DATA = 0x10,
REG_RING_SELECT = 0x11,
REG_FREQ_COUNT3 = 0x12,
REG_GEN_ENABLE = 0x16,
REG_TEMPORAL = 0x17,
REG_FREQ_GEN = 0x18,
REG_HEALTHY = 0x20,
REG_HEALTH_CTRL = 0x21,
REG_RCT_CUTOFF = 0x22,
REG_APT_HIGH = 0x23,
REG_APT_LOW = 0x24,
REG_MAX_DEAD = 0x25,
REG_TRIAL_HIGH = 0x26,
REG_TRIAL_LOW = 0x27,
REG_STATS_CTRL = 0x28,
REG_STREAM_CTRL = 0x29,
REG_LEVEL_HIGH = 0x2A,
REG_LEVEL_LOW = 0x2B,
REG_DROP_HIGH = 0x2C,
REG_DROP_LOW = 0x2D,
REG_GEN_HEALTH = 0x30, // + 4g: flags, RCT events, APT events, dead rings
REG_STATS = 0x40,
REG_AMP = 0x42,
REG_STREAM = 0x44,
REG_AMP_CTRL = 0x50,
REG_AMP_SOURCES = 0x51,
REG_AMP_SUB_MS = 0x52,
REG_AMP_SUBS = 0x53,
REG_AMP_STAGE = 0x54, // + 3s: mode, parameter high, parameter low
};
// Control bits
const uint8_t CONTROL_START_CAPTURE = 0x01;
const uint8_t STATUS_CAPTURE_DONE = 0x02;
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;
// Per-generator health flags (REG_GEN_HEALTH + 4g)
const uint8_t FLAG_RCT_FAILED = 0x01, FLAG_APT_FAILED = 0x02, FLAG_TOO_MANY_DEAD = 0x04,
FLAG_ACTIVE = 0x08, FLAG_SWEPT = 0x10, FLAG_HEALTHY = 0x20;
const uint8_t GENERATORS = 4;
const uint8_t STATS_CHANNELS = GENERATORS + 1; // generators, then the combined channel
const uint8_t STATS_SNAPSHOT_BYTES = 72;
const uint8_t AMP_SNAPSHOT_BYTES = 44;
const size_t CAPTURE_BYTES = 32768;
} // namespace strix

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"
};