Molecule Pin Mapping
Produce one canonical markdown file that, for a given molecule (KiCad project in adom-inc/adom-molecules or any .kicad_sch + .kicad_pcb pair), shows:
- Main IC (e.g.
U1) pin number → net → silkscreen label on the PCB perimeter contact (MCx) → datasheet alt-functions (F1 SPI / F2 UART / F3 I2C / F4 PWM / F9 misc / ADC). - For each chip wired through the molecule (sensors, PMICs, etc.): package Pin # / Pin Name / Net / "Connects to (host GPIO or rail)" / strap notes — pulled from the chip's datasheet and cross-checked against the firmware's
board_config.h.
Output goes to a single markdown file, normally MoleculePinMapping.md at the repo root the user is working in.
What the user asked
$ARGUMENTS
If $ARGUMENTS names a molecule path (GitHub URL, local dir, or molecule name), use it directly. Otherwise ask once with AskUserQuestion which molecule + which firmware tree to cross-reference; do not loop on clarifying questions.
Phase 1 — Acquire the KiCad files
Two cases:
A. Molecule lives in adom-inc/adom-molecules on GitHub
gh api repos/adom-inc/adom-molecules/contents/"<CATEGORY>/<MOLECULE_DIR>" \
--jq '.[] | {name, sha, size, type}'
Then pull the schematic and the PCB. The PCB is usually >1 MB so the --jq .content route hits a payload limit; use the git blob API instead with the sha you got above:
mkdir -p /tmp/mol && cd /tmp/mol
# small file: contents endpoint is fine
gh api repos/adom-inc/adom-molecules/contents/"<PATH>/<NAME>.kicad_sch" \
--jq .content | base64 -d > sch.kicad_sch
# large PCB: use git blob
gh api "repos/adom-inc/adom-molecules/git/blobs/<SHA>" \
--jq .content | base64 -d > pcb.kicad_pcb
Do not try curl on raw.githubusercontent.com for private repos — it 404s without auth. gh api carries the user's token.
B. Local files
If the user gives a path, just Read the .kicad_sch and .kicad_pcb directly (or use Bash cp to a working dir).
Phase 2 — Parse the PCB for net connectivity
The PCB is the authoritative source for pad ↔ net binding (the schematic uses hierarchical labels which are harder to resolve). Use this Python parser — it has worked on every Ray-style and JLCPCB-style KiCad 7/8 PCB tested so far:
import re
with open('pcb.kicad_pcb') as f:
text = f.read()
# net id -> name
nets = {int(m[1]): m[2] for m in re.finditer(r'\(net (\d+) "([^"]*)"\)', text)}
# walk balanced parens to extract every (footprint ...) block
def blocks(tag, src):
out = []; i = 0
while True:
idx = src.find(f'({tag} ', i)
if idx == -1: return out
d = 0; j = idx
while j < len(src):
c = src[j]
if c == '(': d += 1
elif c == ')':
d -= 1
if d == 0: out.append(src[idx:j+1]); i = j+1; break
j += 1
else: return out
footprints = blocks('footprint', text)
def get_ref(fp):
m = re.search(r'\(property "Reference" "([^"]*)"', fp); return m and m.group(1)
def get_value(fp):
m = re.search(r'\(property "Value" "([^"]*)"', fp); return m and m.group(1)
def get_pos(fp):
m = re.search(r'\n\s*\(at ([-\d.]+) ([-\d.]+)', fp)
return (float(m.group(1)), float(m.group(2))) if m else None
def get_pads(fp):
pads = []
for pm in re.finditer(r'\(pad "([^"]*)"', fp):
s = pm.start(); d = 0; k = s
while k < len(fp):
c = fp[k]
if c == '(': d += 1
elif c == ')':
d -= 1
if d == 0: break
k += 1
block = fp[s:k+1]
nm = re.search(r'\(net (\d+)', block)
pads.append((pm.group(1), int(nm.group(1)) if nm else None))
return pads
fps = {get_ref(fp): {'value': get_value(fp), 'pos': get_pos(fp), 'pads': get_pads(fp)}
for fp in footprints if get_ref(fp)}
Now you have a ref → {value, pos, pads:[(padname, netid)]} map plus a netid → name map. Everything else is queries.
Phase 3 — Identify the main IC and the perimeter contacts
- Main IC: usually
U1. Confirm by checkingvalue(e.g.RP2350_80QFN). - Perimeter contacts: reference designators starting with
MC(machine contact). They are placed footprints on the board edge. Sometimes the schematic declares moreMCreferences than are physically placed (those will be in the.kicad_schbut absent from.kicad_pcb— call that out as "(pad not placed)").
Build the mapping table by joining U1's pads to MC pads via shared netid:
# net -> [MCs on that net]
mc_on_net = {}
for ref, fp in fps.items():
if ref.startswith('MC'):
for p, n in fp['pads']:
if n: mc_on_net.setdefault(n, []).append(ref)
for padname, netid in fps['U1']['pads']:
netname = nets.get(netid, '')
mcs = mc_on_net.get(netid, [])
# → row: padname, netname, mcs
Phase 4 — Match silkscreen labels to perimeter contacts
The numbers/names you see printed on the board next to each MC live as (gr_text "..." (at X Y) (layer "F.SilkS")) entries (and sometimes B.SilkS). They are not stored on the footprint — you must spatial-match.
The trick that actually works:
- Compute the bounding box of all MC positions; this gives
xmin/xmax/ymin/ymax. - For each MC, decide which edge it sits on by which extreme it's closest to (Top / Bottom / Left / Right).
- Search inward from that edge for the nearest silk text, with a tight perpendicular window (≤ 0.9 mm) and a wider axial window (≤ 2.5 mm). The asymmetric window is what disambiguates between adjacent rows of labels — a centered nearest-neighbor search picks the wrong one when alt-function labels (e.g.
Tx0,Rx1) and bare numbers are stacked in the same column.
import math
labels = [(m.group(1), float(m.group(2)), float(m.group(3)), m.group(4))
for m in re.finditer(
r'\(gr_text\s+"([^"]*)"\s*\(at ([-\d.]+) ([-\d.]+)(?:\s+[-\d.]+)?\)[^()]*\(layer "([^"]*)"',
text)]
xs = [p['pos'][0] for r,p in fps.items() if r.startswith('MC')]
ys = [p['pos'][1] for r,p in fps.items() if r.startswith('MC')]
xmin,xmax,ymin,ymax = min(xs),max(xs),min(ys),max(ys)
def silk_for(mx, my):
edge = min([(mx-xmin,'L'),(xmax-mx,'R'),(my-ymin,'T'),(ymax-my,'B')])[1]
cands = []
for t,lx,ly,lay in labels:
if 'SilkS' not in lay: continue
dx, dy = lx-mx, ly-my
if edge=='T' and 0.3<dy<2.5 and abs(dx)<0.9: cands.append((abs(dx), t))
elif edge=='B' and -2.5<dy<-0.3 and abs(dx)<0.9: cands.append((abs(dx), t))
elif edge=='L' and 0.3<dx<2.5 and abs(dy)<0.9: cands.append((abs(dy), t))
elif edge=='R' and -2.5<dx<-0.3 and abs(dy)<0.9: cands.append((abs(dy), t))
cands.sort()
return cands[0][1].replace('\n',' ') if cands else ''
If the windows are too tight, MCs near corners come back empty — widen one axis at a time, never both. If the windows are too loose, alt-function labels on the row above/below leak in.
Phase 5 — Add datasheet alt-functions for the main IC
For RP2350 and RP2040 the GPIO function table (F1 SPI / F2 UART / F3 I2C / F4 PWM / F5 SIO / F6-F8 PIO / F9 clock-USB-XIP / F11 trace) follows a regular pattern:
- F1 SPI:
(GPIO % 4)→{0: RX, 1: CSn, 2: SCK, 3: TX}and(GPIO // 8) % 2selects SPI0/SPI1. - F2 UART: similar regular pattern,
(GPIO // 4) % 2selects UART0/UART1. - F3 I2C:
(GPIO % 2)→ SDA/SCL;(GPIO // 2) % 2selects I2C0/I2C1. - F4 PWM:
GPIO // 2→ channel;GPIO % 2→ A/B. - ADC on RP2350B: GPIO40-47 = ADC0-7. (RP2350A has ADC on 26-29.)
- F9 USB DET / VBUS EN / OVCUR / clock-GPIN / clock-GPOUT: enumerated in datasheet table — quote it as-is rather than computing.
For other ICs, transcribe their datasheet pin description table directly into the per-chip section. Mark "(per datasheet — confirm with current revision)" if you only have a generic family pinout, not the exact rev for that part number.
Phase 6 — Cross-reference firmware
Read <firmware>/include/board_config.h (and any header it includes for pin defs). Pull out:
- I2C bus assignment (which
GPnnis SDA, which is SCL, which I2C peripheral) - I2C clock and pull-up strategy
- Any INT / DRDY / HIRQ / RESET / CS pin assignments
- I2C addresses (in
sensor_common.hor similar)
Put this in a "Firmware expectations" table at the bottom and explicitly mark sensor pins as "Not wired — polling-only" when no GPIO is assigned. Don't invent pin numbers.
If the firmware moves later (e.g. SDA/SCL get reassigned to a different GPIO pair), the only places to edit are: per-sensor tables (the "Connects to" column) and the "Firmware expectations" table at the bottom. Keep those two coherent.
Phase 7 — Compose the markdown
One file, this structure (preserve heading levels — the file is read by humans skimming for ## ChipName):
# Molecule Pin Mapping — <Board / Project Name>
<one-paragraph context: what board, which firmware trees consume it,
which I2C bus / SDA / SCL is shared, pull-up scheme>
---
## <Molecule Name> — Perimeter Contact Pin Map
Source: `<repo>` → `<path>/`. U1 = <chip>. MC1..MCn = castellated machine-pin contacts.
| MC | Silk | Net | RP2350 pin | Functions |
| ...
**RP2350B pins NOT routed to a perimeter contact:**
- Power: ...
- Crystal / VREG / USB / QSPI: ...
---
## <Sensor 1>
Package: <pkg>. I2C address <addr> (<strap>). Pinout per <datasheet section>.
| Pin # | Pin Name | Net on board | Connects to (host GPIO / rail) | Notes |
| ...
---
## <Sensor 2>
...
---
## Firmware expectations (must match wiring)
| Signal | Expected pin | Source |
| ...
**Wiring check — before power-on:**
1. ...
Common pitfalls (worth flagging in the output)
- MC numbering is not always continuous. Some refs (e.g.
MC54-MC57) exist in the schematic but the footprint is not placed. Show them as a single collapsed row with(pad not placed). - Pads associated with multiple identical pins. GND pads on a QFN show many
pad "81"entries on the same net; collapse to one row in the user-facing table. - Silk labels with newlines (e.g.
LDO\nENis a two-line label). Normalize to a single space when displaying. - Shared labels like a single
SWDcovering both MC28 and MC29 — note "(IO)" and "(CLK)" suffixes in the table to distinguish. - Address straps: the silk often labels the function (e.g.
Rx1,Tx0) rather than the GPIO number; both are correct and worth noting.
When to stop and ask
- If
$ARGUMENTSdoesn't name a molecule, ask once for path/URL. - If multiple firmware trees could be the host, ask once which to use as the source of truth for
board_config.h. - Otherwise, don't ask between phases — just produce the file and let the user diff.
Verification
After writing the file:
- Open it and spot-check 3 random rows in the U1 → MC table by re-running the Python parser to confirm the netname matches.
- Confirm at least one cross-reference back to the firmware (e.g. the SDA/SCL pin numbers in the bottom table appear in the per-sensor "Connects to" columns).
- Spot-check that no MC row is missing a silk label except the explicitly "(pad not placed)" ones.