============================================================================== Claude Code session : dd8e9d23-c4a8-4e13-86b4-038741818ade Project directory : /home/agent/TE_ting Started : 2026-07-17 17:06:49 Ended : 2026-07-17 17:57:48 User prompts : 7 Claude Code version : 2.1.212 ============================================================================== [2026-07-17 17:06:49] user: please read ./pinmapping_findings.md. If we wanted to run the tests in there under a stock micropython firmware, what else do we need in order to get a suitable firmware? Details about the flash? anything else? [2026-07-17 17:06:51] thinking: [not recorded in transcript] [2026-07-17 17:06:51] tool use: Read file_path: /home/agent/TE_ting/pinmapping_findings.md [2026-07-17 17:06:51] tool result: Read 1 # Findings: RP2350 ↔ NAU88L21 pin mapping (EP-2350 "Ting") 2 3 Session date: 2026-07-17. Method: live read-only register introspection over the 4 serial REPL via a pyserial raw-REPL driver (`scratch_repl.py`), plus static 5 inspection of the firmware image. `mpremote` was **not** used (unreliable here); 6 raw pyserial worked well. 7 8 ## TL;DR 9 10 | Signal | GPIO | Confidence | Evidence | 11 |-------------------|-----:|-----------|----------| 12 | **I²C SDA** | **GPIO14** | High | external pull-up + I2C1-capable + I2C1 enabled | 13 | **I²C SCL** | **GPIO15** | High | external pull-up + I2C1-capable + I2C1 enabled | 14 | I²S DIN (ADC, codec→MCU) | GPIO8 | High | PIO SM IN pin; pad is an input | 15 | I²S DOUT (DAC, MCU→codec) | GPIO9 | High | PIO SM OUT pin; driven output | 16 | I²S BCLK | GPIO10 | High (pin) / Med (which-of-10/11) | PIO SM side-set pin | 17 | I²S LRCLK / WS | GPIO11 | High (pin) / Med (which-of-10/11) | PIO SM side-set pin | 18 | MCLK (codec master clock) | GPIO16 | Medium | 2nd PIO SM, high-rate side-set, driven output | 19 20 The **I²C control bus is hardware I2C1 on GPIO14 (SDA) / GPIO15 (SCL)**. This is 21 the primary answer requested. The NAU88L21's own ACK (0x1B/0x54) could not be 22 captured cleanly on the running firmware (see caveats), but every passive 23 indicator points to this pin pair, and it is the only pin pair on the board that 24 carries the I²C electrical signature. 25 26 --- 27 28 ## 1. Environment / important discovery about the firmware 29 30 - Firmware reports `MicroPython v1.26.0-preview`, `_machine='EP-2350 1.0.8'`, 31 `machine='EP-2350 with RP2350'`. Chip is an **RP2350A** (only GPIO0–29 exist; 32 GPIO30–47 read NULL / `help('modules')` shows `GPIO0..GPIO29`). 33 - **There is no `machine` module** in this custom firmware. Available modules: 34 `rp2, _rp2, uctypes, micropython, teenage, ui, spl, fx, test, vfs, os, ...`. 35 Register access was therefore done with **`uctypes.struct(addr, ...)`** rather 36 than `machine.mem32`. This matters for follow-up work (see §6). 37 - The REPL stays fully interactive while audio runs. Register reads are safe. 38 - There is a factory-test module `test` (`test.chip_id/mcu/audio/adc/...`) and the 39 audio/CODEC bring-up lives in the compiled C modules `ui`/`spl`/`fx`, not Python. 40 41 ## 2. Primary result — I²C (SDA/SCL) 42 43 ### Key evidence chain 44 45 1. **No GPIO is in I²C function (FUNCSEL==2) at the REPL.** So the CODEC I²C is 46 configured once in C at boot and the pins are then parked in another function — 47 live mux alone does not show it. 48 49 2. **Hardware I2C1 is still enabled; I2C0 is not.** Reading the DesignWare I²C 50 peripheral registers: 51 - `I2C1 @ 0x40098000`: `IC_ENABLE=1`, `IC_TAR=0x018`, `IC_CON=0x165`. 52 - `I2C0 @ 0x40090000`: `IC_ENABLE=0`, `IC_TAR=0x055` (0x055 is the *reset 53 default* of `IC_TAR`/`IC_SAR` → I2C0 was effectively never used). 54 → The active control bus is **I2C1**. `IC_TAR=0x18` is the last-addressed 55 device; `0x18` is a classic accelerometer address (the device has an 56 accelerometer, `ui.acc`/shaker) sharing the same control bus. 57 58 3. **GPIO14 and GPIO15 uniquely show the I²C electrical signature.** For every 59 pin I compared the pad pull config against the actual line level: 60 - GPIO14, GPIO15: pad has **internal pull-DOWN** (`PADS=0x056`, PDE=1, PUE=0), 61 the pin is **not driven** (`OETOPAD=0`), yet it **reads HIGH** (`GPIO_IN=1`). 62 The only way an undriven, internally-pulled-down pin sits high is an 63 **external pull-up** → these are open-drain bus lines held idle-high. 64 - No other pin has this pattern. (Pins 1/3/19/21 also read high but have 65 *internal* pull-ups and are PIO-driven — not I²C. When I released them to 66 inputs they fell low, i.e. no external pull-up.) 67 68 4. **GPIO14/15 are exactly the I2C1 SDA/SCL pins** in the RP2350 fixed function 69 map: pins `(4k+2, 4k+3)` = I2C1 SDA/SCL, so GPIO14 = I2C1 **SDA** (even), 70 GPIO15 = I2C1 **SCL** (odd). This matches the one enabled peripheral (I2C1). 71 72 5. **Touching the pair resets the device.** Both times I actively drove/reclocked 73 GPIO14/15 (crude bit-bang, and a hardware-I2C1 reconfigure) the firmware 74 hung and the board watchdog-reset / re-enumerated over USB. That the *pair* 75 is disruptive is consistent with it being a live, shared bus (accelerometer 76 polling collided with my traffic). The board always recovered from flash. 77 78 Everything converges: **I²C = I2C1, SDA=GPIO14, SCL=GPIO15, with external 79 pull-ups, currently parked in FUNCSEL=PWM(F3) between transactions.** 80 81 ### Caveats / what is NOT yet proven 82 83 - I never got a clean **ACK from the NAU88L21 itself (0x1B or 0x54)**, so: 84 - the **CSB strap state** (→ 0x1B vs 0x54) is unknown, and 85 - strictly, I confirmed "the I²C bus and its pins", and that an accelerometer 86 (0x18) lives on it, but not the codec's presence by direct ACK. 87 My active scans were the *only* things that crashed the firmware, precisely 88 because a second master was fighting the running one. A clean scan needs the 89 TE firmware to not be driving the bus concurrently (see §6 — stock MicroPython). 90 - The codec is almost certainly on this same I2C1 bus (I2C0 was never used), but 91 a follow-up scan should verify by reading a known NAU88L21 ID register. 92 93 ## 3. I²S (audio bus) — bonus, read-only 94 95 Only two PIO state machines are enabled. Decoding their `PINCTRL`: 96 97 - **I²S data/clocks** — SM0 of the PIO register block at `0x50200000`, 98 `CLKDIV≈5.0`, `PINCTRL`: `OUT_BASE=9 (cnt1)`, `IN_BASE=8`, 99 `SIDESET_BASE=10 (cnt2)`. Cross-checked against the actual pad direction 100 (`GPIOn_STATUS.OETOPAD` / `INFROMPAD`): 101 - **GPIO8** = **DIN** (input; ADC/mic data codec→MCU) — pad is an input. 102 - **GPIO9** = **DOUT** (output; DAC data MCU→codec) — driven output. 103 - **GPIO10 / GPIO11** = **BCLK** and **LRCLK/WS** (the two side-set clocks; 104 both driven outputs). Which is which is not yet disambiguated (needs the PIO 105 program / a scope). Convention makes GPIO10=BCLK, GPIO11=LRCLK likely. 106 - **MCLK candidate** — SM0 of the PIO block at `0x50300000`, `CLKDIV≈4.0`, 107 side-set on **GPIO16** with `SIDE_EN` (a single high-rate clock output). GPIO16 108 is a driven output. This looks like the codec **MCLK**. 109 110 ### An unresolved labeling quirk (does not change the GPIO numbers) 111 112 The pins carrying I²S (GPIO8–11) read `FUNCSEL=6` (=PIO1) and MCLK (GPIO16) 113 reads `FUNCSEL=7` (=PIO2), yet the *matching, enabled* state-machine configs sit 114 in the register blocks one lower (0x50200000 "PIO0" and 0x50300000 "PIO1" 115 respectively). I could not reconcile this off-by-one purely from registers. 116 It does **not** affect the pin assignments above (those are anchored on actual 117 pad direction + the SM's own PINCTRL), only the PIO-instance *name*. Worth a 118 glance under stock firmware where `rp2.PIO`/`StateMachine` introspection is easy. 119 120 ## 4. Full active-pin snapshot (post-reset, read-only) 121 122 FUNCSEL, pad-driven (OEtoPAD), input level, pad reg, internal pulls: 123 124 ``` 125 gpio fn OEpad IN pad PUE PDE note 126 0 SIO 1/0* - 056 0 1 (dynamic: LED/switch) 127 1 PIO0 0 1 05a 1 0 internal pull-up (PIO-driven) 128 2 PIO0 1 1 056 0 1 driven high 129 3 PIO0 0 1 05a 1 0 internal pull-up 130 4 SIO 1 0 056 0 1 131 5 SIO 1 0 056 0 1 132 6 SIO 1 0 056 0 1 133 8 PIO1 0 0 05a 1 0 I2S DIN (input) 134 9 PIO1 1 0 056 0 1 I2S DOUT (output) 135 10 PIO1 1 - 056 0 1 I2S BCLK/LRCLK (side-set) 136 11 PIO1 1 - 056 0 1 I2S BCLK/LRCLK (side-set) 137 14 PWM 0 1 056 0 1 *** I2C1 SDA (external pull-up) *** 138 15 PWM 0 1 056 0 1 *** I2C1 SCL (external pull-up) *** 139 16 PIO2 1 0 056 0 1 MCLK candidate (side-set, clkdiv 4) 140 17 SIO 1 0 056 0 1 141 18 SIO 1 0 056 0 1 142 19 PIO0 0 1 05a 1 0 internal pull-up 143 20 PIO0 0 0 05a 1 0 144 21 PIO0 0 1 05a 1 0 internal pull-up 145 24 SIO 1 0 056 0 1 146 25 PIO0 1 0 056 0 1 (likely status LED) 147 ``` 148 `pad 0x056` = PDE(pull-down), `0x05a` = PUE(pull-up); both have IE=1, SchmittEn, 149 drive=4mA. I2C0 disabled; I2C1 enabled, TAR=0x18. 150 151 ## 5. Reference register locations used (RP2350, verified vs datasheet) 152 153 ``` 154 IO_BANK0 0x40028000 GPIOn: STATUS @ +8n (OETOPAD=bit13, OUTTOPAD=bit9, INFROMPAD=bit17) 155 CTRL @ +8n+4 (FUNCSEL = CTRL[4:0]) 156 PADS_BANK0 0x40038000 GPIOn pad @ +4+4n (IE=b6, OD=b7, DRV=b5:4, PUE=b3, PDE=b2, SCH=b1) 157 SIO 0xd0000000 GPIO_IN +0x004, GPIO_OUT +0x010, GPIO_OE +0x030 (SET/CLR +8/+0x10) 158 I2C0/I2C1 0x40090000 / 0x40098000 IC_CON +0x00, IC_TAR +0x04, IC_DATA_CMD +0x10, 159 IC_ENABLE +0x6c, IC_ENABLE_STATUS +0x9c, IC_TX_ABRT_SOURCE +0x80 160 PIO0/1/2 0x50200000 / 0x50300000 / 0x50400000 CTRL +0, SMx PINCTRL @ +0xdc+0x18*x 161 FUNCSEL: 0 SPI, 1 UART, 2 I2C, 3 PWM, 4 SIO, 5 PIO0, 6 PIO1, 7 PIO2, 31 NULL 162 ``` 163 164 ## 6. Suggested follow-up tests (recommended: stock MicroPython) 165 166 The cleanest confirmation is to **temporarily flash stock MicroPython for the 167 Pico 2 / RP2350** (board `RPI_PICO2`), which restores the `machine` module and — 168 crucially — means **no competing firmware is driving the I²C bus**, so a scan 169 won't cause contention. Keep `ep-2350_firmware_1_0_8.uf2` handy to restore the TE 170 firmware afterward (drag-and-drop over BOOTSEL). All tests below are quick. 171 172 **T1 — Confirm I²C pins + enumerate devices (settles SDA/SCL + finds codec):** 173 ```python 174 from machine import Pin, I2C, SoftI2C 175 # hardware I2C1 is SDA=GP14, SCL=GP15: 176 i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=100_000) 177 print([hex(a) for a in i2c.scan()]) # expect 0x1b or 0x54 (codec) AND 0x18 (accel) 178 # also try SoftI2C in case of any HW-mux surprise: 179 s = SoftI2C(scl=Pin(15), sda=Pin(14), freq=100_000) 180 print([hex(a) for a in s.scan()]) 181 ``` 182 Success = the scan returns the NAU88L21 address → proves GPIO14/15 and reveals 183 the CSB strap (0x1B = CSB0, 0x54 = CSB1). 184 185 **T2 — Positively identify the NAU88L21 (not just "something ACKed"):** 186 Read a NAU88L21 ID/reset register over I²C (16-bit register address, 16-bit data, 187 big-endian per its datasheet), e.g. read the software-reset/device-ID region: 188 ```python 189 addr = 0x1b # or whatever T1 found 190 i2c.writeto(addr, bytes([0x00, 0x00])) # set register pointer to 0x0000 191 print(i2c.readfrom(addr, 2).hex()) # read back 16-bit reg value 192 ``` 193 Compare against the NAU88L21 datasheet's ID/default value to be 100% sure it is 194 the codec and not another device. 195 196 **T3 — Disambiguate BCLK vs LRCLK and confirm MCLK (GPIO16):** 197 Under stock firmware you can't easily see the *original* config, but you can: 198 - reason from I²S timing: LRCLK = sample rate (e.g. 48 kHz), BCLK = LRCLK × bits × 199 channels; MCLK (GP16) is much faster (256×/384× fs). A logic-analyzer/scope on 200 GP10, GP11, GP16 settles it instantly. 201 - Or, better, capture the *TE firmware's* live PIO program: in a session on the 202 original firmware, read the PIO instruction memory (`INSTR_MEM`, base+0x048 for 203 32 words) of the enabled block at 0x50200000 and decode the side-set to see the 204 clock phase relationship. (Read-only, safe.) 205 206 **T4 — If staying on the TE firmware (no reflash), safe read-only re-checks:** 207 Re-run the passive dumps in this repo's scratch scripts — they never disturb the 208 bus. Do **not** bit-bang or reconfigure I2C1 live: both attempts here crashed the 209 running firmware because its own driver was mid-transaction. Any *active* probe 210 must first quiesce the firmware's audio/accel loop (not obviously possible from 211 the REPL), which is why stock MicroPython (T1) is the recommended route. 212 213 ## 7. Files produced this session 214 215 - `scratch_repl.py` — pyserial classic raw-REPL driver (send code blob, capture 216 stdout/stderr). Reusable for follow-up sessions on the TE firmware. 217 - scratchpad scripts: `a1_dump.py` (FUNCSEL), `a2_dump.py` (pads+SIO), 218 `status_dump.py` (OE/IN to pad), `pio_recheck.py` (PIO SMs), `i2c_regs.py` 219 (I²C peripheral state), plus the (now-known-disruptive) active probes. 220 - `/tmp/fw.bin` — UF2 unpacked to a flat image at load base 0x10000000 221 (family 0xe48bff59 = RP2350), for static analysis. 222 [2026-07-17 17:07:25] thinking: [not recorded in transcript] [2026-07-17 17:07:50] agent: I've read the findings. Here's what you'd need to assemble a stock MicroPython firmware that can actually run T1–T3, and the gotchas that matter for this specific board. ## Which build - The chip is confirmed **RP2350A** (GPIO0–29 only). The right stock target is **`RPI_PICO2`** — *not* `RPI_PICO2-W` (that pulls in CYW43 wireless you don't have) and not the RP2350B/`_PICO2` 48-pin variants. - **ARM vs RISC-V:** RP2350 has both Cortex-M33 and Hazard3 RISC-V cores, and MicroPython ships a separate UF2 per architecture. Grab the **ARM** build (`RPI_PICO2`), not `RPI_PICO2-RISCV`. This matches the TE firmware, whose UF2 you found uses family **`0xe48bff59` = RP2350 ARM-S (secure)**. Using the same family ID is what lets the bootloader accept it. - Version: v1.24+ is fine; anything ≥ the v1.26-preview the TE firmware reports is ideal but not required for `I2C.scan()`. ## Flash details - RP2350 has **no internal program flash** — it boots from **external QSPI flash**. The stock `RPI_PICO2` UF2 is built assuming the **Pico 2's 4 MB** part and lays the MicroPython filesystem at a fixed offset near the top of a 4 MB map. - If the EP-2350's flash chip is **≥4 MB**, the stock UF2 boots fine and the tests work — **the I²C/PIO tests don't touch the filesystem at all**, so a size mismatch is harmless for T1–T3. - If it's **<4 MB** (unlikely on an audio board but worth confirming), the default FS geometry would be wrong; you'd want a board variant with the correct `MICROPY_HW_FLASH_STORAGE_BYTES`. For just scanning I²C you could ignore the FS entirely. - **Reflashing wipes the TE firmware and anything TE stored in the flash filesystem** (calibration/user data lives in flash, not OTP). Re-dropping `ep-2350_firmware_1_0_8.uf2` restores the firmware image but **won't restore any flash-resident cal/user partition**. If you suspect factory calibration data is stored there, back up the raw flash first (via `picotool save` over BOOTSEL) before overwriting. ## The two things most likely to bite you 1. **Secure boot / OTP lock (the real risk).** RP2350 can be OTP-configured to enforce **signed boot**. If TE burned that fuse, the BOOTSEL mass-storage device will **reject unsigned stock MicroPython** and you're stuck — OTP is one-way. The fact that their UF2 is family `…59` (ARM-**S**ecure) doesn't tell you whether signing is *enforced* — they could ship signed images without locking the fuse. **Check OTP before you rely on this route:** `picotool otp dump` (or read the relevant OTP rows) over BOOTSEL will tell you whether secure-boot enforcement is set. If it is, plan B is running your scan another way, not reflashing. 2. **Crystal frequency.** Stock `RPI_PICO2` assumes a **12 MHz XOSC**. Most RP2350 boards use 12 MHz, but audio boards sometimes pick an audio-friendly crystal. If the EP-2350's crystal isn't 12 MHz, stock firmware will mis-clock and **USB won't enumerate** — you'd then need a custom board def with the right `XOSC` value. Worth confirming from the schematic/BOM before flashing. ## Also needed - **A way into BOOTSEL** — a physical BOOTSEL button (hold while plugging USB), or `picotool reboot -u` / a `machine.bootloader()`-equivalent. Confirm the board exposes the button or a test pad; the findings don't mention one. (The board already re-enumerates over USB on watchdog reset, so USB is wired.) - **`picotool`** installed on the host — useful for OTP dump, flash backup, and forcing BOOTSEL. - **Keep `ep-2350_firmware_1_0_8.uf2`** for restore (already noted in §6). ## One shortcut worth knowing For T1/T2 you don't need to *persist* anything — once stock MicroPython enumerates, you can paste the scan straight over the REPL (same pyserial raw-REPL driver you already have in `scratch_repl.py`). The only write to flash is the one-time UF2 drop. --- Net: the "suitable firmware" is just the **stock `RPI_PICO2` ARM UF2** — no custom build needed for the I²C scan *provided* (a) OTP secure-boot isn't enforced and (b) the board runs a 12 MHz crystal. Those two checks are the whole ballgame; flash size only matters if you care about the filesystem, which these tests don't. Want me to draft the exact `picotool otp dump` / flash-backup command sequence, or check the current MicroPython download page for the precise `RPI_PICO2` file to grab? [2026-07-17 17:13:16] user: yes, please do that. [2026-07-17 17:13:22] thinking: [not recorded in transcript] [2026-07-17 17:13:22] tool use: ToolSearch query: select:WebFetch [2026-07-17 17:13:22] tool result: ToolSearch [tool reference: ?] [2026-07-17 17:13:25] tool use: WebFetch url: https://micropython.org/download/RPI_PICO2/ prompt: List the exact firmware download filenames available on this page for the ARM (not RISC-V) build, including the latest stable version and any preview/nightly. Give the full .uf2 filenames and the version numbers. [2026-07-17 17:13:35] tool result: WebFetch # ARM Firmware Downloads for Raspberry Pi Pico 2 ## Latest Stable Release - `RPI_PICO2-20260406-v1.28.0.uf2` (v1.28.0) ## Previous Stable Releases - `RPI_PICO2-20251209-v1.27.0.uf2` (v1.27.0) - `RPI_PICO2-20250911-v1.26.1.uf2` (v1.26.1) - `RPI_PICO2-20250809-v1.26.0.uf2` (v1.26.0) - `RPI_PICO2-20250415-v1.25.0.uf2` (v1.25.0) - `RPI_PICO2-20241129-v1.24.1.uf2` (v1.24.1) - `RPI_PICO2-20241025-v1.24.0.uf2` (v1.24.0) ## Preview/Development Builds - `RPI_PICO2-20260714-v1.29.0-preview.536.g532428cc6d.uf2` - `RPI_PICO2-20260713-v1.29.0-preview.522.g42a8911382.uf2` - `RPI_PICO2-20260706-v1.29.0-preview.510.gf67ab9efe1.uf2` - `RPI_PICO2-20260703-v1.29.0-preview.467.g964803ab74.uf2` [2026-07-17 17:13:37] thinking: [not recorded in transcript] [2026-07-17 17:14:02] agent: Got the current file list. Here's the complete kit. ## The exact firmware to grab **`RPI_PICO2-20260406-v1.28.0.uf2`** — current stable (v1.28.0), ARM build for the RP2350A. - Direct: `https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2` - Page: `https://micropython.org/download/RPI_PICO2/` Deliberately **not** picking: `RPI_PICO2-RISCV` (wrong core), `RPI_PICO2-W` (CYW43 you don't have), or the v1.29 previews (no reason to chase a nightly for an `I2C.scan()`). v1.28.0 stable is the right call. If wget'ing on the host: ```bash cd ~/Downloads wget https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2 ``` ## Pre-flight checks + flash sequence (picotool) Do these **in order** — steps 1–2 are the go/no-go gates from before (OTP secure boot, and a flash backup). Nothing here writes to the board until step 3. ```bash # 0. Get into BOOTSEL: hold BOOTSEL while plugging USB, OR from a running # REPL run machine.bootloader() (stock) — but TE fw has no `machine`, # so on the TE firmware use picotool to force it: picotool reboot -u -f # -u = reboot into BOOTSEL (USB mass storage) # -f = force even though the app is running picotool info -a # sanity: should show RP2350, flash size, etc. # 1. *** GO/NO-GO: is signed boot enforced in OTP? *** picotool otp dump # dump all OTP rows picotool otp get BOOT_FLAGS0 # <-- look for secure-boot enforcement # Key rows to read: BOOT_FLAGS0/1 and the CRIT1 (CRIT_SECURE_BOOT_ENABLE) bit. # If secure-boot-enable is SET -> stock UNSIGNED MicroPython will be REJECTED. # OTP is one-way; if it's set, STOP and use the read-only REPL route instead. # 2. *** Back up the existing flash BEFORE overwriting *** # (RP2350 boots from external QSPI flash; reflashing wipes TE fw + any # flash-stored calibration/user partition.) picotool save -a whole_flash_backup.bin # save entire flash to a file # (or scope it: picotool save -r 0x10000000 0x10400000 fw_region.bin ) # 3. Flash stock MicroPython (only after 1 & 2 pass) picotool load RPI_PICO2-20260406-v1.28.0.uf2 -x # -x = execute/reboot after load # ...or simply drag-and-drop the .uf2 onto the RP2350 mass-storage drive. ``` After it reboots, MicroPython should enumerate as a USB serial REPL (this is the **12 MHz crystal** assumption — if it fails to enumerate, that's your crystal mismatch, not a bad flash). ## Run the scan (paste over the REPL) You already have `scratch_repl.py`; or use any serial terminal. On stock firmware the `machine` module is back: ```python from machine import Pin, I2C, SoftI2C i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=100_000) print([hex(a) for a in i2c.scan()]) # expect 0x18 (accel) + 0x1b or 0x54 (codec) s = SoftI2C(scl=Pin(15), sda=Pin(14), freq=100_000) print([hex(a) for a in s.scan()]) # cross-check ``` ## Restore the TE firmware when done ```bash picotool reboot -u -f picotool load ep-2350_firmware_1_0_8.uf2 -x # or drag-and-drop the .uf2 # If you need the flash partition data back too: # picotool load whole_flash_backup.bin -o 0x10000000 ``` Note the restore brings back the **firmware image**; if TE kept calibration in a flash filesystem partition, that only comes back from `whole_flash_backup.bin` (step 2) — which is exactly why you take it. --- Two reminders on the gates: **step 1 (OTP)** is the one that can hard-block this whole approach, and it's irreversible, so read it carefully before step 3. **Step 2 (backup)** protects any factory data in flash. If OTP turns out to enforce secure boot, the fallback is staying on the TE firmware with the read-only passive probes (T4) — no reflash. Want me to add a small `picotool otp`-parsing helper, or wire the scan into your existing `scratch_repl.py` driver so it runs end-to-end once stock MP is up? [2026-07-17 17:14:47] user: Can you please write that into ./firmware_testing_instructions.md for me [2026-07-17 17:15:18] tool use: Write file_path: /home/agent/TE_ting/firmware_testing_instructions.md content: # Running the pin-mapping tests under stock MicroPython (EP-2350 "Ting") Goal: temporarily replace the TE firmware with **stock MicroPython for the RP2350A** so the `machine` module is available and no competing firmware is driving the I²C bus, then run the T1–T3 scans from `pinmapping_findings.md`. The chip is a confirmed **RP2350A** (GPIO0–29 only). The tests address pins by GPIO number, so the generic Pico 2 build is sufficient — no custom board definition is required for `I2C.scan()`. --- ## The exact firmware to grab **`RPI_PICO2-20260406-v1.28.0.uf2`** — current stable (v1.28.0), **ARM** build for the RP2350A. - Direct: `https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2` - Page: `https://micropython.org/download/RPI_PICO2/` Deliberately **not**: - `RPI_PICO2-RISCV` — wrong core (we want the Cortex-M33 / ARM build, matching the TE firmware's UF2 family `0xe48bff59` = RP2350 ARM-S). - `RPI_PICO2-W` — pulls in CYW43 wireless this board doesn't have. - the v1.29 previews — no reason to chase a nightly for an `I2C.scan()`. Download on the host: ```bash cd ~/Downloads wget https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2 ``` --- ## Two things that can block or break this route 1. **Secure boot / OTP lock (the real risk, irreversible).** RP2350 can be OTP-configured to enforce **signed boot**. If TE burned that fuse, BOOTSEL will **reject unsigned stock MicroPython** and you are stuck — OTP is one-way. The TE UF2 using the ARM-**S**ecure family ID does *not* prove the fuse is set (they could sign images without locking it). **Check OTP before reflashing** (step 1 below). If enforcement is set, do **not** reflash — use the read-only fallback (T4 in `pinmapping_findings.md`). 2. **Crystal frequency.** Stock `RPI_PICO2` assumes a **12 MHz XOSC**. If the EP-2350 uses a different crystal, stock firmware will mis-clock and **USB won't enumerate**. If it fails to come up as a serial device after flashing, that's a crystal mismatch (needs a custom board def), not a bad flash. Flash-size note: the RP2350 boots from **external QSPI flash**; stock `RPI_PICO2` assumes the Pico 2's **4 MB** part and lays its filesystem near the top of a 4 MB map. The I²C/PIO tests **don't touch the filesystem**, so a size mismatch is harmless for T1–T3. Reflashing does, however, overwrite the TE firmware and anything TE stored in the flash filesystem (calibration/user data lives in flash, not OTP) — hence the backup in step 2. --- ## Pre-flight checks + flash sequence (picotool) Do these **in order**. Steps 1–2 are the go/no-go gates; nothing writes to the board until step 3. ```bash # 0. Get into BOOTSEL: hold BOOTSEL while plugging USB, OR force it with # picotool (the TE firmware has no `machine` module, so machine.bootloader() # is not available there): picotool reboot -u -f # -u = reboot into BOOTSEL (USB mass storage) # -f = force even though the app is running picotool info -a # sanity: should show RP2350, flash size, etc. # 1. *** GO/NO-GO: is signed boot enforced in OTP? *** picotool otp dump # dump all OTP rows picotool otp get BOOT_FLAGS0 # <-- look for secure-boot enforcement # Key rows: BOOT_FLAGS0/1 and the CRIT1 (CRIT_SECURE_BOOT_ENABLE) bit. # If secure-boot-enable is SET -> stock UNSIGNED MicroPython will be REJECTED. # OTP is one-way; if it's set, STOP and use the read-only REPL route (T4). # 2. *** Back up the existing flash BEFORE overwriting *** # (Reflashing wipes TE fw + any flash-stored calibration/user partition.) picotool save -a whole_flash_backup.bin # save entire flash to a file # (or scope it: picotool save -r 0x10000000 0x10400000 fw_region.bin ) # 3. Flash stock MicroPython (only after 1 & 2 pass) picotool load RPI_PICO2-20260406-v1.28.0.uf2 -x # -x = execute/reboot after load # ...or simply drag-and-drop the .uf2 onto the RP2350 mass-storage drive. ``` After it reboots, MicroPython should enumerate as a USB serial REPL (this is the 12 MHz crystal assumption — a failure to enumerate points at a crystal mismatch). --- ## Run the scan (paste over the REPL) Use the existing `scratch_repl.py` driver, or any serial terminal. On stock firmware the `machine` module is back: ```python from machine import Pin, I2C, SoftI2C i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=100_000) print([hex(a) for a in i2c.scan()]) # expect 0x18 (accel) + 0x1b or 0x54 (codec) s = SoftI2C(scl=Pin(15), sda=Pin(14), freq=100_000) print([hex(a) for a in s.scan()]) # cross-check ``` Success = the scan returns the NAU88L21 address (`0x1b` = CSB0, `0x54` = CSB1), which proves GPIO14/15 and reveals the CSB strap. See T2/T3 in `pinmapping_findings.md` for positively identifying the codec by ID register and disambiguating BCLK/LRCLK/MCLK. --- ## Restore the TE firmware when done ```bash picotool reboot -u -f picotool load ep-2350_firmware_1_0_8.uf2 -x # or drag-and-drop the .uf2 # If you need the flash partition data back too: # picotool load whole_flash_backup.bin -o 0x10000000 ``` Restore brings back the **firmware image**; if TE kept calibration in a flash filesystem partition, that only returns from `whole_flash_backup.bin` (step 2) — which is exactly why you take it. --- ## If OTP blocks reflashing Fall back to the **read-only passive probes (T4)** on the running TE firmware — re-run the scratch dump scripts, which never disturb the bus. Do **not** bit-bang or reconfigure I2C1 live (both attempts crashed the running firmware because its own driver was mid-transaction). [2026-07-17 17:15:22] tool result: Write File created successfully at: /home/agent/TE_ting/firmware_testing_instructions.md (file state is current in your context — no need to Read it back) [2026-07-17 17:15:28] agent: Written to `./firmware_testing_instructions.md`. It captures the whole flow: - **Firmware pick** — `RPI_PICO2-20260406-v1.28.0.uf2` (v1.28.0 stable, ARM), with the direct link and why not RISC-V / -W / previews. - **The two gates** — OTP secure-boot check (irreversible, go/no-go) and the 12 MHz crystal caveat, plus the flash-size note. - **Ordered picotool sequence** — BOOTSEL → OTP dump → flash backup → flash, with nothing writing to the board until the gates pass. - **The REPL scan**, and the **restore** procedure including the partition-data restore from the backup. - **Fallback** to the read-only T4 probes if OTP blocks reflashing. Want me to also wire the scan into `scratch_repl.py` so it runs end-to-end once stock MP enumerates? [2026-07-17 17:26:49] user: I have the device in bootloader mode. It appears as "./media/timc/TING BOOT". But the picotool info command has this output: ```$ picotool info -a No accessible RP-series devices in BOOTSEL mode were found.``` [2026-07-17 17:27:29] thinking: [not recorded in transcript] [2026-07-17 17:27:45] agent: Two different USB interfaces are in play here, and that's the clue. When a Pi-series chip is in BOOTSEL, it normally exposes **both** a UF2 mass-storage drive *and* a **PICOBOOT vendor interface**. Drag-and-drop uses the drive; **picotool uses PICOBOOT**. You're seeing the drive mount fine (`TING BOOT`) but picotool can't reach PICOBOOT — which is either a permissions issue or a sign the bootloader isn't exposing PICOBOOT at all. Work through these on your machine (use `! ` to run them in-session if you like): **1. Permissions — the most common Linux cause. Try first:** ```bash sudo picotool info -a ``` If that works, it's just udev. Install the rules so you don't need sudo: ```bash # picotool ships these; location varies by install sudo cp /usr/share/picotool/udev/99-picotool.rules /etc/udev/rules.d/ 2>/dev/null \ || sudo cp $(dirname $(which picotool))/../share/picotool/udev/99-picotool.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules && sudo udevadm trigger ``` **2. Is the PICOBOOT interface even there?** ```bash lsusb | grep -i 2e8a ``` - `2e8a:000f` = RP2350 in BOOTSEL — the PICOBOOT interface is present, so this is a permissions problem (go back to step 1 / `sudo`). - If you see a `2e8a:` device but **not** `000f`, or you only see a mass-storage device with no `2e8a` vendor interface, then this bootloader is **mass-storage-only** and picotool can't talk to it — see step 4. **3. Identify what this bootloader actually is:** ```bash cat "/media/timc/TING BOOT/INFO_UF2.TXT" ``` This is important. The **stock RP2350 bootrom labels its drive `RP2350`**, not `TING BOOT`. A custom label means TE shipped a **customized bootloader**, and `INFO_UF2.TXT` will tell you the model, board-ID, and — crucially — the **UF2 family it accepts**. Please paste that file's contents back to me. **Why this matters:** a custom "TING BOOT" bootloader ties directly to the secure-boot risk I flagged. If it only accepts TE-signed UF2s, dropping stock MicroPython on the drive will **silently fail** (the file copies, the device reboots, and it comes back running the old firmware or re-mounts as bootloader) — no error, just no change. And if it doesn't expose PICOBOOT, the OTP check and flash backup (steps 1–2 of the plan) aren't possible via picotool. **4. If PICOBOOT genuinely isn't available:** you can still *attempt* the flash by drag-and-drop: ```bash cp ~/Downloads/RPI_PICO2-20260406-v1.28.0.uf2 "/media/timc/TING BOOT/" sync ``` Watch what happens: if the drive unmounts and the board re-enumerates as a **USB serial device**, stock MicroPython took — great. If it re-mounts as `TING BOOT` again, the bootloader **rejected** it (signing/family lock) — stop and fall back to the read-only T4 probes on the TE firmware. Start with `sudo picotool info -a` and the `INFO_UF2.TXT` dump, and paste both back — that'll tell us whether this is a five-second permissions fix or a locked custom bootloader. [2026-07-17 17:30:11] user: sudo picotool info outputs the same message. Here are the contents of INFO_UF2.TXT: ```UF2 Bootloader v1.0 Model: EP-2350 Board-ID: 830002036254102695``` [2026-07-17 17:31:05] thinking: [not recorded in transcript] [2026-07-17 17:31:22] agent: That `INFO_UF2.TXT` is decisive, and it changes the picture: **this is not the RP2350's ROM BOOTSEL — it's a custom, flash-resident TE bootloader.** The RP2350 boot ROM always reports `Model: Raspberry Pi RP2350` / `Board-ID: RP2350-B...` — those strings live in silicon and can't be changed. `Model: EP-2350` with a per-device numeric Board-ID and a `TING BOOT` volume label means TE shipped their **own UF2 bootloader in flash** (their firmware-update mode). That's exactly why picotool fails even with `sudo`: **picotool needs the ROM's PICOBOOT vendor interface, and a flash bootloader doesn't expose it.** No permissions fix will change that. Confirm quickly: ```bash lsusb ``` You'll almost certainly *not* see `2e8a:000f` (RP2350 ROM BOOTSEL). Whatever VID:PID the TING BOOT drive is — paste it — but the label + INFO already tell the story. ### What this means for the plan Two consequences, both important: 1. **The safety gates can't be run from here.** The OTP secure-boot check and the flash backup (steps 1–2) both require ROM BOOTSEL / PICOBOOT. From TE's own bootloader you can't dump OTP or image the flash. 2. **Do not drag stock MicroPython onto `TING BOOT`.** A custom bootloader flashes app UF2s to *its own expected offset and format*. The stock `RPI_PICO2` UF2 is based at `0x10000000` (the very bottom of flash), which is where the TE bootloader itself lives. Best case it's rejected for a format/address/signature mismatch; worst case it partially overwrites the bootloader and you lose your recovery path. Not worth the gamble. ### To do this properly, you need the real ROM BOOTSEL That's the mode picotool can see (`2e8a:000f`, drive labeled `RP2350`) and that cleanly accepts the standard MicroPython UF2. Options, in order of safety: - **How did you get into `TING BOOT`?** If it was a button combo or a menu, that's TE's *update* mode, not ROM BOOTSEL. Knowing the entry method helps — some vendor bootloaders have a "reboot to ROM/BOOTSEL" option. - **Physical BOOTSEL button / test pad** held during power-up. The pin-mapping findings never found one — check the board/schematic for a BOOTSEL pad. - **No-button fallback:** momentarily ground the **QSPI flash CS pin** at power-on. The ROM then finds no valid image and drops straight to ROM USB boot → `2e8a:000f` appears and picotool works. Standard trick, but it means probing the flash chip's CS. ### Honest reassessment The reflash route just got materially riskier: a custom bootloader in the mix, **no way to verify the OTP secure-boot gate**, and **no way to back up flash first**. My earlier "check OTP before you commit" gate is now un-checkable from this mode — which itself argues for caution. Given that, I'd lean toward **staying on the TE firmware and using the read-only T4 probes** unless you have a clean, confirmed path into ROM BOOTSEL (real button/pad, or you're comfortable grounding the flash CS). The passive route can't return the codec's ACK, but it also can't brick anything. Can you paste the `lsusb` line for the device and tell me how you entered `TING BOOT`? That determines whether a safe ROM-BOOTSEL path exists. [2026-07-17 17:35:06] user: lsusb output: ```Bus 001 Device 023: ID 2367:0620 Teenage Engineering EP-2350 Boot``` I entered TING BOOT by holding down the handle and double pressing the reset button. I found this in the firmware update instructions here: https://teenage.engineering/guides/ep-2350 [2026-07-17 17:35:15] thinking: [not recorded in transcript] [2026-07-17 17:35:17] tool use: WebFetch url: https://teenage.engineering/guides/ep-2350 prompt: Describe the firmware update process for the EP-2350. How do you enter bootloader/update mode? What file format is the firmware (e.g. .uf2)? Is there any mention of a BOOTSEL button, recovery mode, factory reset, or restoring firmware? Quote any details about how the firmware file is applied (drag-and-drop to a USB drive, an app, etc.). [2026-07-17 17:35:29] tool result: WebFetch # EP-2350 Firmware Update Process ## Entering Update Mode To access the bootloader, you must "remove the lower lid and connect with usb-c to your computer" while holding the device's handle. Then "double click the small button above the usb-c port." ## File Format & Application The guide does not specify the firmware file format (e.g., .uf2, .bin). However, it describes a straightforward installation method: "a disk called 'TING BOOT' or 'rp2350' will appear on your computer. drag and drop the firmware file here and wait until transferred and installed." ## Key Details - Firmware files are available on the downloads page - You must keep the handle pressed throughout the entire update process - The device automatically restarts when the installation completes - No mention of BOOTSEL buttons, recovery modes, factory reset procedures, or firmware restoration options The process is designed for simplicity—essentially a drag-and-drop file transfer to a mounted USB drive rather than requiring dedicated software or complex recovery procedures. [2026-07-17 17:37:06] thinking: [not recorded in transcript] [2026-07-17 17:37:26] agent: That confirms it. `2367:0620` is **Teenage Engineering's own USB vendor ID** (0x2367) — so `TING BOOT` is a TE-authored bootloader with a TE USB stack, not the Raspberry Pi ROM (`2e8a`). picotool will *never* see this mode, sudo or not. OTP check and flash backup are off the table from here. But the guide gave us one genuinely useful line: > "a disk called **'TING BOOT' or 'rp2350'** will appear… drag and drop the firmware file here" That tells us two important things: 1. **TE's firmware UF2 is a standard RP2350 UF2** — the *same file* works whether it lands on their `TING BOOT` bootloader or the raw `rp2350` ROM drive. That means it targets the normal base `0x10000000` with family `0xe48bff59` — **identical format to the stock MicroPython `RPI_PICO2` UF2.** So format-wise, `TING BOOT` may well accept the MicroPython file. 2. **The RP2350 ROM BOOTSEL (`rp2350`, `2e8a:000f`) is reachable on this board** — USB is wired to the ROM. That's your picotool-capable, guaranteed-recovery path if you ever need it. ### This de-risks the reflash more than I first thought Here's the recovery insight: **once stock MicroPython is running, it brings back `machine.bootloader()`**, which cleanly reboots the chip into the `rp2350` ROM BOOTSEL. From there you drag `ep-2350_firmware_1_0_8.uf2` and you're restored. So the return path does **not** depend on `TING BOOT` surviving — MicroPython provides its own software route back. So the failure modes of just dragging the MicroPython UF2 onto `TING BOOT` are: - **Rejected** (TE bootloader checks a signature/magic) → drive simply re-mounts as `TING BOOT`. **Zero harm.** You've learned it's locked; fall back to read-only T4. - **Accepted and boots** → MicroPython REPL appears; run the I²C scan; restore later via `machine.bootloader()` → drag TE UF2. Clean. - **Accepted but doesn't enumerate** — the one real hazard. If the EP-2350's crystal isn't 12 MHz, MicroPython flashes but never comes up as USB serial, so you can't call `machine.bootloader()`. `TING BOOT` may also be gone. **Recovery then needs the hardware route:** briefly ground the QSPI flash's CS pin at power-up to force the `rp2350` ROM BOOTSEL, then re-flash TE firmware. ### My recommendation The drag-drop experiment is reasonable **only if you're prepared for that last fallback** — i.e., the board is already open, and you're willing to momentarily ground the flash chip's CS pin to reach ROM BOOTSEL if MicroPython doesn't enumerate. If you're comfortable with that backstop, the experiment is worth it and its bad outcomes are all recoverable. If you'd rather not touch the flash chip with a probe under any circumstance, then the honest call is to **stay on the TE firmware and run the read-only T4 probes** — the I²C=GPIO14/15 mapping is already High-confidence; T1/T2 only add the codec's direct ACK and the CSB strap, which may not be worth even a small brick risk. Note one bonus: if `TING BOOT` *accepts* the UF2 and MicroPython *runs*, that empirically proves OTP secure-boot signing is **not** enforced (an unsigned image executed) — the very gate we couldn't check with picotool. How do you want to proceed? - **A —** Try the drag-drop onto `TING BOOT` now (you're OK with the flash-CS recovery fallback if needed). - **B —** Stay on TE firmware, read-only T4 probes only (zero brick risk). - **C —** First locate/verify the QSPI flash CS pin (and ideally the crystal value) so recovery is fully de-risked *before* trying A. I'd lean **A if you have a fine-tip probe and a steady hand**, otherwise **B**. Want me to update `firmware_testing_instructions.md` to reflect all this — the TE custom bootloader, the drag-drop path, and the `machine.bootloader()` recovery? [2026-07-17 17:56:05] user: please update firmware_testing_instructions.md to reflect these findings. We'll try to figure out details about the crystal and QSPI flash in a future session. [2026-07-17 17:56:07] tool use: Read file_path: /home/agent/TE_ting/firmware_testing_instructions.md [2026-07-17 17:56:07] tool result: Read 1 # Running the pin-mapping tests under stock MicroPython (EP-2350 "Ting") 2 3 Goal: temporarily replace the TE firmware with **stock MicroPython for the 4 RP2350A** so the `machine` module is available and no competing firmware is 5 driving the I²C bus, then run the T1–T3 scans from `pinmapping_findings.md`. 6 7 The chip is a confirmed **RP2350A** (GPIO0–29 only). The tests address pins by 8 GPIO number, so the generic Pico 2 build is sufficient — no custom board 9 definition is required for `I2C.scan()`. 10 11 --- 12 13 ## The exact firmware to grab 14 15 **`RPI_PICO2-20260406-v1.28.0.uf2`** — current stable (v1.28.0), **ARM** build 16 for the RP2350A. 17 18 - Direct: `https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2` 19 - Page: `https://micropython.org/download/RPI_PICO2/` 20 21 Deliberately **not**: 22 23 - `RPI_PICO2-RISCV` — wrong core (we want the Cortex-M33 / ARM build, matching 24 the TE firmware's UF2 family `0xe48bff59` = RP2350 ARM-S). 25 - `RPI_PICO2-W` — pulls in CYW43 wireless this board doesn't have. 26 - the v1.29 previews — no reason to chase a nightly for an `I2C.scan()`. 27 28 Download on the host: 29 30 ```bash 31 cd ~/Downloads 32 wget https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2 33 ``` 34 35 --- 36 37 ## Two things that can block or break this route 38 39 1. **Secure boot / OTP lock (the real risk, irreversible).** RP2350 can be 40 OTP-configured to enforce **signed boot**. If TE burned that fuse, BOOTSEL 41 will **reject unsigned stock MicroPython** and you are stuck — OTP is 42 one-way. The TE UF2 using the ARM-**S**ecure family ID does *not* prove the 43 fuse is set (they could sign images without locking it). **Check OTP before 44 reflashing** (step 1 below). If enforcement is set, do **not** reflash — use 45 the read-only fallback (T4 in `pinmapping_findings.md`). 46 47 2. **Crystal frequency.** Stock `RPI_PICO2` assumes a **12 MHz XOSC**. If the 48 EP-2350 uses a different crystal, stock firmware will mis-clock and **USB 49 won't enumerate**. If it fails to come up as a serial device after flashing, 50 that's a crystal mismatch (needs a custom board def), not a bad flash. 51 52 Flash-size note: the RP2350 boots from **external QSPI flash**; stock 53 `RPI_PICO2` assumes the Pico 2's **4 MB** part and lays its filesystem near the 54 top of a 4 MB map. The I²C/PIO tests **don't touch the filesystem**, so a size 55 mismatch is harmless for T1–T3. Reflashing does, however, overwrite the TE 56 firmware and anything TE stored in the flash filesystem (calibration/user data 57 lives in flash, not OTP) — hence the backup in step 2. 58 59 --- 60 61 ## Pre-flight checks + flash sequence (picotool) 62 63 Do these **in order**. Steps 1–2 are the go/no-go gates; nothing writes to the 64 board until step 3. 65 66 ```bash 67 # 0. Get into BOOTSEL: hold BOOTSEL while plugging USB, OR force it with 68 # picotool (the TE firmware has no `machine` module, so machine.bootloader() 69 # is not available there): 70 picotool reboot -u -f # -u = reboot into BOOTSEL (USB mass storage) 71 # -f = force even though the app is running 72 picotool info -a # sanity: should show RP2350, flash size, etc. 73 74 # 1. *** GO/NO-GO: is signed boot enforced in OTP? *** 75 picotool otp dump # dump all OTP rows 76 picotool otp get BOOT_FLAGS0 # <-- look for secure-boot enforcement 77 # Key rows: BOOT_FLAGS0/1 and the CRIT1 (CRIT_SECURE_BOOT_ENABLE) bit. 78 # If secure-boot-enable is SET -> stock UNSIGNED MicroPython will be REJECTED. 79 # OTP is one-way; if it's set, STOP and use the read-only REPL route (T4). 80 81 # 2. *** Back up the existing flash BEFORE overwriting *** 82 # (Reflashing wipes TE fw + any flash-stored calibration/user partition.) 83 picotool save -a whole_flash_backup.bin # save entire flash to a file 84 # (or scope it: picotool save -r 0x10000000 0x10400000 fw_region.bin ) 85 86 # 3. Flash stock MicroPython (only after 1 & 2 pass) 87 picotool load RPI_PICO2-20260406-v1.28.0.uf2 -x # -x = execute/reboot after load 88 # ...or simply drag-and-drop the .uf2 onto the RP2350 mass-storage drive. 89 ``` 90 91 After it reboots, MicroPython should enumerate as a USB serial REPL (this is the 92 12 MHz crystal assumption — a failure to enumerate points at a crystal 93 mismatch). 94 95 --- 96 97 ## Run the scan (paste over the REPL) 98 99 Use the existing `scratch_repl.py` driver, or any serial terminal. On stock 100 firmware the `machine` module is back: 101 102 ```python 103 from machine import Pin, I2C, SoftI2C 104 i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=100_000) 105 print([hex(a) for a in i2c.scan()]) # expect 0x18 (accel) + 0x1b or 0x54 (codec) 106 s = SoftI2C(scl=Pin(15), sda=Pin(14), freq=100_000) 107 print([hex(a) for a in s.scan()]) # cross-check 108 ``` 109 110 Success = the scan returns the NAU88L21 address (`0x1b` = CSB0, `0x54` = CSB1), 111 which proves GPIO14/15 and reveals the CSB strap. See T2/T3 in 112 `pinmapping_findings.md` for positively identifying the codec by ID register and 113 disambiguating BCLK/LRCLK/MCLK. 114 115 --- 116 117 ## Restore the TE firmware when done 118 119 ```bash 120 picotool reboot -u -f 121 picotool load ep-2350_firmware_1_0_8.uf2 -x # or drag-and-drop the .uf2 122 # If you need the flash partition data back too: 123 # picotool load whole_flash_backup.bin -o 0x10000000 124 ``` 125 126 Restore brings back the **firmware image**; if TE kept calibration in a flash 127 filesystem partition, that only returns from `whole_flash_backup.bin` (step 2) — 128 which is exactly why you take it. 129 130 --- 131 132 ## If OTP blocks reflashing 133 134 Fall back to the **read-only passive probes (T4)** on the running TE firmware — 135 re-run the scratch dump scripts, which never disturb the bus. Do **not** 136 bit-bang or reconfigure I2C1 live (both attempts crashed the running firmware 137 because its own driver was mid-transaction). 138 [2026-07-17 17:56:13] thinking: [not recorded in transcript] [2026-07-17 17:56:58] tool use: Write file_path: /home/agent/TE_ting/firmware_testing_instructions.md content: # Running the pin-mapping tests under stock MicroPython (EP-2350 "Ting") Goal: temporarily replace the TE firmware with **stock MicroPython for the RP2350A** so the `machine` module is available and no competing firmware is driving the I²C bus, then run the T1–T3 scans from `pinmapping_findings.md`. The chip is a confirmed **RP2350A** (GPIO0–29 only). The tests address pins by GPIO number, so the generic Pico 2 build is sufficient — no custom board definition is required for `I2C.scan()`. > **Status (2026-07-17):** The board uses a **custom Teenage Engineering > bootloader**, not the RP2350 ROM BOOTSEL — so **picotool cannot be used** for > this board in its normal update mode (see below). The viable path is > **drag-and-drop onto the `TING BOOT` drive**. Two hardware unknowns remain to > be settled in a future session: the **crystal frequency** and the **QSPI flash > chip / CS pin** (needed for worst-case recovery). --- ## Key discovery: the bootloader is TE's own, not the RP2350 ROM Entering update mode (**hold the handle + double-click the small button above USB-C**, per ) mounts a drive called **`TING BOOT`** with `INFO_UF2.TXT`: ``` UF2 Bootloader v1.0 Model: EP-2350 Board-ID: 830002036254102695 ``` and enumerates on USB as: ``` ID 2367:0620 Teenage Engineering EP-2350 Boot ``` - USB VID **`2367` = Teenage Engineering** (not `2e8a` = Raspberry Pi). The RP2350 boot ROM's strings (`Model: Raspberry Pi RP2350`) live in silicon and cannot be changed — so this is a **flash-resident custom TE bootloader**, i.e. TE's firmware-update mode, **not** the chip's ROM BOOTSEL. - **Consequence: `picotool` will never see this mode** (it needs the ROM's PICOBOOT vendor interface, which a custom bootloader doesn't expose). `sudo` does not help. Therefore **the OTP secure-boot check and the flash backup are NOT possible from here.** The earlier picotool-based plan does not apply to this board. ### But the format is standard, so drag-drop may just work TE's own guide says the update drive appears as **"`TING BOOT` or `rp2350`"** and you drag the *same* firmware file to either. That means TE firmware UF2s are **standard RP2350 UF2s** (base `0x10000000`, family `0xe48bff59` = RP2350 ARM-S) — **identical in format to the stock MicroPython `RPI_PICO2` UF2**. So the `TING BOOT` bootloader may well accept the MicroPython file. It also confirms the **RP2350 ROM BOOTSEL (`rp2350`, USB `2e8a:000f`) is reachable on this board** (USB is wired to the ROM) — that is the picotool-capable, guaranteed-recovery path if ever needed. --- ## The exact firmware to grab **`RPI_PICO2-20260406-v1.28.0.uf2`** — current stable (v1.28.0), **ARM** build for the RP2350A. - Direct: `https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2` - Page: `https://micropython.org/download/RPI_PICO2/` Deliberately **not**: - `RPI_PICO2-RISCV` — wrong core (we want the Cortex-M33 / ARM build, matching the TE firmware's UF2 family `0xe48bff59` = RP2350 ARM-S). - `RPI_PICO2-W` — pulls in CYW43 wireless this board doesn't have. - the v1.29 previews — no reason to chase a nightly for an `I2C.scan()`. Download on the host: ```bash cd ~/Downloads wget https://micropython.org/resources/firmware/RPI_PICO2-20260406-v1.28.0.uf2 ``` --- ## Risk assessment for the drag-drop route Since we cannot pre-check OTP or back up flash, understand the failure modes before trying. **Good news: recovery does NOT depend on `TING BOOT` surviving** — once stock MicroPython runs, `machine.bootloader()` reboots the chip straight into the `rp2350` ROM BOOTSEL, from which you drag the TE UF2 to restore. Dragging the MicroPython UF2 onto `TING BOOT` can end three ways: 1. **Rejected** (TE bootloader checks a signature/magic) → the drive simply re-mounts as `TING BOOT`. **Zero harm.** It's locked → fall back to read-only T4. 2. **Accepted and boots** → MicroPython REPL appears; run the scan; restore later via `machine.bootloader()` → drag TE UF2. Clean. (This outcome also *empirically proves OTP secure-boot is NOT enforced* — an unsigned image ran.) 3. **Accepted but does NOT enumerate** — the one real hazard. If the EP-2350's crystal isn't 12 MHz, MicroPython flashes but never comes up as USB serial, so `machine.bootloader()` is unreachable, and `TING BOOT` may be gone too. **Recovery then requires the hardware route** (below). ### ⚠️ Prerequisites still to settle (future session) - **Crystal frequency.** Stock `RPI_PICO2` assumes a **12 MHz XOSC**. If the EP-2350's crystal differs, expect outcome #3. Verify from schematic/BOM before relying on this. - **QSPI flash chip + CS pin location.** Needed for the hardware recovery route. Identify the flash IC and its CS pad on the (already-opened) board. **Do not attempt the reflash until you are prepared for the hardware-recovery fallback**, i.e. you can briefly ground the QSPI flash CS pin at power-up to force the `rp2350` ROM BOOTSEL. If you are not willing to probe the flash chip, use the read-only T4 route instead (zero brick risk). --- ## Procedure — drag-and-drop route ``` 1. Enter TE update mode: remove the lower lid, connect USB-C, HOLD the handle, then double-click the small button above the USB-C port. -> a drive named "TING BOOT" appears. Keep holding the handle per TE's guide. 2. Copy the MicroPython UF2 onto that drive: cp ~/Downloads/RPI_PICO2-20260406-v1.28.0.uf2 "/media//TING BOOT/" sync Then watch: - Drive re-mounts as TING BOOT, unchanged -> REJECTED (outcome #1). Go to T4. - Board reboots and a USB SERIAL device appears -> SUCCESS (outcome #2). Go to step 3. - Board reboots but NOTHING enumerates -> outcome #3. Hardware recovery. 3. Run the scan over the new MicroPython REPL (see below). ``` --- ## Run the scan (paste over the REPL) Use the existing `scratch_repl.py` driver, or any serial terminal. On stock firmware the `machine` module is back: ```python from machine import Pin, I2C, SoftI2C i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=100_000) print([hex(a) for a in i2c.scan()]) # expect 0x18 (accel) + 0x1b or 0x54 (codec) s = SoftI2C(scl=Pin(15), sda=Pin(14), freq=100_000) print([hex(a) for a in s.scan()]) # cross-check ``` Success = the scan returns the NAU88L21 address (`0x1b` = CSB0, `0x54` = CSB1), which proves GPIO14/15 and reveals the CSB strap. See T2/T3 in `pinmapping_findings.md` for positively identifying the codec by ID register and disambiguating BCLK/LRCLK/MCLK. --- ## Restore the TE firmware when done **If MicroPython is running (normal case):** ```python import machine machine.bootloader() # reboots into the rp2350 ROM BOOTSEL (USB 2e8a:000f) ``` Then drag `ep-2350_firmware_1_0_8.uf2` onto the `rp2350` drive (or, since this is real ROM BOOTSEL, `picotool load ep-2350_firmware_1_0_8.uf2 -x` now works). **Alternatively**, if `TING BOOT` still enters via handle + double-click after the foreign flash, just drag the TE UF2 there as usual. --- ## Hardware recovery (outcome #3 — MicroPython flashed but no USB) Only needed if the reflash left the board with no working firmware and no way into a bootloader: 1. Power off. **Momentarily ground the QSPI flash chip's CS pin** while applying power. With no readable image, the RP2350 ROM falls through to USB boot and presents the **`rp2350`** drive (`2e8a:000f`). 2. Release CS, then restore TE firmware: drag `ep-2350_firmware_1_0_8.uf2` onto `rp2350`, or `picotool load ep-2350_firmware_1_0_8.uf2 -x`. > Requires knowing the flash IC and CS pad location — **still to be identified** > (see prerequisites). Do not attempt the reflash without this recovery path > understood. --- ## Zero-risk alternative: read-only T4 probes If any of the above risk is unacceptable, **stay on the TE firmware** and run the read-only passive probes (T4 in `pinmapping_findings.md`): re-run the scratch dump scripts, which never disturb the bus. The I²C = GPIO14/15 mapping is already High-confidence; T1/T2 only add the codec's direct ACK and the CSB strap. Do **not** bit-bang or reconfigure I2C1 live — both attempts crashed the running firmware because its own driver was mid-transaction. [2026-07-17 17:57:11] tool result: Write The file /home/agent/TE_ting/firmware_testing_instructions.md has been updated successfully. (file state is current in your context — no need to Read it back) [2026-07-17 17:57:14] thinking: [not recorded in transcript] [2026-07-17 17:57:20] agent: Done. `firmware_testing_instructions.md` now reflects the actual situation. Key changes: - **New "Key discovery" section** up top documenting that `TING BOOT` is TE's custom bootloader (`2367:0620`, `Model: EP-2350`), **not** the RP2350 ROM — so picotool is out, and the OTP check / flash backup are impossible from this mode. - **The picotool-based plan is removed** and replaced with the **drag-and-drop route** onto `TING BOOT`, including the guide's "`TING BOOT` or `rp2350`" detail that tells us the UF2 format matches stock MicroPython. - **Three-outcome risk assessment** (rejected / boots / flashed-but-no-USB) with the crucial recovery insight: `machine.bootloader()` gets you back to ROM BOOTSEL once MicroPython runs, so recovery doesn't depend on `TING BOOT` surviving. - **Two prerequisites flagged for the future session** — crystal frequency and QSPI flash chip/CS-pin location — called out as blockers for the worst-case hardware recovery. - **Restore** section rewritten around `machine.bootloader()`, plus a **hardware recovery** section (ground flash CS) and the **zero-risk T4** alternative. Since this bootloader fact is non-obvious and will matter next session, let me save it to memory: [2026-07-17 17:57:30] tool use: Write file_path: /home/agent/.claude/projects/-home-agent-TE-ting/memory/ep2350-custom-bootloader.md content: --- name: ep2350-custom-bootloader description: EP-2350 update mode is TE's own flash bootloader (not RP2350 ROM) — picotool can't be used metadata: type: project --- The EP-2350 "TING BOOT" update mode is a **custom Teenage Engineering flash-resident bootloader**, NOT the RP2350 ROM BOOTSEL. Evidence: enter via hold-handle + double-click the button above USB-C (per ). Drive `TING BOOT`; `INFO_UF2.TXT` = `UF2 Bootloader v1.0 / Model: EP-2350 / Board-ID: 830002036254102695`; USB `2367:0620` (VID 2367 = Teenage Engineering, not 2e8a = Raspberry Pi). RP2350 ROM strings are fixed in silicon, so a custom Model string proves it's a flash bootloader. **Why:** picotool needs the ROM PICOBOOT interface, which this mode does NOT expose → picotool is unusable here (sudo doesn't help), so no OTP check and no flash backup from this mode. **How to apply:** To flash stock MicroPython (see [[ep2350-repl-access]] and `firmware_testing_instructions.md`), drag-drop the `RPI_PICO2` UF2 onto `TING BOOT` (TE UF2s are standard RP2350 format, base 0x10000000, family 0xe48bff59 — same as MicroPython, so it may be accepted). Recovery after a successful flash: `machine.bootloader()` → `rp2350` ROM BOOTSEL (`2e8a:000f`) → drag `ep-2350_firmware_1_0_8.uf2`. Worst case (flashed but no USB, e.g. non-12MHz crystal): ground QSPI flash CS at power-up to force ROM BOOTSEL. **Unsettled prerequisites** (future session): crystal frequency and QSPI flash chip/CS-pin location. [2026-07-17 17:57:30] tool result: Write File created successfully at: /home/agent/.claude/projects/-home-agent-TE-ting/memory/ep2350-custom-bootloader.md (file state is current in your context — no need to Read it back) [2026-07-17 17:57:32] tool use: Read file_path: /home/agent/.claude/projects/-home-agent-TE-ting/memory/MEMORY.md [2026-07-17 17:57:32] tool result: Read 1 - [EP-2350 REPL access](ep2350-repl-access.md) — use pyserial raw-REPL (not mpremote); no `machine` module; never actively drive pins on live firmware 2 - [EP-2350 pin map](ep2350-pin-map.md) — I²C=I2C1 GPIO14/15; I²S GPIO8-11 + MCLK GPIO16 3 [2026-07-17 17:57:40] tool use: Edit replace_all: false file_path: /home/agent/.claude/projects/-home-agent-TE-ting/memory/MEMORY.md old_string: - [EP-2350 pin map](ep2350-pin-map.md) — I²C=I2C1 GPIO14/15; I²S GPIO8-11 + MCLK GPIO16 new_string: - [EP-2350 pin map](ep2350-pin-map.md) — I²C=I2C1 GPIO14/15; I²S GPIO8-11 + MCLK GPIO16 - [EP-2350 custom bootloader](ep2350-custom-bootloader.md) — TING BOOT is TE's flash bootloader (2367:0620), not RP2350 ROM; picotool unusable; drag-drop UF2 [2026-07-17 17:57:40] tool result: Edit The file /home/agent/.claude/projects/-home-agent-TE-ting/memory/MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back) [2026-07-17 17:57:48] agent: Updated the instructions file and saved the bootloader discovery to memory (with the crystal + flash-CS prerequisites noted for next session). Summary of where things stand: - **`TING BOOT` = TE custom bootloader** (`2367:0620`), not RP2350 ROM → picotool can't be used; OTP/backup can't be done from this mode. - **Path forward is drag-drop** the `RPI_PICO2` UF2 onto `TING BOOT`; format matches so it may be accepted. - **Recovery** is `machine.bootloader()` once MicroPython runs; hardware fallback (ground flash CS) only if it flashes but won't enumerate. - **Two things to pin down next session** before attempting: crystal frequency (12 MHz?) and the QSPI flash chip / CS-pin location. When you're ready to tackle the crystal and flash details, I can help identify them from the board photos, a schematic/BOM, or by reading the flash JEDEC ID.