From eaf8cd284a8ade571919a26b55b7902e5d0d49e4 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 25 Oct 2026 15:29:36 +0200 Subject: [PATCH] feat: add device firmware with USB API, StrixFPGA library and API check tools --- .gitignore | 4 +- README.md | 58 +- .../libraries/StrixFPGA/library.properties | 10 + arduino/libraries/StrixFPGA/src/StrixFPGA.cpp | 66 ++ arduino/libraries/StrixFPGA/src/StrixFPGA.h | 29 + .../libraries/StrixFPGA/src/StrixRegisters.h | 74 ++ .../StrixFPGA/src}/fpga_image.cpp | 0 arduino/strix/API.md | 51 ++ arduino/strix/strix.ino | 725 ++++++++++++++---- arduino/strix_capture/strix_capture.ino | 92 +-- devbox.json | 14 +- tools/api_check.py | 119 +++ tools/jitter.py | 4 +- tools/stream_entropy.py | 44 ++ 14 files changed, 1037 insertions(+), 253 deletions(-) create mode 100644 arduino/libraries/StrixFPGA/library.properties create mode 100644 arduino/libraries/StrixFPGA/src/StrixFPGA.cpp create mode 100644 arduino/libraries/StrixFPGA/src/StrixFPGA.h create mode 100644 arduino/libraries/StrixFPGA/src/StrixRegisters.h rename arduino/{strix_capture => libraries/StrixFPGA/src}/fpga_image.cpp (100%) create mode 100644 arduino/strix/API.md create mode 100644 tools/api_check.py create mode 100644 tools/stream_entropy.py diff --git a/.gitignore b/.gitignore index ae8d1ce..ae25438 100644 --- a/.gitignore +++ b/.gitignore @@ -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) diff --git a/README.md b/README.md index 578186b..e8f1339 100644 --- a/README.md +++ b/README.md @@ -3,48 +3,48 @@ 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 `. `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 --- -## [Development Notes](NOTES.md) \ No newline at end of file +## [Development Notes](NOTES.md) diff --git a/arduino/libraries/StrixFPGA/library.properties b/arduino/libraries/StrixFPGA/library.properties new file mode 100644 index 0000000..26b3750 --- /dev/null +++ b/arduino/libraries/StrixFPGA/library.properties @@ -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 diff --git a/arduino/libraries/StrixFPGA/src/StrixFPGA.cpp b/arduino/libraries/StrixFPGA/src/StrixFPGA.cpp new file mode 100644 index 0000000..74e8c22 --- /dev/null +++ b/arduino/libraries/StrixFPGA/src/StrixFPGA.cpp @@ -0,0 +1,66 @@ +#include "StrixFPGA.h" + +#include +#include + +#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(); +} diff --git a/arduino/libraries/StrixFPGA/src/StrixFPGA.h b/arduino/libraries/StrixFPGA/src/StrixFPGA.h new file mode 100644 index 0000000..174d2c8 --- /dev/null +++ b/arduino/libraries/StrixFPGA/src/StrixFPGA.h @@ -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 + +#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; +}; diff --git a/arduino/libraries/StrixFPGA/src/StrixRegisters.h b/arduino/libraries/StrixFPGA/src/StrixRegisters.h new file mode 100644 index 0000000..845f373 --- /dev/null +++ b/arduino/libraries/StrixFPGA/src/StrixRegisters.h @@ -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 + +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 diff --git a/arduino/strix_capture/fpga_image.cpp b/arduino/libraries/StrixFPGA/src/fpga_image.cpp similarity index 100% rename from arduino/strix_capture/fpga_image.cpp rename to arduino/libraries/StrixFPGA/src/fpga_image.cpp diff --git a/arduino/strix/API.md b/arduino/strix/API.md new file mode 100644 index 0000000..b390e47 --- /dev/null +++ b/arduino/strix/API.md @@ -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 [hex\|bin]` | `n` (1–65536) entropy bytes from the stream source. `hex` (default): one line of hex. `bin`: a line `BIN `, 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 ` | 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. diff --git a/arduino/strix/strix.ino b/arduino/strix/strix.ino index 1d99807..0f88af7 100644 --- a/arduino/strix/strix.ino +++ b/arduino/strix/strix.ino @@ -1,150 +1,593 @@ -#include -#include +// 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 " 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 -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"); - } - } - - 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(); + 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); } } - -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); -} diff --git a/arduino/strix_capture/strix_capture.ino b/arduino/strix_capture/strix_capture.ino index 5e115d8..12c255b 100644 --- a/arduino/strix_capture/strix_capture.ino +++ b/arduino/strix_capture/strix_capture.ino @@ -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 -#include -#include "wiring_private.h" +#include -// 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(); } diff --git a/devbox.json b/devbox.json index 874e6ed..b7dc124 100644 --- a/devbox.json +++ b/devbox.json @@ -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" ], diff --git a/tools/api_check.py b/tools/api_check.py new file mode 100644 index 0000000..8dba37c --- /dev/null +++ b/tools/api_check.py @@ -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() diff --git a/tools/jitter.py b/tools/jitter.py index be15963..411e76d 100644 --- a/tools/jitter.py +++ b/tools/jitter.py @@ -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") diff --git a/tools/stream_entropy.py b/tools/stream_entropy.py new file mode 100644 index 0000000..ac15997 --- /dev/null +++ b/tools/stream_entropy.py @@ -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 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()