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 ADR-005 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.
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.