# i2s_mic streaming example — UART-bridge variant **For classic ESP32 (and other boards) with a separate USB-to-UART bridge chip** — a second, distinct COM port appears when you plug in, wired to the chip's real UART0 pins (true of most classic ESP32 devkits, using an onboard CP2102, CH340, FTDI, etc.). This example reuses that same UART0 — the one your programming/console cable already talks to — for the audio stream too. No extra hardware needed, same single cable. If your board instead has no separate bridge chip (the cable you flash through is the chip's native USB-Serial-JTAG peripheral — common on ESP32-C3/S3/C6 devkits), use the sibling example **`i2s_mic_usb_jtag_example`** instead. The two are not interchangeable: this one needs a real UART with a negotiable baud rate, which a native USB-CDC endpoint is not. Captures audio from an I2S digital MEMS microphone using the `i2s_mic` component and streams it to a PC, which either plays it back in real time or records it to a WAV file. Developed and directly tested with an **MSM261S4030H0**, which worked unmodified. See "Microphone compatibility" below. **Everything about the transport (handshake, sync bytes, header layout, 16-bit downconversion) lives in this example, not in `i2s_mic` itself.** The component only knows about I2S and filled buffers. ## Wiring | Mic pin | Example default (adjust for your board) | |---|---| | SCK (BCLK) | GPIO 16 | | WS (LRCLK) | GPIO 17 | | SD (DOUT) | GPIO 18 | | L/R | GND | | VDD | 3.3V | | GND | GND | Change `GPIO_BCK` / `GPIO_WS` / `GPIO_DATA` in `main/app_main.c` if your board uses different pins. **Make sure L/R is actually tied to GND or VDD, never left floating** — a floating L/R pin is a common source of hum/noise on its own. These GPIO defaults are classic-ESP32 numbers, chosen to avoid UART0's own pins (GPIO1 TX / GPIO3 RX) and the usual SPI-flash pin range. ## Microphone compatibility `i2s_mic` has no part-specific logic at all — it's a generic I2S receiver that copies whatever the DMA hands it. This example was developed and directly tested with an **MSM261S4030H0**, which worked unmodified. The INMP441 is used as the reference part in the wiring diagram and text below simply because it's the most widely documented I2S MEMS mic — but it has **not** actually been tested against this example. Compatibility with it is expected, not confirmed, since it shares the same 24-bit-in-32-bit Philips format as the MSM261S4030H0. Other parts using that same format (e.g. ICS-43434, SPH0645) are likely compatible for the same reason, also untested. **On stereo capture:** this example requests STEREO and discards one slot (`KEEP_SLOT`), the same defensive pattern used in `i2s_mic_usb_jtag_example` — even though the specific mono-RX interleaving bug that motivated it was diagnosed on ESP32-C3, not classic ESP32 (ESP-IDF's own documentation suggests plain mono capture may work correctly on classic ESP32/S2). It's kept here for consistency and because it costs almost nothing; if you've verified plain mono works cleanly on your exact board, feel free to simplify to `channel_count = 1` / `I2S_SLOT_MODE_MONO`. ## Build and flash This example vendors a local copy of `i2s_mic` under `components/i2s_mic/` so it builds standalone without needing the component published to the ESP Component Registry first. Requires ESP-IDF v6.0.x. ```bash idf.py set-target esp32 # or whichever target has your bridge chip idf.py build idf.py -p /dev/ttyUSB0 flash monitor ``` Once you see `=== READY, waiting for trigger ===`, quit the monitor (Ctrl+], or Ctrl+T then Ctrl+X) so the PC script can open the port itself — the ESP32 and the monitor can't both hold it at once. ## Run the PC-side script ```bash pip install pyserial pip install sounddevice # only needed for --mode play # Record to a WAV file (Ctrl+C to stop and finalize the file): python pc/mic_audio_rcv.py --port COM5 --mode record --out capture.wav # Or listen live: python pc/mic_audio_rcv.py --port COM5 --mode play ``` (`COM5` on Windows; `/dev/ttyUSB0` or similar on Linux/macOS.) ## What's actually happening 1. `main/app_main.c` configures `i2s_mic` for 16 kHz, **stereo**, **32-bit** samples — INMP441 outputs 24-bit audio MSB-justified in a 32-bit slot, and `i2s_mic` only supports 16- or 32-bit slots (24-bit is rejected at `init()`), so 32-bit is the correct choice there. 2. Filled buffers are handed from the ISR callback to a FreeRTOS queue; a worker task (`audio_sender_task`) keeps only `KEEP_SLOT` of each interleaved stereo pair and downconverts that 32-bit sample to 16-bit (`sample >> 16`) before writing it out over UART0. If your recordings are too quiet or too loud, adjust the shift amount or add explicit gain there — it's a one-line change and entirely independent of `i2s_mic`. 3. The ISR **re-posts a different buffer from the pool**, not the one it just delivered — that buffer is still awaiting conversion/transmission by `audio_sender_task`. If the pool is briefly empty (consumer running behind), the ISR simply skips re-posting for that cycle; `i2s_mic` surfaces the resulting drop through `overflow_cb`, logged periodically by `overflow_report_task` whenever logging is enabled (it's silenced once the binary PCM stream starts, to avoid corrupting it). 4. `host_comm_init()` reuses **UART0** — the same peripheral the console and programming connection already use — via `uart_vfs_dev_use_driver()` to take over its full interrupt-driven driver. The handshake connects at a safe 115200 baud, waits for a validated trigger byte, then switches to 460800 for the actual streaming, matching the classic "slow-handshake-then-fast-bulk-transfer" pattern. 5. The trigger read loops until it sees the specific `TRIGGER_BYTE` value (`0xA5`) rather than accepting the first byte it gets. This matters because the header is only ever sent once — if a stray byte were accepted as the trigger prematurely, the PC script would be left waiting forever for a header that already went out before it connected. 6. Bandwidth check: 16 kHz × 16-bit mono = 32,000 bytes/s of payload, comfortably under UART @ 460800 baud's ~46,000 bytes/s raw capacity — `uart_write_bytes()`'s own blocking-when-the-TX-ring-is-full behavior is what ultimately paces the pipeline if the host ever falls behind. 460800 was chosen over the more common 921600 because several cheap USB-serial bridge chips (CH340/CP2102 clones especially) don't reliably honor 921600 on the PC/driver side — the ESP can transmit at that rate just fine, but the PC's OS/driver may silently fail to switch to match it, causing the two ends to talk past each other with zero visible error. 460800 is much more consistently supported while still leaving healthy headroom over the required 32,000 bytes/s. ## Known limitations of this example (not of `i2s_mic`) - Streams indefinitely; there's no clean remote "stop" — just stop the PC script (Ctrl+C, which finalizes the WAV correctly) or reset the board. - No compression, no VAD — deliberately minimal, to keep the focus on how `i2s_mic` itself is used. - Sharing UART0 for both the ESP-IDF console and the audio stream means you lose console logging for the duration of a capture.
To create a project from this example, run:
idf.py create-project-from-example "embedblocks/i2s_mic=0.1.3:uart"