t27.aiРусский

Sampling and aliasing

You will learn

Why a tone above half the sample rate comes back as a lower one, and which way it folds.

A sampler sees a tone at f as one at |f - fs round(f/fs)|: the spectrum folds like paper at every multiple of fs/2. Sampled at 100, a tone at 70 is seen at 30, and so is one at 130. In every other Nyquist zone the fold runs backwards and the alias comes out mirrored. The time view shows why nothing after the sampler can tell: the samples of the 70 tone lie exactly on a 30 tone. The spec signals.t27 computes the fold, the zone and every point drawn.

Try it

Find three input frequencies that land on 30 at a rate of 100; then move fs and watch the corners of the triangle move.

Open the interactive lesson →

Sampling and aliasing: the fold of every frequency
Sampling and aliasing: the fold of every frequency ↗

Set a tone and a sample rate: the tone folds back into 0 .. fs/2 like paper, and below, its samples fit a lower tone exactly.

specs/fpga/dsp/signals.t27

// SPDX-License-Identifier: Apache-2.0
// specs/fpga/dsp/signals.t27 -- signals on an FPGA: quantization noise, sampling, the NCO, the DFT, FIR and CIC filters, multirate and the FFT
// Host: gHashTag/trinity apps/website public/widgets/{sqnr,alias-fold,nco,dft-bin,fir-response,
//       fir-design,fir-cost,decimator,cic-growth,polyphase,fft-butterfly,bit-reverse,fft-scaling,
//       fir-fit,fir-on-board}/ (modules 3 and 5 to 9 of the course arithmetic-and-dsp): each
//       widget's logic.js is these fn bodies compiled to wasm (scripts/t27-logic.mjs in
//       gHashTag/999-multibots-telegraf, types stripped); the pages write no formula of their own.
//
// WHY ONE FILE: every section below needs a sine or a decibel. They are written once, here
// (sine by a range-reduced Taylor series, ln by the atanh series), instead of once per topic.
//
// QUANTIZATION NOISE (W. Kester, Analog Devices MT-001). An ideal N-bit quantizer driven by a
// full-scale sine has SQNR = 20 log10(2^N) + 10 log10(3/2) = 6.02 N + 1.76 dB: signal power
// FSR^2 / 8 against noise Delta^2 / 12. sqnr_measured() quantizes a real sine (a mid-rise
// quantizer, levels at (k + 1/2) Delta) over `samples` points holding `cycles` whole periods and
// divides the powers; at few bits the noise is not uniform and the formula is optimistic.
//
// SAMPLING. A tone at f sampled at fs is seen at |f - fs round(f / fs)| (the alias), in the
// first Nyquist zone 0 .. fs/2. A numerically controlled oscillator adds a tuning word M to an
// N-bit phase accumulator each clock: f_out = M f_clk / 2^N, resolution f_clk / 2^N (Analog
// Devices MT-085, the DDS tutorial: 100 MHz, N = 32, M = 2^30 gives 25 MHz in steps of
// 0.0233 Hz). The top P bits of the phase address a sine table of A-bit words. A DFT of length
// L puts a tone of f at bin f L / fs; a tone between bins leaks into all of them (dft_power
// is normalised so that a full-scale tone on a bin reads 0 dB).
//
// FIR FILTERS. The L-point moving average has |H(f)| = |sin(pi f L) / (L sin(pi f))|, with nulls
// at multiples of 1/L (L = 4 at f = 1/8: 0.65328). Windowed-sinc design: tap k of an odd-length
// low-pass of cutoff fc is 2 fc sinc(2 fc (k - c)), c = (taps - 1) / 2, times a window
// (Hamming 0.54 - 0.46 cos(2 pi k / (taps - 1)), Blackman 0.42 - 0.5 cos + 0.08 cos 2x), scaled
// to a DC gain of 1, then rounded to B-bit integers (2^(B-1) is 1.0). Symmetric taps share a
// multiplier through the DSP48E1 pre-adder, and a slice clocked C times per sample serves C
// taps: DSP slices = ceil(ceil(taps / 2 or taps) / C).
//
// MULTIRATE (E. B. Hogenauer, "An economical class of digital filters for decimation and
// interpolation", IEEE Trans. ASSP 29(2), 1981). Keeping every R-th sample moves the rate to
// fs / R and folds everything within `band` of a multiple of fs / R onto the band. An N-stage
// CIC decimator with differential delay M has DC gain (R M)^N, grows ceil(N log2(R M)) bits
// (B_in = 16, N = 4, R = 8, M = 1: gain 4096, 12 bits, 28-bit registers) and the response
// |sin(pi R M f) / (R M sin(pi f))|^N. A polyphase decimator splits the taps into R phases of
// ceil(taps / R) and spends taps MACs per output, against taps * R for filtering at the input
// rate and throwing R - 1 of R outputs away.
//
// THE FFT (J. W. Cooley, J. W. Tukey, Math. Comp. 19, 1965). A radix-2 FFT of L = 2^s points
// runs s stages of L/2 butterflies, (L/2) log2 L in all against L^2 multiplies for the direct
// DFT. Decimation in time takes its input in bit-reversed order (L = 8: 1 -> 4, 3 -> 6). Stage s
// pairs i with i XOR 2^s and multiplies by the twiddle W_L^t, t = (i mod 2^s) (L / 2^(s+1)).
// fft_value() runs the whole FFT in integers (each product rounded) on one of four inputs and
// optionally halves every stage: unscaled, a full-scale DC input grows log2 L bits; the
// conservative unscaled word is B_in + log2 L + 1 (AMD/Xilinx PG109, the FFT LogiCORE).
//
// THE BENCH. The XC7A200T has 740 DSP48E1 slices (Xilinx DS180). hw_fir_out() runs the FIR of
// fir_tap_q() the way RTL does: Q1.15 input, integer taps, a wide accumulator, round half up
// and saturate to 16 bits; ref_fir_out() is the same filter in f64.
//
// WHAT IT DOES NOT CLAIM: no device timing (path_ps only adds delays the reader sets), no
// floating-point DSP, and the 1.76 dB, the NCO spur rule and the PG109 width are the textbook
// figures, recomputed here, not measured on a board.
// Claim status: checked by the tests below; every vector was recomputed in Python first.
// phi^2 + 1/phi^2 = 3 | TRINITY

module fpga::dsp::signals {

    pub const KIND : str = "widget-logic";
    pub const ID : str = "signals";
    pub const VERSION : u8 = 1;
    pub const PI : f64 = 3.141592653589793;
    pub const TWO_PI : f64 = 6.283185307179586;
    pub const HALF_PI : f64 = 1.5707963267948966;
    pub const LN2 : f64 = 0.6931471805599453;
    pub const LN10 : f64 = 2.302585092994046;
    pub const DB_FLOOR : f64 = -300.0;
    pub const DSP48_ON_200T : u16 = 740;
    pub const WIN_RECT : u8 = 0;
    pub const WIN_HAMMING : u8 = 1;
    pub const WIN_BLACKMAN : u8 = 2;
    pub const WIN_HANN : u8 = 3;
    pub const MAX_TAPS : u8 = 127;
    pub const MAX_FFT : u16 = 256;
    pub const PAT_DC : u8 = 0;
    pub const PAT_TONE : u8 = 1;
    pub const PAT_NYQUIST : u8 = 2;
    pub const PAT_NOISE : u8 = 3;
    pub const LFSR_SEED : u16 = 44257;
    pub const Q15_ONE : i32 = 32768;
    pub const HW_AMPLITUDE : f64 = 0.45;

    // ---- Shared: sine, cosine, logarithms -------------------------------------------------

    // floor() for |v| < 2^62.
    fn floor_of(v: f64) -> f64 {
        var t : f64 = (v as i64) as f64;
        if (t > v) {
            t = t - 1.0;
        }
        return t;
    }

    // sin(x): reduce to [-pi/2, pi/2], then the Taylor series to x^25.
    fn sine(x: f64) -> f64 {
        var r : f64 = x - TWO_PI * floor_of((x + PI) / TWO_PI);
        if (r > HALF_PI) {
            r = PI - r;
        }
        if (r < 0.0 - HALF_PI) {
            r = 0.0 - PI - r;
        }
        const r2 = r * r;
        var term : f64 = r;
        var sum : f64 = r;
        var k : f64 = 1.0;
        while (k < 12.5) {
            term = 0.0 - term * r2 / ((2.0 * k) * (2.0 * k + 1.0));
            sum = sum + term;
            k = k + 1.0;
        }
        return sum;
    }

    fn cosine(x: f64) -> f64 {
        return sine(x + HALF_PI);
    }

    // ln(v) for v > 0: v = m 2^e with m in [1, 2), ln m = 2 atanh((m - 1) / (m + 1)).
    fn ln_pos(v: f64) -> f64 {
        var m : f64 = v;
        var e : f64 = 0.0;
        while (m >= 2.0) {
            m = m / 2.0;
            e = e + 1.0;
        }
        while (m < 1.0) {
            m = m * 2.0;
            e = e - 1.0;
        }
        const t = (m - 1.0) / (m + 1.0);
        const t2 = t * t;
        var power : f64 = t;
        var sum : f64 = 0.0;
        var odd : f64 = 1.0;
        while (power > 0.000000000000000001) {
            sum = sum + power / odd;
            power = power * t2;
            odd = odd + 2.0;
        }
        return 2.0 * sum + e * LN2;
    }

    // A power ratio in dB, with a floor for a true zero.
    fn db_power(p: f64) -> f64 {
        if (p <= 0.000000000000000000000000000001) {
            return DB_FLOOR;
        }
        return 10.0 * ln_pos(p) / LN10;
    }

    // sin(2 pi f t): a tone, for drawing.
    fn tone(f: f64, t: f64) -> f64 {
        return sine(TWO_PI * f * t);
    }

    // ---- Quantization noise --------------------------------------------------------------

    // 6.02 N + 1.76 dB, as 10 log10(1.5 * 4^N).
    fn sqnr_ideal(bits: u8) -> f64 {
        var p : f64 = 1.5;
        var i : u8 = 0;
        while (i < bits) {
            p = p * 4.0;
            i = i + 1;
        }
        return db_power(p);
    }

    // A mid-rise quantizer of full scale +-1: levels at (k + 1/2) Delta, Delta = 2 / 2^N.
    fn quantize(x: f64, bits: u8) -> f64 {
        const top = ((1 as i64) << (bits - 1)) as f64;
        const delta = 1.0 / top;
        var k : f64 = floor_of(x / delta);
        if (k > top - 1.0) {
            k = top - 1.0;
        }
        if (k < 0.0 - top) {
            k = 0.0 - top;
        }
        return (k + 0.5) * delta;
    }

    // Sample k of a full-scale sine holding `cycles` periods in `samples` points.
    fn test_sine(k: u16, samples: u16, cycles: u16) -> f64 {
        return sine(TWO_PI * (cycles as f64) * (k as f64) / (samples as f64));
    }

    // Signal power over noise power, in dB, of that sine through the quantizer.
    fn sqnr_measured(bits: u8, samples: u16, cycles: u16) -> f64 {
        var ps : f64 = 0.0;
        var pn : f64 = 0.0;
        var k : u16 = 0;
        while (k < samples) {
            const s = test_sine(k, samples, cycles);
            const e = quantize(s, bits) - s;
            ps = ps + s * s;
            pn = pn + e * e;
            k = k + 1;
        }
        return db_power(ps / pn);
    }

    // ---- Sampling ------------------------------------------------------------------------

    // Where a tone at f lands when sampled at fs: |f - fs round(f / fs)|.
    fn alias_freq(f: f64, fs: f64) -> f64 {
        const d = f - fs * floor_of(f / fs + 0.5);
        if (d < 0.0) {
            return 0.0 - d;
        }
        return d;
    }

    // +1 when the alias keeps the tone's phase, -1 when the fold mirrors it (f above the
    // nearest multiple of fs: +1; below: -1).
    fn alias_sign(f: f64, fs: f64) -> i8 {
        if (f - fs * floor_of(f / fs + 0.5) < 0.0) {
            return -1;
        }
        return 1;
    }

    // The Nyquist zone of f: 1 for 0 .. fs/2, 2 for fs/2 .. fs, and so on.
    fn nyquist_zone(f: f64, fs: f64) -> u16 {
        return (floor_of(2.0 * f / fs) as u16) + 1;
    }

    // 2^n as f64, n <= 64.
    fn two_to(n: u8) -> f64 {
        var r : f64 = 1.0;
        var i : u8 = 0;
        while (i < n) {
            r = r * 2.0;
            i = i + 1;
        }
        return r;
    }

    // The NCO: output frequency, resolution and the tuning word for a wanted frequency.
    fn nco_fout(fclk: f64, nbits: u8, word: u32) -> f64 {
        return (word as f64) * fclk / two_to(nbits);
    }

    fn nco_resolution(fclk: f64, nbits: u8) -> f64 {
        return fclk / two_to(nbits);
    }

    fn nco_word(fclk: f64, nbits: u8, fout: f64) -> u32 {
        var w : f64 = floor_of(fout * two_to(nbits) / fclk + 0.5);
        const top = two_to(nbits) - 1.0;
        if (w > top) {
            w = top;
        }
        if (w < 0.0) {
            w = 0.0;
        }
        return w as u32;
    }

    // The accumulator after k clocks: (k M) mod 2^N.
    fn nco_phase(word: u32, k: u32, nbits: u8) -> f64 {
        const p = (word as f64) * (k as f64);
        const span = two_to(nbits);
        return p - span * floor_of(p / span);
    }

    // The output sample after k clocks: the top P phase bits address an A-bit sine table.
    fn nco_sample(word: u32, k: u32, nbits: u8, pbits: u8, abits: u8) -> f64 {
        const addr = floor_of(nco_phase(word, k, nbits) / two_to(nbits - pbits));
        const scale = two_to(abits - 1) - 1.0;
        return floor_of(sine(TWO_PI * addr / two_to(pbits)) * scale + 0.5) / scale;
    }

    // The rule of thumb for phase truncation: the worst spur sits about 6.02 dB per kept phase
    // bit below the carrier.
    fn nco_spur_db(pbits: u8) -> f64 {
        return 0.0 - db_power(two_to(2 * pbits));
    }

    // ---- The DFT -------------------------------------------------------------------------

    // Window value at point k of len: rect, Hamming, Blackman, Hann; symmetric for a FIR
    // (denominator len - 1), periodic for a DFT (denominator len).
    fn window(kind: u8, k: u16, len: u16, symmetric: bool) -> f64 {
        if (kind == WIN_RECT) {
            return 1.0;
        }
        var den : f64 = len as f64;
        if (symmetric) {
            den = den - 1.0;
        }
        const a = TWO_PI * (k as f64) / den;
        if (kind == WIN_HAMMING) {
            return 0.54 - 0.46 * cosine(a);
        }
        if (kind == WIN_BLACKMAN) {
            return 0.42 - 0.5 * cosine(a) + 0.08 * cosine(2.0 * a);
        }
        return 0.5 - 0.5 * cosine(a);
    }

    // The bin a frequency falls in, and the width of one bin.
    fn bin_of(f: f64, fs: f64, len: u16) -> f64 {
        return f * (len as f64) / fs;
    }

    fn bin_width(fs: f64, len: u16) -> f64 {
        return fs / (len as f64);
    }

    // Point n of the DFT's input: a sine of k0 cycles per len points, windowed.
    fn dft_input(len: u16, k0: f64, n: u16, win: u8) -> f64 {
        return window(win, n, len, false) * sine(TWO_PI * k0 * (n as f64) / (len as f64));
    }

    // |X_k|^2 of a windowed sine at k0 cycles per len points, over (sum w / 2)^2.
    fn dft_power(len: u16, k0: f64, k: u16, win: u8) -> f64 {
        var re : f64 = 0.0;
        var im : f64 = 0.0;
        var sw : f64 = 0.0;
        var n : u16 = 0;
        while (n < len) {
            const w = window(win, n, len, false);
            const x = dft_input(len, k0, n, win);
            const a = TWO_PI * (k as f64) * (n as f64) / (len as f64);
            re = re + x * cosine(a);
            im = im - x * sine(a);
            sw = sw + w;
            n = n + 1;
        }
        return (re * re + im * im) / ((sw / 2.0) * (sw / 2.0));
    }

    fn dft_db(len: u16, k0: f64, k: u16, win: u8) -> f64 {
        return db_power(dft_power(len, k0, k, win));
    }

    // ---- FIR filters ---------------------------------------------------------------------

    // |H(f)| of the len-point moving average.
    fn ma_gain(len: u8, f: f64) -> f64 {
        var den : f64 = (len as f64) * sine(PI * f);
        if (den < 0.0) {
            den = 0.0 - den;
        }
        if (den < 0.000000000001) {
            return 1.0;
        }
        var num : f64 = sine(PI * f * (len as f64));
        if (num < 0.0) {
            num = 0.0 - num;
        }
        return num / den;
    }

    // Output sample k of the len-point moving average of the tone sin(2 pi f n), n >= 0.
    fn ma_out(len: u8, f: f64, k: i32) -> f64 {
        var s : f64 = 0.0;
        var j : u8 = 0;
        while (j < len) {
            const n = k - (j as i32);
            if (n >= 0) {
                s = s + tone(f, n as f64);
            }
            j = j + 1;
        }
        return s / (len as f64);
    }

    // The raw windowed-sinc tap k of an odd-length low-pass with cutoff fc (cycles/sample).
    fn sinc_tap(taps: u8, k: u8, fc: f64, win: u8) -> f64 {
        const t = (k as f64) - ((taps as f64) - 1.0) / 2.0;
        var h : f64 = 2.0 * fc;
        if (t > 0.0000001 || t < -0.0000001) {
            h = sine(TWO_PI * fc * t) / (PI * t);
        }
        return h * window(win, k as u16, taps as u16, true);
    }

    // The sum of the raw taps: dividing by it sets the DC gain to 1.
    fn tap_sum(taps: u8, fc: f64, win: u8) -> f64 {
        var s : f64 = 0.0;
        var j : u8 = 0;
        while (j < taps) {
            s = s + sinc_tap(taps, j, fc, win);
            j = j + 1;
        }
        return s;
    }

    // Tap k scaled so that the taps sum to 1 (DC gain 1).
    fn fir_tap(taps: u8, k: u8, fc: f64, win: u8) -> f64 {
        return sinc_tap(taps, k, fc, win) / tap_sum(taps, fc, win);
    }

    // The integer a B-bit tap holds: round(raw / sum * 2^(B-1)), 2^(B-1) standing for 1.0.
    fn quant_tap(raw: f64, sum: f64, bits: u8) -> i32 {
        const v : f64 = floor_of(raw / sum * two_to(bits - 1) + 0.5);
        return v as i32;
    }

    // Tap k as a B-bit integer.
    fn fir_tap_q(taps: u8, k: u8, fc: f64, win: u8, bits: u8) -> i32 {
        return quant_tap(sinc_tap(taps, k, fc, win), tap_sum(taps, fc, win), bits);
    }

    // |H(f)|^2 of the designed filter; bits = 0 keeps the taps in f64.
    fn fir_power(taps: u8, fc: f64, win: u8, bits: u8, f: f64) -> f64 {
        const s = tap_sum(taps, fc, win);
        var re : f64 = 0.0;
        var im : f64 = 0.0;
        var k : u8 = 0;
        while (k < taps) {
            const raw = sinc_tap(taps, k, fc, win);
            var c : f64 = raw / s;
            if (bits > 0) {
                c = (quant_tap(raw, s, bits) as f64) / two_to(bits - 1);
            }
            const a = TWO_PI * f * (k as f64);
            re = re + c * cosine(a);
            im = im - c * sine(a);
            k = k + 1;
        }
        return re * re + im * im;
    }

    fn fir_db(taps: u8, fc: f64, win: u8, bits: u8, f: f64) -> f64 {
        return db_power(fir_power(taps, fc, win, bits, f));
    }

    // The highest response from fstop to 1/2, on 201 points: the stopband the filter keeps.
    fn fir_stop_db(taps: u8, fc: f64, win: u8, bits: u8, fstop: f64) -> f64 {
        var worst : f64 = DB_FLOOR;
        var j : u16 = 0;
        while (j <= 200) {
            const f = fstop + (0.5 - fstop) * (j as f64) / 200.0;
            const d = fir_db(taps, fc, win, bits, f);
            if (d > worst) {
                worst = d;
            }
            j = j + 1;
        }
        return worst;
    }

    // Multipliers a FIR needs: half (rounded up) when the taps are symmetric.
    fn fir_mults(taps: u8, symmetric: bool) -> u8 {
        if (symmetric) {
            return (taps + 1) / 2;
        }
        return taps;
    }

    // DSP48E1 slices when each slice runs `clocks` times per sample.
    fn fir_dsps(taps: u8, symmetric: bool, clocks: u32) -> u32 {
        const m = fir_mults(taps, symmetric) as u32;
        return (m + clocks - 1) / clocks;
    }

    // The slice tap k runs on: symmetric taps k and taps-1-k share a multiplier (the pre-adder),
    // and each slice serves `clocks` multipliers one after another.
    fn fir_tap_dsp(taps: u8, k: u8, symmetric: bool, clocks: u32) -> u32 {
        var m : u8 = k;
        if (symmetric && taps - 1 - k < k) {
            m = taps - 1 - k;
        }
        return (m as u32) / clocks;
    }

    // Clocks per sample: floor(f_clk / f_s), at least 1.
    fn clocks_per_sample(fclk: f64, fs: f64) -> u32 {
        const c : f64 = floor_of(fclk / fs);
        if (c < 1.0) {
            return 1;
        }
        if (c > 1000000.0) {
            return 1000000;
        }
        return c as u32;
    }

    // How many such filters the XC7A200T's 740 slices hold.
    fn filters_on_200t(dsps: u32) -> u32 {
        if (dsps == 0) {
            return 0;
        }
        return (DSP48_ON_200T as u32) / dsps;
    }

    // Adder levels between registers: a direct-form adder tree has ceil(log2 taps); a
    // systolic chain (one adder per slice, the cascade between them) has one.
    fn adder_levels(taps: u8, systolic: bool) -> u8 {
        if (systolic) {
            return 1;
        }
        var lv : u8 = 0;
        var cap : u16 = 1;
        while (cap < (taps as u16)) {
            cap = cap * 2;
            lv = lv + 1;
        }
        return lv;
    }

    // A register-to-register path: a fixed part plus one adder delay per level (all set by
    // the reader); the slack against the clock period.
    fn path_ps(levels: u8, level_ps: u16, fixed_ps: u16) -> u32 {
        return (fixed_ps as u32) + (levels as u32) * (level_ps as u32);
    }

    fn slack_ps(fclk_mhz: f64, levels: u8, level_ps: u16, fixed_ps: u16) -> f64 {
        return 1000000.0 / fclk_mhz - (path_ps(levels, level_ps, fixed_ps) as f64);
    }

    // ---- Multirate -----------------------------------------------------------------------

    // The rate after keeping every r-th sample, and where a tone at f lands at that rate.
    fn decim_rate(fs: f64, r: u16) -> f64 {
        return fs / (r as f64);
    }

    fn decim_alias(f: f64, fs: f64, r: u16) -> f64 {
        return alias_freq(f, decim_rate(fs, r));
    }

    // True when a tone at f lands inside 0 .. band after decimating fs by r.
    fn folds_into(f: f64, fs: f64, r: u16, band: f64) -> bool {
        return alias_freq(f, fs / (r as f64)) <= band;
    }

    // The CIC: DC gain (R M)^N, bit growth, output width and the response at f (cycles per
    // input sample).
    fn cic_gain(n: u8, r: u16, m: u8) -> f64 {
        var g : f64 = 1.0;
        var i : u8 = 0;
        while (i < n) {
            g = g * (r as f64) * (m as f64);
            i = i + 1;
        }
        return g;
    }

    fn cic_growth(n: u8, r: u16, m: u8) -> u8 {
        const g = cic_gain(n, r, m);
        var b : u8 = 0;
        var cap : f64 = 1.0;
        while (cap < g) {
            cap = cap * 2.0;
            b = b + 1;
        }
        return b;
    }

    fn cic_out_bits(bin: u8, n: u8, r: u16, m: u8) -> u8 {
        return bin + cic_growth(n, r, m);
    }

    fn cic_response(n: u8, r: u16, m: u8, f: f64) -> f64 {
        const rm = (r as f64) * (m as f64);
        var den : f64 = rm * sine(PI * f);
        if (den < 0.0) {
            den = 0.0 - den;
        }
        if (den < 0.000000000001) {
            return 1.0;
        }
        var one : f64 = sine(PI * rm * f);
        if (one < 0.0) {
            one = 0.0 - one;
        }
        one = one / den;
        var out : f64 = 1.0;
        var i : u8 = 0;
        while (i < n) {
            out = out * one;
            i = i + 1;
        }
        return out;
    }

    fn cic_db(n: u8, r: u16, m: u8, f: f64) -> f64 {
        const a = cic_response(n, r, m, f);
        return db_power(a * a);
    }

    // Polyphase: taps per phase, which phase tap k sits in, MACs per output either way.
    fn poly_taps(taps: u8, r: u8) -> u8 {
        return (taps + r - 1) / r;
    }

    fn poly_phase(k: u8, r: u8) -> u8 {
        return k % r;
    }

    fn macs_per_output(taps: u8, r: u8, polyphase: bool) -> u32 {
        if (polyphase) {
            return taps as u32;
        }
        return (taps as u32) * (r as u32);
    }

    // ---- The FFT -------------------------------------------------------------------------

    fn fft_stages(len: u16) -> u8 {
        var s : u8 = 0;
        var m : u16 = len;
        while (m > 1) {
            m = m / 2;
            s = s + 1;
        }
        return s;
    }

    fn fft_butterflies(len: u16) -> u32 {
        return ((len as u32) / 2) * (fft_stages(len) as u32);
    }

    fn dft_mults(len: u16) -> u32 {
        return (len as u32) * (len as u32);
    }

    // i with its low `bits` bits in reverse order.
    fn bit_reverse(i: u16, bits: u8) -> u16 {
        var r : u16 = 0;
        var v : u16 = i;
        var b : u8 = 0;
        while (b < bits) {
            r = (r << 1) | (v & 1);
            v = v >> 1;
            b = b + 1;
        }
        return r;
    }

    // Stage s of a decimation-in-time FFT: i's partner and the twiddle exponent t of W_L^t.
    fn bf_partner(i: u16, s: u8) -> u16 {
        return i ^ ((1 as u16) << s);
    }

    fn bf_twiddle(i: u16, s: u8, len: u16) -> u16 {
        return (i & (((1 as u16) << s) - 1)) * (len >> (s + 1));
    }

    fn twiddle_re(t: u16, len: u16) -> f64 {
        return cosine(TWO_PI * (t as f64) / (len as f64));
    }

    fn twiddle_im(t: u16, len: u16) -> f64 {
        return 0.0 - sine(TWO_PI * (t as f64) / (len as f64));
    }

    // The 16-bit Fibonacci LFSR with taps 16, 14, 13, 11 (seed 0xACE1 -> 0x5670).
    fn lfsr_next(s: u16) -> u16 {
        const bit = (s ^ (s >> 2) ^ (s >> 3) ^ (s >> 5)) & 1;
        return (s >> 1) | (bit << 15);
    }

    // An LFSR state as a sample in -max .. max.
    fn noise_value(state: u16, bits: u8) -> f64 {
        const mx = two_to(bits - 1) - 1.0;
        const span = 2.0 * mx + 1.0;
        const v = state as f64;
        return v - span * floor_of(v / span) - mx;
    }

    // Input sample n of a pattern at B bits: DC, one cycle of cosine, +-max alternating,
    // or LFSR noise; max = 2^(B-1) - 1.
    fn fft_input(pattern: u8, len: u16, bits: u8, n: u16) -> f64 {
        const mx = two_to(bits - 1) - 1.0;
        if (pattern == PAT_DC) {
            return mx;
        }
        if (pattern == PAT_TONE) {
            return floor_of(mx * cosine(TWO_PI * (n as f64) / (len as f64)) + 0.5);
        }
        if (pattern == PAT_NYQUIST) {
            if (n % 2 == 0) {
                return mx;
            }
            return 0.0 - mx;
        }
        var s : u16 = LFSR_SEED;
        var k : u16 = 0;
        while (k <= n) {
            s = lfsr_next(s);
            k = k + 1;
        }
        return noise_value(s, bits);
    }

    // An integer radix-2 DIT FFT of the pattern run for `stage` stages: bit-reversed input,
    // every product rounded, optionally halved (and rounded) after each stage. `what` picks
    // the answer: 0 the real part of element idx, 1 its imaginary part, 2 the largest |re|
    // or |im| of any element.
    fn fft_core(pattern: u8, len: u16, bits: u8, scaled: bool, stage: u8, idx: u16, what: u8) -> f64 {
        var re : [256]f64 = [0.0; 256];
        var im : [256]f64 = [0.0; 256];
        const lb = fft_stages(len);
        var state : u16 = LFSR_SEED;
        var n : u16 = 0;
        while (n < len) {
            var x : f64 = 0.0;
            if (pattern == PAT_NOISE) {
                state = lfsr_next(state);
                x = noise_value(state, bits);
            } else {
                x = fft_input(pattern, len, bits, n);
            }
            re[bit_reverse(n, lb)] = x;
            n = n + 1;
        }
        var s : u8 = 0;
        while (s < stage && s < lb) {
            const half = (1 as u16) << s;
            var i : u16 = 0;
            while (i < len) {
                if ((i & half) == 0) {
                    const j = i + half;
                    const t = bf_twiddle(i, s, len);
                    const c = twiddle_re(t, len);
                    const d = twiddle_im(t, len);
                    const tr = floor_of(re[j] * c - im[j] * d + 0.5);
                    const ti = floor_of(re[j] * d + im[j] * c + 0.5);
                    const ar = re[i];
                    const ai = im[i];
                    re[i] = ar + tr;
                    im[i] = ai + ti;
                    re[j] = ar - tr;
                    im[j] = ai - ti;
                    if (scaled) {
                        re[i] = floor_of(re[i] / 2.0 + 0.5);
                        im[i] = floor_of(im[i] / 2.0 + 0.5);
                        re[j] = floor_of(re[j] / 2.0 + 0.5);
                        im[j] = floor_of(im[j] / 2.0 + 0.5);
                    }
                }
                i = i + 1;
            }
            s = s + 1;
        }
        if (what == 0) {
            return re[idx];
        }
        if (what == 1) {
            return im[idx];
        }
        var worst : f64 = 0.0;
        var k : u16 = 0;
        while (k < len) {
            var a : f64 = re[k];
            var b : f64 = im[k];
            if (a < 0.0) {
                a = 0.0 - a;
            }
            if (b < 0.0) {
                b = 0.0 - b;
            }
            if (a > worst) {
                worst = a;
            }
            if (b > worst) {
                worst = b;
            }
            k = k + 1;
        }
        return worst;
    }

    // Element idx (the imaginary part when imag) after `stage` stages.
    fn fft_value(pattern: u8, len: u16, bits: u8, scaled: bool, stage: u8, idx: u16, imag: bool) -> f64 {
        if (imag) {
            return fft_core(pattern, len, bits, scaled, stage, idx, 1);
        }
        return fft_core(pattern, len, bits, scaled, stage, idx, 0);
    }

    // The largest |re| or |im| after `stage` stages.
    fn fft_peak(pattern: u8, len: u16, bits: u8, scaled: bool, stage: u8) -> f64 {
        return fft_core(pattern, len, bits, scaled, stage, 0, 2);
    }

    // Two's complement bits that hold +-peak.
    fn bits_for(peak: f64) -> u8 {
        var b : u8 = 1;
        var cap : f64 = 1.0;
        while (cap <= peak && b < 64) {
            cap = cap * 2.0;
            b = b + 1;
        }
        return b;
    }

    // The conservative unscaled output width: B_in + log2 L + 1.
    fn fft_unscaled_bits(bits: u8, len: u16) -> u8 {
        return bits + fft_stages(len) + 1;
    }

    // ---- The capstone: a FIR as RTL runs it ----------------------------------------------

    // Input sample k (Q1.15): HW_AMPLITUDE times the sum of two tones, rounded and saturated.
    fn hw_input(k: i32, fa: f64, fb: f64) -> i32 {
        if (k < 0) {
            return 0;
        }
        const t = k as f64;
        var v : f64 = floor_of(HW_AMPLITUDE * (tone(fa, t) + tone(fb, t)) * (Q15_ONE as f64) + 0.5);
        if (v > 32767.0) {
            v = 32767.0;
        }
        if (v < -32768.0) {
            v = -32768.0;
        }
        return v as i32;
    }

    // Output sample k: integer taps, a wide accumulator, round half up, saturate to 16 bits.
    fn hw_fir_out(k: i32, taps: u8, fc: f64, win: u8, bits: u8, fa: f64, fb: f64) -> i32 {
        const s = tap_sum(taps, fc, win);
        var acc : i64 = 0;
        var j : u8 = 0;
        while (j < taps) {
            const q = quant_tap(sinc_tap(taps, j, fc, win), s, bits) as i64;
            acc = acc + q * (hw_input(k - (j as i32), fa, fb) as i64);
            j = j + 1;
        }
        const half = (1 as i64) << (bits - 2);
        var y : i64 = (acc + half) >> (bits - 1);
        if (y > 32767) {
            y = 32767;
        }
        if (y < -32768) {
            y = -32768;
        }
        return y as i32;
    }

    // The same filter in f64 on the unrounded input, in output LSBs.
    fn ref_fir_out(k: i32, taps: u8, fc: f64, win: u8, fa: f64, fb: f64) -> f64 {
        const s = tap_sum(taps, fc, win);
        var acc : f64 = 0.0;
        var j : u8 = 0;
        while (j < taps) {
            const n = k - (j as i32);
            if (n >= 0) {
                const t = n as f64;
                acc = acc + sinc_tap(taps, j, fc, win) / s * HW_AMPLITUDE * (tone(fa, t) + tone(fb, t)) * (Q15_ONE as f64);
            }
            j = j + 1;
        }
        return acc;
    }

    // How far the RTL output sits from the f64 filter, in output LSBs.
    fn hw_error(k: i32, taps: u8, fc: f64, win: u8, bits: u8, fa: f64, fb: f64) -> f64 {
        return (hw_fir_out(k, taps, fc, win, bits, fa, fb) as f64) - ref_fir_out(k, taps, fc, win, fa, fb);
    }

    // Accumulator bits for the worst case: sum |q| * 32768, signed.
    fn hw_acc_bits(taps: u8, fc: f64, win: u8, bits: u8) -> u8 {
        const s = tap_sum(taps, fc, win);
        var m : f64 = 0.0;
        var j : u8 = 0;
        while (j < taps) {
            var q : f64 = quant_tap(sinc_tap(taps, j, fc, win), s, bits) as f64;
            if (q < 0.0) {
                q = 0.0 - q;
            }
            m = m + q;
            j = j + 1;
        }
        return bits_for(m * (Q15_ONE as f64));
    }

    fn near(a: f64, b: f64, tol: f64) -> bool {
        return a - b < tol && b - a < tol;
    }

    // ---- Tests ---------------------------------------------------------------------------

    test "sine, cosine and the logarithm agree with their tables" {
        assert(near(sine(PI / 6.0), 0.5, 0.000000000001));
        assert(near(sine(0.0 - 2.0), -0.9092974268, 0.0000000001));
        assert(near(sine(100.0), -0.5063656411, 0.0000000001));
        assert(near(cosine(PI), -1.0, 0.000000000001));
        assert(near(ln_pos(10.0), LN10, 0.000000000001));
        assert(near(ln_pos(0.001), -6.9077552790, 0.0000000001));
        assert(near(db_power(2.0), 3.0103, 0.0001));
        assert(db_power(0.0) == DB_FLOOR);
    }

    test "each bit buys about 6 dB" {
        assert(near(sqnr_ideal(16), 98.0905, 0.0001));
        assert(near(sqnr_ideal(1), 7.7815, 0.0001));
        assert(near(sqnr_measured(1, 4096, 127), 6.4443, 0.0001));
        assert(near(sqnr_measured(8, 4096, 127), 49.7937, 0.0001));
        assert(near(sqnr_measured(12, 4096, 127), 74.0046, 0.0001));
        assert(near(quantize(1.0, 3), 0.875, 0.000000001));
        assert(near(quantize(0.0, 3), 0.125, 0.000000001));
    }

    test "a tone above fs/2 comes back lower" {
        assert(alias_freq(70.0, 100.0) == 30.0);
        assert(alias_freq(130.0, 100.0) == 30.0);
        assert(alias_freq(240.0, 100.0) == 40.0);
        assert(alias_freq(25.0, 100.0) == 25.0);
        assert(alias_sign(70.0, 100.0) == -1);
        assert(alias_sign(130.0, 100.0) == 1);
        assert(nyquist_zone(70.0, 100.0) == 2);
        assert(nyquist_zone(130.0, 100.0) == 3);
        assert(nyquist_zone(49.9, 100.0) == 1);
    }

    test "the NCO: 100 MHz, N = 32, M = 2^30 is 25 MHz" {
        assert(nco_fout(100000000.0, 32, 1073741824) == 25000000.0);
        assert(near(nco_resolution(100000000.0, 32), 0.0232830644, 0.0000000001));
        assert(nco_word(100000000.0, 32, 25000000.0) == 1073741824);
        assert(nco_word(100000000.0, 32, 1000000.0) == 42949673);
        assert(nco_phase(1073741824, 5, 32) == 1073741824.0);
        assert(near(nco_sample(1073741824, 1, 32, 12, 16), 1.0, 0.000000001));
        assert(near(nco_spur_db(12), -72.2472, 0.0001));
    }

    test "a tone between bins leaks" {
        assert(near(dft_db(64, 8.0, 8, WIN_RECT), 0.0, 0.000001));
        assert(near(dft_db(64, 8.5, 8, WIN_RECT), -3.7235, 0.0001));
        assert(near(dft_db(64, 8.5, 8, WIN_HANN), -1.4243, 0.0001));
        assert(dft_db(64, 8.0, 9, WIN_RECT) < -200.0);
        assert(near(dft_db(64, 8.5, 20, WIN_RECT), -33.1660, 0.0001));
        assert(near(dft_db(64, 8.5, 20, WIN_HANN), -73.8682, 0.0001));
        assert(bin_of(1000.0, 8000.0, 64) == 8.0);
        assert(near(dft_input(64, 8.0, 2, WIN_RECT), 1.0, 0.000000000001));
        assert(dft_input(64, 8.0, 0, WIN_HANN) == 0.0);
        assert(bin_width(8000.0, 64) == 125.0);
    }

    test "the moving average and its nulls" {
        assert(near(ma_gain(4, 0.125), 0.65328, 0.00001));
        assert(ma_gain(4, 0.25) < 0.0000001);
        assert(near(ma_gain(5, 0.1), 0.6472135955, 0.0000000001));
        assert(ma_gain(8, 0.0) == 1.0);
        assert(near(ma_out(4, 0.125, 10), 0.6532814824 * sine(TWO_PI * 0.125 * 8.5), 0.0000000001));
        assert(near(ma_out(2, 0.25, 0), 0.0, 0.0000000001));
    }

    test "a windowed-sinc low-pass and its integer taps" {
        assert(fir_tap_q(31, 15, 0.1, WIN_HAMMING, 16) == 6542);
        assert(fir_tap_q(31, 11, 0.1, WIN_HAMMING, 16) == 1297);
        assert(fir_tap_q(31, 8, 0.1, WIN_HAMMING, 16) == -832);
        assert(fir_tap_q(31, 0, 0.1, WIN_HAMMING, 16) == 0);
        assert(near(fir_db(31, 0.1, WIN_HAMMING, 0, 0.0), 0.0, 0.0000001));
        assert(near(fir_db(31, 0.1, WIN_HAMMING, 0, 0.1), -6.0518, 0.0001));
        assert(near(fir_stop_db(31, 0.1, WIN_RECT, 0, 0.2), -32.8291, 0.0001));
        assert(near(fir_stop_db(31, 0.1, WIN_HAMMING, 0, 0.2), -62.6372, 0.0001));
        assert(near(fir_stop_db(31, 0.1, WIN_BLACKMAN, 0, 0.2), -76.1229, 0.0001));
        assert(near(fir_stop_db(31, 0.1, WIN_HAMMING, 8, 0.2), -32.8362, 0.0001));
    }

    test "folding the taps onto slices" {
        assert(fir_mults(31, true) == 16);
        assert(fir_mults(31, false) == 31);
        assert(fir_dsps(31, true, 1) == 16);
        assert(fir_dsps(31, true, 4) == 4);
        assert(fir_dsps(31, false, 4) == 8);
        assert(fir_tap_dsp(31, 30, true, 4) == 0);
        assert(fir_tap_dsp(31, 15, true, 4) == 3);
        assert(fir_tap_dsp(31, 30, false, 4) == 7);
        assert(clocks_per_sample(200000000.0, 48000.0) == 4166);
        assert(clocks_per_sample(100000000.0, 200000000.0) == 1);
        assert(filters_on_200t(16) == 46);
        assert(adder_levels(31, false) == 5);
        assert(adder_levels(31, true) == 1);
        assert(path_ps(5, 400, 900) == 2900);
        assert(near(slack_ps(250.0, 5, 400, 900), 1100.0, 0.000001));
    }

    test "decimation folds, the CIC grows" {
        assert(folds_into(95.0, 400.0, 4, 10.0));
        assert(folds_into(205.0, 400.0, 4, 10.0));
        assert(!folds_into(50.0, 400.0, 4, 10.0));
        assert(decim_rate(400.0, 4) == 100.0);
        assert(decim_alias(130.0, 400.0, 4) == 30.0);
        assert(cic_gain(4, 8, 1) == 4096.0);
        assert(cic_growth(4, 8, 1) == 12);
        assert(cic_out_bits(16, 4, 8, 1) == 28);
        assert(cic_growth(4, 10, 1) == 14);
        assert(cic_growth(5, 64, 2) == 35);
        assert(cic_growth(8, 4096, 2) == 104);
        assert(near(cic_db(4, 8, 1, 0.0625), -15.4661, 0.0001));
        assert(cic_response(4, 8, 1, 0.125) < 0.0000001);
    }

    test "polyphase does the work at the low rate" {
        assert(poly_taps(31, 4) == 8);
        assert(poly_taps(32, 4) == 8);
        assert(poly_phase(13, 4) == 1);
        assert(macs_per_output(31, 4, false) == 124);
        assert(macs_per_output(31, 4, true) == 31);
    }

    test "the FFT counts, the order and the twiddles" {
        assert(fft_stages(256) == 8);
        assert(fft_butterflies(256) == 1024);
        assert(dft_mults(256) == 65536);
        assert(bit_reverse(1, 3) == 4);
        assert(bit_reverse(3, 3) == 6);
        assert(bit_reverse(11, 5) == 26);
        assert(bf_partner(5, 1) == 7);
        assert(bf_twiddle(3, 1, 8) == 2);
        assert(bf_twiddle(6, 2, 8) == 2);
        assert(near(twiddle_im(2, 8), -1.0, 0.000000000001));
        assert(lfsr_next(44257) == 22128);
    }

    test "FFT words grow, and halving every stage holds them" {
        assert(fft_value(PAT_DC, 8, 8, false, 3, 0, false) == 1016.0);
        assert(fft_value(PAT_TONE, 8, 8, false, 3, 1, false) == 509.0);
        assert(fft_value(PAT_TONE, 8, 8, false, 3, 7, false) == 509.0);
        assert(fft_peak(PAT_DC, 16, 12, false, 4) == 32752.0);
        assert(fft_peak(PAT_DC, 16, 12, true, 4) == 2047.0);
        assert(fft_peak(PAT_TONE, 16, 12, false, 4) == 16374.0);
        assert(fft_peak(PAT_TONE, 16, 12, true, 4) == 1024.0);
        assert(fft_peak(PAT_NOISE, 16, 12, false, 4) == 5826.0);
        assert(fft_peak(PAT_NOISE, 16, 12, true, 4) == 365.0);
        assert(bits_for(32752.0) == 16);
        assert(fft_unscaled_bits(16, 256) == 25);
    }

    test "the capstone FIR follows its f64 twin" {
        assert(hw_input(5, 0.02, 0.3) == 8667);
        assert(hw_fir_out(5, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3) == 57);
        assert(hw_fir_out(2, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3) == 19);
        assert(hw_fir_out(4, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3) == 50);
        assert(hw_fir_out(60, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3) == -8657);
        assert(hw_fir_out(100, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3) == -14007);
        assert(near(ref_fir_out(60, 31, 0.1, WIN_HAMMING, 0.02, 0.3), -8657.0284, 0.0001));
        assert(near(hw_error(60, 31, 0.1, WIN_HAMMING, 16, 0.02, 0.3), 0.0284, 0.0001));
        assert(hw_acc_bits(31, 0.1, WIN_HAMMING, 16) == 32);
        assert(hw_acc_bits(63, 0.05, WIN_BLACKMAN, 18) == 34);
    }

    test "VERSION says what the header says" {
        assert(VERSION == 1);
        assert(filters_on_200t(1) == 740);
        assert(fft_stages(MAX_FFT) == 8);
    }
}
// phi^2 + 1/phi^2 = 3 | TRINITY

Open the lesson's spec in the player ↗

All lessons