← back to projects

project

TRNG WebApp — Ultra96

Networked True Random Number Generator  /  Avnet Ultra96 · Zynq UltraScale+ MPSoC ZU3CG

fpga trng zynq mpsoc ultra96 sha-256 nist sp 800-90b petalinux open source

live

Last value received

Board 0x96
offline
no contact yet
Random values received
since the backend went up
POST /trng — latest payload
{
  "status": "waiting for the board to report…"
}

overview

What is it?

trng-webapp-ultra96 — overview

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.

lut7@dev:~/projects/trng-webapp-ultra96$ 

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.

end-to-end dataflow
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.

why ring oscillators
  • Free-running, uncorrelated, native to the FPGA fabric
  • Per-oscillator phase noise driven by thermal + flicker physics
  • n_ro oscillators XOR'd together amplify jitter and decorrelate
  • Sampled into the synchronous domain — one raw bit per clock
in-fabric conditioning
  • 440 raw bits assembled into a single SHA-256 message block
  • sha256_stream440 feeds 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.

rtl/ro_trng.v — entropy source (excerpt)
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

board specs
ItemValueRole
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.

Repetition Count Test (RCT)

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.

Adaptive Proportion Test (APT)

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 parameters
TestParameterValue
RCTcutoff C (max identical run)32 bits
APTwindow W512 bits
APTcutoff 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.

POST /trng — application/json
{
  "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.

trng_uio.py — single read
$ python3 trng_uio.py
random: c1ac5e344f23e936c610915a53516c3b88d847860817ba8b6a9b04b18cc36d8e…
hash: 4eceb744166401913a861630543b379f6695ee88b9004cb6b6f843a4c1112a6d
RCT: PASS APT: WARMING
trng_uio.py --count 4 — window fills, APT passes
$ python3 trng_uio.py --count 4
RCT: PASS APT: WARMING
RCT: PASS APT: PASS
RCT: PASS APT: PASS
RCT: PASS APT: PASS

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.

Raw min-entropy
0.7428 / bit
Input min-entropy
≈ 327 bits
Conditioned output
256 bits
Full-entropy margin
≈ 71 bits

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.

project status
  • 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
lut7@dev:~/projects/trng-webapp-ultra96$