kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-10-04 19:24:42 +08:00
共同撰写人 Claude Opus 5.5
父节点 1d842cf45f
当前提交 bfe094a5d7
共修改 173 个文件,包含 401864 行新增和 0 行删除
+155
查看文件
@@ -0,0 +1,155 @@
"""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()