文件

156 行
8.9 KiB
Python

"""Install explicit net classes and the R2 routing contract without altering nets."""
import json
import re
import shutil
import pcbnew as pcb
from optimize_pcb_layout import ROOT, OUT, digest
def main():
contract = {
'revision':'R2',
'direction':{'F.Cu':'horizontal','B.Cu':'vertical',
'max_transverse_escape_mm':2.0,
'exceptions':[],
'policy':'Long transverse/diagonal tracks require explicit reviewed UUID exceptions; copper zones are exempt'},
'stackup_status':'PRELIMINARY_FABRICATOR_CONFIRMATION_REQUIRED',
'usb':{'target_ohms':90,'tolerance_percent':10,'width_mm':.28,'gap_mm':.18,
'max_pair_skew_mm':.5,'max_uncoupled_mm':5,'max_vias_per_leg':2,
'pair_names_native_recognized':False,
'reference_policy':'F.Cu requires uninterrupted In1.Cu GND; B.Cu requires uninterrupted In2.Cu GND. Reserve local GND corridors in the nominal power layer before changing signal layers',
'max_return_via_distance_mm':1.0},
'power':{'input_design_continuous_A':10,'input_fuse_A':12,'phone_channel_A':4,
'main_bus_preferred_width_mm':6,'main_bus_min_trunk_mm':4,
'phone_preferred_width_mm':2,'phone_min_trunk_mm':1.5,
'max_narrow_escape_mm':2,'via_drill_mm':.3,'via_diameter_mm':.6,
'phone_parallel_vias_min':6,'input_parallel_vias_min':12,
'note':'Starting geometry only: qualify current, voltage drop, thermal rise and via plating with board house'},
'placement':{'min_body_spacing_mm':.35,'battery_bulk_lower_projection_margin_mm':1,
'max_buck_vin_cap_pin_distance_mm':5,'max_bootstrap_pin_distance_mm':5,
'max_feedback_pin_distance_mm':5,'max_shunt_ina_pin_distance_mm':10,
'max_crystal_pin_distance_mm':7,'max_hub_bypass_pin_distance_mm':5,
'height_gap_mm':13,'height_status':'Bulk-cap projections reserved; remaining component heights unverified'},
'boards':{},
}
for n in (1,2):
name = f'PhoneRail-Board{n}'
path = ROOT/name/(name+'.kicad_pro')
project = json.loads(path.read_text(encoding='utf-8'))
source = OUT / ('rules-input-'+name+'.kicad_pcb')
shutil.copy2(path.with_suffix('.kicad_pcb'),source)
b = pcb.LoadBoard(str(source))
parts = {f.GetReference():f for f in b.GetFootprints()}
def net(ref,pin):
return next(p.GetNetname() for p in parts[ref].Pads() if p.GetNumber()==str(pin))
nets = {p.GetNetname() for f in b.GetFootprints() for p in f.Pads() if p.GetNetname()}
mapping = {s:'Default' for s in nets}
usb_pairs = [('USB_DP','USB_DM')] if n == 1 else [('UP_DP','UP_DM')]+[
(f'USB{ch}_DP{suffix}',f'USB{ch}_DM{suffix}') for ch in range(1,6) for suffix in ('','J')]
for plus,minus in usb_pairs:
assert plus in nets and minus in nets
mapping[plus] = mapping[minus] = 'USB90'
for s in nets:
if s in {'SCL','SDA','INA_ALERT'}:
mapping[s] = 'I2C'
elif s.startswith('VDAC') or s.startswith('NTC'):
mapping[s] = 'ANALOG'
elif re.fullmatch(r'VO[1-5](_P)?',s):
mapping[s] = 'PWR4A'
elif s == '+5V' or re.fullmatch(r'VBUS[1-5]',s):
mapping[s] = 'PWR5V'
elif s.startswith('+3V3') or s in {'VDD18','VDD18PLL'}:
mapping[s] = 'PWR3V3'
elif s == '+12V':
mapping[s] = 'PWR12V'
elif 'FAN_12V_SW' in s:
mapping[s] = 'FAN_PWR'
if n == 1:
for ref,pin in [('J1',2),('F1',2),('Q1',5)]:
mapping[net(ref,pin)] = 'PWR12V'
for ch in range(1,6):
mapping[net(f'U{ch}',11)] = 'SWITCH'
mapping[net(f'U{ch}',7)] = 'FEEDBACK'
mapping[net(f'U{ch}',8)] = 'FEEDBACK'
mapping[net(f'U{ch+13}',4)] = 'PWR4A'
for pin in (15,17,18,19):
mapping[net(f'U{ch+13}',pin)] = 'ANALOG'
for ref in ('U19','U30'):
mapping[net(ref,2)] = 'SWITCH'
mapping[net(ref,4)] = 'FEEDBACK'
else:
for s in ('XTAL1','XTAL2','RBIAS'):
mapping[s] = 'CLOCK_LOCAL'
net_settings = project['net_settings']
base = dict(next(c for c in net_settings['classes'] if c['name']=='Default'))
base.update(clearance=.15,track_width=.25,via_diameter=.6,via_drill=.3,
diff_pair_width=.28,diff_pair_gap=.18,diff_pair_via_gap=.25)
classes = [('Default',.25,.15),('USB90',.28,.15),('I2C',.25,.15),('ANALOG',.2,.15),
('FEEDBACK',.2,.15),('CLOCK_LOCAL',.2,.15),('SWITCH',1,.15),
('PWR4A',2,.2),('PWR5V',1,.2),('PWR12V',4,.2),('FAN_PWR',1.2,.2),('PWR3V3',.5,.15)]
net_settings['classes'] = [dict(base,name=c,track_width=w,clearance=gap) for c,w,gap in classes]
# Use exact escaped net names; patterns cannot accidentally capture BOOT/FB.
net_settings['netclass_assignments'] = {k:[v] for k,v in sorted(mapping.items()) if v!='Default'}
net_settings['netclass_patterns'] = [{'pattern':k,'netclass':v} for k,v in sorted(mapping.items()) if v!='Default']
ds = project['board']['design_settings']
ds['rules'].update(min_clearance=.127,min_track_width=.127,min_via_diameter=.6,
min_through_hole_diameter=.3,min_hole_to_hole=.25,
min_via_annular_width=.15,min_copper_edge_clearance=.3,
min_connection=.2,solder_mask_clearance=.05,solder_mask_min_width=.1)
ds['track_widths'] = [0,.15,.2,.25,.28,.5,1,1.2,1.5,2,3,4,6]
ds['via_dimensions'] = [{'diameter':0,'drill':0},{'diameter':.6,'drill':.3},{'diameter':.8,'drill':.4}]
ds['diff_pair_dimensions'] = [{'width':0,'gap':0,'via_gap':0},{'width':.28,'gap':.18,'via_gap':.25}]
ds['defaults']['zones'].update(min_clearance=.2,min_thickness=.2,thermal_relief_gap=.25,
thermal_relief_spoke_width=.4,min_island_area=5)
path.write_text(json.dumps(project,indent=2,ensure_ascii=False)+'\n',encoding='utf-8')
rules = '''(version 1)
# F.Cu horizontal, B.Cu vertical. Direction and explicit pair checks:
# scripts/verify_pcb_layout.py + pcb-routing-rules.json.
# Net names are unchanged; DP/DM suffixes are not presumed native pairs.
(rule "R2 inner layers reserved for reference and power zones"
(condition "A.Layer == 'In1.Cu' || A.Layer == 'In2.Cu'")
(constraint disallow track))
(rule "R2 USB differential target - requires recognized pair"
(condition "A.hasNetclass('USB90')")
(constraint track_width (min 0.15mm) (opt 0.28mm) (max 0.30mm))
(constraint diff_pair_gap (min 0.15mm) (opt 0.18mm) (max 0.21mm))
(constraint diff_pair_uncoupled (max 5mm))
(constraint skew (max 0.5mm)))
(rule "R2 switch copper stays on component side with no vias"
(condition "A.hasNetclass('SWITCH')")
(constraint disallow via)
(constraint length (max 12mm)))
(rule "R2 feedback and oscillator do not change layers"
(condition "A.hasNetclass('FEEDBACK') || A.hasNetclass('CLOCK_LOCAL')")
(constraint disallow via)
(constraint length (max 15mm)))
(rule "R2 switch tracks isolated from sensitive nets"
(condition "A.Type == 'Track' && A.hasNetclass('SWITCH') && (B.hasNetclass('FEEDBACK') || B.hasNetclass('ANALOG') || B.hasNetclass('USB90') || B.hasNetclass('CLOCK_LOCAL'))")
(constraint clearance (min 0.5mm)))
(rule "R2 ordinary vias stay outside SMD lands"
(condition "A.Type == 'Via' && B.Type == 'Pad' && B.Pad_Type == 'SMD'")
(constraint physical_clearance (min 0.1mm)))
'''
path.with_suffix('.kicad_dru').write_text(rules,encoding='utf-8')
contract['boards'][name] = {'ground_reference_layer':'In1.Cu' if n==1 else 'In2.Cu',
'power_zone_layer':'In2.Cu' if n==1 else 'In1.Cu',
'usb_pairs':[{'positive':p,'negative':m} for p,m in usb_pairs], 'net_classes':mapping}
(ROOT/'pcb-routing-rules.json').write_text(json.dumps(contract,indent=2)+'\n',encoding='utf-8')
manifest = OUT/'placement.json'
data = json.loads(manifest.read_text(encoding='utf-8'))
# KiCad normalizes project JSON during schematic/DRC operations. Placement
# protects the board and DRU files it can overwrite, not that mutable cache.
data['generated_files'] = {k:v for k,v in data['generated_files'].items() if not k.endswith('.kicad_pro')}
for n in (1,2):
name = f'PhoneRail-Board{n}'
for ext in ('.kicad_dru',):
p = ROOT/name/(name+ext)
data['generated_files'][str(p.relative_to(ROOT))] = digest(p)
manifest.write_text(json.dumps(data,indent=2)+'\n',encoding='utf-8')
print('R2 classes, native rules, explicit pair/direction contract installed')
if __name__=='__main__':
main()