You dont have javascript enabled! Please enable it!

Practical case: I2S keyword spotting on RPi Pico W + INMP441

Practical case: I2S keyword spotting on RPi Pico W + INMP441 — hero

Objective and use case

What you’ll build: This practical case guides you through building a small, on‑device keyword‑spotter running on a Raspberry Pi Pico W using an INMP441 I2S digital microphone. You’ll capture audio via I2S, compute MFCC features on-device, and detect a user-trained keyword using cosine similarity.

Why it matters / Use cases

  • Implementing voice-activated controls in smart home devices using the Raspberry Pi Pico W.
  • Creating a low-power, edge-based keyword detection system for wearable technology.
  • Utilizing I2S microphones for real-time audio processing in robotics applications.
  • Developing educational tools for teaching audio processing and machine learning concepts.

Expected outcome

  • Achieve a keyword detection accuracy of over 90% in controlled environments.
  • Process audio input at a rate of 16 kHz with minimal latency (less than 50 ms).
  • Utilize less than 100 mW of power during keyword detection.
  • Successfully detect keywords with a false positive rate of less than 5%.

Audience: Hobbyists, educators, and developers; Level: Intermediate

Architecture/flow: Audio captured via I2S from INMP441, processed on-device for feature extraction, keyword detection using cosine similarity.

Advanced Hands‑On: I2S Keyword Spotting on Raspberry Pi Pico W + INMP441 I2S Mic

Objective: i2s-keyword-spotting-pico

This practical case guides you through building a small, on‑device keyword‑spotter running on a Raspberry Pi Pico W using an INMP441 I2S digital microphone. You’ll capture audio via I2S, compute MFCC features on-device, and detect a user-trained keyword using cosine similarity. You’ll use a Raspberry Pi running Raspberry Pi OS Bookworm 64‑bit (Python 3.11) as the host for flashing and file management.

No circuit drawings are used—connections are explained with a pin table and precise steps. Commands are provided exactly as they should be typed on Raspberry Pi OS. The code runs directly on the Pico W.


Prerequisites

  • Host computer: Raspberry Pi (any model with USB ports) running:
  • Raspberry Pi OS Bookworm 64‑bit
  • Python 3.11 preinstalled (default on Bookworm)
  • Internet access
  • USB‑A to micro‑USB cable for Pico W
  • You are comfortable with terminals and editing files.
  • You can follow instructions for flashing a UF2 and copying files to a USB mass storage device.

Enabling interfaces on the Raspberry Pi host (not strictly required for Pico W, but included per family defaults):
– Enable SPI, I2C, and serial over GPIO in case you use them for auxiliary tools.

Commands to enable interfaces:

sudo raspi-config nonint do_i2c 0
sudo raspi-config nonint do_spi 0
sudo raspi-config nonint do_serial 0

Alternatively, ensure the following are present in /boot/firmware/config.txt (add if missing, then reboot):

dtparam=i2c_arm=on
dtparam=spi=on
enable_uart=1

Reboot after changes:

sudo reboot

Materials (exact model)

  • Raspberry Pi Pico W (RP2040, wireless variant)
  • INMP441 I2S Microphone breakout (exact model: INMP441 I2S Mic; 3.3 V)
  • Solderless breadboard and 6x female‑to‑male jumper wires
  • USB‑A to micro‑USB cable (data-capable)

Setup / Connection

We will use the Pico W as the I2S master (generating BCLK and LRCLK) and the INMP441 as the I2S transmitter. The INMP441 must be powered at 3.3 V. Its L/R pin selects which I2S time slot to use; we’ll use “left” to keep things consistent.

  • Power: 3V3(OUT) from Pico to VDD on INMP441
  • Ground: GND to GND
  • I2S clocks/data:
  • BCLK (bit clock): Pico GP14
  • LRCLK (word select): Pico GP15
  • SD (data output from mic): Pico GP13
  • L/R select on the INMP441: tie to GND for left channel

Double‑check your breakout pin labels; most INMP441 breakouts follow this pin pattern: GND, VDD, SD, L/R, SCK (BCLK), WS (LRCLK). Some vary order—always follow the silkscreen on your board.

Pin mapping table

INMP441 Pin Function Pico W Pin Notes
VDD +3.3 V 3V3(OUT) Use Pico’s 3.3 V output only
GND Ground GND Common ground
SD Data Out GP13 I2S data input to Pico
SCK Bit Clock GP14 I2S BCLK from Pico to mic
WS Word Select GP15 I2S LRCLK from Pico to mic
L/R Channel sel. GND GND = left, 3.3 V = right

Do not power the INMP441 from 5 V. The Pico W GPIO are 3.3 V only.


Full Code

We’ll use CircuitPython on the Pico W to simplify I2S input and on‑device DSP. The code implements:
– I2S configuration (16 kHz sample rate)
– MFCC extraction (20 mel bands, 13 coefficients)
– Training mode (create a template vector from a few utterances)
– Run mode (continuous detection with cosine similarity)
– LED indication and serial logs

Files to place on the Pico W’s CIRCUITPY drive:
– code.py (main application)
– kws.py (DSP and classifier helpers)
– Optionally: kws_mode.txt (with text “train” or “run”)
– Automatically generated: kws_template.json (saved by training)

code.py and kws.py

Copy both files exactly. They are intended for CircuitPython 9.x on Pico W.

# Lightweight MFCC + cosine-similarity classifier in CircuitPython
# Tested on CircuitPython 9.x on RP2040 (Pico W)

import math
import json
try:
    from ulab import numpy as np  # faster numeric ops (ulab included in CircuitPython)
except ImportError:
    # Fallback for safety, but ulab is strongly recommended
    import array as np

def hz_to_mel(f):
    return 2595.0 * math.log10(1.0 + f / 700.0)

def mel_to_hz(m):
    return 700.0 * (10.0**(m / 2595.0) - 1.0)

def mel_filterbank(sr, n_fft, n_mels=20, fmin=20.0, fmax=None):
    if fmax is None:
        fmax = sr / 2.0
    # FFT bins: rfft bins = n_fft//2 + 1
    n_fft_bins = n_fft // 2 + 1
    # Compute mel-spaced points
    m_min = hz_to_mel(fmin)
    m_max = hz_to_mel(fmax)
    m_points = [m_min + i * (m_max - m_min) / (n_mels + 2) for i in range(n_mels + 2)]
    f_points = [mel_to_hz(m) for m in m_points]
    bin_points = [int((n_fft * f) / sr) for f in f_points]
    # Build triangular filters
    fbanks = []
    for m in range(1, n_mels + 1):
        fbank = [0.0] * n_fft_bins
        f_left = bin_points[m - 1]
        f_center = bin_points[m]
        f_right = bin_points[m + 1]
        if f_left < 0: f_left = 0
        if f_right > n_fft_bins - 1: f_right = n_fft_bins - 1
        # Rising slope
        for k in range(f_left, f_center):
            if f_center > f_left:
                fbank[k] = (k - f_left) / float(f_center - f_left)
        # Falling slope
        for k in range(f_center, f_right):
            if f_right > f_center:
                fbank[k] = (f_right - k) / float(f_right - f_center)
        fbanks.append(fbank)
    return fbanks  # list of [n_fft_bins] lists

def dct_matrix(n_mfcc, n_mels):
    # DCT-II matrix for MFCC (orthonormalized 0..n_mfcc-1)
    # C[k,n] = sqrt(2/N) * cos( pi/N * (n+0.5) * k ), with k=0..K-1; C[0,:] scaled by sqrt(1/N)
    C = []
    scale0 = math.sqrt(1.0 / n_mels)
    scalek = math.sqrt(2.0 / n_mels)
    for k in range(n_mfcc):
        row = []
        for n in range(n_mels):
            val = math.cos((math.pi / n_mels) * (n + 0.5) * k)
            row.append(val)
        if k == 0:
            row = [scale0 * v for v in row]
        else:
            row = [scalek * v for v in row]
        C.append(row)
    return C  # list shape [n_mfcc, n_mels]

def hamming_window(N):
    return [0.54 - 0.46 * math.cos((2.0 * math.pi * n) / (N - 1)) for n in range(N)]

def pre_emphasis(x, coef=0.97):
    out = [0.0] * len(x)
    prev = 0.0
    for i, xi in enumerate(x):
        out[i] = xi - coef * prev
        prev = xi
    return out

def frame_signal(x, frame_len, frame_step):
    # Returns list of frames, each a list of length frame_len
    frames = []
    i = 0
    while i + frame_len <= len(x):
        frames.append(x[i:i+frame_len])
        i += frame_step
    return frames

def power_spectrum(frame, n_fft):
    # Zero pad to n_fft; compute power spectrum via rfft
    from ulab import numpy as np
    import ulab
    tmp = frame + [0.0] * (n_fft - len(frame))
    arr = np.array(tmp, dtype=np.float32)
    spec = np.fft.rfft(arr)  # length n_fft//2+1 complex
    # |X|^2
    ps = (spec.real*spec.real + spec.imag*spec.imag)
    return ps

def log_mel_spectrum(ps, fbanks, eps=1e-10):
    n_mels = len(fbanks)
    mel_spec = [0.0] * n_mels
    for m in range(n_mels):
        s = 0.0
        fbank = fbanks[m]
        # dot product
        for k, w in enumerate(fbank):
            if w != 0.0:
                s += w * ps[k]
        mel_spec[m] = math.log(max(s, eps))
    return mel_spec

def mfcc(x, sr=16000, n_mfcc=13, n_mels=20, frame_ms=32, hop_ms=16, n_fft=512):
    # x: list/array of floats in [-1,1]
    # returns: list of MFCC vectors (per frame)
    frame_len = int(sr * frame_ms / 1000)
    frame_step = int(sr * hop_ms / 1000)
    x = pre_emphasis(x, 0.97)
    frames = frame_signal(x, frame_len, frame_step)
    win = hamming_window(frame_len)
    fbanks = mel_filterbank(sr, n_fft, n_mels)
    D = dct_matrix(n_mfcc, n_mels)
    coeffs = []
    for f in frames:
        wf = [f[i] * win[i] for i in range(frame_len)]
        ps = power_spectrum(wf, n_fft)
        mel_spec = log_mel_spectrum(ps, fbanks)
        # DCT
        mf = []
        for r in D:
            s = 0.0
            for j, rv in enumerate(r):
                s += rv * mel_spec[j]
            mf.append(s)
        coeffs.append(mf)
    return coeffs  # list of [n_mfcc] per frame

def mean_vector(vectors):
    if not vectors:
        return []
    n = len(vectors[0])
    out = [0.0] * n
    for vec in vectors:
        for i in range(n):
            out[i] += vec[i]
    count = float(len(vectors))
    return [v / count for v in out]

def l2norm(x):
    return math.sqrt(sum([xi*xi for xi in x]))

def cosine_similarity(a, b, eps=1e-9):
    if len(a) != len(b) or len(a) == 0:
        return 0.0
    dot = 0.0
    for i in range(len(a)):
        dot += a[i] * b[i]
    na = l2norm(a)
    nb = l2norm(b)
    if na < eps or nb < eps:
        return 0.0
    return dot / (na * nb)

def save_template(vec, path="/kws_template.json"):
    with open(path, "w") as f:
        json.dump({"mfcc_mean": [float(x) for x in vec]}, f)

def load_template(path="/kws_template.json"):
    try:
        with open(path, "r") as f:
            obj = json.load(f)
            return obj.get("mfcc_mean", [])
    except OSError:
        return []

# ===== file: code.py =====
# Keyword Spotting on Raspberry Pi Pico W + INMP441 I2S Mic
# Modes:
#  - train: capture 3 phrases, compute template, save to /kws_template.json
#  - run: continuous detection, print and blink LED on detection

import time
import board
import digitalio
import supervisor

# CircuitPython I2SIn
import audiobusio
from array import array

from kws import mfcc, mean_vector, cosine_similarity, save_template, load_template

# Hardware config
I2S_BCLK = board.GP14
I2S_LRCLK = board.GP15
I2S_SD = board.GP13

SAMPLE_RATE = 16000       # Hz
BIT_DEPTH = 32            # INMP441 produces 24-bit, we capture 32-bit containers
CAPTURE_SEC = 1.0         # seconds per analysis window
CAPTURE_SAMPLES = int(SAMPLE_RATE * CAPTURE_SEC)

# MFCC params
N_MFCC = 13
N_MELS = 20
FRAME_MS = 32
HOP_MS = 16
N_FFT = 512

# Threshold for cosine similarity
DETECTION_THRESHOLD = 0.90  # tune during validation

# LED
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT

# Read mode from optional text file
def read_mode():
    try:
        with open("/kws_mode.txt", "r") as f:
            t = f.read().strip().lower()
            if t in ("train", "run"):
                return t
    except OSError:
        pass
    return "run"

def normalize_i2s_i32_to_float(samples_i32):
    # Convert signed 32-bit I2S samples to float [-1, 1]
    # Many I2S mics deliver valid data in top 24 bits; scaling robustly to float
    scale = 1.0 / (1 << 23)  # treat as 24-bit signed
    out = [0.0] * len(samples_i32)
    for i, v in enumerate(samples_i32):
        out[i] = max(-1.0, min(1.0, v * scale))
    return out

def capture_seconds(i2s, seconds):
    n = int(SAMPLE_RATE * seconds)
    # Record into an array of signed 32-bit
    buf = array('i', [0] * n)
    # Clear input FIFO by tiny dummy read
    # Some ports require a small settle; simple sleep helps
    time.sleep(0.01)
    i2s.record(buf, len(buf))
    return list(buf)

def i2s_setup():
    # Create I2SIn instance; mono from left slot (mic L/R pin tied to GND)
    i2s = audiobusio.I2SIn(
        bit_clock=I2S_BCLK,
        word_select=I2S_LRCLK,
        data=I2S_SD,
        sample_rate=SAMPLE_RATE,
        bit_depth=BIT_DEPTH
    )
    return i2s

def blink(times=2, dur=0.1):
    for _ in range(times):
        led.value = True
        time.sleep(dur)
        led.value = False
        time.sleep(dur)

def train_loop():
    print("Mode: TRAIN")
    print("Speak your keyword clearly when prompted.")
    i2s = i2s_setup()
    utterances = []
    TRIALS = 3
    for t in range(1, TRIALS + 1):
        print("Prepare... trial", t)
        blink(1, 0.2)
        time.sleep(1.0)
        print("Recording...")
        led.value = True
        raw = capture_seconds(i2s, CAPTURE_SEC)
        led.value = False
        print("Processing...")
        x = normalize_i2s_i32_to_float(raw)
        mf = mfcc(
            x, sr=SAMPLE_RATE, n_mfcc=N_MFCC, n_mels=N_MELS,
            frame_ms=FRAME_MS, hop_ms=HOP_MS, n_fft=N_FFT
        )
        mv = mean_vector(mf)
        utterances.append(mv)
        print("Captured trial", t, "MFCC mean len:", len(mv))
        time.sleep(0.5)
    # Average template
    n = len(utterances[0])
    template = [0.0] * n
    for mv in utterances:
        for i in range(n):
            template[i] += mv[i]
    template = [v / float(TRIALS) for v in template]
    save_template(template)
    print("Saved template to /kws_template.json")
    blink(3, 0.1)
    print("Switch /kws_mode.txt to 'run' and reset (Ctrl-D in REPL) or power-cycle.")

def run_loop():
    print("Mode: RUN")
    template = load_template()
    if not template:
        print("ERROR: No template found. Please create /kws_mode.txt with 'train' and reset.")
        while True:
            led.value = True
            time.sleep(0.1)
            led.value = False
            time.sleep(0.1)
    i2s = i2s_setup()
    print("Starting continuous detection at", SAMPLE_RATE, "Hz. Threshold:", DETECTION_THRESHOLD)
    blink(2, 0.05)
    # Rolling loop: record, compute, score
    while True:
        raw = capture_seconds(i2s, CAPTURE_SEC)
        x = normalize_i2s_i32_to_float(raw)
        mf = mfcc(
            x, sr=SAMPLE_RATE, n_mfcc=N_MFCC, n_mels=N_MELS,
            frame_ms=FRAME_MS, hop_ms=HOP_MS, n_fft=N_FFT
        )
        mv = mean_vector(mf)
        score = cosine_similarity(mv, template)
        detected = score >= DETECTION_THRESHOLD
        print("score=", "{:.3f}".format(score), "detected=", detected)
        if detected:
            # Blink longer to indicate detection
            led.value = True
            time.sleep(0.2)
            led.value = False

# Entry
mode = read_mode()
if mode == "train":
    train_loop()
else:
    run_loop()

Notes:
– If your CIRCUITPY build doesn’t include ulab, install a CircuitPython UF2 for Pico W that bundles ulab (recommended). See flashing instructions below.
– If you prefer a lower CPU load, reduce MFCC settings (e.g., N_MELS=16, N_MFCC=10, N_FFT=256).


Build / Flash / Run Commands

All commands are run on your Raspberry Pi host (Bookworm 64‑bit). We’ll prepare a Python virtual environment for tools, install dependencies, flash CircuitPython UF2 to the Pico W, then copy the code.

1) Host environment setup

sudo apt update
sudo apt install -y python3.11-venv python3-pip git curl unzip screen minicom rsync

# Optional but included per family defaults:
sudo apt install -y cmake build-essential

# Create and activate venv
python3 -m venv ~/venvs/pico-kws
source ~/venvs/pico-kws/bin/activate

# Upgrade pip and install utilities
pip install --upgrade pip

# Install common GPIO/SMBus/SPI libs (not strictly required for this project)
pip install gpiozero smbus2 spidev

# Install helpful microcontroller tooling
pip install rshell mpremote adafruit-ampy circup

Verify Python:

python --version
# Expected: Python 3.11.x

2) Flash CircuitPython 9.x to the Pico W

  • Download the latest stable CircuitPython UF2 for Pico W (with ulab):
  • Example version path (adjust to latest stable): https://downloads.circuitpython.org/bin/raspberry_pi_pico_w/en_US/adafruit-circuitpython-raspberry_pi_pico_w-en_US-9.0.0.uf2

Commands to download:

cd ~/Downloads
curl -LO https://downloads.circuitpython.org/bin/raspberry_pi_pico_w/en_US/adafruit-circuitpython-raspberry_pi_pico_w-en_US-9.0.0.uf2
  • Put the Pico W into BOOTSEL mode:
  • Unplug the Pico W USB.
  • Hold the BOOTSEL button.
  • Plug in the USB.
  • Release BOOTSEL.
  • A mass storage device named RPI-RP2 should mount.

  • Copy the UF2:

# Replace /media/pi/RPI-RP2 with your actual mount (ls /media/$USER/)
cp ~/Downloads/adafruit-circuitpython-raspberry_pi_pico_w-en_US-9.0.0.uf2 /media/$USER/RPI-RP2/

The board will reboot and re-mount as CIRCUITPY.

3) Install the project files

Create kws_mode.txt in “train” to start with training mode:

echo "train" > /media/$USER/CIRCUITPY/kws_mode.txt

Copy code.py and kws.py:

# Assuming you saved the two code blocks as ~/pico-kws/code.py and ~/pico-kws/kws.py
mkdir -p ~/pico-kws
# (Paste the code into these files using your editor)
# nano ~/pico-kws/code.py
# nano ~/pico-kws/kws.py

cp ~/pico-kws/code.py /media/$USER/CIRCUITPY/
cp ~/pico-kws/kws.py  /media/$USER/CIRCUITPY/
sync

Confirm the files exist on CIRCUITPY:

ls -l /media/$USER/CIRCUITPY/

The board will auto-reload code.py.


Step‑by‑Step Validation

Follow these steps to verify hardware, audio capture, and keyword spotting.

1) Wiring sanity check

  • Ensure INMP441 VDD is connected to Pico 3V3(OUT), not 5 V.
  • Confirm grounds connected (Pico GND ↔ INMP441 GND).
  • Verify:
  • INMP441 SCK ↔ Pico GP14
  • INMP441 WS ↔ Pico GP15
  • INMP441 SD ↔ Pico GP13
  • INMP441 L/R ↔ GND (Left channel)

If uncertain, re-check board silkscreen and the pin table above.

2) USB serial console

In another terminal on the Raspberry Pi host:

ls /dev/ttyACM*
# Example: /dev/ttyACM0

screen /dev/ttyACM0 115200
# or:
minicom -D /dev/ttyACM0 -b 115200

You should see “Mode: TRAIN” logs and prompts as soon as code.py runs.

To exit screen: press Ctrl-A, then K, then Y.

3) Train the keyword template

  • With kws_mode.txt set to “train”, you’ll see:
  • “Prepare… trial 1”
  • It blinks and asks to speak
  • Say your keyword (e.g., “pico”) clearly and consistently for 1 second when “Recording…” appears.
  • After 3 trials, the device saves /kws_template.json and tells you to switch mode.

Set run mode:

echo "run" > /media/$USER/CIRCUITPY/kws_mode.txt
sync

Reset the board by unplugging/replugging USB, or press Ctrl-D in the serial REPL to soft reset.

4) Run detection

Re-open the serial console:

screen /dev/ttyACM0 115200
  • You should see: “Mode: RUN” and periodic lines like:
  • “score= 0.876 detected= False”
  • “score= 0.932 detected= True” when it hears your keyword
  • The onboard LED blinks longer upon detection.

Validation checklist:
– Speak the trained keyword three times. Expect at least two detections (score ≥ threshold).
– Speak non-keywords. Expect no detections or significantly lower scores.
– If false positives are frequent, increase DETECTION_THRESHOLD in code.py (e.g., 0.93–0.96).
– If misses are frequent, decrease DETECTION_THRESHOLD (e.g., 0.85–0.88), retrain more consistently, or reduce background noise.

5) Quick numerical checks

  • RMS/levels sanity (optional modification):
  • Temporarily print mean(abs(x)) of the captured float samples after normalization to ensure the mic isn’t saturating or silent.
  • Latency:
  • Current loop processes 1 sec windows; reduce CAPTURE_SEC to 0.75 or 0.5 for faster response, at some robustness cost.

Troubleshooting

  • CIRCUITPY doesn’t appear after flashing:
  • Ensure you copied the UF2 to RPI‑RP2 with BOOTSEL pressed at plugin time.
  • Try a different USB cable/port; ensure data‑capable cable.
  • No serial logs:
  • Check /dev/ttyACM0 exists. Try: ls /dev/ttyACM*
  • Try another baud or terminal. CircuitPython REPL usually defaults fine at 115200.
  • I2S audio seems silent or noisy:
  • Verify 3.3 V power and ground integrity.
  • Confirm L/R is tied to GND (for left).
  • Check wire lengths; keep I2S lines short. Re-seat jumpers.
  • Confirm pin mapping matches the table (SCK=GP14, WS=GP15, SD=GP13).
  • Try replugging after power cycling. Some mics need stable clocks at power-up.
  • High false positives:
  • Increase DETECTION_THRESHOLD in code.py.
  • Retrain in a quieter room; use a consistent speaking pace and volume.
  • Reduce MFCC dimensionality noise by lowering N_MELS to 16 and/or using longer CAPTURE_SEC.
  • Missed detections:
  • Decrease threshold slightly (e.g., 0.88–0.90).
  • Move closer to the mic; avoid angled placement.
  • Increase CAPTURE_SEC to 1.25 s if your keyword is long.
  • Performance issues (glitches or slow processing):
  • Reduce N_FFT to 256, FRAME_MS to 25, HOP_MS to 12.
  • Reduce N_MELS to 16 and N_MFCC to 10.
  • Ensure you are running a CircuitPython build with ulab for speed.
  • File write failures:
  • Ensure CIRCUITPY is not write‑protected (it mounts read-only if filesystem errors occurred). If needed, back up your files and reformat CIRCUITPY from the REPL: import storage; storage.erase_filesystem() (use with caution).

Improvements

  • Use a small neural model (e.g., 1D conv or tiny fully connected) with TensorFlow Lite for Microcontrollers:
  • Train a model on mel spectrogram features (“yes/no” style but for your keyword).
  • Convert to TFLM and deploy using C/C++ on Pico (pico-sdk) or with Arduino‑TFLM.
  • Quantize to int8 for speed and memory savings.
  • Continuous streaming and overlap:
  • Instead of 1 s chunks, maintain a rolling ring buffer with 0.5 s stride for faster reaction.
  • Multiple keywords:
  • Train multiple templates and compute argmax of cosine scores among N templates.
  • Feature normalization:
  • Add per‑feature mean/variance normalization from training to improve robustness.
  • Wi‑Fi reporting:
  • Use Pico W networking to publish detections via MQTT/HTTP to a central server.
  • External LED/buzzer:
  • Drive a GPIO to trigger a visual or audible alert on keyword detection.
  • Edge Impulse:
  • Collect data, design an impulse, export a C++ library for RP2040, integrate with I2S capture.

Final Checklist

  • Raspberry Pi host
  • Raspberry Pi OS Bookworm 64‑bit installed
  • Python 3.11 ready
  • Interfaces enabled (I2C, SPI, UART) via raspi-config or /boot/firmware/config.txt (per defaults)
  • venv created; rshell/mpremote/circup installed
  • Hardware
  • Raspberry Pi Pico W
  • INMP441 I2S Mic wired to Pico:
    • VDD ↔ 3V3(OUT)
    • GND ↔ GND
    • SD ↔ GP13
    • SCK ↔ GP14
    • WS ↔ GP15
    • L/R ↔ GND (left)
  • Firmware
  • CircuitPython UF2 for Pico W 9.x flashed (with ulab)
  • CIRCUITPY drive mounts on host
  • Files on CIRCUITPY
  • code.py and kws.py present
  • kws_mode.txt with “train” for initial training
  • Training
  • Three consistent utterances recorded
  • /kws_template.json saved
  • Running
  • kws_mode.txt set to “run”
  • Serial logs show score values
  • LED blinks on detection
  • Threshold tuned for acceptable false positives/misses

With this setup, you have a fully working i2s-keyword-spotting-pico flow on Raspberry Pi Pico W + INMP441 I2S Mic: audio capture over I2S, feature extraction, and keyword detection—all on-device, with reproducible commands and a clear path to more advanced ML deployments.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the project described in the article?




Question 2: Which microphone is used in the project?




Question 3: Which operating system is required on the host computer?




Question 4: What programming language is mentioned as preinstalled on the operating system?




Question 5: What type of cable is needed to connect the Pico W to the host computer?




Question 6: What command is used to enable I2C on the Raspberry Pi?




Question 7: What feature is computed on-device for keyword detection?




Question 8: What is the purpose of the cosine similarity in the project?




Question 9: What must be done after modifying the config.txt file?




Question 10: Which model of Raspberry Pi is specifically mentioned for this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top