# Plan: Finding the RP2350 ↔ NAU88L21 pin connections (I²C + I²S)

Goal: determine which RP2350 GPIOs the NAU88L21 audio CODEC is wired to on the
EP-2350 "Ting". We need the **I²C control bus** (SDA/SCL) and the **I²S audio
bus** (BCLK, LRCLK/WS, DAC data-out `DOUT`, ADC data-in `DIN`, and possibly
MCLK).

---

## 1. What we already know (from the material in this directory)

- **MCU:** RP2350 (dual-core, MicroPython firmware). Reg base addresses below are
  confirmed from `RP-008373-DS-2-rp2350-datasheet.pdf`.
- **CODEC:** Nuvoton **NAU88L21**. From `DS_NAU88L21_DataSheet_EN_Rev2.8.pdf`:
  - 7-bit **I²C device address = `0x1B`** when `CSB`=0, or **`0x54`** when `CSB`=1.
    (`GPIO1/CSB` pin selects it.) This is the signature we scan for.
  - Audio interface is standard **I²S / left-justified / PCM**, so on the RP2350
    side it is almost certainly implemented in **PIO** (RP2 has no hardware I²S).
- **Firmware structure:** `rom_main.py` imports custom C modules baked into the
  UF2: `ui`, `spl`, `fx`. The CODEC bring-up (I²C register writes + I²S/PIO
  start) happens **inside these compiled modules**, *not* in Python.
- **The `micropython/` checkout is stock upstream** (`origin =
  micropython/micropython`, tag ~`v1.29.0-preview`, **no** TE board dir, **no**
  `nau88`/`codec` references). So it is only the *base* MicroPython TE forked
  from — it will **not** contain the pin map. It is still useful as a reference
  for RP2 register layout and for building `mpy-cross`/`mpremote` if needed.
- **Key consequence:** because the running firmware has *already configured* the
  CODEC before it drops us at the REPL, the pins are **already muxed to their
  final functions**. We can therefore read the mux/peripheral config live and
  simply *observe* the answer, rather than having to guess-and-test. This makes
  passive introspection (Approach A/B) the primary strategy.

---

## 2. Connection / tooling setup (prerequisite for all live approaches)

- Serial REPL on `/dev/ttyACM0`. `mpremote` is **not** installed, but:
  - `pyserial 3.5` **is** installed → we can drive the raw REPL directly, or
  - build/run `micropython/tools/mpremote/` from the checkout.
- Recommended: a small **pyserial helper** that enters raw-paste / raw REPL mode
  (Ctrl-A), sends a code blob, and reads back the result. This avoids the
  line-editor and is scriptable/repeatable.
- The firmware auto-runs `rom_main.py` and then services audio via a C callback,
  so the **REPL should remain interactive** while audio runs. If it doesn't,
  Ctrl-C to interrupt, or use raw REPL. We must be careful **not** to `import`/
  re-init in a way that tears down the running CODEC config we want to observe
  (see Safety, §7).
- First action: confirm liveness — read the chip ID / `sys.implementation`,
  `os.uname()`, and `import machine` succeeds.

---

## 3. Approach A — Passive mux introspection (PRIMARY, non-invasive)

Read the RP2350 IO mux registers that the firmware has *already* programmed, and
decode which pins carry which peripheral. Purely read-only.

### A1. Dump every GPIO's `FUNCSEL` from IO_BANK0

- `IO_BANK0_BASE = 0x40028000`. Per GPIO *n*: `GPIOn_STATUS @ base + 8*n`,
  `GPIOn_CTRL @ base + 8*n + 4`. `FUNCSEL = CTRL[4:0]`.
- Read `CTRL` for GPIO 0..47 with `machine.mem32[]`.
- Decode `FUNCSEL` using the RP2350 GPIO function table (confirmed from
  datasheet §1.2 pinout):

  | FUNCSEL | Function family |
  |--------:|-----------------|
  | 0 (F0)  | SPI |
  | 1 (F1)  | UART |
  | **2 (F2)** | **I²C**  ← find SDA/SCL here |
  | 3 (F3)  | PWM |
  | 4 (F4)  | SIO (software GPIO) |
  | **5 (F5)** | **PIO0** ← I²S candidate |
  | **6 (F6)** | **PIO1** ← I²S candidate |
  | **7 (F7)** | **PIO2** ← I²S candidate |
  | 8–11    | clocks / trace / QMI / USB debug |
  | 31      | NULL (unconnected) |

- **I²C pins fall straight out:** any pin with `FUNCSEL==2` is an I²C pin. The
  GPIO number tells us the instance and role via the datasheet's fixed pattern
  (even pin in a pair = SDA, odd = SCL; the pair index alternates I2C0/I2C1).
  Expect exactly two such pins → that's our SDA/SCL.
- **I²S candidate pins:** pins with `FUNCSEL==5/6/7` are driven by a PIO block —
  these are our I²S candidates (plus MCLK, which may be a PIO pin or a `clk_gpout`
  on FUNCSEL 8). Narrow them down in Approach C.

### A2. Corroborate with PADS_BANK0

- `PADS_BANK0_BASE = 0x40038000`. Check input-enable / output-disable / pull
  config per pin. I²C pins are typically input-enabled with pulls (often external
  on this board); I²S clock/data pins show output-enabled drivers. Helps
  distinguish `DOUT`(output) from `DIN`(input) and clocks(output) among the PIO
  pins.

**Deliverable of Approach A:** a full table `GPIO → function`, from which the two
I²C pins are essentially *proven*, and the I²S pin set is *narrowed to a handful*.

---

## 4. Approach B — Active I²C confirmation (targeted, low-risk)

Confirm the I²C pins found in A1 actually talk to the NAU88L21.

- **Preferred (non-disruptive):** if the firmware exposes a way to read a CODEC
  register, use it. Otherwise construct a `machine.I2C` (hardware) or
  `machine.SoftI2C` on the **exact pins found in A1** and run `.scan()`.
  - **Success signature:** the scan returns **`0x1B`** (or `0x54`). That both
    confirms the pins *and* tells us the `CSB` strap state.
  - Optionally read a known register (e.g. device/chip-ID register per the
    NAU88L21 map) to be fully certain it's the CODEC and not another I²C device.
- If more than one I²C bus/pin-pair appeared in A1, scan each; the one ACKing
  `0x1B`/`0x54` is the CODEC bus.
- **Risk control:** only instantiate I²C on pins A1 already showed as
  `FUNCSEL==2`. Re-driving the *correct* pins is safe; the concern is only
  transient contention if the firmware polls the CODEC concurrently — see §7.

---

## 5. Approach C — Pin down the I²S signals via PIO registers

The A1 dump gives the *set* of PIO-driven pins; this step assigns each to a
specific I²S signal (BCLK / LRCLK / DOUT / DIN / MCLK) — again read-only.

- PIO bases: `PIO0 = 0x50200000`, `PIO1 = 0x50300000`, `PIO2 = 0x50400000`.
- For each state machine (SM0..3) of each PIO, read:
  - `SMx_PINCTRL` → `OUT_BASE/OUT_COUNT`, `SET_BASE/SET_COUNT`,
    `SIDESET_BASE/SIDESET_COUNT`, `IN_BASE`. I²S typically uses **side-set for
    the two clocks** (BCLK + LRCLK/WS on consecutive pins) and OUT/IN for the
    data pins.
  - `CTRL` (which SMs are enabled) and `SMx_CLKDIV` (the divider — sanity-check it
    matches an audio bit-clock, e.g. for 48 kHz × frame size).
  - `SMx_EXECCTRL` (`JMP_PIN` etc.), `FSTAT`/`FDEBUG` to see which FIFOs are live.
- Map: enabled SM whose `SIDESET_BASE`+count covers 2 pins ⇒ **BCLK, LRCLK**
  (consecutive GPIOs). The `OUT_BASE` data pin ⇒ **DOUT** (playback, RP2350→CODEC);
  an SM with active `IN`/RX FIFO ⇒ **DIN** (mic capture, CODEC→RP2350).
- Cross-check output/input direction against the PADS/`GPIO_OE` state from A2.
- **MCLK:** if the CODEC needs a master clock, look for a pin on `clk_gpout`
  (FUNCSEL 8) via `CLOCKS` GPOUT config, or an extra PIO pin.

**Deliverable of Approach C:** each I²S candidate pin labeled BCLK / LRCLK /
DOUT / DIN (/ MCLK), with direction confirmed.

---

## 6. Approach D — Static firmware analysis (corroboration / fallback)

Independent confirmation that doesn't rely on the live device, and a fallback if
the REPL is uncooperative.

- Convert `ep-2350_firmware_1_0_8.uf2` → raw `.bin` (uf2 → bin: strip 512-byte
  block headers; each block carries its target load address — use `picotool` if
  available, or a tiny Python UF2 parser; the checkout has UF2 tooling).
- Search the image for the pin numbers/config we derived live, to confirm:
  - Look for GPIO-init constant patterns and I²C/PIO setup. On RP2 these often
    appear as writes of `FUNCSEL` values or SDK `gpio_set_function(pin, fn)`
    call sites, and PIO program loads.
  - Disassemble around them (`arm-none-eabi-objdump`/`Ghidra`/`radare2` for
    Cortex-M33 / ARMv8-M) to read the literal pin numbers passed to
    `i2c_init`, `pio_gpio_init`, `sm_config_set_*_pins`, etc.
  - Grep the image for the NAU88L21 **register-init byte sequences** to confirm
    which I²C address prefix is used.
- Use this only to *corroborate* the live findings (and to recover MCLK/reset/IRQ
  pins that may not be obvious live). Live introspection (A–C) is authoritative
  because it reflects the actual running config.

---

## 7. Safety considerations

- **Non-destructive first.** Approaches A and C are pure `mem32` reads — zero risk.
  Do them before anything active.
- Do **not** blindly `machine.Pin(n, OUT)` toggle pins to "probe" — that can
  fight the running CODEC/PIO drivers and, worst case, cause contention on a pin
  wired to a CODEC output. Rely on reading the mux instead.
- For Approach B, only bind I²C to pins A1 proved are `FUNCSEL==2`; keep bus
  speed standard (100–400 kHz). Accept a possible transient NACK if the firmware
  accesses the CODEC at the same instant — retry rather than escalate.
- Avoid re-flashing / entering BOOTSEL. Everything here is observation over the
  existing REPL; the UF2 on disk is our recovery image if needed.
- Keep each REPL snippet small and idempotent; capture raw output to files under
  the scratchpad for the record.

---

## 8. Execution order (decision tree)

1. **Setup (§2):** establish scriptable raw-REPL over pyserial; confirm `machine`
   works. *(gate: REPL usable)*
2. **A1 + A2 (§3):** dump all GPIO `FUNCSEL` + pad config → table.
   → **I²C pins identified**, I²S candidates narrowed. *(this alone likely answers
   the I²C question)*
3. **B (§4):** `I2C.scan()` on the identified pins → expect `0x1B`/`0x54`.
   → **I²C pins confirmed + CSB strap known.**
4. **C (§5):** read PIO SM `PINCTRL`/`CTRL`/`CLKDIV` → label BCLK/LRCLK/DOUT/DIN
   (+MCLK). → **I²S pins confirmed.**
5. **D (§6):** disassemble UF2 to corroborate and recover any aux pins
   (MCLK/reset/IRQ). *(fallback if steps 2–4 blocked, else confirmation)*
6. Produce final **pin map table**: `signal → GPIO`, with the evidence source for
   each.

---

## 9. Concrete reference constants (verified from the datasheets)

```
IO_BANK0_BASE = 0x40028000   # GPIOn: STATUS @ +8n, CTRL @ +8n+4 ; FUNCSEL = CTRL[4:0]
PADS_BANK0_BASE = 0x40038000
I2C0_BASE = 0x40090000
I2C1_BASE = 0x40098000
PIO0_BASE = 0x50200000
PIO1_BASE = 0x50300000
PIO2_BASE = 0x50400000

FUNCSEL: 0=SPI 1=UART 2=I2C 3=PWM 4=SIO 5=PIO0 6=PIO1 7=PIO2
         8..11=clk/trace/QMI/USB  31=NULL

NAU88L21 I²C 7-bit addr: 0x1B (CSB=0) | 0x54 (CSB=1)
NAU88L21 audio IF: I²S/left-justified/PCM  → implemented in RP2350 PIO
```

## 10. Open questions to resolve while executing

- Does the REPL stay live while audio runs, or must we interrupt the callback?
- Is there a firmware-exposed CODEC read path (avoids touching I²C at all)?
- Which PIO instance/SM(s) run I²S, and is MCLK generated (PIO vs `clk_gpout`)?
- Are there additional CODEC control lines (reset, IRQ/jack-detect) to map too?
