Noise Generation on Hardware

Procedural noise and test signals on a microcontroller

Generating noise on a microcontroller serves two purposes: producing controlled test signals (a known-noise injection for ADC characterisation) and procedural content (terrain, textures, audio). The two algorithms that map best to embedded hardware are Voss-McCartney for \(1/f\) audio-band noise and integer Perlin for procedural textures, both avoid floating-point where it matters and run in deterministic time.

This page uses the workshop’s capability ladder rather than the default platform pair: the lesson here is precisely that these generators are integer-only and run on the smallest tiers, the ATmega328P Xplained Mini (8-bit AVR, 2 KB RAM) and the FRDM-KL25Z (Cortex-M0+), and are therefore trivially fast on the default ESP32-S3 and NUCLEO-F446RE, where the same C compiles unchanged (both LFSR and Perlin code below are plain C99 with flash-resident tables; on the F446RE mark perm[] const as shown and it lands in flash, on the ESP32-S3 it lands in the memory-mapped flash cache automatically). Cycle estimates for each tier are given per algorithm.


Voss-McCartney on an integer MCU

The Voss-McCartney algorithm (see the main page) runs with integer state and addition only. No FFT, no filter, no multiply: ideal for an 8-bit AVR or a Cortex-M0+. Each octave is a counter that triggers a new random value from an LFSR (linear feedback shift register) when it reaches zero.

// Voss-McCartney pink noise generator, integer-only, 8 octaves.
// Uses an LFSR for each octave to avoid calling rand() (which is slow and
// often has poor spectral properties).
// Output: 16-bit signed integer, approximately pink spectrum.

#include <stdint.h>

#define VM_OCTAVES 8

static uint16_t lfsr_state[VM_OCTAVES];   // one LFSR per octave
static int16_t  value[VM_OCTAVES];        // current output per octave
static uint8_t  counter[VM_OCTAVES];      // countdown to next update

void vm_init(uint32_t seed) {
    for (int i = 0; i < VM_OCTAVES; i++) {
        // XOR spreads the seed bits over a nonzero constant; the zero
        // guard below is then the only degenerate case.  (OR would force
        // 8 of the 16 bits to 1 and discard most of the seed's entropy.)
        lfsr_state[i] = (uint16_t)(seed >> (i * 4)) ^ 0xACE1u;
        if (lfsr_state[i] == 0) lfsr_state[i] = 1;
        value[i] = (int16_t)lfsr_state[i];  // initial value from LFSR
        counter[i] = (uint8_t)(1 << i);     // period = 2^i samples
    }
}

int16_t vm_next(void) {
    int32_t sum = 0;
    for (int i = 0; i < VM_OCTAVES; i++) {
        if (--counter[i] == 0) {
            // 16-bit Galois LFSR update.
            uint16_t lsb = lfsr_state[i] & 1u;
            lfsr_state[i] >>= 1;
            if (lsb) lfsr_state[i] ^= 0xB400u;  // primitive polynomial
            value[i] = (int16_t)lfsr_state[i];
            counter[i] = (uint8_t)(1 << i);
        }
        sum += value[i];
    }
    // Scale: 8 octaves of 16-bit values → divide by octave count.
    return (int16_t)(sum / VM_OCTAVES);
}

Each sample is a handful of decrements, shifts, and XORs per octave: 30–40 cycles per sample on a Cortex-M0+, well within an audio-rate loop.

Spectral quality note: An LFSR produces spectrally white noise (flat PSD). The octave-summed output is approximately pink (1/f) within the LFSR’s spectral flatness limits. For critical measurements, generate the sequence offline, characterise its PSD with Welch, and store it in flash as a lookup table.


The LFSR on an FPGA: 16 flip-flops and four XOR gates

The C above is emulation. A Galois LFSR is a piece of gateware: a shift register whose feedback taps are XORs, which is why it costs a load, a shift, a mask, a branch, an XOR and a store on a microcontroller and one clock edge with no arithmetic at all in fabric. The same \(x^{16} + x^{14} + x^{13} + x^{11} + 1\) that the 0xB400 mask encodes becomes four XOR gates in front of sixteen flip-flops, producing one new pseudo-random word every cycle.

VHDL: parameterised Galois LFSR

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

-- Galois LFSR: the same recurrence as the C above, one step per enabled clock.
-- TAPS is the XOR mask applied when the bit shifted out is '1'. x"B400" is the
-- primitive polynomial x^16 + x^14 + x^13 + x^11 + 1, giving the maximal
-- period 2**WIDTH - 1 = 65535.
entity galois_lfsr is
  generic (
    WIDTH : positive := 16;
    TAPS  : std_logic_vector(WIDTH-1 downto 0) := x"B400";
    SEED  : std_logic_vector(WIDTH-1 downto 0) := x"ACE1"  -- MUST NOT be all zeros
  );
  port (
    clk : in  std_logic;
    rst : in  std_logic;                            -- synchronous, reloads SEED
    en  : in  std_logic;                            -- advance one step
    q   : out std_logic_vector(WIDTH-1 downto 0)
  );
end entity;

architecture rtl of galois_lfsr is
  signal state : std_logic_vector(WIDTH-1 downto 0) := SEED;
begin
  process(clk)
    variable shifted : std_logic_vector(WIDTH-1 downto 0);
  begin
    if rising_edge(clk) then
      if rst = '1' then
        state <= SEED;
      elsif en = '1' then
        shifted := '0' & state(WIDTH-1 downto 1);   -- shift right one place
        if state(0) = '1' then
          state <= shifted xor TAPS;                -- feedback into the taps
        else
          state <= shifted;
        end if;
      end if;
    end if;
  end process;

  q <= state;
end architecture;

The all-zero word is a lock-up state: it shifts to itself forever, which is why SEED must be non-zero and why a reset that clears the register instead of loading a seed kills the generator silently. A design that cannot guarantee a non-zero seed should feed the register from a NOR of the top WIDTH-1 bits, which folds the zero state into the cycle and gives a de Bruijn sequence of period \(2^{16}\).

The whole Voss-McCartney structure follows the same way: each octave is a counter, and a counter reaching zero is a comparator. Eight octaves is eight of these blocks and an adder tree, running one sample per clock rather than the 30-40 cycles the Cortex-M0+ needs.

# Bit-accurate model of the VHDL galois_lfsr entity above. It is also, line for
# line, the C: the point of the exercise is that the two are the same recurrence.
WIDTH, TAPS, SEED = 16, 0xB400, 0xACE1

def lfsr_step(state):
    """One enabled clock edge of the entity above."""
    shifted = state >> 1
    return shifted ^ TAPS if state & 1 else shifted

# The sequence is maximal-length: every non-zero word appears exactly once.
seen, state = set(), SEED
for _ in range(2 ** WIDTH):
    if state in seen:
        break
    seen.add(state)
    state = lfsr_step(state)

print(f"period      = {len(seen)}")
print(f"returns to  = 0x{state:04X}  (seed was 0x{SEED:04X})")
print(f"zero state  -> 0x{lfsr_step(0):04X}  (locks up, as warned above)")

assert len(seen) == 2 ** WIDTH - 1, "B400 must give the maximal period 65535"
assert state == SEED, "a maximal LFSR must close its cycle back onto the seed"
assert 0 not in seen, "the all-zero word is outside the cycle"
assert lfsr_step(0) == 0, "the all-zero word is a fixed point, not a transient"
period      = 65535
returns to  = 0xACE1  (seed was 0xACE1)
zero state  -> 0x0000  (locks up, as warned above)
Not simulated

The VHDL above is a faithful transcription of the recurrence checked in Python, but it has not been through a VHDL simulator here: it is verified by inspection and against the model above. Run it through your own ghdl or vendor flow before trusting it on hardware.


Integer Perlin noise

Perlin noise is typically implemented with floating-point gradients and the quintic fade. A fixed-point version uses 8-bit or 16-bit integer arithmetic and a precomputed permutation table in flash. This is enough for procedural terrain on a small OLED display or a sensor test pattern at a few hundred points per second.

// Fixed-point 1-D Perlin noise, 8-bit coordinates, 8-bit output.
// Suitable for procedural patterns on a small display or sensor test.
// Permutation table: 256 entries stored in flash.

#include <stdint.h>

static const uint8_t perm[512] = {151,160,137,91,90,15,131,13,201,95,96,53,
    194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,
    120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,
    149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,
    77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,
    40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,
    169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,
    217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,
    47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,
    70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,
    224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,
    191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,
    106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,
    222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,
    // Doubled for wrap-free indexing:
    151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,
    69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,
    252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,
    171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,
    122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,
    63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,
    188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,
    38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,
    42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,
    43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,
    218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,
    145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,
    115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,
    141,128,195,78,66,215,61,156,180};

// Fixed-point quintic fade: 6t^5 - 15t^4 + 10t^3.
// Input: t in [0, 255] (Q0.8 fixed-point). Output: faded value in [0, 255].
static uint8_t fade_q8(uint8_t t) {
    // Precomputed using integer arithmetic: 6t^5 - 15t^4 + 10t^3.
    // For Q0.8: each power loses 8 bits, so we scale appropriately.
    uint16_t t2 = (uint16_t)t * t >> 8;       // t²
    uint16_t t3 = (uint16_t)t2 * t >> 8;      // t³
    uint16_t t4 = (uint16_t)t3 * t >> 8;      // t⁴
    uint16_t t5 = (uint16_t)t4 * t >> 8;      // t⁵
    // 10t³ - 15t⁴ + 6t⁵, all in Q0.8.
    int16_t result = (int16_t)(10 * (uint16_t)t3)
                   - (int16_t)(15 * (uint16_t)t4)
                   + (int16_t)( 6 * (uint16_t)t5);
    if (result < 0) result = 0;
    if (result > 255) result = 255;
    return (uint8_t)result;
}

// Compute Perlin noise at integer coordinate x (Q8.0 fixed-point).
// Returns signed value roughly in [-128, 127].
int8_t perlin_1d_q8(uint16_t x) {
    uint8_t xi = (uint8_t)(x >> 8);           // integer part
    uint8_t xf = (uint8_t)(x & 0xFF);         // fractional part, Q0.8

    uint8_t h0 = perm[xi];
    uint8_t h1 = perm[xi + 1];

    // Gradient: hash bit 0 determines sign, magnitude is fractional offset.
    int16_t g0 = (h0 & 1) ? -(int16_t)xf : (int16_t)xf;
    int16_t g1 = (h1 & 1) ? -(int16_t)(xf - 255) : (int16_t)(xf - 255);

    uint8_t u = fade_q8(xf);
    // Interpolate: g0 + u*(g1 - g0) in Q8.  The result is already
    // bounded to [-128, 127] for every input (exhaustively verified),
    // so it casts to int8_t directly with no scaling.
    int16_t interp = g0 + ((int16_t)(g1 - g0) * u >> 8);
    return (int8_t)interp;
}

This fixed-point Perlin uses 8-bit coordinates and produces 8-bit output. The permutation table is the standard Perlin table stored in flash (512 bytes). Each evaluation is a handful of table lookups, shifts, and small multiplies: fast enough for a procedural terrain raster at display refresh rates on any Cortex-M part.


Where this fits

  • ADC test signals: Generate a known noise sequence (white, pink, or a Perlin terrain mapped to a DAC), feed it to your ADC, and compare the measured PSD against the known input. This characterises the ADC noise floor more reliably than a quiet-input measurement alone.
  • Procedural textures on embedded displays: A small TFT or OLED with a Perlin-generated background pattern needs no image assets: just the permutation table and a few hundred evaluations per frame.
  • Sensor validation: A controlled-noise injection into a sensor simulator (e.g., a programmable resistor for a thermistor input) lets you verify the entire analog front end + ADC + DSP pipeline against a known noise source.

The companion topic on ADC noise takes the test-signal idea further: inject known noise, measure the ADC output, and compute ENOB, SINAD, and noise floor from real hardware.

References