feat: add device firmware with USB API, StrixFPGA library and API check tools
This commit is contained in:
parent
7777384495
commit
eaf8cd284a
14 changed files with 1037 additions and 253 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -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)
|
||||
|
|
|
|||
56
README.md
56
README.md
|
|
@ -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
|
||||
|
||||
---
|
||||
|
|
|
|||
10
arduino/libraries/StrixFPGA/library.properties
Normal file
10
arduino/libraries/StrixFPGA/library.properties
Normal 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
|
||||
66
arduino/libraries/StrixFPGA/src/StrixFPGA.cpp
Normal file
66
arduino/libraries/StrixFPGA/src/StrixFPGA.cpp
Normal 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();
|
||||
}
|
||||
29
arduino/libraries/StrixFPGA/src/StrixFPGA.h
Normal file
29
arduino/libraries/StrixFPGA/src/StrixFPGA.h
Normal 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;
|
||||
};
|
||||
74
arduino/libraries/StrixFPGA/src/StrixRegisters.h
Normal file
74
arduino/libraries/StrixFPGA/src/StrixRegisters.h
Normal 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
51
arduino/strix/API.md
Normal 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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
14
devbox.json
14
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"
|
||||
],
|
||||
|
|
|
|||
119
tools/api_check.py
Normal file
119
tools/api_check.py
Normal 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()
|
||||
|
|
@ -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
44
tools/stream_entropy.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue