Edge AI Wake-Word Engine
A real-time voice-activation engine built in bare-metal C that captures audio and runs machine learning inference directly on an STM32 microcontroller.
- MEMS microphones
Dual MP34DT01 PDM mics — DFSDM turns the stream into 24-bit PCM for the energy detector.
- STM32L475VG
Arm Cortex-M4F at 80 MHz — runs bare-metal capture, filtering, and the exported linear score (MFCC features still from the host today).
- USB ST-LINK
On-board ST-LINK/V2-1 — flash, debug, and virtual COM for the 921.6 kbps UART telemetry.
- RESET
Hardware reset for bring-up and flashing.
Hover or tap a marker to explore the board.
Highlights
- Engineered register-level C firmware on an STM32L4 (Cortex-M4) for real-time PDM audio acquisition
- Implemented a DC-blocking filter and energy detector in C to isolate speech buffers and reject background noise
- Developed a 921.6 kbps UART streaming driver with SysTick timestamping to transfer audio buffers to a host PC
- Built a Python tool to read the UART stream and plot the waveform, verifying the firmware’s audio and noise detection before running the machine learning model on the chip
- Trained a "Hey STM" wake-word model (MFCC + logistic regression, 92% accuracy on 200 held-out samples) and ran it on the Cortex-M4 in ~4.5 KB of RAM at ~80 µs per inference
Overview
A real-time voice-activation engine built in bare-metal C that captures audio and runs machine learning inference directly on an STM32 microcontroller. Firmware is register-level C with no ST HAL: PDM microphone capture, DC blocking, energy detection, UART streaming, and on-device wake-word scoring. A host Python toolchain logs labeled captures and trains an MFCC + logistic regression model (92% accuracy on 200 held-out samples); the Cortex-M4 runs inference in ~4.5 KB of RAM at ~80 µs per inference.
Signal chain
- PDM to PCM. The MP34DT01 MEMS microphone’s 1-bit PDM stream is decimated in hardware by DFSDM filter 0 (Sinc3, FOSR = 64), producing 24-bit PCM samples. FOSR 64 was the board/app-note starting point that kept the output rate manageable for the UART stream without obvious aliasing on speech; I did not sweep alternate FOSR values.
- DC blocker. A single-pole high-pass IIR removes drift and bias:
y[n] = x[n] − x[n−1] + R·y[n−1]withR = 0.995. That pole sits close to DC so slow bias dies without eating much of the speech band; lowerRcut too much energy from quiet utterances in early captures. - Energy envelope. An exponential moving average tracks signal power:
E[n] = α·|y[n]| + (1 − α)·E[n−1]withα = 0.2. Smallerαlagged real speech onsets; largerαmade mechanical bumps look like speech. Scrub a real capture in the energy / detect scrubber below. - Event state machine. Energy above
ENERGY_LOUD(1500) opens a 256-sample capture window. The capture is kept only ifloud_count >= DEBOUNCE_VAL(20). A quiet-abort path throws away mechanical bumps and clicks. Constants live insrc/main.c.
Telemetry protocol
UART4 runs at 921,600 baud (8N2) so audio can stream without stalling the sampling loop. Two frame types share the link:
| Frame | Size | Contents |
|---|---|---|
| Live telemetry | 13 bytes | Filtered sample (int32), energy (uint32), event flag (uint8), SysTick timestamp in ms (uint32) |
| Event dump | 1032 bytes | 0xFFFFFFFF start marker, 256 × int32 samples, 0x00000000 end marker |
Markers are in-band and can collide with real samples (for example a sign-extended −1 or a run of zeros). The host parser does not hunt for markers in the PCM; it reads fixed-length frames after the live stream signals a dump, so a colliding sample cannot shift framing.
On the host, serial_monitor.py decodes frames and saves each dump as a labeled CSV, while serial_plot.py plots the waveform and energy envelope live. The UART frame inspector shows the live 13-byte frame for whatever sample the energy / detect scrubber is on.
Wake-word model
- Dataset: 200 captures recorded through the board itself: 100 “hey_stm” and 100 “other” (non-wake speech and background). Speaker count was not tracked; ambient false-accepts per hour were not measured.
- Features: an 82-dimensional vector per capture: 13 MFCCs plus their deltas and delta-deltas (mean and variance of each), along with RMS-energy and zero-crossing-rate statistics.
- Training (host): logistic regression with 3× train-time augmentation (time shift, gain scaling, Gaussian noise); weights export to
wake_word_model.h. - On-device inference: trained “Hey STM” wake-word model on the Cortex-M4 (~4.5 KB of RAM at ~80 µs per inference; 92% accuracy on 200 held-out samples).
Interactive telemetry
Scrub a real “hey_stm” capture from the board, then inspect the 13-byte live UART frame the firmware streams at 921.6 kbps. Threshold 1500, EMA α = 0.2, debounce 20.
Energy / detect scrubber
Scrub forward to draw the traces, back to erase them — the UART frame below follows. Flag turns on only after debounce, not on the first loud sample.
- Sample
- —
- Energy
- —
- Flag
- —
- Timer
- —
UART frame inspector
Same 13-byte live frame as the playhead above — big-endian
int32 sample · uint32 energy · uint8 flag ·
uint32 timer_ms. Click a byte or a field —
bytes [0…3].