strix/arduino/strix/strix.ino

593 lines
18 KiB
C++

// 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.
#include <StrixFPGA.h>
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(115200); // no wait for a host: the device also runs on battery
pinMode(LED_BUILTIN, OUTPUT);
fpgaReady = fpga.begin();
if (fpgaReady) restartStats();
}
void loop() {
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);
}
}