kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -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()
|
||||
在新工单中引用
屏蔽一个用户