# NAU88L21 CODEC ↔ RP2350 pin mapping — validated results

Session date: 2026-07-18. Method: **stock MicroPython v1.25.0 (RPI_PICO2 build)**
flashed on the EP-2350 "Ting" FX, driven over the serial REPL at `/dev/ttyACM0`
with a raw-REPL helper (`scratchpad/repl.py`). Unlike the earlier read-only work
on the TE firmware (`pinmapping_findings.md`), here **nothing else is driving the
buses**, so we can actively configure the codec over I²C and drive/observe pins.

## TL;DR — corrected pin map

| Signal | GPIO | Status | Notes |
|--------|-----:|--------|-------|
| I²C SDA | **GPIO14** | Confirmed | scan returns codec + accel |
| I²C SCL | **GPIO15** | Confirmed | scan returns codec + accel |
| I²S **MCLK** | **GPIO12** | **Confirmed (CORRECTED)** | was wrongly GPIO16 in old findings |
| I²S **BCLK** | **GPIO11** | **Confirmed (was ambiguous)** | = MCLK/8, the fast bit clock |
| I²S **FS / LRCLK / WS** | **GPIO10** | **Confirmed (was ambiguous)** | = MCLK/256 = BCLK/32 |
| I²S **DIN** (codec ADCOUT → MCU) | GPIO8 | **Confirmed** | codec drives it; drive gated by I2S_TRI ⇒ codec output |
| I²S **DOUT** (MCU → codec DACIN) | GPIO9 | **Confirmed** | MCU drives pad 0/1 with no contention ⇒ codec input |

### ⚠️ Two corrections vs `pinmapping_findings.md`

1. **MCLK is GPIO12, NOT GPIO16.** GPIO12 was not even in the old firmware pin
   snapshot (it jumped 11→14). GPIO16 was only a "Medium" guess there and is
   **not** the codec master clock — driving a clock on GPIO16 produces zero codec
   response. What GPIO16 actually is remains unidentified (candidate: a clock to a
   different peripheral, DMIC, or a status/LED line — worth a future look).
2. **BCLK and LRCLK are the opposite of the old convention guess.** Old findings
   guessed GPIO10=BCLK, GPIO11=LRCLK "by convention." Measurement proves the
   reverse: **GPIO11 = BCLK, GPIO10 = FS/LRCLK.**

---

## Codec identity (Task 1 — DONE)

- I²C scan on I2C1 (SDA=GP14, SCL=GP15) returns `0x18` (accelerometer) and
  **`0x1b`** (the NAU88L21). `0x1b` ⇒ **CSB strap = 0** (0x1b vs 0x54).
- Read of device-ID register **0x58 = `0x1b23`**. This matches the datasheet
  SILICON REVISION default pattern `X0011 01X 00100000` exactly (only bit 8,
  the live MICDET status bit, differs from the datasheet's `0x18xx`). ⇒ positively
  the NAU88L21.
- NAU88L21 register access convention: **16-bit register address, 16-bit data,
  big-endian**: write `[regHi, regLo]` then read 2 bytes; write `[regHi, regLo,
  dataHi, dataLo]`.

## Clock/data-pin proof (Tasks 2 & 3 — DONE)

Put the codec in **I²S master mode** so it generates BCLK/FS from MCLK. Then the
absolute frequencies and their ratios uniquely identify each line.

Setup registers (from datasheet, verified live):
- `0x03 CLK_DIVIDER` left at default `0x0050` → `SYSCLK_SRC=0` (MCLK pin drives
  SYSCLK directly, **no FLL needed**), `MCLK_SRC=1`.
- `0x01 ENA_CTRL` default `0x00FF` → `CLK_I2S_EN=1` already (no extra enable
  needed to generate clocks).
- `0x1D I2S_PCM_CTRL2` written **`0x301B`**:
  - `I2S_TRI[15]=0` (must clear — default 1 tri-states all I²S pins!)
  - `LRC_DIV[13:12]=11` → FS = BCLK / 32
  - `MS0[3]=1` → **master mode**
  - `BCLK_DIV[2:0]=011` → BCLK = MCLK / 8
- `0x1C I2S_PCM_CTRL1` left default `0x000A` (standard I²S, 24-bit).

Drove MCLK on **GPIO12** with `machine.PWM` and measured GP10/GP11 by
transition-counting in Python over a timed window (`scratchpad/measure_freq.py`):

```
MCLK GP12=100000 Hz | GP10=   390 Hz  GP11= 12501 Hz | expect BCLK=12500 FS=390
MCLK GP12=200000 Hz | GP10=   781 Hz  GP11= 25001 Hz | expect BCLK=25000 FS=781
MCLK GP12=400000 Hz | GP10=  1563 Hz  GP11= 49998 Hz | expect BCLK=50000 FS=1562
control MCLK=GP16 200k | GP10=0 GP11=0 (expect ~0)
```

- GP11 tracks **MCLK/8** = BCLK; GP10 tracks **MCLK/256 = BCLK/32** = FS. Both
  scale perfectly linearly across three MCLK frequencies → not coincidence.
- Removing MCLK (or driving it on GPIO16 instead) → clocks stop → GP12 is MCLK.
- The pin sweep (`scratchpad/sweep_mclk.py`) tried MCLK on every candidate GPIO;
  **only GPIO12** brought the codec clocks alive.

Pull-test on the data pins while codec master + `I2S_TRI=0` (drive internal
pull-up then pull-down, read level each time):
- **GP8**: reads 0 with both pulls → **externally driven LOW** = codec ADCOUT
  (output, `ADCDAT0_OE=1` "always out", idling at 0 with ADC disabled).
- **GP9**: reads 1 with both pulls → held HIGH by an external (board) pull-up,
  consistent with **DACIN**, a codec *input* the codec never drives.
- GP10/GP11 also read "driven" (they're the codec's BCLK/FS master outputs).

---

## Task 4 — DIN=GP8 / DOUT=GP9 positively proven (DONE 2026-07-18)

Both data pins are now confirmed by a direct, register-controlled demonstration
(no analog ADC/DAC bring-up needed). Same environment as above: stock MicroPython
v1.25.0, codec in I²S master mode, driven over the raw-REPL. Scripts in
`scratchpad/`: `t2c_oe_tri.py` (drive/tri-state sweep) and `t3_dout.py`
(GP9 active-drive + GP8 ownership contrast). Reg 0x1D bit map used:
`I2S_TRI=bit15`, `LRC_DIV[13:12]`, `ADCDAT0_OE=bit4`, `MS0=bit3`, `BCLK_DIV[2:0]`
(verified against datasheet default `0x8010` and the working master value `0x301B`).

### GP8 = ADCOUT (codec **output**) — proven
Probe GP8 with the MCU's internal pull-up while toggling the codec's I²S output
drivers via `I2S_TRI` (reg 0x1D bit15):

```
GP8 w/ MCU pull-up: codec-driving (TRI=0) = 0   codec-released (TRI=1) = 1
```

The codec forces GP8 **low against the MCU pull-up** whenever its I²S drivers are
enabled, and lets go (MCU pull-up wins → 1) the instant `I2S_TRI=1` tri-states the
I²S pads. A pin whose drive is gated by the codec's own I²S output-enable register
is a codec **output** = ADCOUT. (Idles low because ADCDAT0_OE=1 "always out" and
the ADC is producing zeros.)

### GP9 = DACIN (codec **input**) — proven
With the codec's I²S interface active (master, TRI=0, MCLK on), the MCU drives GP9
and reads the **real pad** (RP2350 SIO `GPIO_IN` @ 0xD0000004, not the output
latch):

```
drive GP9=0 -> pad 0    drive GP9=1 -> pad 1    (both directions, repeatable)
GP9 w/ MCU pull-up: codec-driving = 1   codec-released = 1   (codec never drives it)
```

The MCU owns the pad completely with **no contention** — only the weak board
pull-up on the DACIN net (which also explains the idle-high). The codec never
forces GP9 in any state. A pin the codec only ever *receives* is a codec
**input** = DACIN. Since ADCOUT and DACIN are the only two I²S data lines and GP8
is the (only) codec data output, GP9=DACIN closes by elimination as well.

> Note: enabling the ADC digitally (reg 0x01 LADCEN=bit10 / RADCEN=bit11, i.e.
> `|= 0x0C00`) is **not** sufficient to see GP8 toggle — with the analog
> front-end unpowered the ADC outputs all-zero samples, so ADCOUT idles low. The
> `I2S_TRI` output-enable test above proves the pin without any analog bring-up;
> a full toggling-data demo would need VMID/bias/PGA/MICBIAS power-up (regs
> 0x66/0x74/0x76/0x77/0x7E/0x7F) which is left as optional future analog work.

### Environment reminder to get back to this state
1. Confirm stock MicroPython is still flashed: over `/dev/ttyACM0`,
   `os.uname().machine` should say `Raspberry Pi Pico2 with RP2350`.
   (If the TE firmware is back, re-flash `RPI_PICO2` per
   `firmware_testing_instructions.md`.)
2. Raw-REPL driver: the previous `scratchpad/repl.py` lives only in the
   ephemeral per-session scratchpad. A recovered/hardened copy is
   `scratchpad/rp.py` (`rp.run(code)`), which parses the framed raw-REPL reply by
   scanning for the two `\x04` terminators (the old `endswith`-based reader was
   racy and returned empty output). The top-level `scratch_repl.py` still does not
   work on this build.
3. Helper snippet (I²C + codec master + MCLK) is in `scratchpad/measure_freq.py`;
   copy its `rd/wr/freq` helpers and the `wr(0x1D,0x301B)` + `PWM(Pin(12),...)`
   bring-up. The `rd/wr` I²C helpers are also reproduced in `t2c_oe_tri.py` /
   `t3_dout.py`.

### Optional future work (not needed — pins already proven above)
- **Full toggling-data ADC demo:** power up the analog front-end so ADCOUT carries
  live (noisy) data instead of idle zeros, then watch GP8 toggle only while MCLK is
  present. Needs VMID (`0x66 BIAS_ADJ` VMIDEN), global bias (`0x76 BOOST` BIASEN),
  MICBIAS (`0x74` POWERUP), FEPGA enable (`0x77`), PGA gain up (`0x7E`), PGA power
  (`0x7F` PUPL/PUPR) on top of `0x01 |= 0x0C00` (LADCEN=bit10/RADCEN=bit11).
- **Gold-standard full-duplex loopback:** MCU as I²S master (codec slave, `MS0=0`)
  via a hand-written `rp2.PIO` program (side-set BCLK=GP11 + FS=GP10, `out` GP9,
  `in` GP8, MCLK GP12 by PWM). Note there is **no** digital DACIN→ADCOUT loopback
  in the part — the only loopback is `MISC_CTRL 0x55 D2A_LOOP` = ADC→DAC (wrong
  direction) — so DACIN can't be echoed digitally; the direction test above is the
  practical proof.
- **Analog confirmation (needs hardware):** enable the DAC, stream a tone on GP9,
  scope/listen at the headphone jack (JKTIP/JKR). Most literal DACIN proof but
  requires a DAC/charge-pump/HP-amp bring-up and external measurement.

### Also worth doing next session
- **Identify what GPIO16 actually is** (old MCLK false-positive). It was a driven
  clock-ish output under TE firmware; confirm it is not connected to the codec.
- Optional: re-confirm MCLK=GP12 by the codec's **Missing-MCLK IRQ** (`0x10` bit5,
  unmask via `0x12` bit5=0). In this session that status bit stayed asserted in
  all states — it likely needs additional internal-clock/oscillator setup to work,
  so the master-mode frequency method above was used instead and is definitive.

## Files (this session, in scratchpad)
- `repl.py` — working raw-REPL driver for stock MicroPython on this board.
- `freqcount.py` — PIO edge-counter (NOTE: unreliable for self-driven pins due to
  GPIO-function conflicts; the plain Python `freq()` transition-counter in
  `measure_freq.py` was used for the real, externally-driven codec signals).
- `sweep_mclk.py` — sweeps candidate MCLK pins; found GPIO12.
- `measure_freq.py` — the definitive BCLK/FS-vs-MCLK measurement.
- `diag.py`, `test_master.py` — earlier diagnostics.
