This firmware turns a Raspberry Pi Pico 2 (RP2350) into a 6-channel USB audio input device (microphone). It captures six independent I2S data lines from six ICS-43434 MEMS microphones (SEL hardwired GND, left channel only), moves the samples into memory with DMA, and streams them to the host over USB Audio Class 2.0 (UAC2) using TinyUSB.
| Property | Value |
|---|---|
| Class | USB Audio Class 2.0 (UAC2), isochronous IN |
| Channels | 6 |
| Sample rate | 48 kHz |
| Sample format | 24-bit signed, packed little-endian (S24_3LE, 3 bytes/sample) |
| USB packet | 1 ms = 48 × 6 × 3 = 864 bytes (within the 1023-byte full-speed iso limit) |
| Product string | Pico 6ch Microphone 48k/24 |
The host sees a standard 6-channel 48 kHz / 24-bit capture device and can record
all six microphones simultaneously (e.g. with arecord, Audacity, Reaper, OBS).
arecord -D hw:<card>,0 -c 6 -r 48000 -f S24_3LE -d 3 capture.wav- The Pico is the I2S master. One PIO state machine generates the shared
WS/SCKclocks for all six microphones. - Six PIO RX state machines (one per mic SD line on GP16/18/20/26/27/28) capture serial data. Mics 1–3 use PIO0; mics 4–6 use PIO1 (same clkdiv, started together). Each line reads the left I2S slot (SEL hardwired GND on modules).
- DMA moves the captured 32-bit slots into a double-buffered memory region; the
USB device task packs them into 24-bit
S24_3LEframes.
Board: Seeed Grove Shield for Pi Pico v1.0. Shield power switch → 3.3 V (never 5 V — destroys ICS-43434).
Each microphone needs two 4-pin Grove cables (clock + data). Cable colours are the same on every cable — only the shield socket and mic socket change.
| Pin | Wire colour | Usual name |
|---|---|---|
| 1 | white | signal (lower-number pin) |
| 2 | yellow | signal (higher-number pin) |
| 3 | red | VCC (3.3 V from shield) |
| 4 | black | GND |
On the mic breakout, strap SEL → GND (left channel only). The yellow wire (pin 2) on the data cable is not connected on the module.
Shield Each mic module (×6)
──────── ────────────────────
[UART0]─────── Debug Probe (no mic cable)
[UART1]──clock──► Mic1 CLK IN ──► CLK OUT ──► Mic2 ──► Mic3
[I2C0]──clock───► Mic4 CLK IN ──► CLK OUT ──► Mic5 ──► Mic6
[D16]──data─────► Mic1 DATA
[D18]──data─────► Mic2 DATA
[D20]──data─────► Mic3 DATA
[A0]──data──────► Mic4 DATA
[A1]──data──────► Mic5 DATA
[A2]──data──────► Mic6 DATA
[I2C1]────────── PS1240 piezo (no mic)
From shield into CLK IN on mic 1 (branch A) and mic 4 (branch B); daisy-chain CLK OUT → CLK IN along each branch. Do not use red (pin 3) on clock cables.
| Pin | Wire | Shield / mic signal | GPIO |
|---|---|---|---|
| 1 | white | SCK | GP9 |
| 2 | yellow | WS | GP8 |
| 3 | red | not used | — |
| 4 | black | GND | GND |
| Branch | Plug shield clock socket into | Then daisy-chain |
|---|---|---|
| Mics 1–3 | UART1 | Mic1 → Mic2 → Mic3 |
| Mics 4–6 | I2C0 | Mic4 → Mic5 → Mic6 |
(UART1 and I2C0 are the same GP8/GP9 on the PCB — two sockets, one clock bus.)
Plug shield end into the port in the Mic column; plug mic end into DATA. Use pin 1 (white) for SD on analog ports A0–A2 (do not use pin 2 there).
| Pin | Wire | Mic module (ICS-43434) | Shield port |
|---|---|---|---|
| 1 | white | SD (serial data) | see table below |
| 2 | yellow | not used (SEL = GND on PCB) | — |
| 3 | red | VDD (3.3 V) | same socket |
| 4 | black | GND | same socket |
| Mic | USB ch | Shield data socket | SD GPIO (pin 1 white) |
|---|---|---|---|
| 1 | 1 | D16 | GP16 |
| 2 | 2 | D18 | GP18 |
| 3 | 3 | D20 | GP20 |
| 4 | 4 | A0 | GP26 |
| 5 | 5 | A1 | GP27 |
| 6 | 6 | A2 | GP28 |
Analog ports A0–A2: the shield ties A0 pin 2 to A1 pin 1 (GP27) and A1 pin 2 to A2 pin 1 (GP28). Use only pin 1 (white) on each port — one mic per socket.
| Pin | Wire | GPIO | Signal | → Probe connector U |
|---|---|---|---|---|
| 1 | white | GP1 | UART RX | ← Probe TX (orange) |
| 2 | yellow | GP0 | UART TX | → Probe RX (yellow) |
| 3 | red | — | not used | — |
| 4 | black | GND | ground | GND |
Baud rate: 115200 (./serial.sh).
Passive piezo between the two signal wires. Do not use red or black on this cable.
| Pin | Wire | GPIO | Signal |
|---|---|---|---|
| 1 | white | GP7 | Piezo B |
| 2 | yellow | GP6 | Piezo A |
| 3 | red | — | not used |
| 4 | black | — | not used |
| Grove port | Function | GPIO |
|---|---|---|
| UART0 | Debug UART | GP0 TX, GP1 RX |
| UART1 | Clock branch mics 1–3 | GP9 SCK, GP8 WS |
| I2C0 | Clock branch mics 4–6 | GP9 SCK, GP8 WS |
| I2C1 | Piezo beacon | GP6, GP7 |
| D16 / D18 / D20 | Mic 1–3 SD | GP16 / GP18 / GP20 |
| A0 / A1 / A2 | Mic 4–6 SD (pin 1) | GP26 / GP27 / GP28 |
| (header) | Grove DPS310 barometer (optional) | GP2 SDA / GP3 SCL (I2C1) |
Seeed 101020812 Infineon DPS310 (300–1200 hPa, −40–85 °C). Wire to free
header pins GP2 (SDA) / GP3 (SCL) + 3V3/GND — not to shield I2C0/I2C1
(those are mic clocks / piezo). Firmware probes 0x77 then 0x76; if the pins
are busy or no device answers, baro is skipped. CLI: BARO (aliases DPS,
PRESSURE); also in STATUS, boot dump, and heartbeat (baro=…hPa).
v1.2.3: moist-air speed of sound (Cramer/NPL-style) from DPS310 T+P
and assumed RH (default 50 %, set with BARO RH <0-100>):
Arden-Buck (p_{sat}), (x_w=RH\cdot p_{sat}/p), then
(c=331.36\sqrt{T_K/273.15},(1+0.314 x_w+0.037 x_w^2)).
Shown as SOUND c=… / BARO … c=…; fed into DOA TDOA (default 343 m/s
without baro). Details: wiring_and_bom.md,
Seeed wiki.
BOM and module layout: wiring_and_bom.md.
Alongside the USB sound card the firmware runs an autonomous acoustic front-end:
-
Multi-source DOA (drone + vehicle + bird + walker + wind) with CLI + DET log. core1 runs parallel front-ends:
Class Band Cue Tracks drone ~800 Hz–6 kHz continuous, wind gate kept, crest limit up to 2 wind LPF ~250 Hz same energy as the gate → intensity + steered az/el status vehicle ~80 Hz–2.5 kHz pass-by → ICE / EV up to 2 bird ~2–8 kHz chirp bout → songbird / corvid / bird up to 2 walker ~150 Hz–600 Hz footstep bout (≥3 steps) → human/cat/dog 1 Entity gallery (
entity_store) = classification templates (ACID dual-slot, last two flash sectors). Detection log (detection_log) = timed events in a separate flash region (two sectors below the gallery) withfirst_seen,last_seen,occurrence,max_gap_ms. DET timestamps are written only afterTIME SYNC <unix>since boot.DET EXPORTemits JSON Lines (NVREVT) for smart-NVR event correlation.UART CLI (help on boot / first byte / USB mount):
HELP,STATUS,TIME/TIME INFO/TIME SYNC,LOG ON|OFF,DET LIST|EXPORT|BACKUP|IMPORT|DEL|CLEAR,ENT LIST|EXPORT|IMPORT,DRONE LIST|CLEAR,BARO,RID LIST|ON|OFF. SeeTEST_SCENARIOS_1.0.6.md. -
OpenDroneID / EU Direct Remote ID (v1.1.0+). On CYW43 boards (
PICO_BOARD=pico2_w,pimoroni_pico_plus2_w_rp2350, or other Pico W variants) the firmware scans BLE advertisements for service UUID0xFFFAand parses ASTM F3411 Basic ID + Location + System messages (e.g. Dronetag / built-in RID). Tracks show up in the heartbeat asrid=N, viaRID LIST, and as DET classremoteid. v1.1.1: System-message timestamp (ODID 2019 epoch → Unix) auto-appliesTIME SYNCwhen unsynced or when drift ≥ 2 s (rate-limited), so DET timestamps work without a UARTTIME SYNC. v1.1.2:TIME INFOreports sync source / age / quality; known drones (+ RID fields) persist in a flash ACID store (DRONE LIST);STATUSdumps all stored data and statistics (also printed on boot). v1.2.1: optional Grove DPS310 barometer on GP2/GP3 (BARO). Non-wirelesspico2builds compile a stub for RID. Flash end layout: DRONE / BT TLV / DET / ENT (two sectors each).SRC class=wind az=180.0 el=10.0 inten=-22.5dB SRC class=drone id=0 az=137.4 el=22.8 conf=0.7 lvl=-31.2dB DET id=3 class=wind first=1720000000 last=1720000012 occ=8 max_gap_ms=400 az=180.0 el=10.0 inten=-22.5dB TRACKS drone=1 vehicle=0 bird=0 walker=0 wind=1 entity=0- Species / ICE·EV / wind direction / re-ID are best-effort heuristics (v1.0.7: soft score bands, gait regularity, ambiguity→generic bird, tighter cross-class gates). Still not a trained model.
- Walker tracking assumes one walking entity at a time.
-
Array geometry. A cube standing on a vertex, 512 mm edge. Mics 1–3 are the three upper faces (mic 1 = north, then +120°, +240° azimuth, all at +35.26° elevation); mics 4–6 are the opposite lower faces (−35.26° elevation, azimuths interleaved by 60°). Select the edge at build time with any integer size —
HET68_DOA_EDGE_MM=150 ./build.sh(default 512 mm);DOA_MAXLAGand the comparison window derive from it automatically. How to physically build the cube, what to build it from, and a full edge-length trade-off analysis (accuracy, speed, compute, memory) are inarray_cube_design.md. -
Synchronisation beacon. The PS1240 piezo emits a periodic BPSK-modulated m-sequence burst on a ~4 kHz carrier (its resonance), driven differentially via the GP6/GP7 software H-bridge. This pseudo-noise code is what neighbouring nodes will cross-correlate (matched filter) for acoustic ranging / clock sync; the receive/ranging side is a later phase. The beacon count appears in the heartbeat (
bcn=).
Pre-built firmware for each tagged release is published on GitHub Releases:
https://github.com/fedurca/het68/releases
Create a GitHub Release whose tag is SemVer core (semver.org)
— x.y.z or vx.y.z (e.g. 1.2.4 / v1.2.4) — and CI builds firmware for
pico2, pico2_w (RP2350 + CYW43439 Wi‑Fi/BT), and Pico Plus 2 W, then
attaches .uf2 / .elf assets to that release.
The Pico SDK is cloned locally into ./pico-sdk (gitignored); no environment
variables are required. On a fresh checkout:
./scripts/fetch_pico_sdk.sh # Pico SDK 2.2.0 + TinyUSB submodule
./build.shSee BUILDING.md for details.
The default board is pico2 (Raspberry Pi Pico 2, RP2350A, 4 MB flash, no PSRAM).
Select a different board with PICO_BOARD:
./build.sh # default: pico2
PICO_BOARD=pico2_w ./build.sh # Pico 2 W: RP2350A + CYW43439 Wi-Fi/BT
PICO_BOARD=pimoroni_pico_plus2_w_rp2350 ./build.sh # 16 MB flash, 8 MB PSRAM, Wi-Fi/BT| Board | PICO_BOARD |
Notes |
|---|---|---|
| Pico 2 | pico2 |
RP2350A, 4 MB flash, no wireless |
| Pico 2 W | pico2_w |
RP2350A + CYW43439 Wi‑Fi/BT (OpenDroneID) |
| Pico Plus 2 W | pimoroni_pico_plus2_w_rp2350 |
RP2350B, 16 MB flash, 8 MB PSRAM, Wi‑Fi/BT |
The Pimoroni Pico Plus 2 W (RP2350B) is the recommended upgrade for multi-node
work: 16 MB flash, 8 MB PSRAM (headroom for recording / on-device detection), and
2.4 GHz Wi-Fi + Bluetooth for networking nodes. Official Pico 2 W is the
drop-in wireless Pico 2 (same 4 MB flash, CYW43439). Boards whose LED lives on a
wireless module do not define PICO_DEFAULT_LED_PIN, so the heartbeat LED is
skipped there (the UART heartbeat still runs). Rationale, PSRAM sizing, power
(all these boards are 3.3 V designs; USB needs 3.3 V USB_OTP_VDD, so a
fully-1.8 V node is custom-hardware only), and alternatives are in
array_cube_design.md.
Upstream Pico SDK 2.2.0 (commit a1438dff) ships TinyUSB 0.18.0, which
does not yet contain all fixes needed for stable RP2350 UAC2 6-channel streaming.
This repo carries local patches under patches/ — full rationale and
per-fix notes are in PATCHES.md.
| What | Where |
|---|---|
| Patch script | patches/apply_all.py |
| Detailed fix list | PATCHES.md |
Reference .patch (ISO activate, PR 2937) |
patches/tinyusb-0.18.0-pr2937-iso-activate.patch |
What they fix (summary):
- RP2350 USB device controller quirks (
EP_ABORTspin, NULL EP0 control register) -O3undefined behaviour fromTU_VERIFY/TU_ASSERTin TinyUSB audio/stack code- UAC2 enumeration race on Linux (
SET_INTERFACEvs. first ISO IN packet,err -110) - ISO endpoint busy-flag handling after abort
How to apply:
Patches are applied automatically on every ./build.sh run (idempotent: reset
patched files to git HEAD inside pico-sdk/lib/tinyusb, then re-apply). To apply
manually:
python3 patches/apply_all.pyExpected output ends with ✓ All patches OK. Do not edit files under
pico-sdk/ by hand — add changes to patches/apply_all.py or a patch file instead.
Target SDK version: patches are written and tested against the vendored tree
Pico SDK 2.2.0 / TinyUSB 0.18.0 (pico-sdk/lib/tinyusb). After upgrading
the SDK submodule, re-run apply_all.py and check for [pattern not found] errors;
see PATCHES.md for upstream-overlap notes (some fixes may already be merged).
./build.shThis produces build/pico_6mic_soundcard.uf2 (and .elf). The default board is
PICO_BOARD=pico2.
- Bench test without microphones — build the diagnostic firmware, which bypasses I2S and streams a simulated 1 kHz tone on all channels:
HET68_USB_DIAG=ON ./build.shDownload a pre-built .uf2 / .elf from
GitHub Releases, or build locally
(./scripts/fetch_pico_sdk.sh && ./build.sh).
- Via Raspberry Pi Debug Probe (SWD), recommended for the lab setup:
./fixdebugger.sh # frees the USB bus, then flashes the ELF over SWD
./fixdebugger.sh --test # also runs a short arecord smoke test- Via UF2 / BOOTSEL: hold
BOOTSELwhile plugging in the Pico, then copybuild/pico_6mic_soundcard.uf2(or the versioned asset from the release) to theRP2350mass-storage volume.
10-second 6-channel reference recording (stops PipeWire so arecord can open the
device directly):
./record10s.sh # -> /tmp/lab_6ch_10s.wav
./record10s.sh capture.wav # custom pathAfter reset the Pico enumerates as a standard 6-channel 48 kHz / 24-bit USB audio device on Windows, macOS and Linux. Select it as the input device in any audio application and record from all six microphones at once.
Copyright (C) 2025-2026 het68 project contributors.
This program is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License v3.0 as published by the Free Software
Foundation. This program is distributed WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
full license text in LICENSE or https://www.gnu.org/licenses/gpl-3.0.html.
Note: the vendored pico-sdk/ and the TinyUSB patches under patches/ carry their
own upstream licenses (BSD-3-Clause / MIT) and are not covered by this project's GPLv3.
