# EP-2350 LED pin map — SOLVED

Session date: 2026-07-19. Method: stock MicroPython v1.25.0 (RPI_PICO2) over the
raw REPL (`scratchpad/rp.py`), driving candidate GPIOs one at a time while a USB
camera photographed the LED bar. Photos under
`scratchpad/` (`grid_e.jpg` is the decisive one-LED-at-a-time strip).

## TL;DR

Eight LEDs, **eight dedicated GPIOs, one per LED, active HIGH**. No matrix, no
charlieplexing, no shift register, no enable line.

| Position (top → bottom) | Red column | White column |
|---|---:|---:|
| 1 (nearest handle) | **GP16** | **GP0** |
| 2 | **GP24** | **GP6** |
| 3 | **GP18** | **GP4** |
| 4 | **GP17** | **GP5** |

Note the columns are **not** in GPIO order — GP6 sits above GP4/GP5 in the white
column, and the red column runs 16, 24, 18, 17.

```python
from machine import Pin
Pin(2, Pin.OUT, value=1)                       # power latch first, always

RED   = [16, 24, 18, 17]                       # top -> bottom
WHITE = [0,  6,  4,  5]                        # top -> bottom
leds  = [Pin(n, Pin.OUT, value=0) for n in RED + WHITE]

leds[0].value(1)          # top red on
```

## Control scheme

- **Drive HIGH = on, LOW = off.** LED anode on the GPIO, cathode to ground
  through a series resistor (each LED has its own).
- **Full brightness is just a digital `1`.** Pad drive strength makes no
  difference: setting `PADS_BANK0` DRIVE from the default 4 mA to 12 mA on all
  eight produced **no visible change**, so the series resistors set the current,
  not the pad. Don't bother raising drive strength.
- **Dimming is plain PWM** and works well. `PWM(Pin(n)); freq(1000);
  duty_u16(...)` gave a clean, monotonic brightness range; duty ≈ 1200/65535 is
  a comfortable "dim indicator" level and is what most of the photos above use.
- **High-Z (`Pin.IN`, no pull) = fully off.** Verified after first driving the
  pins low, to defeat pad retention.

### ⚠️ PWM gotcha: GP0 and GP16 share a PWM slice/channel

Configuring PWM on **GP16** and on **GP0** targets the *same* slice+channel, so
the second `duty_u16()` silently overrides the first, and both pins output the
same waveform. This bit the first mapping run: a sweep that put *all eight* pins
into PWM function made the top white LED (GP0) follow GP16.

Consequences:

- **Top red and top white cannot be PWM-dimmed independently.** Use PWM on one
  and plain on/off (SIO) on the other, or bit-bang one in software.
- The mirroring only appears when a pin's function is actually muxed to PWM. If
  GP0 is left as a normal `Pin.OUT`, PWM on GP16 does not affect it.

## Why all 8 were glowing dimly at session start

Not a hardware feature — it was **leftover state from the button/knob sessions**,
which configure every GPIO as `Pin.IN, Pin.PULL_UP`. The internal pull-up
(~50–80 kΩ) sources roughly 40 µA into each LED, which is plenty for a visible
dim glow. Reproduced exactly: setting the eight pins back to `Pin.IN,
Pin.PULL_UP` recreates the original dim look (`scratchpad/H_pullup.jpg` vs
`baseline.jpg`); `Pin.IN, None` after a driven low gives full off.

So a "dim standby glow" is available for free without PWM if you ever want it —
just enable the internal pull-up instead of driving the pin.

## Evidence

1. **Candidate set from the TE-firmware pin snapshot** (`pinmapping_findings.md`
   §4): the unexplained SIO-driven pins were GP0, 4, 5, 6, 17, 18, 24, 25 —
   exactly eight, which looked like a perfect fit.
2. **All eight driven high → all LEDs bright**, confirming active-high.
3. **One-at-a-time sweep** gave a clean 1:1 mapping for seven of them, but
   **GP25 lit nothing**, and the top red stayed lit no matter what. So GP25 is
   *not* an LED and the candidate set was wrong by one.
4. **Second sweep over the remaining pins** (7, 12, 13, 16, 22, 23, 26, 27):
   driving **GP16 low turned the top red off**; driving it high turned it on.
   GP16 is the 4th red.
5. **Re-mapped all eight at low PWM duty**, one pin in PWM function at a time
   with the rest driven low, to eliminate camera bloom — this is the strip that
   produced the table above (`grid_e.jpg`).
6. **Cross-checked with pair patterns**: `{GP4, GP5, GP17, GP18}` lit the bottom
   two of each column; `{GP0, GP6, GP16, GP24}` lit the top two
   (`grid_xy.jpg`). Both match the table.

### Method note: the camera will lie to you

Two traps, both of which produced a wrong intermediate map:

- **Bloom.** At full brightness the LEDs flare into their neighbours, and a
  single lit LED reads as two or three. Every ambiguous frame here was resolved
  by dropping to PWM duty ~1200 and `eq=brightness=-0.30` on the ffmpeg filter
  chain. Do the mapping dim, not bright.
- **Auto-exposure.** The camera re-gains between shots, so "this LED looks
  brighter than in the last photo" means nothing across frames. Only
  within-frame comparisons are valid.

(A stale-first-frame effect was suspected and ruled out: capturing 15 frames and
keeping the last reproduced the same result as `-frames 1`.)

## Resolves / updates other files

- `pinmapping_findings.md` §4 listed GP0 as *"dynamic: LED/switch"* (correct —
  it is the top white LED) and GP25 as *"likely status LED"* — **wrong**, GP25
  drives no LED and remains unidentified.
- `power_hold_findings.md` and `button_findings.md` both list **GP16** as an
  unexplained driven PIO clock output under TE firmware. It is the **top red
  LED**. The TE firmware presumably drives it from PIO for glitch-free
  brightness ramping alongside the other seven.

## Remaining unidentified GPIOs

GP7, GP12, GP13, GP22, GP23, GP25, GP26, GP27 — none of them light an LED
(swept both high and low with no visible change).
