0hmX/pcbgolf-routing-checkpoint-20261012

A PCBGolf assembly combining a modeled PCB with copper/component bodies and seven native connector housings.

Version
0.0.3
License
unset
Stars
0

firmware-support/build_map.py

#!/usr/bin/env python3
"""Generate the four-port contract from the authoritative master and ST XML."""
import csv, hashlib, json, pathlib, re, xml.etree.ElementTree as ET
H = pathlib.Path(__file__).resolve().parent
ROOT = H.parents[3]
MASTER = ROOT / 'work/candidates/two_layer_mvp/project/full.net'
MAPPING = ROOT / 'work/agents/mcu/mcu.json'
ST = ROOT / 'work/agents/mcu/STM32H725VGTx.xml'
netlist = ET.parse(MASTER).getroot()
nets, pads = {}, {}
for n in netlist.find('nets'):
    nodes = [dict(x.attrib) for x in n.findall('node')]
    nets[n.attrib['name']] = nodes
    for x in nodes:
        if x['ref'] == 'U3': pads[x['pin']] = {'net': n.attrib['name'], 'function': x.get('pinfunction')}
st = {p.attrib['Position']: p for p in ET.parse(ST).getroot().findall('{http://dummy.com}Pin')}
rows = []
for m in json.loads(MAPPING.read_text())['mapping']:
    if 'old_port' not in m: continue
    pin = m['new_pad']; port = m['new_port']; fn = m['function']; n = pads[pin]['net']
    assert st[pin].attrib['Name'].split('(')[0].split('-')[0] == port, (pin, port)
    signals = {s.attrib['Name'] for s in st[pin]}
    if fn not in ('HSE crystal',): assert fn in signals, (pin, fn, signals)
    item = dict(m, net=n, package_pin=int(pin), stm32_signal=fn, connector_signal=n)
    if fn.startswith('ADC'):
        item.update(adc=int(fn[3]), adc_channel=int(fn.split('INP')[1]), resolution_bits=12 if fn.startswith('ADC3') else 16)
        resistors = [x for x in nets[n] if x['ref'].startswith('R')]
        for r in resistors:
            other = [name for name, nodes in nets.items() if any(x['ref']==r['ref'] and x['pin']!=r['pin'] for x in nodes)]
            if other: item.update(connector_signal=other[0], series_resistor=r['ref'])
    if fn.startswith('FDCAN'): item['alternate_function'] = 5 if fn.startswith('FDCAN3') else 9
    elif fn.startswith('SDMMC1'): item['alternate_function'] = 12
    elif fn.startswith('I2C2'): item['alternate_function'] = 4
    elif fn.startswith('USB_OTG'): item['alternate_function'] = 10
    rows.append(item)
assert len(rows)==60, len(rows)
assert len({m['new_port'] for m in rows})==60
imon = [r for r in rows if r['net'].endswith('_IMON')]
assert [(r['net'],r['new_port'],r['adc'],r['adc_channel']) for r in imon] == [('CH1_IMON','PC4',1,4),('CH2_IMON','PB1',1,5),('CH3_IMON','PC5',1,8),('CH4_IMON','PA6',1,3)]
for ref in ['R70','R71','R72','R73']:
    c = netlist.find("components/comp[@ref='%s']" % ref)
    assert float(c.findtext('value').lower().replace('k','e3'))==4700
report = {'status':'STATIC_BOARD_SUPPORT_NOT_LINKED_OR_HARDWARE_TESTED_FIRMWARE','mcu':'STM32H725VGT6','package':'LQFP100','source_hashes':{str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest() for p in [MASTER,MAPPING,ST]},'signals':rows,'source_urls':json.loads(MAPPING.read_text())['sources'][:2]}
(H/'board-map.json').write_text(json.dumps(report,indent=2)+'\n')
with (H/'board-map.csv').open('w') as f:
    fields=['net','connector_signal','old_port','new_port','package_pin','stm32_signal','alternate_function','adc','adc_channel','resolution_bits']
    w=csv.DictWriter(f,fieldnames=fields,extrasaction='ignore');w.writeheader();w.writerows(rows)
def pin(r):
    p=re.fullmatch('P([A-Z])(\d+)(?:_C)?',r['new_port']);assert p
    return "{'%s', %s}" % p.groups()
s=[r for r in rows if r['stm32_signal']=='GPIO']
lines=['/* Generated by build_map.py; all positions come from the full master. */']
for name, ns in [('power_pins',[f'CH{i}_PWR_EN' for i in range(1,5)]),('standby_pins',[f'CAN{i}_EN' for i in range(4)]),('led_pins',['LED_R','LED_G','LED_B']),('sbu_pins',[f'CH{i}_SBU{j}_{role}' for i in range(1,5) for j in (1,2) for role in ('IGN','RELAY')])]:
    lines.append('static const pcbgolf_pin_t %s[] = {%s};' % (name,', '.join(pin(next(r for r in rows if r['net']==n)) for n in ns)))
lines += ['static const pcbgolf_pin_t button_pin = '+pin(next(r for r in rows if r['net']=='BTN'))+';','static const struct {pcbgolf_pin_t pin; uint8_t af;} alternate_pins[] = {']
for r in rows:
    if 'alternate_function' in r and not r['net'].startswith('CAN3_'): lines.append('  {%s, %s}, /* %s */' % (pin(r),r['alternate_function'],r['net']))
lines.append('};')
lines.append('const pcbgolf_adc_t pcbgolf_imon[4] = {')
for r in imon: lines.append('  {%s, %s, %s, %s}, /* %s */' % (pin(r),r['adc'],r['adc_channel'],r['resolution_bits'],r['net']))
lines.append('};\nconst pcbgolf_adc_t pcbgolf_sbu[4][2] = {')
for i in range(1,5):
    group=[next(r for r in rows if r['connector_signal']==f'CH{i}_SBU{j}' and 'adc' in r) for j in (1,2)]
    lines.append('  {%s}, /* port %s: SBU1, SBU2 */' % (', '.join('{%s, %s, %s, %s}' % (pin(r),r['adc'],r['adc_channel'],r['resolution_bits']) for r in group),i))
lines.append('};')
(H/'board-pins.inc').write_text('\n'.join(lines)+'\n')
print(json.dumps({'active_signals':len(rows),'adc_signals':sum('adc' in r for r in rows),'master_sha256':report['source_hashes'][str(MASTER.relative_to(ROOT))],'all_st_functions_verified':True},indent=2))