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

4
.gitignore vendored
View file

@ -22,8 +22,8 @@ fpga/**/simulation/
.venv/
# Generated FPGA image and capture data
arduino/strix_capture/bitstream.h
arduino/strix_capture/signature.h
arduino/libraries/StrixFPGA/src/bitstream.h
arduino/libraries/StrixFPGA/src/signature.h
captures/
# Local builds of external tools (onod3000)

View file

@ -3,46 +3,46 @@
A portable wireless True Random Number Generator built on an Arduino MKR Vidor 4000.
## Planned Design
## Design
Entropy comes from FPGA ring oscillators whose sampled timing jitter is XOR-combined into output bits, with no pseudo-random post-processing. A SAMD21 serves the results over WiFi and USB.
Entropy comes from FPGA ring oscillators whose sampled timing jitter is XOR-combined into output bits, with no pseudo-random post-processing. A SAMD21 serves the results over USB (WiFi is planned).
- Separate stream per entropy source
- Raw and processed output modes, with a von Neumann extractor that can be toggled per output path
- On-device health tests (NIST SP 800-90B repetition count and adaptive proportion)
- Live statistics: bias, z-score, random walk and autocorrelation
- Configurable random-walk / majority-vote bias amplifier
- 4 independent generators × 90 ring oscillators. Each generator XORs its rings every 120 MHz clock (spatial) and then several consecutive bits (temporal, default 5): about 24 Mbps per generator.
- Separate stream per generator, plus a combined stream of all healthy generators
- On-device health tests per generator: NIST SP 800-90B repetition count and adaptive proportion, plus a dead-ring monitor
- Live statistics per trial: bias, z-score, random walk and lag-1 autocorrelation, with rolling and cumulative windows
- Configurable random-walk / majority-vote bias amplifier on a bias path and an autocorrelation path, with sub-trial voting
- Von Neumann extractor that can be toggled on the stream, statistics and amplifier paths
- Raw ring captures and frequency measurement for characterization
## Set Up Dev Environment
The USB protocol is documented in [arduino/strix/API.md](arduino/strix/API.md).
Windows:
## Layout
1. Install Arduino IDE from here or via chocolatey: `choco install arduino`
2. Install [Intel Quartus Lite](https://www.intel.com/content/www/us/en/software-kit/825278/intel-quartus-prime-lite-edition-design-software-version-23-1-1-for-windows.html). In the list of devices select `Cyclone 10 LP device support`
- `fpga/rtl/`: Verilog sources; `fpga/sim/`: testbenches; `fpga/src/`: Quartus project
- `arduino/strix/`: device firmware
- `arduino/strix_capture/`: characterization tool firmware, used by `tools/jitter.py`
- `arduino/libraries/`: `StrixFPGA` (FPGA boot, SPI access, embedded FPGA image) and `VidorBoot` (vendored subset of Arduino's VidorPeripherals)
- `tools/`: FPGA image converter, jitter and statistics experiments, API check
## FPGA Development
## Development (Linux)
- Intel Quartus
- Open Project > select `strix.qpf` from the `fpga/src` directory in this repo.
The toolchain is managed by [devbox](https://www.jetify.com/devbox): `arduino-cli`, Intel Quartus Prime Lite (Cyclone 10 LP only, through a Nix flake in `nix/`), Icarus Verilog, Python and Rust.
## MCU Development
- Arduino IDE
- Boards Manager > Install 'Arduino SAMD Boards' (search for 'Vidor 4000')
- Library Manager > Install
- VidorPeripherals
- WiFiNINA (1.8.5, not higher)
- Open Project > `strix.ino` from the `arduino/strix` directory in this repo
1. `devbox run setup`: install the Arduino SAMD core and libraries
2. `devbox run sim`: run the FPGA testbenches
3. `devbox run fpga`: build the FPGA bitstream (about 10 minutes)
4. `devbox run image`: embed the bitstream in the `StrixFPGA` library
5. `devbox run build`, then `devbox run upload`: build and flash the firmware (`PORT` defaults to `/dev/ttyACM0`)
6. `devbox run api-check`: end-to-end check of the USB API
Serial access requires membership in the `dialout` group. For characterization, flash the capture tool with `devbox run upload-capture` and use `devbox run jitter <freq|rings|sweep|output|health|stats|amp|pairs>`. `devbox run onod-build` builds the onod3000 test suite used by `jitter output` and `api-check`.
## TODO
- Implement ring-oscillator entropy generators on the MKR Vidor 4000 FPGA
- Fetch random numbers from the FPGA via SPI
- Serve random numbers via USB
- Make Vidor pairable via Bluetooth/Wifi
- serve randomness wirelessly
- battery pack
- Serve randomness over WiFi (NINA pass-through in the FPGA is wired, firmware not yet)
- Make Vidor pairable via Bluetooth
- Battery pack
- 3D case
---

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

51
arduino/strix/API.md Normal file
View file

@ -0,0 +1,51 @@
# STRIX USB API
The firmware speaks a line protocol over the board's USB serial port (115200 baud setting, ignored by native USB). Send one command per line. Every reply is a single JSON line, except `entropy`. Errors look like `{"error":"..."}`, and settings changes confirm with `{"ok":true}`.
## Commands
| Command | Reply |
|---|---|
| `id` | Device, firmware and FPGA version, generator and ring counts |
| `entropy <n> [hex\|bin]` | `n` (1–65536) entropy bytes from the stream source. `hex` (default): one line of hex. `bin`: a line `BIN <n>`, then `n` raw bytes. Refused if the stream source is not healthy. If the source stops delivering for 2 s, hex ends with an error line and binary ends early. |
| `health` | Per generator: healthy, active, swept, test failures and event counts, dead rings. Stream FIFO level and dropped bytes. |
| `health clear` | Clears latched test failures and event counters |
| `stats` | Latest statistics trial: per channel (`g0`–`g3`, `combined`) bits, ones, bias z and lag-1 autocorrelation z for the trial, the rolling window and the cumulative total (with random walk position). Also trials counted and trials missed. |
| `stats reset` | Restarts trials and clears the rolling and cumulative windows |
| `amp` | Latest bias amplifier trial: per path (`bias`, `autocorr`) amplified bits, ones, z, sub-trial votes (`1`, `0`, `.` for no vote) and the trial result (`null` on a tie) |
| `get` | All settings |
| `set <key> <value>` | Changes a setting (see below) |
| `watch stats` / `watch amp` / `watch off` | Pushes the `stats` or `amp` reply after every new trial |
## Settings
| Key | Values | Effect |
|---|---|---|
| `temporal` | 1–8 | Consecutive bits XORed per generator output bit (default 5: about 24 Mbps per generator). The amplifier needs at least 4. |
| `generators` | 0–15 | Bit mask of running generators |
| `trial_ms` | 1–65535 | Statistics trial length; restarts the statistics |
| `stats_vn` | 0/1 | Von Neumann extractor on the statistics path; restarts the statistics |
| `rolling` | 1–50 | Trials in the rolling window |
| `stream_source` | 0–4 | Entropy stream source: generator 0–3, or 4 for the XOR of all healthy generators (default). Flushes the stream. |
| `stream_vn` | 0/1 | Von Neumann extractor on the entropy stream; flushes the stream |
| `amp_sources` | 0–15 | Generators feeding the amplifier (healthy ones only) |
| `amp_method` | `walk`/`vote` | Bounded random walk or majority vote in all three stages |
| `amp_stages` | `a,b,c` | Bound or window of the three stages, each 1–65535 (1 = passthrough). The total amplification is a × b × c. |
| `amp_sub_ms` | 1–255 | Sub-trial length |
| `amp_subtrials` | 1–15 | Sub-trials per amplifier trial |
| `amp_vn` | 0/1 | Von Neumann extractor on the amplifier path |
Changing an `amp_*` setting restarts the amplifier.
## Example
```
> id
{"device":"STRIX","firmware":"0.1.0","fpga_ready":true,"fpga_version":7,"generators":4,"rings_per_generator":90}
> entropy 8
3F09A2C47E11D05B
> set amp_stages 8,8,4
{"ok":true}
```
The device also runs without a USB host (on battery). The LED is on while every enabled generator is healthy.

View file

@ -1,150 +1,593 @@
#include <SPI.h>
#include <WiFiNINA.h>
// STRIX firmware for the Arduino MKR Vidor 4000: boots the STRIX FPGA image and serves
// entropy, health, live statistics and bias amplifier trials over USB serial.
//
// Protocol: one command per line; replies are single JSON lines, except `entropy`, which
// replies with hex text or a "BIN <n>" header followed by n raw bytes. See API.md.
char ssid[] = "Strix";
char pass[] = "owl12345678";
const int led = LED_BUILTIN;
const int rngPins[] = {A1, A2, A3, A4}; // Array of RNG pins
const int numPins = 4;
int status = WL_IDLE_STATUS;
WiFiServer server(80);
#include <StrixFPGA.h>
const int CHUNK_SIZE = 1000; // Adjust this value as needed
using namespace strix;
const char FIRMWARE_VERSION[] = "0.1.0";
const unsigned long POLL_MS = 10;
const unsigned long ENTROPY_TIMEOUT_MS = 2000;
const size_t MAX_ENTROPY_BYTES = 65536;
const size_t CHUNK_BYTES = 256;
const uint8_t MAX_ROLLING = 50;
const char *CHANNEL_NAMES[STATS_CHANNELS] = {"g0", "g1", "g2", "g3", "combined"};
const char *PATH_NAMES[2] = {"bias", "autocorr"};
StrixFPGA fpga;
bool fpgaReady = false;
// Statistics windows, built from the FPGA's per-trial snapshots
struct Counts {
uint32_t bits, ones, agreements;
};
struct Totals {
uint64_t bits, ones, agreements;
};
struct StatsTrial {
uint32_t sequence;
uint8_t healthy;
bool vonNeumann;
uint16_t trialMs;
Counts channels[STATS_CHANNELS];
};
StatsTrial latestStats;
bool haveStats = false;
Counts history[MAX_ROLLING][STATS_CHANNELS];
uint8_t historyCount = 0, historyNext = 0, rolling = 10;
Totals totals[STATS_CHANNELS];
uint32_t trialsCounted = 0, missedTrials = 0;
// After a restart the snapshot still holds the last old trial until the first new one ends.
bool awaitingRestart = false;
uint32_t staleSequence = 0;
struct AmpPath {
uint32_t bits, ones;
uint16_t votes, voted;
bool result, valid;
};
struct AmpTrial {
uint32_t sequence;
uint8_t sources, subtrials, subMs;
bool vonNeumann, overflow;
AmpPath paths[2];
};
AmpTrial latestAmp;
bool haveAmp = false;
enum Watch { WATCH_OFF, WATCH_STATS, WATCH_AMP };
Watch watching = WATCH_OFF;
// Helpers
uint32_t word32(const uint8_t *bytes) {
return (uint32_t)bytes[0] << 24 | (uint32_t)bytes[1] << 16 | (uint32_t)bytes[2] << 8 | bytes[3];
}
void printU64(uint64_t value) {
char digits[21];
int i = 20;
digits[i] = '\0';
do {
digits[--i] = '0' + value % 10;
value /= 10;
} while (value);
Serial.print(&digits[i]);
}
void printI64(int64_t value) {
if (value < 0) {
Serial.print('-');
printU64((uint64_t)(-value));
} else {
printU64((uint64_t)value);
}
}
void printBool(bool value) { Serial.print(value ? "true" : "false"); }
// Bias z = (2 ones - n) / sqrt(n); lag-1 autocorrelation z = (2 agreements - n) / sqrt(n).
void printZ(double numerator, double bits) {
if (bits <= 0) {
Serial.print("null");
} else {
Serial.print(numerator / sqrt(bits), 3);
}
}
void printWindow(uint64_t bits, uint64_t ones, uint64_t agreements, bool withWalk) {
Serial.print("{\"bits\":");
printU64(bits);
Serial.print(",\"ones\":");
printU64(ones);
Serial.print(",\"z\":");
printZ(2.0 * (double)ones - (double)bits, (double)bits);
Serial.print(",\"autocorr_z\":");
printZ(2.0 * (double)agreements - (double)bits, (double)bits);
if (withWalk) {
Serial.print(",\"walk\":");
printI64(2 * (int64_t)ones - (int64_t)bits);
}
Serial.print('}');
}
void printError(const char *message) {
Serial.print("{\"error\":\"");
Serial.print(message);
Serial.println("\"}");
}
void printOk() { Serial.println("{\"ok\":true}"); }
// FPGA polling
void resetStatsWindows() {
memset(totals, 0, sizeof(totals));
historyCount = historyNext = 0;
trialsCounted = missedTrials = 0;
haveStats = false;
}
uint32_t snapshotSequence() {
uint8_t raw[4];
fpga.readBurst(REG_STATS, raw, 4);
return word32(raw);
}
void restartStats(uint8_t vonNeumann) {
staleSequence = snapshotSequence();
awaitingRestart = true;
fpga.write(REG_STATS_CTRL, vonNeumann | STATS_RESTART);
resetStatsWindows();
}
void restartStats() { restartStats(fpga.read(REG_STATS_CTRL) & STATS_VON_NEUMANN); }
void restartAmplifier() {
fpga.write(REG_AMP_CTRL, (fpga.read(REG_AMP_CTRL) & AMP_VON_NEUMANN) | AMP_RESTART);
haveAmp = false;
}
void printStats();
void printAmp();
void pollStats() {
uint8_t raw[STATS_SNAPSHOT_BYTES];
fpga.readBurst(REG_STATS, raw, STATS_SNAPSHOT_BYTES);
uint32_t sequence = word32(raw);
if (sequence != word32(raw + STATS_SNAPSHOT_BYTES - 4)) return; // torn, retry next poll
if (haveStats && sequence == latestStats.sequence) return;
if (awaitingRestart) {
if (sequence == staleSequence) return;
awaitingRestart = false;
}
if (haveStats && sequence > latestStats.sequence + 1) missedTrials += sequence - latestStats.sequence - 1;
latestStats.sequence = sequence;
latestStats.healthy = raw[4];
latestStats.vonNeumann = raw[5] & 1;
latestStats.trialMs = raw[6] << 8 | raw[7];
for (uint8_t c = 0; c < STATS_CHANNELS; c++) {
const uint8_t *p = raw + 8 + 12 * c;
Counts counts = {word32(p), word32(p + 4), word32(p + 8)};
latestStats.channels[c] = counts;
history[historyNext][c] = counts;
totals[c].bits += counts.bits;
totals[c].ones += counts.ones;
totals[c].agreements += counts.agreements;
}
historyNext = (historyNext + 1) % MAX_ROLLING;
if (historyCount < MAX_ROLLING) historyCount++;
trialsCounted++;
haveStats = true;
if (watching == WATCH_STATS) printStats();
}
void pollAmp() {
uint8_t raw[AMP_SNAPSHOT_BYTES];
fpga.readBurst(REG_AMP, raw, AMP_SNAPSHOT_BYTES);
uint32_t sequence = word32(raw);
if (sequence != word32(raw + AMP_SNAPSHOT_BYTES - 4)) return;
if (haveAmp && sequence == latestAmp.sequence) return;
latestAmp.sequence = sequence;
latestAmp.sources = raw[4] & 0x0F;
latestAmp.vonNeumann = raw[4] & 0x10;
latestAmp.overflow = raw[4] & 0x20;
latestAmp.subtrials = raw[5];
latestAmp.subMs = raw[6];
for (uint8_t p = 0; p < 2; p++) {
const uint8_t *b = raw + 8 + 16 * p;
AmpPath &path = latestAmp.paths[p];
path.bits = word32(b);
path.ones = word32(b + 4);
path.votes = b[8] << 8 | b[9];
path.voted = b[10] << 8 | b[11];
path.result = b[13] & 1;
path.valid = b[13] & 2;
}
haveAmp = true;
if (watching == WATCH_AMP) printAmp();
}
// Commands
void printId() {
Serial.print("{\"device\":\"STRIX\",\"firmware\":\"");
Serial.print(FIRMWARE_VERSION);
Serial.print("\",\"fpga_ready\":");
printBool(fpgaReady);
Serial.print(",\"fpga_version\":");
Serial.print(fpga.version());
Serial.print(",\"generators\":");
Serial.print(fpgaReady ? fpga.read(REG_GENERATORS) : 0);
Serial.print(",\"rings_per_generator\":");
Serial.print(fpgaReady ? fpga.read(REG_RINGS) : 0);
Serial.println('}');
}
void printHealth() {
uint8_t healthy = fpga.read(REG_HEALTHY);
Serial.print("{\"healthy\":");
Serial.print(healthy);
Serial.print(",\"generators\":[");
for (uint8_t g = 0; g < GENERATORS; g++) {
uint8_t flags = fpga.read(REG_GEN_HEALTH + 4 * g);
if (g) Serial.print(',');
Serial.print("{\"healthy\":");
printBool(flags & FLAG_HEALTHY);
Serial.print(",\"active\":");
printBool(flags & FLAG_ACTIVE);
Serial.print(",\"swept\":");
printBool(flags & FLAG_SWEPT);
Serial.print(",\"rct_failed\":");
printBool(flags & FLAG_RCT_FAILED);
Serial.print(",\"apt_failed\":");
printBool(flags & FLAG_APT_FAILED);
Serial.print(",\"rct_events\":");
Serial.print(fpga.read(REG_GEN_HEALTH + 4 * g + 1));
Serial.print(",\"apt_events\":");
Serial.print(fpga.read(REG_GEN_HEALTH + 4 * g + 2));
Serial.print(",\"dead_rings\":");
Serial.print(fpga.read(REG_GEN_HEALTH + 4 * g + 3));
Serial.print('}');
}
Serial.print("],\"stream\":{\"level\":");
Serial.print((uint16_t)fpga.read(REG_LEVEL_HIGH) << 8 | fpga.read(REG_LEVEL_LOW));
Serial.print(",\"dropped\":");
Serial.print((uint16_t)fpga.read(REG_DROP_HIGH) << 8 | fpga.read(REG_DROP_LOW));
Serial.println("}}");
}
void printStats() {
if (!haveStats) {
printError("no trial completed yet");
return;
}
uint8_t window = rolling < historyCount ? rolling : historyCount;
Serial.print("{\"trial\":");
Serial.print(latestStats.sequence);
Serial.print(",\"trial_ms\":");
Serial.print(latestStats.trialMs);
Serial.print(",\"von_neumann\":");
printBool(latestStats.vonNeumann);
Serial.print(",\"healthy\":");
Serial.print(latestStats.healthy);
Serial.print(",\"trials\":");
Serial.print(trialsCounted);
Serial.print(",\"missed\":");
Serial.print(missedTrials);
Serial.print(",\"rolling_trials\":");
Serial.print(window);
Serial.print(",\"channels\":[");
for (uint8_t c = 0; c < STATS_CHANNELS; c++) {
uint64_t bits = 0, ones = 0, agreements = 0;
for (uint8_t i = 0; i < window; i++) {
const Counts &counts = history[(historyNext + MAX_ROLLING - 1 - i) % MAX_ROLLING][c];
bits += counts.bits;
ones += counts.ones;
agreements += counts.agreements;
}
const Counts &trial = latestStats.channels[c];
if (c) Serial.print(',');
Serial.print("{\"name\":\"");
Serial.print(CHANNEL_NAMES[c]);
Serial.print("\",\"trial\":");
printWindow(trial.bits, trial.ones, trial.agreements, false);
Serial.print(",\"rolling\":");
printWindow(bits, ones, agreements, false);
Serial.print(",\"total\":");
printWindow(totals[c].bits, totals[c].ones, totals[c].agreements, true);
Serial.print('}');
}
Serial.println("]}");
}
void printAmp() {
if (!haveAmp) {
printError("no trial completed yet");
return;
}
Serial.print("{\"trial\":");
Serial.print(latestAmp.sequence);
Serial.print(",\"sources\":");
Serial.print(latestAmp.sources);
Serial.print(",\"von_neumann\":");
printBool(latestAmp.vonNeumann);
Serial.print(",\"subtrials\":");
Serial.print(latestAmp.subtrials);
Serial.print(",\"sub_ms\":");
Serial.print(latestAmp.subMs);
Serial.print(",\"overflow\":");
printBool(latestAmp.overflow);
Serial.print(",\"paths\":[");
for (uint8_t p = 0; p < 2; p++) {
const AmpPath &path = latestAmp.paths[p];
if (p) Serial.print(',');
Serial.print("{\"name\":\"");
Serial.print(PATH_NAMES[p]);
Serial.print("\",\"bits\":");
Serial.print(path.bits);
Serial.print(",\"ones\":");
Serial.print(path.ones);
Serial.print(",\"z\":");
printZ(2.0 * path.ones - (double)path.bits, (double)path.bits);
Serial.print(",\"votes\":\"");
for (uint8_t i = 0; i < latestAmp.subtrials; i++) {
Serial.print(path.voted >> i & 1 ? (path.votes >> i & 1 ? '1' : '0') : '.');
}
Serial.print("\",\"result\":");
if (path.valid) {
Serial.print(path.result ? 1 : 0);
} else {
Serial.print("null");
}
Serial.print('}');
}
Serial.println("]}");
}
// Streams `count` entropy bytes as hex or binary. Stops early (binary: short output, hex: an
// error line) if the selected source stops delivering.
void sendEntropy(size_t count, bool binary) {
uint8_t control = fpga.read(REG_STREAM_CTRL);
uint8_t source = control & 0x07;
uint8_t healthy = fpga.read(REG_HEALTHY);
bool sourceHealthy = source == STREAM_COMBINED ? healthy != 0 : (healthy >> source & 1);
if (!sourceHealthy) {
printError("stream source is not healthy");
return;
}
if (binary) {
Serial.print("BIN ");
Serial.println(count);
}
static uint8_t chunk[CHUNK_BYTES];
size_t sent = 0;
unsigned long lastData = millis();
while (sent < count) {
uint16_t level = (uint16_t)fpga.read(REG_LEVEL_HIGH) << 8 | fpga.read(REG_LEVEL_LOW);
if (level == 0) {
if (millis() - lastData > ENTROPY_TIMEOUT_MS) {
if (!binary) {
Serial.println();
printError("entropy timeout");
}
return;
}
continue;
}
size_t length = count - sent;
if (length > level) length = level;
if (length > CHUNK_BYTES) length = CHUNK_BYTES;
fpga.readBurst(REG_STREAM, chunk, length);
if (binary) {
Serial.write(chunk, length);
} else {
for (size_t i = 0; i < length; i++) {
if (chunk[i] < 0x10) Serial.print('0');
Serial.print(chunk[i], HEX);
}
}
sent += length;
lastData = millis();
}
if (!binary) Serial.println();
}
void printConfig() {
Serial.print("{\"temporal\":");
Serial.print(fpga.read(REG_TEMPORAL));
Serial.print(",\"generators\":");
Serial.print(fpga.read(REG_GEN_ENABLE));
Serial.print(",\"trial_ms\":");
Serial.print((uint16_t)fpga.read(REG_TRIAL_HIGH) << 8 | fpga.read(REG_TRIAL_LOW));
Serial.print(",\"stats_vn\":");
Serial.print(fpga.read(REG_STATS_CTRL) & STATS_VON_NEUMANN);
Serial.print(",\"rolling\":");
Serial.print(rolling);
uint8_t stream = fpga.read(REG_STREAM_CTRL);
Serial.print(",\"stream_source\":");
Serial.print(stream & 0x07);
Serial.print(",\"stream_vn\":");
Serial.print(stream & STREAM_VON_NEUMANN ? 1 : 0);
Serial.print(",\"amp_sources\":");
Serial.print(fpga.read(REG_AMP_SOURCES));
Serial.print(",\"amp_method\":\"");
Serial.print(fpga.read(REG_AMP_STAGE) & 1 ? "vote" : "walk");
Serial.print("\",\"amp_stages\":\"");
for (uint8_t s = 0; s < 3; s++) {
if (s) Serial.print(',');
Serial.print((uint16_t)fpga.read(REG_AMP_STAGE + 3 * s + 1) << 8 | fpga.read(REG_AMP_STAGE + 3 * s + 2));
}
Serial.print("\",\"amp_sub_ms\":");
Serial.print(fpga.read(REG_AMP_SUB_MS));
Serial.print(",\"amp_subtrials\":");
Serial.print(fpga.read(REG_AMP_SUBS));
Serial.print(",\"amp_vn\":");
Serial.print(fpga.read(REG_AMP_CTRL) & AMP_VON_NEUMANN);
Serial.println('}');
}
bool inRange(long value, long low, long high) { return value >= low && value <= high; }
void setConfig(const String &key, const String &value) {
long number = value.toInt();
if (key == "temporal" && inRange(number, 1, 8)) {
fpga.write(REG_TEMPORAL, number);
} else if (key == "generators" && inRange(number, 0, 15)) {
fpga.write(REG_GEN_ENABLE, number);
} else if (key == "trial_ms" && inRange(number, 1, 65535)) {
fpga.write(REG_TRIAL_HIGH, number >> 8);
fpga.write(REG_TRIAL_LOW, number & 0xFF);
restartStats();
} else if (key == "stats_vn" && inRange(number, 0, 1)) {
restartStats(number);
} else if (key == "rolling" && inRange(number, 1, MAX_ROLLING)) {
rolling = number;
} else if (key == "stream_source" && inRange(number, 0, 4)) {
fpga.write(REG_STREAM_CTRL, (fpga.read(REG_STREAM_CTRL) & STREAM_VON_NEUMANN) | number | STREAM_FLUSH);
} else if (key == "stream_vn" && inRange(number, 0, 1)) {
fpga.write(REG_STREAM_CTRL, (fpga.read(REG_STREAM_CTRL) & 0x07) | (number ? STREAM_VON_NEUMANN : 0) | STREAM_FLUSH);
} else if (key == "amp_sources" && inRange(number, 0, 15)) {
fpga.write(REG_AMP_SOURCES, number);
restartAmplifier();
} else if (key == "amp_method" && (value == "walk" || value == "vote")) {
for (uint8_t s = 0; s < 3; s++) fpga.write(REG_AMP_STAGE + 3 * s, value == "vote");
restartAmplifier();
} else if (key == "amp_stages") {
long stages[3];
int start = 0;
for (uint8_t s = 0; s < 3; s++) {
int comma = value.indexOf(',', start);
if ((s < 2) != (comma >= 0)) {
printError("amp_stages needs three comma-separated values");
return;
}
stages[s] = value.substring(start, comma < 0 ? value.length() : comma).toInt();
if (!inRange(stages[s], 1, 65535)) {
printError("amp_stages values must be 1-65535");
return;
}
start = comma + 1;
}
for (uint8_t s = 0; s < 3; s++) {
fpga.write(REG_AMP_STAGE + 3 * s + 1, stages[s] >> 8);
fpga.write(REG_AMP_STAGE + 3 * s + 2, stages[s] & 0xFF);
}
restartAmplifier();
} else if (key == "amp_sub_ms" && inRange(number, 1, 255)) {
fpga.write(REG_AMP_SUB_MS, number);
restartAmplifier();
} else if (key == "amp_subtrials" && inRange(number, 1, 15)) {
fpga.write(REG_AMP_SUBS, number);
restartAmplifier();
} else if (key == "amp_vn" && inRange(number, 0, 1)) {
fpga.write(REG_AMP_CTRL, number | AMP_RESTART);
haveAmp = false;
} else {
printError("unknown setting or value out of range");
return;
}
printOk();
}
void handleCommand(String line) {
line.trim();
int space = line.indexOf(' ');
String command = space < 0 ? line : line.substring(0, space);
String args = space < 0 ? "" : line.substring(space + 1);
args.trim();
int argSpace = args.indexOf(' ');
String first = argSpace < 0 ? args : args.substring(0, argSpace);
String rest = argSpace < 0 ? "" : args.substring(argSpace + 1);
rest.trim();
if (command.length() == 0) return;
if (command == "id") {
printId();
} else if (!fpgaReady) {
printError("FPGA not ready");
} else if (command == "entropy") {
long count = first.toInt();
if (!inRange(count, 1, MAX_ENTROPY_BYTES) || (rest.length() && rest != "hex" && rest != "bin")) {
printError("usage: entropy <1-65536> [hex|bin]");
} else {
sendEntropy(count, rest == "bin");
}
} else if (command == "health") {
if (first == "clear") {
fpga.write(REG_HEALTH_CTRL, HEALTH_CLEAR);
printOk();
} else {
printHealth();
}
} else if (command == "stats") {
if (first == "reset") {
restartStats();
printOk();
} else {
printStats();
}
} else if (command == "amp") {
printAmp();
} else if (command == "get") {
printConfig();
} else if (command == "set" && first.length() && rest.length()) {
setConfig(first, rest);
} else if (command == "watch" && (first == "stats" || first == "amp" || first == "off")) {
watching = first == "stats" ? WATCH_STATS : first == "amp" ? WATCH_AMP : WATCH_OFF;
printOk();
} else {
printError("unknown command");
}
}
// Main
char lineBuffer[128];
size_t lineLength = 0;
void readSerial() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (lineLength) {
lineBuffer[lineLength] = '\0';
handleCommand(String(lineBuffer));
lineLength = 0;
}
} else if (lineLength < sizeof(lineBuffer) - 1) {
lineBuffer[lineLength++] = c;
}
}
}
void setup() {
Serial.begin(9600);
while (!Serial) {
; // Wait for serial port to connect. Needed for native USB port only
}
Serial.println("Access Point Web Server");
pinMode(led, OUTPUT); // Set the LED pin mode
digitalWrite(led, HIGH); // Ensure LED is off at boot
for (int i = 0; i < numPins; i++) {
pinMode(rngPins[i], INPUT); // Set the RNG pin modes
}
// Check for the WiFi module
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
while (true);
}
// Print the network name (SSID)
Serial.print("Creating access point named: ");
Serial.println(ssid);
// Create open network
status = WiFi.beginAP(ssid, pass);
if (status != WL_AP_LISTENING) {
Serial.println("Creating access point failed");
while (true);
}
// Wait 10 seconds for connection
delay(10000);
// Start the web server on port 80
server.begin();
// Print WiFi status
printWiFiStatus();
Serial.begin(115200); // no wait for a host: the device also runs on battery
pinMode(LED_BUILTIN, OUTPUT);
fpgaReady = fpga.begin();
if (fpgaReady) restartStats();
}
void loop() {
if (status != WiFi.status()) {
status = WiFi.status();
if (status == WL_AP_CONNECTED) {
Serial.println("Device connected to AP");
} else {
Serial.println("Device disconnected from AP");
readSerial();
static unsigned long lastPoll = 0;
if (fpgaReady && millis() - lastPoll >= POLL_MS) {
lastPoll = millis();
pollStats();
pollAmp();
// LED on while every enabled generator is healthy
uint8_t enabled = fpga.read(REG_GEN_ENABLE);
digitalWrite(LED_BUILTIN, enabled && (fpga.read(REG_HEALTHY) & enabled) == enabled);
}
}
WiFiClient client = server.available();
if (client) {
String request = client.readStringUntil('\r');
client.flush();
// Check if the request is a GET request and contains "length"
if (request.indexOf("GET") >= 0 && request.indexOf("length=") >= 0) {
int lengthIndex = request.indexOf("length=") + 7;
int lengthEndIndex = request.indexOf(" ", lengthIndex);
String lengthString = request.substring(lengthIndex, lengthEndIndex);
int length = lengthString.toInt();
// Send the response to the client in chunks
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/plain");
client.println("Connection: close");
client.println();
sendRandomHex(client, length);
}
delay(1);
client.stop();
}
}
void sendRandomHex(WiFiClient &client, int length) {
int bitsNeeded = length * 8;
int byteValue[numPins] = {0};
int bitCount = 0;
char hexBuffer[CHUNK_SIZE * 2 + 1]; // Buffer to hold the hex representation of the chunk
int bufferIndex = 0;
for (int i = 0; i < bitsNeeded; i += numPins) {
for (int j = 0; j < numPins; j++) {
int bit = analogRead(rngPins[j]) & 1; // Read the least significant bit from each pin
byteValue[j] = (byteValue[j] << 1) | bit;
// Blink LED according to the bit value from A1
if (j == 0) { // Only for A1
digitalWrite(led, bit ? LOW : HIGH);
}
}
bitCount++;
if (bitCount == 8) {
for (int j = 0; j < numPins; j++) {
snprintf(hexBuffer + bufferIndex, 3, "%02X", byteValue[j]); // Convert byte to hex string
bufferIndex += 2;
byteValue[j] = 0;
}
bitCount = 0;
}
// Send the data in chunks to avoid memory overflow
if (bufferIndex >= CHUNK_SIZE * 2) {
digitalWrite(led, HIGH);
hexBuffer[bufferIndex] = '\0';
client.print(hexBuffer);
bufferIndex = 0;
}
}
// Ensure any remaining data is sent
if (bufferIndex > 0) {
digitalWrite(led, HIGH);
hexBuffer[bufferIndex] = '\0';
client.print(hexBuffer);
}
// Ensure all data is sent
client.flush();
}
void printWiFiStatus() {
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
Serial.print("To see this page in action, open a browser to http://");
Serial.println(ip);
}

View file

@ -1,5 +1,6 @@
// STRIX raw capture tool: boots the STRIX FPGA image and gives the host (tools/jitter.py)
// register access and raw captures over USB serial.
// register access and raw captures over USB serial. For characterization only; the device
// firmware is arduino/strix.
//
// Commands (one per line, numbers in decimal):
// id print the FPGA identification
@ -10,73 +11,28 @@
// stats print the latest statistics trial snapshot as hex
// amp print the latest bias amplifier trial snapshot as hex
#include <SPI.h>
#include <VidorBoot.h>
#include "wiring_private.h"
#include <StrixFPGA.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);
using namespace strix;
enum : uint8_t {
REG_ID0 = 0x00,
REG_VERSION = 0x04,
REG_CONTROL = 0x0B,
REG_STATUS = 0x0C,
REG_DATA = 0x10,
REG_STATS = 0x40,
REG_AMP = 0x42,
};
const uint8_t READ = 0x80;
const uint8_t CONTROL_START_CAPTURE = 0x01;
const uint8_t STATUS_CAPTURE_DONE = 0x02;
const size_t CAPTURE_BYTES = 32768;
const size_t CHUNK_BYTES = 512;
const size_t STATS_SNAPSHOT_BYTES = 72;
const size_t AMP_SNAPSHOT_BYTES = 44;
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;
}
StrixFPGA fpga;
void printId() {
char id[5] = {0};
for (uint8_t i = 0; i < 4; i++) id[i] = readRegister(REG_ID0 + i);
for (uint8_t i = 0; i < 4; i++) id[i] = fpga.read(REG_ID0 + i);
Serial.print(id);
Serial.print(" v");
Serial.println(readRegister(REG_VERSION));
Serial.println(fpga.read(REG_VERSION));
}
void capture() {
writeRegister(REG_CONTROL, CONTROL_START_CAPTURE);
fpga.write(REG_CONTROL, CONTROL_START_CAPTURE);
// 262,144 samples at 120 MHz take up to 143 s at the maximum decimation of 65535.
unsigned long start = millis();
while (!(readRegister(REG_STATUS) & STATUS_CAPTURE_DONE)) {
while (!(fpga.read(REG_STATUS) & STATUS_CAPTURE_DONE)) {
if (millis() - start > 180000) {
Serial.println("ERR timeout");
return;
@ -88,21 +44,18 @@ void capture() {
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();
fpga.readBurst(REG_DATA, chunk, CHUNK_BYTES);
Serial.write(chunk, CHUNK_BYTES);
}
}
void printSnapshot(uint8_t reg, size_t length) {
beginTransaction(READ | reg);
uint8_t snapshot[STATS_SNAPSHOT_BYTES];
fpga.readBurst(reg, snapshot, length);
for (size_t i = 0; i < length; i++) {
uint8_t value = strixSpi.transfer(0);
if (value < 0x10) Serial.print('0');
Serial.print(value, HEX);
if (snapshot[i] < 0x10) Serial.print('0');
Serial.print(snapshot[i], HEX);
}
endTransaction();
Serial.println();
}
@ -127,9 +80,9 @@ void handleCommand(String line) {
if (command == "id") {
printId();
} else if (command == "r" && count == 1 && reg >= 0 && reg < 0x80) {
Serial.println(readRegister(reg));
Serial.println(fpga.read(reg));
} else if (command == "w" && count == 2 && reg >= 0 && reg < 0x80 && value >= 0 && value < 256) {
writeRegister(reg, value);
fpga.write(reg, value);
Serial.println("OK");
} else if (command == "cap") {
capture();
@ -146,20 +99,9 @@ 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)) {
if (!fpga.begin()) {
Serial.println("ERR FPGA boot failed");
}
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);
printId();
}

View file

@ -18,14 +18,14 @@
"setup": [
"arduino-cli core update-index",
"arduino-cli core install arduino:samd",
"arduino-cli lib install VidorPeripherals WiFiNINA@1.8.5"
"arduino-cli lib install WiFiNINA@1.8.5"
],
"build": [
"arduino-cli compile --fqbn arduino:samd:mkrvidor4000 arduino/strix"
"arduino-cli compile --fqbn arduino:samd:mkrvidor4000 --libraries arduino/libraries 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_generator.v fpga/rtl/health_monitor.v fpga/rtl/stats_block.v fpga/rtl/bias_amplifier.v fpga/rtl/amp_path.v fpga/rtl/amp_stage.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v",
"iverilog -g2005 -DSIM -o /tmp/strix-sim/tb_strix_core fpga/sim/tb_strix_core.v fpga/rtl/strix_core.v fpga/rtl/entropy_generator.v fpga/rtl/health_monitor.v fpga/rtl/stats_block.v fpga/rtl/entropy_stream.v fpga/rtl/bias_amplifier.v fpga/rtl/amp_path.v fpga/rtl/amp_stage.v fpga/rtl/freq_counter.v fpga/rtl/ring_osc.v fpga/rtl/raw_capture.v fpga/rtl/spi_slave.v",
"vvp -n /tmp/strix-sim/tb_strix_core",
"iverilog -g2005 -o /tmp/strix-sim/tb_amp_stage fpga/sim/tb_amp_stage.v fpga/rtl/amp_stage.v",
"vvp -n /tmp/strix-sim/tb_amp_stage"
@ -34,14 +34,20 @@
"cd fpga/src && quartus_sh --flow compile strix"
],
"image": [
"python3 tools/vidor_image.py fpga/src/output_files/strix.ttf arduino/strix_capture"
"python3 tools/vidor_image.py fpga/src/output_files/strix.ttf arduino/libraries/StrixFPGA/src"
],
"build-capture": [
"arduino-cli compile --fqbn arduino:samd:mkrvidor4000 --libraries arduino/libraries arduino/strix_capture"
],
"upload": [
"arduino-cli upload --fqbn arduino:samd:mkrvidor4000 -p ${PORT:-/dev/ttyACM0} arduino/strix"
],
"upload-capture": [
"arduino-cli upload --fqbn arduino:samd:mkrvidor4000 -p ${PORT:-/dev/ttyACM0} arduino/strix_capture"
],
"api-check": [
"python3 tools/api_check.py --port ${PORT:-/dev/ttyACM0}"
],
"onod-build": [
"cargo build --release --manifest-path ${ONOD3000_DIR:-$HOME/Projects/randonautica/code/onod3000}/Cargo.toml --target-dir .build/onod3000"
],

119
tools/api_check.py Normal file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""End-to-end check of the STRIX firmware's USB API (arduino/strix, see API.md).
Runs every command once, checks the replies, measures entropy throughput, and runs the onod3000
suite on 256 KB from the entropy stream.
"""
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
import serial
class Device:
def __init__(self, port: str):
self.serial = serial.Serial(port, 115200, timeout=5)
time.sleep(0.5)
self.serial.reset_input_buffer()
def line(self, command: str) -> str:
self.serial.write(f"{command}\n".encode())
return self.serial.readline().decode().strip()
def json(self, command: str) -> dict:
reply = self.line(command)
try:
return json.loads(reply)
except json.JSONDecodeError:
sys.exit(f"{command!r}: not JSON: {reply!r}")
def entropy_bin(self, count: int) -> bytes:
self.serial.write(f"entropy {count} bin\n".encode())
header = self.serial.readline().decode().strip()
if header != f"BIN {count}":
sys.exit(f"entropy bin: unexpected header {header!r}")
data = self.serial.read(count)
if len(data) != count:
sys.exit(f"entropy bin: {len(data)} of {count} bytes")
return data
def check(condition: bool, message: str) -> None:
print(("ok " if condition else "FAIL ") + message)
if not condition:
check.failures += 1
check.failures = 0
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", default="/dev/ttyACM0")
parser.add_argument("--onod", type=Path, default=Path(".build/onod3000/release/onod3000"))
args = parser.parse_args()
device = Device(args.port)
info = device.json("id")
check(info.get("device") == "STRIX" and info.get("fpga_ready"), f"id: {info}")
health = device.json("health")
check(health.get("healthy") == 15, f"health: all 4 generators healthy (mask {health.get('healthy')})")
config = device.json("get")
check(config.get("temporal") == 5 and config.get("stream_source") == 4, f"get: {config}")
hex_line = device.line("entropy 32")
check(len(hex_line) == 64 and all(c in "0123456789ABCDEF" for c in hex_line), f"entropy 32 hex: {hex_line}")
start = time.time()
device.entropy_bin(65536)
elapsed = time.time() - start
check(True, f"entropy 65536 bin: {65536 / elapsed / 1024:.0f} KB/s")
check(device.json("set rolling 5") == {"ok": True}, "set rolling 5")
check(device.json("get").get("rolling") == 5, "get rolling == 5")
check("error" in device.json("set temporal 9"), "set temporal 9 is refused")
device.json("set rolling 10")
device.json("stats reset")
time.sleep(1.2)
stats = device.json("stats")
channels = stats.get("channels", [])
check(len(channels) == 5 and stats.get("trials", 0) >= 4 and stats.get("missed") == 0,
f"stats: {stats.get('trials')} trials, {stats.get('missed')} missed, "
f"combined total z {channels[-1]['total']['z'] if channels else None}")
amp = device.json("amp")
check(len(amp.get("paths", [])) == 2 and not amp.get("overflow"),
f"amp: trial {amp.get('trial')}, bias {amp['paths'][0]['bits']} bits, votes {amp['paths'][0]['votes']}")
check(device.json("watch stats") == {"ok": True}, "watch stats")
trials = [json.loads(device.serial.readline()).get("trial") for _ in range(3)]
device.json("watch off")
device.serial.reset_input_buffer()
check(trials == sorted(trials) and len(set(trials)) == 3, f"watch stats pushed trials {trials}")
if args.onod.exists():
sample = Path("captures/api_entropy.bin")
sample.parent.mkdir(exist_ok=True)
sample.write_bytes(b"".join(device.entropy_bin(65536) for _ in range(4)))
report = subprocess.run([str(args.onod), "-f", str(sample)], capture_output=True, text=True).stdout
summary = next((l for l in report.splitlines() if "tests passed" in l), "no summary")
passed = int(summary.split("/")[0]) if "/" in summary else 0
# 15 tests at p < 0.01: one chance failure happens in about 14% of runs on ideal data.
check(passed >= 14, f"onod3000 on 256 KB from the stream: {summary}")
else:
print(f"skip onod3000 ({args.onod} not built)")
print(f"\n{check.failures} failure(s)")
sys.exit(1 if check.failures else 0)
if __name__ == "__main__":
main()

View file

@ -82,8 +82,8 @@ class Board:
while self.serial.readline():
pass
self.serial.timeout = 200
if not identity.startswith("STRX v6"):
sys.exit(f"unexpected board identity: {identity!r} (needs the v6 FPGA image)")
if not identity.startswith("STRX v7"):
sys.exit(f"unexpected board identity: {identity!r} (needs the v7 FPGA image)")
self.generators = self.read(REG_GENERATORS)
self.rings = self.read(REG_RINGS)
print(f"board: {identity}, {self.generators} generators x {self.rings} rings")

44
tools/stream_entropy.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Writes STRIX entropy to stdout continuously, for piping into test suites
(e.g. `python3 tools/stream_entropy.py | qngmeter`). Uses the firmware's `entropy <n> bin`
command (arduino/strix/API.md)."""
import argparse
import sys
import time
import serial
CHUNK = 65536
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--port", default="/dev/ttyACM0")
parser.add_argument("--bytes", type=int, default=0, help="stop after this many bytes (0 = run until killed)")
args = parser.parse_args()
device = serial.Serial(args.port, 115200, timeout=5)
time.sleep(0.5)
device.reset_input_buffer()
out = sys.stdout.buffer
sent = 0
try:
while args.bytes == 0 or sent < args.bytes:
count = CHUNK if args.bytes == 0 else min(CHUNK, args.bytes - sent)
device.write(f"entropy {count} bin\n".encode())
header = device.readline().decode(errors="replace").strip()
if header != f"BIN {count}":
sys.exit(f"device: {header or 'timeout'}")
data = device.read(count)
if len(data) != count:
sys.exit(f"short read: {len(data)} of {count} bytes")
out.write(data)
out.flush()
sent += count
except (BrokenPipeError, KeyboardInterrupt):
pass
if __name__ == "__main__":
main()