Recipes¶
Task-focused walkthroughs for the most common use-cases. Each one names the calls to make and the pitfalls to watch for. Each assumes the standard include/ using statements:
Run a spectrum analyzer¶
- Pick a transform size with a single macro that also sizes the sample buffer:
#define FFT_LENGTH 512, thenFFT<FFT_LENGTH> fft;. The size is a power of two between 32 and 4096. AVR is capped at 256 (see Configuration & Platforms). - Select a window once in
setup():fft.setWindowType(WindowType::BlackmanHarris);. - Per frame, fill a
float samples[FFT_LENGTH]buffer from your ADC or aSignalGenerator, then transform and read it out:
removeDC(samples, FFT_LENGTH); // strip DC so it can't dominate bin 0
fft.calculate(samples); // copy + window + transform + magnitude
int bin = fft.peakBin(); // dominant bin, DC skipped
float hz = fft.peakFrequency(SAMPLE_RATE);
fft.postScaleDB(-100.0f, 1.0f); // magnitudes -> dBFS, after any linear read
Warning
- The real spectrum is
N/2 + 1bins (result[0 .. N/2], DC to Nyquist). Everything above that is stale.fft.peakBin()scopes the search to that range and skips DC for you. On a bare buffer,findPeak(mags, N/2 + 1, 1)does the same and returns an absolute bin. See FFT output format. - To turn a bin into a frequency by hand, use
hz = bin * sampleRate / Nwith the full FFT sizeN. - Run
findPeak(andSINADandENOB) on the linear magnitudes first, then callpostScaleDB, which overwrites the buffer with dB in place. ItsmaxScaleis your input's full scale:1.0ffor normalized synthesis, or the counts or volts at full scale for ADC data. A wrongmaxScaleonly shifts the dB axis. It does not move the peak bin.
Reference: the FFTSpectrumPeak example.
Run an FFT from a real ADC¶
Read a block of ADC counts and transform them directly. The integer overload
casts to float for you, so there is no separate conversion step. On Teensy, we
recommend Pedvide's ADC library for the capture itself; a SignalCore
ParallelSampler is also in development.
uint16_t adcBuffer[FFT_LENGTH];
const uint32_t periodUs = 1000000UL / SAMPLE_RATE; // pacing sets the real rate
uint32_t next = micros();
for (int i = 0; i < FFT_LENGTH; i++) {
while ((int32_t)(micros() - next) < 0) { /* wait for this sample's slot */ }
adcBuffer[i] = (uint16_t)analogRead(A0);
next += periodUs;
}
fft.calculate(adcBuffer); // integer overload: counts -> float
int bin = fft.peakBin(); // skips DC, where the ADC bias sits
fft.postScaleDB(-100.0f, ADC_FULL_SCALE); // dBFS against the ADC's count range
Warning
- The integer overloads (
calculate(uint16_t*)andcalculate(int16_t*)) cannot remove DC in place, so the ADC's bias lands entirely in bin 0. Skipping bin 0 in the search handles it. To strip it explicitly, copy the counts into a float buffer and callremoveDC()beforecalculate().removeDConly operates on the float path. - The read pacing is your sample rate.
analogReadtiming sets it, and the spectrum only reachesSAMPLE_RATE / 2, the Nyquist limit. A tone above that aliases down to a lower, wrong frequency. postScaleDB'smaxScaleis the ADC full scale in counts ((1 << bits) - 1), because the integer path leaves magnitudes in counts.
Reference: the FFTFromAnalogInput example.
Measure SINAD and ENOB¶
Grade a tone, or the converter that captured it, from a single transform.
removeDC(samples, FFT_LENGTH);
fft.calculate(samples);
float* mags = (float*)fft; // read metrics on LINEAR magnitudes
float sinad = SINAD(mags, FFT_LENGTH / 2 + 1); // dB
float enob = ENOB(mags, FFT_LENGTH / 2 + 1); // bits, = (SINAD - 1.76) / 6.02
Warning
- Compute
SINADandENOBbeforepostScaleDB. They read the linear magnitudes thatcalculate()produces, andpostScaleDBoverwrites the buffer with dB. - A single non-coherent FFT understates ENOB. A tone that does not
complete a whole number of cycles in the window leaks across bins and
reads as noise. Choose a frequency of
cycles * sampleRate / Nwith an integercyclesso the tone closes on itself. A coherent tone like this needs no window. - Pass the real-bin count
N/2 + 1, notN.
Reference: the SignalQualityMetrics example.
Add an oscilloscope-style triggered trace¶
- Create a trigger:
Trigger trig(TriggerMode::RisingEdge);. With no threshold it auto-thresholds at the signal midpoint. - Capture into a buffer larger than the output window, roughly 1.5 times the
size (
CAPTURE_N > OUTPUT_N), so the search has headroom on both sides of the edge. - Per frame, align and render:
float* view = trig.calculateOffset(buffer, OUTPUT_N, CAPTURE_N);
if (trig.valid()) {
// draw view[0 .. OUTPUT_N - 1]: a phase-stable, edge-aligned frame
}
By default the edge sits at the center of the frame. Call trig.setPosition(0.25f)
to move it left and keep more pre-trigger history.
Warning
- Always gate rendering on
trig.valid(). When no edge is found,calculateOffsetreturns the unaligned buffer, so an ungated draw shows a jittery frame. - If you set a fixed threshold with
setThreshold(), the trigger skips its automatic min/max scan and the signal range keeps its default of 0 to 3.3 (volts) — calltrig.setMinMax(lo, hi)in that case, because the hysteresis margin is derived from that range. With the auto-threshold (nosetThreshold()call) the range is re-scanned on every call andsetMinMax()is unnecessary. - The hysteresis latch persists between calls. Call
trig.reset()(ortrig.begin(...)) when the signal source changes.
Reference: the EdgeTriggeredFrame example.
Generate a test signal in software¶
No analog hardware is required. Synthesize a waveform and read it sample by sample.
SignalGenerator sig(SAMPLE_RATE);
void setup() {
sig.setCarrier(SignalShape::Sine, 1000.0); // 1 kHz tone
sig.setAM(SignalShape::Sine, 7.0, 30.0); // optional: 30% AM at 7 Hz
sig.setNoise(3.0); // optional: 3% gaussian noise
}
void loop() {
float s = (float)sig.nextSample(0); // 0 = full-resolution float
// feed s into an FFT buffer, print it, etc.
}
Warning
- Call
setCarrier()before the firstnextSample(). The default carrier frequency is 0, which produces no phase advance and so a silent, flat output. - AM and noise amplitudes are percentages, not linear gains.
setNoise(3.0)means 3 percent, not 3 times. nextSample(0)returns the raw bipolar float carrier. Any non-zero argumentNreturns something else entirely: an unsigned N-bit code in[0, 2^N)with a half-scale DC offset baked in, ready for a DAC —removeDC()matters again if you FFT that output.
Reference: the SignalGeneratorBasic and
EMASmoother examples.
Smooth or max-hold a spectrum¶
Both processors run on the FFT magnitude buffer, frame by frame. Size them to
the number of real bins, N/2 + 1.
ExponentialMovingAverage ema(FFT_LENGTH / 2 + 1, 0.2f);
PeakHoldProcessor hold(FFT_LENGTH / 2 + 1, 0.95f);
// per frame, after fft.calculate(...):
ema.update((float*)fft); // smoothed spectrum
hold.update((float*)fft, FFT_LENGTH / 2 + 1); // per-bin max-hold
float* smoothed = ema;
float* bars = hold;
Warning
sampleWeight(EMA) anddecay(peak-hold) are opposite conventions.sampleWeightis the weight on the new sample, so a higher value tracks faster and smooths less.decayis the fraction of the old peak kept, so a higher value falls off more slowly. Do not copy one constant into the other.- Both processors accept an optional input length on
update()and clamp tomin(inputSize, size). The single-argument form assumes the buffer holds at leastsize()elements — prefer the two-argument form when the input length can vary. - Both take their memory from the heap when constructed. On RAM-tight boards
such as AVR, construct them once early in
setup()to avoid fragmentation.
Reference: the SpectrumSmoothingPeakHold
example, which contrasts the two processors on a live swept spectrum.
Enable CMSIS hardware FFT on a non-Teensy ARM board¶
Teensy 4.x uses CMSIS-DSP automatically. Other ARM boards (Arduino Giga, STM32, SAMD51) can opt in:
Warning
- The define must appear before the include.
- If the core does not ship
arm_math.hat all, the build now stops with a clear#errornaming the macro. If the core ships the header but does not link the CMSIS-DSP objects, you still get link errors forarm_rfft_fast_*— the define alone does not pull the library in. - No other code changes are needed. The generic and CMSIS backends produce the same output, bin for bin.
Choose a window function¶
Set the window once with fft.setWindowType(WindowType::…). The choice trades
main-lobe width (frequency resolution) against sidelobe suppression (the ability
to see a small tone next to a large one).
| Window | Good for |
|---|---|
Rectangle / None |
Transient capture; maximum frequency resolution, worst leakage |
Hann |
A sensible general-purpose default |
Hamming |
General use; a slightly different sidelobe shape than Hann |
BlackmanHarris |
Separating closely spaced tones (deep sidelobes); the example default |
FlatTop |
When amplitude accuracy matters more than frequency resolution |
Welch, Triangle, Nuttall, Blackman, BlackmanNuttall |
Intermediate trade-offs |
Print the active window on a serial readout with getWindowName(wt), or
getWindowName(wt, true) for the short label.
Compare windows / measure leakage¶
To see the trade-off rather than just pick from the table, run one captured tone through several windows and compare where the energy lands. Place the tone deliberately off-bin (for example 1234.5 Hz) so the leakage is there to measure.
void analyzeWindow(WindowType win) {
fft.setWindowType(win);
fft.calculate(samples); // windows its own internal copy
float* mags = (float*)fft;
int peakBin = fft.peakBin(); // dominant bin, DC skipped
// Worst sidelobe: the largest magnitude outside a guard band around the peak.
float sidelobe = 0.0f;
for (int i = 1; i <= FFT_LENGTH / 2; i++) {
if (abs(i - peakBin) <= 2) continue;
if (mags[i] > sidelobe) sidelobe = mags[i];
}
Serial.print(getWindowNameAbbr(win)); // then peakBin and 20*log10(sidelobe)…
}
Warning
fft.calculate()windows a private copy of the input, so the sharedsamplesbuffer survives intact. Set a window, transform, and repeat, all from the same capture.- Leakage only shows when the tone sits between bins. On an exact bin
center (
cycles * sampleRate / N) even a Rectangle window leaks nothing, which hides the whole lesson. - The main lobe has skirts of its own. Exclude a guard band (±2 bins here) around the peak so they are not mistaken for a sidelobe.
Reference: the WindowComparison example.
Created by VoidLoop · Founded by Gregory Kovacs · Written by Zachariah Magee