metronome/hardware/eda/gen_bom.py
Me Here 0dc9daf54f PM_K-1 hardware: consolidated BOM + LAYOUT.md + PCB-layout tutorial
- gen_bom.py + BOM_board.csv: authoritative BOM generated from board.net (70 line items,
  167 placements), grouped with MPNs; refs match the integrated netlist; DNP ICs flagged.
  (Supersedes the early hand-written BOM.csv, which used per-block refs.)
- LAYOUT.md: routing rulebook for board.net -- 4-layer stackup, the grounding/star-point
  strategy, switcher loop isolation, analog separation, USB diff pair, RP2350/crystal/flash,
  thermal, DNP blocks, pre-fab confirm list, DRC checklist.
- pcb_layout_tutorial.md: beginner orientation -- use KiCad; the schematic/netlist=contract
  vs layout=physical-realization paradigm; the import->place->route->pour->DRC->Gerber
  workflow; vocabulary; how our files fit; learning resources; honest expectations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:15:15 -05:00

76 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Generate a consolidated BOM from the integrated board.net.
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/gen_bom.py
Reads hardware/kicad/board.net, groups like parts, attaches MPNs, writes
hardware/BOM_board.csv (authoritative part list keyed to the board.net references).
"""
import re, csv, os, collections
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
NET = os.path.join(ROOT, "hardware/kicad/board.net")
OUT = os.path.join(ROOT, "hardware/BOM_board.csv")
# value -> (manufacturer, MPN, note)
MPN = {
"RP2350A": ("Raspberry Pi", "RP2350A", "MCU (QFN-60); via KiCad lib symbol"),
"W25Q128JVS": ("Winbond", "W25Q128JVSIQ", "16MB QSPI flash"),
"PCM5102A": ("TI", "PCM5102APWR", "I2S DAC; SCK->GND (MCLK-less)"),
"THAT1240": ("THAT Corp", "THAT1240S08-U", "0dB balanced line receiver; 2nd-src INA134/SSM2141"),
"THAT1646": ("THAT Corp", "THAT1646S08-U", "balanced line driver, +6dB; 2nd-src DRV134/SSM2142"),
"OPA1641": ("TI", "OPA1641AID", "JFET Hi-Z DI buffer"),
"OPA1612": ("TI", "OPA1612AIDR", "dual: recon filter + summer"),
"TPS65131": ("TI", "TPS65131RGER", "dual boost/inverter -> +/-18V"),
"TPS7A4901": ("TI", "TPS7A4901DGNR", "+15V ultra-low-noise LDO; confirm Vfb"),
"TPS7A3001": ("TI", "TPS7A3001DGNR", "-15V ultra-low-noise LDO; confirm Vfb"),
"AP2112K-3.3": ("Diodes", "AP2112K-3.3TRG1", "3V3 LDO; confirm SOT-23-5 pinout"),
"ULN2003A": ("TI", "ULN2003ADR", "shared relay driver (3 of 7 ch used)"),
"RV-8803-C7": ("Micro Crystal", "RV-8803-C7", "I2C RTC; confirm footprint"),
"LM393": ("TI", "LM393DR", "DNP - SIG/CLIP comparator"),
"H11L1": ("Vishay", "H11L1M", "DNP - MIDI opto IN; confirm pinout"),
"74LVC14": ("Nexperia", "74LVC14APW", "DNP - MIDI OUT/THRU buffer"),
"PAM8302A": ("Diodes", "PAM8302AASCR", "DNP - monitor speaker amp"),
"USBLC6-2SC6": ("STMicro", "USBLC6-2SC6", "USB ESD"),
"TQ2SA-5V": ("Panasonic", "TQ2SA-5V", "DPDT signal relay, gold; K1 select/K2 mute/K3 gnd-lift"),
"12MHz": ("Abracon", "ABM8-272-12.000MHZ-T3", "RP2350 crystal; confirm load caps"),
"USB_C_Receptacle": ("GCT", "USB4085-GF-A", "USB-C; 24-pin sym vs 16-pin part - resolve at layout"),
"CR2032": ("Keystone", "1066", "coin-cell holder (RTC backup)"),
"MBRM120": ("onsemi", "MBRM120ET3G", "Schottky rectifier (switcher)"),
"BAT54": ("onsemi", "BAT54", "Schottky (RTC diode-OR / peak-detect)"),
"1N4148WS": ("onsemi", "1N4148WS", "fast diode (input clamps / MIDI protect)"),
"4.7uH": ("Wurth/EPCOS", "7447789004 / B82462-G4472", "switcher inductor"),
"3.3uH": ("Abracon", "AOTA-B201610S3R3-101-T", "RP2350 core SMPS inductor"),
"600R": ("Murata", "BLM18KG..", "ferrite bead (USB VBUS input)"),
}
comps = re.findall(r'\(comp\s*\(ref "([^"]+)"\)\s*\(value "([^"]+)"\)(.*?)\(libsource', open(NET).read(), re.S)
def fp(blk):
m = re.search(r'\(footprint "([^"]+)"', blk); return m.group(1) if m else ""
groups = collections.OrderedDict()
for ref, val, blk in comps:
key = (val, fp(blk))
groups.setdefault(key, []).append(ref)
def sortkey(r):
m = re.match(r'([A-Za-z]+)(\d+)', r); return (m.group(1), int(m.group(2))) if m else (r, 0)
rows = []
for (val, foot), refs in groups.items():
refs = sorted(refs, key=sortkey)
man, mpn, note = MPN.get(val, ("", "", ""))
if "Potentiometer" in foot: # the level-cal trimmer (value "10k" but a pot)
man, mpn, note = ("Bourns", "3296W-1-103LF", "output level cal trim (25-turn)")
# heuristic DNP flag
dnp = any(s in note for s in ("DNP",))
rows.append([len(refs), " ".join(refs), val, foot, man, mpn, ("DNP" if dnp else ""), note])
rows.sort(key=lambda r: (r[6] == "DNP", -r[0], r[2])) # populated first, then by qty
with open(OUT, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["Qty", "Refs", "Value", "Footprint", "Manufacturer", "MPN", "Populate", "Notes"])
w.writerows(rows)
print("wrote", OUT)
print("line items:", len(rows), " total placements:", sum(r[0] for r in rows))