ESP32-LyraT I2S Microphone Capture on Jetson Orin Nano - Application Guide¶
Goal: Use an ESP32-LyraT audio dev kit as a practical I2S microphone/audio frontend for the Jetson Orin Nano 8GB / Super Developer Kit 40-pin header, so you can test Jetson I2S capture, AHUB routing, and JetPack audio debugging before designing a custom microphone board.
Hub: Multimedia
Audio foundation: Jetson Audio Setup and Development
1. What this application is testing¶
This is not a normal "plug in a USB mic" test.
This application tests the real embedded-audio path:
microphone frontend
-> codec / ADC
-> I2S serial audio
-> Jetson 40-pin header I2S2
-> AHUB / ADMAIF
-> ALSA capture
-> recording, ASR, wake-word, or voice pipeline
The ESP32-LyraT is useful here because it already has:
- onboard microphones
- an
ES8388audio codec - an exposed I2S header
- ESP-ADF examples for configuring the board-side audio path
The important correction:
Audio is not carried over I2C. On the LyraT, I2C controls the codec registers. The audio samples move over I2S.
So if you say "I2C mic" in this project, usually you mean one of these:
- I2S microphone / I2S audio stream: digital audio data path
- I2C-controlled codec: control path for a codec such as
ES8388
2. Recommended first architecture¶
Use the LyraT as the audio frontend and let the ESP32 side configure the codec.
ESP32-LyraT
onboard mics
|
v
ES8388 codec / ADC
|
| I2S clocks + ADC data on JP4
v
Jetson Orin Nano 40-pin header
I2S2 DIN / SCLK / FS
|
v
Jetson AHUB -> ADMAIF1 -> ALSA arecord
This is the lowest-risk bring-up model because:
- ESP32-LyraT already knows how to initialize the
ES8388 - Jetson only has to prove that it can receive I2S
- you avoid fighting two hosts trying to control the same codec over I2C
Later, for a real product, replace the LyraT with a dedicated codec or ADC board that Jetson controls directly.
3. What not to do first¶
Do not start by trying to make Jetson directly control the LyraT ES8388 over I2C.
That path is possible in theory, but it is a poor first test because:
- the
ES8388is already wired to the ESP32 on the LyraT - the ESP32 firmware normally owns codec initialization
- Jetson would need an ASoC codec driver and machine-driver binding
- two hosts on the same codec control path can conflict
- you may need board rework or firmware changes to fully tri-state the ESP32 side
For first bring-up, treat the LyraT like a configured I2S audio source.
4. Hardware prerequisites¶
- Jetson Orin Nano 8GB / Super Developer Kit
- JetPack 6.x / L4T 36.x or newer
- ESP32-LyraT V4.3 or similar LyraT variant
- USB cable for powering and flashing the LyraT
- jumper wires
- logic analyzer or oscilloscope, strongly recommended
- optional powered speakers or headphones for LyraT-side sanity tests
Voltage rule:
Jetson 40-pin header signals are 3.3 V logic. Do not connect 5 V UART/I2S/control signals.
The LyraT I2S header signals are ESP32-side 3.3 V logic, so direct signal wiring is reasonable. Still share ground.
5. Jetson 40-pin I2S2 pins¶
For the Orin Nano developer kit 40-pin header, the audio guide uses the header-exposed I2S2 path:
| Jetson function | 40-pin header pin | Direction in this test |
|---|---|---|
I2S2_SCLK / bit clock |
12 |
LyraT -> Jetson if LyraT is master |
I2S2_FS / LRCK / frame sync |
35 |
LyraT -> Jetson if LyraT is master |
I2S2_DIN |
38 |
LyraT audio data -> Jetson |
I2S2_DOUT |
40 |
Jetson playback data -> LyraT, optional |
AUD_MCLK |
7 |
optional; do not connect for first capture test |
GND |
6, 9, 14, etc. |
common ground |
For microphone capture, the minimum useful wiring is:
LyraT bit clock -> Jetson pin 12
LyraT LRCK -> Jetson pin 35
LyraT ADC data -> Jetson pin 38
LyraT GND -> Jetson GND
Do not connect both sides as clock masters. If LyraT drives SCLK and LRCK, Jetson must be configured as the I2S clock slave for that DAI link.
6. ESP32-LyraT JP4 I2S header¶
Espressif documents the LyraT V4.3 I2S header JP4 as exposing the board I2S signals:
| LyraT JP4 signal | ESP32 pin | Meaning for this test |
|---|---|---|
MCLK |
GPIO0 |
master clock; usually leave unconnected for first Jetson capture test |
SCLK |
GPIO5 |
I2S bit clock |
LRCK |
GPIO25 |
I2S left/right frame clock |
DSDIN |
GPIO26 |
data into ES8388 DAC, useful for playback into LyraT |
ASDOUT |
GPIO35 |
ADC data out from ES8388, useful for mic capture into Jetson |
GND |
GND |
common reference |
For capture from LyraT microphones into Jetson:
| LyraT signal | Jetson signal | Jetson pin |
|---|---|---|
SCLK |
I2S2_SCLK |
12 |
LRCK |
I2S2_FS |
35 |
ASDOUT |
I2S2_DIN |
38 |
GND |
GND |
6 / 9 / 14 |
Optional playback direction:
| Jetson signal | LyraT signal | Purpose |
|---|---|---|
I2S2_DOUT pin 40 |
DSDIN |
send Jetson audio into LyraT DAC |
Keep playback disconnected until capture works.
7. Bring-up plan¶
Use this order. It prevents chasing software routing while the wire-level clocks are still wrong.
- Prove Jetson internal audio routing.
- Prove LyraT microphones and codec work on the LyraT side.
- Make LyraT output a stable I2S mic stream — see §9 reference firmware
lyrat_jp4_passthroughand §14 verified result for a known-good control. - Verify
SCLK,LRCK, andASDOUTon a scope or logic analyzer. - Enable Jetson 40-pin
I2S2pinmux. - Configure Jetson audio route from
I2S2toADMAIF1. - Record raw audio with
arecord. - Only then add GStreamer, wake-word, ASR, or beamforming.
8. Jetson-side setup¶
Confirm JetPack and ALSA devices¶
cat /etc/nv_tegra_release
uname -a
aplay -l
arecord -l
amixer -c APE controls | grep -E 'I2S2|ADMAIF'
If APE does not exist, stop and fix the Jetson audio stack before wiring external hardware.
How to read a healthy JetPack 6 / R36 baseline¶
If your Jetson shows output like this:
card 1: APE [NVIDIA Jetson Orin Nano APE]- capture and playback devices for
ADMAIF1throughADMAIF20 - mixer controls such as
I2S2 Mux,I2S2 codec frame mode,I2S2 codec master mode, andADMAIF1 Mux
that is a good sign.
It means:
- the
APEsound card is present - the AHUB / ADMAIF fabric is registered
- the
I2S2driver is exposed to ALSA hw:APE,0maps toADMAIF1, which is the first practical external capture endpoint
It does not mean the external header capture path is already working.
You still need:
I2S2enabled on the 40-pin header- the correct master/slave clock relationship
- the correct route from
I2S2intoADMAIF1 - actual
BCLK,LRCK, and data on the wires
Enable 40-pin I2S2 pinmux¶
Use Jetson-IO if your image supports it:
Select the 40-pin header configuration that enables I2S2, save the overlay, and reboot.
After reboot:
Internal loopback first¶
Before using the LyraT, prove that Jetson-side routing is alive:
amixer -c APE cset name="I2S2 Mux" "ADMAIF1"
amixer -c APE cset name="ADMAIF1 Mux" "I2S2"
amixer -c APE cset name="I2S2 Loopback" "on"
aplay -D hw:APE,0 test.wav &
arecord -D hw:APE,0 -r 48000 -c 2 -f S16_LE jetson-i2s2-loopback.wav
This does not prove the external pins work. It proves the AHUB route is sane.
9. LyraT-side setup¶
On the LyraT, use ESP-ADF or ESP-IDF firmware that:
- initializes the
ES8388 - selects the onboard microphone input
- configures sample rate, usually
48000 - configures stereo or mono capture
- drives I2S clocks consistently
- leaves the ADC data visible on
ASDOUT
For the first Jetson test, choose a boring format:
| Parameter | Recommended first value |
|---|---|
| Sample rate | 48000 Hz |
| Channels | 2 |
| Sample format | S16_LE or 16-bit samples inside 32-bit I2S slots |
| Clock role | LyraT master, Jetson slave |
| Data source | LyraT onboard microphone path through ES8388 ADC |
Expected wire-level clocks for a common stereo 48 kHz setup:
| Signal | Expected behavior |
|---|---|
LRCK |
48 kHz |
SCLK |
commonly 1.536 MHz or 3.072 MHz depending on slot width |
ASDOUT |
toggles when microphone signal is active |
If LRCK or SCLK is not present, Jetson cannot capture anything.
Reference firmware: lyrat_jp4_passthrough (verified)¶
A minimal, capture-only ESP-ADF firmware that satisfies all of the requirements above is available in the local esp-adf working tree:
esp-adf/examples/recorder/lyrat_jp4_passthrough/
├── CMakeLists.txt
├── Makefile
├── main/
│ ├── CMakeLists.txt
│ └── lyrat_jp4_passthrough.c
├── partitions_passthrough_example.csv
├── sdkconfig.defaults
└── sdkconfig.defaults.esp32
What it does, in 90 lines of C:
audio_board_init()— uses the in-treelyrat_v4_3board config; pin map atcomponents/audio_board/lyrat_v4_3/board_pins_config.c(no overrides needed).audio_hal_ctrl_codec(... ENCODE, START)— puts ES8388 into ADC mode driving ASDOUT/GPIO35.- Builds an
i2s_streamreader at 48 kHz / 16-bit / stereo / Philips, master role. raw_streamsink + a small drain task discards bytes — the firmware exists only to keepBCLK/LRCKalive and the codec ADC pushing data on the JP4 bus. Jetson is the actual consumer.- Logs
rate=N B/s (expected=192000 B/s)every 2 s as a runtime canary.
Why the drain sink matters: ESP-ADF's i2s_stream reader will stall (and stop the I2S clocks) if no one consumes the ringbuffer. A tiny bytes-to-/dev/null task is the simplest way to guarantee the bus stays live.
Upstream tracking: espressif/esp-adf#1607 (feature request to merge this example upstream).
Build (macOS / Linux):
# One-time host prereqs (macOS)
brew install cmake ninja dfu-util
cd esp-adf/esp-idf && ./install.sh esp32
# Build
cd esp-adf/esp-idf && . ./export.sh
export ADF_PATH=$(realpath ..)
cd ../examples/recorder/lyrat_jp4_passthrough
idf.py set-target esp32 build
Flash artifacts produced (offsets fixed by the project's partitions_passthrough_example.csv):
| File | Offset |
|---|---|
build/bootloader/bootloader.bin |
0x1000 |
build/partition_table/partition-table.bin |
0x8000 |
build/lyrat_jp4_passthrough.bin |
0x10000 |
Flash from a Windows host (one USB micro-B cable to LyraT — auto-bootloader works; the CP2102N enumerates as COMx):
pip install esptool
python -m esptool --chip esp32 -p COM3 -b 460800 erase_flash
python -m esptool --chip esp32 -p COM3 -b 460800 write_flash --flash_mode dio --flash_size detect --flash_freq 80m 0x1000 build\bootloader\bootloader.bin 0x8000 build\partition_table\partition-table.bin 0x10000 build\lyrat_jp4_passthrough.bin
Espressif's "Flash Download Tool" GUI works too, but two gotchas have bitten this exact bring-up:
- Each file row has a checkbox on the left — if unchecked, the row is silently skipped. Symptom:
STARTfinishes in milliseconds and the board boots an unrelated firmware (e.g. the factory Bluetooth speaker demo). - Stale offsets autofill from previous sessions. If
partition-table.binis missing or its offset is wrong, the bootloader loops withflash_parts: partition 0 invalid magic number 0x.../Failed to verify partition table. Re-typing all three offsets and using EraseAll once recovers it.
esptool from PowerShell makes both classes of mistake impossible — the offsets are explicit on the command line.
10. External capture on Jetson¶
Once the LyraT is generating clocks and ADC data, route Jetson capture:
amixer -c APE cset name="I2S2 Loopback" "off"
amixer -c APE cset name="I2S2 codec frame mode" "i2s"
amixer -c APE cset name="I2S2 codec master mode" "cbm-cfm"
amixer -c APE cset name="I2S2 Sample Rate" "48000"
amixer -c APE cset name="I2S2 Capture Audio Channels" "2"
amixer -c APE cset name="I2S2 Client Channels" "2"
amixer -c APE cset name="I2S2 Capture Audio Bit Format" "16"
amixer -c APE cset name="I2S2 Client Bit Format" "16"
amixer -c APE cset name="ADMAIF1 Capture Audio Channels" "2"
amixer -c APE cset name="ADMAIF1 Capture Client Channels" "2"
amixer -c APE cset name="ADMAIF1 Mux" "I2S2"
Try a conservative capture:
Inspect the file:
If playback is silent, inspect signal level:
Why these settings matter:
I2S2 codec frame mode = i2smatches the normal LyraTES8388I2S framingI2S2 codec master mode = cbm-cfmmeans the external board is clock master and Jetson is the slaveADMAIF1 Mux = I2S2routes externalI2S2receive data intohw:APE,0- the channel and bit-format settings keep the I2S CIF side and ADMAIF side aligned
If your LyraT firmware sends 32-bit slots with 16-bit valid microphone samples, try this second variant:
amixer -c APE cset name="I2S2 Capture Audio Bit Format" "32"
amixer -c APE cset name="I2S2 Client Bit Format" "32"
arecord -D hw:APE,0 -r 48000 -c 2 -f S32_LE lyrat-i2s-capture-32.wav
If you intentionally redesign the test so Jetson drives BCLK and LRCK, reverse the clock-role assumption and change:
Useful quick checks:
- silence with no clocks: wiring or LyraT firmware problem
- silence with clocks: I2S data line, codec gain, or route problem
- distorted audio: bit depth, slot width, or master/slave mismatch
- one channel dead: codec input route or channel ordering problem
11. Important limitation: this may need a real ASoC binding¶
Jetson audio is not just "GPIO plus arecord."
For a robust external I2S capture card, Jetson normally needs:
I2S2pinmux as SFIO- an enabled
i2s2controller - a sound-card DAI link
- codec or dummy-codec binding
- correct master/slave clock configuration
- correct sample format and TDM/I2S settings
If you only use mixer commands and arecord, you may reach the limit of what the default Jetson sound card exposes. That is normal.
For a production board, use the full path from the audio deep dive:
12. Direct Jetson control of LyraT ES8388¶
This is the advanced path, not the first bring-up path.
It would look like this:
Jetson I2C -> ES8388 control registers
Jetson I2S2 <-> ES8388 I2S audio
ESP32 side disabled or kept from driving the same bus
Why it is hard:
- the LyraT was designed for ESP32 to own the codec
- the ESP32 and
ES8388are already connected - Jetson and ESP32 must not both drive I2C/I2S
- Linux must have a working
ES8388codec driver and DAI binding - board-level pull-ups and strap behavior may matter
Use this path only if you intentionally turn the LyraT into a codec breakout board.
For a real Jetson audio product, it is usually cleaner to design or buy a small ES7210, ES8388, TLV320, or similar codec board intended to be controlled by the Jetson.
13. Debug checklist¶
Jetson checks¶
arecord -l
aplay -l
amixer -c APE controls | grep I2S2
amixer -c APE controls | grep ADMAIF
dmesg | grep -i -E 'asoc|tegra|i2s|audio'
Wire checks¶
Use a scope or logic analyzer:
LRCKtoggles at the expected sample rateSCLKtoggles continuously during captureASDOUTchanges when sound reaches the microphone- no two devices are driving the same clock line
- ground is shared
Format checks¶
Try common capture variants:
arecord -D hw:APE,0 -r 48000 -c 1 -f S16_LE test-mono.wav
arecord -D hw:APE,0 -r 48000 -c 2 -f S16_LE test-stereo.wav
arecord -D hw:APE,0 -r 48000 -c 2 -f S32_LE test-stereo-32.wav
Use the one that matches the LyraT firmware's I2S slot format.
14. What success looks like¶
Minimum success:
- LyraT outputs stable I2S clocks
- Jetson
I2S2is enabled on the 40-pin header ADMAIF1routes fromI2S2arecordcreates a non-silent WAV file- channel count and sample rate are correct
Better success:
- waveforms show clean clocks and data
- spoken audio is intelligible
- gain is not clipped
- left/right channels are understood
- recording works repeatedly after reboot
Product-level success:
- proper codec or ADC driver
- stable device-tree overlay
- reproducible ALSA route setup
- GStreamer pipeline
- application-level audio health checks
- wake-word or ASR pipeline using the captured audio
Verified test result — LyraT side (2026-05-11)¶
Flashed lyrat_jp4_passthrough.bin (built from esp-adf release/v2.x, ESP-IDF v5.5.3) to an ESP32-LyraT V4.3. Boot log confirms the codec/I2S path is live and DMA is moving real data:
I (172) boot: Loaded app from partition at offset 0x10000
I (197) app_init: Project name: lyrat_jp4_passthrough
I (215) app_init: ESP-IDF: v5.5.3
I (298) JP4_PASSTHROUGH: [1] Init audio board (LyraT v4.3) + ES8388 codec
I (329) JP4_PASSTHROUGH: codec mode=ENCODE (ADC), input=LINPUT1/RINPUT1 (onboard mic)
I (334) JP4_PASSTHROUGH: [3] I2S: rate=48000 Hz, bits=16, channels=2, format=Philips, role=master
I (340) JP4_PASSTHROUGH: JP4 pins: BCLK=GPIO5 LRCK=GPIO25 ASDOUT=GPIO35 MCLK=GPIO0
I (349) JP4_PASSTHROUGH: [4] Capture started — JP4 I2S bus is live. Samples drained on-chip.
I (2381) JP4_PASSTHROUGH: rate=194560 B/s (expected=192000 B/s) total=389120 B
I (4389) JP4_PASSTHROUGH: rate=192512 B/s (expected=192000 B/s) total=774144 B
I (6411) JP4_PASSTHROUGH: rate=194560 B/s (expected=192000 B/s) total=1163264 B
What this confirms about the LyraT side of the bring-up plan (Section 7, steps 1–4):
- ✅ ES8388 enumerated and put in ENCODE/ADC mode
- ✅ Onboard mics (
LINPUT1/RINPUT1) selected - ✅ I2S peripheral running as master,
BCLK/LRCKclocking on JP4 - ✅ DMA is moving 192 ± 1.3 % kB/s =
48000 Hz × 2 ch × 2 Bworth of samples per second (the small overshoot isxTaskGetTickCount()granularity in the 2 s reporting window, not clock drift)
The Jetson side (steps 5–7) is verified separately below — see §14 verified result — Jetson side.
The LyraT-side artifact is a known-good control: if Jetson capture ever regresses after this firmware is flashed, the failure is on the Jetson software path (pinmux, ASoC routing, master/slave config), not the bus.
Verified test result — Jetson side (2026-05-11)¶
After the LyraT bring-up above, all four JP4 jumpers wired to the Jetson Orin Nano 40-pin header (SCLK→12, LRCK→35, ASDOUT→38, GND→6) on a single shared ground. No JetPack rebuild, no out-of-tree driver — only Jetson-IO overlay + amixer route.
Step 1 — confirm overlay applied¶
amixer -c APE controls | grep I2S2 # expect ~19 I2S2 controls present
amixer -c APE cget name="ADMAIF1 Mux" # expect 'I2S2' in items list
arecord -l # expect "card 1: APE ... device 0: XBAR-ADMAIF1-0"
If I2S2 Mux, I2S2 codec frame mode, and I2S2 codec master mode are not in the control list, the 40-pin overlay didn't take — re-run sudo /opt/nvidia/jetson-io/jetson-io.py, save, reboot.
Step 2 — set the AHUB route and I2S format¶
amixer -c APE cset name="ADMAIF1 Mux" "I2S2"
amixer -c APE cset name="I2S2 codec master mode" "cbm-cfm" # LyraT drives BCLK + LRCK
amixer -c APE cset name="I2S2 codec frame mode" "i2s" # Philips I2S framing
amixer -c APE cset name="I2S2 Sample Rate" 48000
amixer -c APE cset name="I2S2 Capture Audio Channels" 2
amixer -c APE cset name="I2S2 Capture Audio Bit Format" 16
amixer -c APE cset name="I2S2 Client Channels" 2
amixer -c APE cset name="I2S2 Client Bit Format" 16
amixer -c APE cset name="ADMAIF1 Capture Audio Channels" 2
amixer -c APE cset name="ADMAIF1 Capture Client Channels" 2
Why each control:
cbm-cfm= "codec-bit-master, codec-frame-master" → Jetson is the I2S slave; LyraT drives both clocks. Matches the firmware'sI2S_ROLE_MASTER.i2sframing = standard Philips I2S. Matches the ES8388 / ourI2S_COMM_FORMAT_STAND_I2S.48000 / 2 ch / 16-biteverywhere → matches the firmware'sSAMPLE_RATE_HZ / SAMPLE_CHANNELS / SAMPLE_BITS. Mismatch on any side here is the most common cause of distortion or silence.
Step 3 — record and inspect¶
arecord -D hw:APE,0 -r 48000 -c 2 -f S16_LE -d 5 lyrat.wav
ls -la lyrat.wav
sox lyrat.wav -n stat
aplay lyrat.wav
Observed result¶
-rw-r--r-- 1 aihpc aihpc 960044 May 10 20:14 lyrat.wav
Samples read: 480000
Length (seconds): 5.000000
Maximum amplitude: 0.233582
Minimum amplitude: -0.178314
Midline amplitude: 0.027634
Mean amplitude: -0.000075
RMS amplitude: 0.021088
Rough frequency: 1356
Volume adjustment: 4.281
Speech ("one two three test") was clearly audible on aplay — bit-accurate capture.
What each number proves:
- File size 960,044 bytes is exact:
5.000 s × 48000 Hz × 2 ch × 2 B + 44 B WAV header = 960044. Zero DMA underruns; LyraT-master / Jetson-slave clocking is rock solid. RMS amplitude 0.021≈ –33 dBFS — normal speech range, well above noise floor.Mean amplitude -0.000075≈ 0 — no DC offset; ES8388 input biasing is clean.Volume adjustment 4.281→ ~12 dB of headroom before clipping. If louder capture is desired, bump ES8388 PGA gain in firmware (es8388_set_mic_gain()) rather than amplifying digitally.Rough frequency 1356— dominant pitch in the speech band; consistent with voice content.
Bring-up plan §7 — final state¶
- ✅ Step 5: 40-pin
I2S2pinmux applied (via Jetson-IO overlay) - ✅ Step 6: AHUB route from
I2S2→ADMAIF1configured; clock direction set to Jetson-slave - ✅ Step 7:
arecord -D hw:APE,0produces a bit-accurate, byte-exact, audible WAV
Known gap — settings do not survive reboot¶
The 9 amixer cset commands are not persistent. alsactl store captures most of them but the NVIDIA APE card's Mux / codec master mode controls have historically not been restored cleanly by alsa-restore.service. For a reproducible bring-up across reboots, wrap the commands in a small systemd unit that runs after sound.target (or after the Jetson-IO overlay loads), or save with alsactl store -f /var/lib/alsa/asound.state and verify with alsactl restore before relying on it.
Next steps anchored to this verified baseline¶
Now that capture is bit-accurate, the next layers (Section 15) are unblocked:
- GStreamer
alsasrcto replacearecordand fan-out to multiple consumers - Streaming ASR (
whisper.cppwith CUDA on Orin) consuming the alsasrc tap - Two-mic delay-and-sum beamforming, taking advantage of LyraT's L+R mics (physical spacing measured 3.5 cm — note this is below Espressif's recommended 4–6.5 cm and the default
MASE_MIC_DISTANCE = 65 mminesp-sr/include/.../esp_mase.h, so any use of Espressif'sesp_afe_doa/MASEAPIs needsmic_distanceexplicitly overridden to0.035) - Eventually, replacing the LyraT with a Jetson-controlled 4-mic codec board (e.g. ES7210) for a real product
15. Next application steps¶
Once the raw capture path works:
- convert the
arecordpath into a GStreamer capture pipeline - add WebRTC VAD or wake-word detection
- test local ASR such as Whisper or a smaller streaming ASR model
- replace LyraT with a dedicated Jetson-controlled codec board
- design a real microphone frontend for the smart-speaker product path
Example GStreamer capture starting point:
gst-launch-1.0 alsasrc device=hw:APE,0 ! \
audio/x-raw,rate=48000,channels=2,format=S16LE ! \
wavenc ! filesink location=lyrat-i2s-gst.wav