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.

Aug 2026
STMicroelectronics B-L475E-IOT01A Discovery kit for IoT node (STM32L475)

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

  1. 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.
  2. DC blocker. A single-pole high-pass IIR removes drift and bias: y[n] = x[n] − x[n−1] + R·y[n−1] with R = 0.995. That pole sits close to DC so slow bias dies without eating much of the speech band; lower R cut too much energy from quiet utterances in early captures.
  3. 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.
  4. Event state machine. Energy above ENERGY_LOUD (1500) opens a 256-sample capture window. The capture is kept only if loud_count >= DEBOUNCE_VAL (20). A quiet-abort path throws away mechanical bumps and clicks. Constants live in src/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].