project
TRNG WebApp — Ultra96
Networked True Random Number Generator / Avnet Ultra96 · Zynq UltraScale+ MPSoC ZU3CG
live
Last value received
{
"status": "waiting for the board to report…"
}
overview
What is it?
TRNG WebApp Ultra96 is a hardware true random number generator built on an Avnet Ultra96 board (Zynq UltraScale+ MPSoC ZU3CG). It harvests physical entropy in the programmable logic, conditions it with an in-fabric SHA-256 core, and pushes the result over the network to this site.
Inside the PL, a bank of ring oscillators is XOR-reduced into one random bit per clock and shifted into a 440-bit raw word. 440 bits (55 bytes) is the largest message that fits in a single SHA-256 block, so one hash call backs the full 256-bit output with enough input entropy to qualify as NIST SP 800-90B vetted conditioning.
The processing system (PetaLinux) reads both the raw word and the digest over AXI4-Lite, runs the NIST SP 800-90B continuous health tests (RCT + APT) on the raw bits, and POSTs the value, the raw stream and the health verdict to the web app together with a board ID.
dataflow
From silicon to the network
Entropy is generated and conditioned entirely in the programmable logic. The PS only triggers a run, reads two words over AXI4-Lite, scores the raw bits and ships the payload over HTTPS.
ring oscillators ─XOR─► rnd_bit ─► trng_assemble ─► random[439:0] (ro_cell × n_ro) │ ├─► sha256_stream440 ─► hash[255:0] │ PS (PetaLinux) ◄──AXI4-Lite── trng_axi ◄──────────┘ │ ├─ NIST SP 800-90B health (RCT + APT) on the 440 raw bits └─ POST { id, trng256, trng_stream, health, temperature } ─► lut7.dev
internals
Entropy pipeline
The ring oscillators are the entropy source; everything from XOR reduction through SHA-256 conditioning runs in the FPGA fabric, leaving the PS to validate and publish.
- Free-running, uncorrelated, native to the FPGA fabric
- Per-oscillator phase noise driven by thermal + flicker physics
n_rooscillators XOR'd together amplify jitter and decorrelate- Sampled into the synchronous domain — one raw bit per clock
- 440 raw bits assembled into a single SHA-256 message block
sha256_stream440feeds 55 bytes, MSB first- Reused RTL
sha256_core→ 256-bit digest - Conditioning happens before the bits ever leave the PL
rtl
The entropy source — in Verilog
At the heart of the PL design is a bank of ring oscillators. Each one is a closed loop
of LUT-based inverters mapped to Xilinx LUT1
primitives; their outputs are XOR-reduced to a single bit and registered onto the
synchronous clock.
module ro_trng #( parameter n_ro = 64 ) ( input wire aclk, /* system clock */ input wire resetn, /* synchronous active-low reset */ output reg rnd_bit /* one random bit per aclk cycle */ ); wire [n_ro-1:0] ro_out; /* n_ro free-running ring oscillators (LUT1 primitives) */ genvar i; generate for (i = 0; i < n_ro; i = i + 1) begin : gen_ro ro_cell u_ro ( .out (ro_out[i]) ); end endgenerate /* XOR reduction sampled into the synchronous domain */ always @(posedge aclk) begin if (!resetn) rnd_bit <= 1'b0; else rnd_bit <= ^ro_out; end endmodule
All resets are synchronous and the design uses a single posedge aclk
domain. Combinational ring-oscillator loops are explicitly waived in the XDC so the
synthesiser does not optimise them away.
hardware
Target & toolchain
| Item | Value | Role |
|---|---|---|
| Board | Avnet Ultra96 | Wi-Fi enabled MPSoC dev board — POSTs results over the network |
| Device | xczu3cg-sbva484-1-e | Zynq UltraScale+ MPSoC ZU3CG — PL entropy + PS host |
| Entropy source | Ring oscillators (LUT1) | XOR-reduced, registered — one raw bit per clock |
| Conditioning | RTL SHA-256 core | 440-bit raw word → single block → 256-bit digest |
| Host | PetaLinux on the PS | Triggers a run, reads over AXI4-Lite, runs health tests, POSTs |
| Bus | AXI4-Lite slave (trng_axi) | PS reads the raw word and the digest via /dev/mem or generic-uio |
| Toolchain | Vivado 2025.1 | PS in an IP-Integrator block design · top trng_webapp_ultra96_bd_wrapper |
| Board ID | 0x96 | Identifies this source in every POST payload |
validation
NIST SP 800-90B health tests
Two continuous tests run on the raw 440-bit word, before the SHA-256 conditioning — hashing would mask a broken source by turning even a constant input into random-looking output. They are cheap, always-on sanity checks that catch a noise source that has died or latched up, not a measure of entropy quality.
Detects a source stuck at a constant value. It counts consecutive identical samples; reaching the cutoff C = 32 identical bits fails the test. The run counter is carried across word boundaries, so a stuck source is caught even if the run straddles two reads.
Detects a subtler loss of entropy — a heavy bias without a full freeze. Over a window of W = 512 bits it counts how many samples match the window's reference; exceeding C = 410 fails. The window is wider than one word, so the host reports WARMING until the first window fills.
| Test | Parameter | Value |
|---|---|---|
| RCT | cutoff C (max identical run) | 32 bits |
| APT | window W | 512 bits |
| APT | cutoff C (max matches of the reference sample) | 410 |
web app
The POST payload
After each run the PS sends one JSON document to the web app: the board ID, the 256-bit conditioned value, the full 440-bit raw stream and the health verdict for that read.
{
"id": "0x96",
"trng256": "4eceb744166401913a861630543b379f6695ee88b9004cb6b6f843a4c1112a6d",
"trng_stream": "c1ac5e34…d70", /* 440 raw bits, 110 hex */
"health": "OK", /* OK · WARMING · FAIL_RCT · FAIL_APT */
"temperature": 42.67
}
Because the health tests are continuous, the host feeds successive reads to one stateful monitor that treats them as a single stream, rather than scoring each 440-bit word in isolation.
usage
On the board
A small Python host script triggers the PL, reads the raw word and its digest over the generic-uio driver, runs the NIST health tests and POSTs the result. Note the APT reporting WARMING until its 512-bit window fills.
measurement
Entropy quality
The continuous tests only detect a broken source. To measure how much entropy a healthy one provides, a 1,000,000-bit capture of the raw ring-oscillator stream was run through the NIST SP 800-90B non-IID estimators on an x86 host.
Ring oscillators never reach 1.0 bit/bit, so 0.74 is a
healthy raw rate: 440 × 0.7428 ≈ 327 bits of input
min-entropy. Since that exceeds the 256-bit output width, the SHA-256 digest qualifies as
full entropy under the SP 800-90B vetted-conditioning rule
(confirmed by ea_conditioning:
h_out = 256, ε = 2-78.32), with a
~28 % margin so the design stays above 256 bits even if the source degrades over
voltage, temperature or age.
- PL entropy path — ring oscillators, 440-bit assembler, RTL SHA-256 core
- AXI4-Lite wrapper (trng_axi) — PS triggers a run and reads both words
- PetaLinux host — NIST SP 800-90B RCT + APT, /dev/mem and generic-uio paths
- Source raw rate measured at 0.7428 bit/bit (SP 800-90B non-IID), ≈ 327 bits in
- POSTs { id, random, hash, health } to the lut7.dev web app — open source on GitHub