- 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>
76 lines
3.9 KiB
Python
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))
|