kicad:EasyEDA→KiCad 工程(Board1/Board2)、库、脚本与评审文档(IC 引脚、风扇、热、EDL 供电、布局规则)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,129 @@
|
||||
"""Verify the Hub ECO and produce a complete, explicit disposition of remaining ERC."""
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
import pcbnew as pcb
|
||||
|
||||
from check_hardware import Board
|
||||
from hub_strap_checks import REFS, STRAPS, verify_hub_straps
|
||||
from revise_hub_straps import OUT, NC, CLI, PCB
|
||||
from schematic_maintenance import child, children, parse, properties
|
||||
from unconnected_inventory import classify, inventory, write_inventory
|
||||
|
||||
|
||||
def snapshot(path, omit=()):
|
||||
root = ET.parse(path).getroot()
|
||||
parts = {c.attrib['ref']: ET.tostring(c) for c in root.findall('components/comp') if c.attrib['ref'] not in omit}
|
||||
nets = {}
|
||||
for net in root.findall('nets/net'):
|
||||
pins = {(p.attrib['ref'], p.attrib['pin']): dict(p.attrib)
|
||||
for p in net.findall('node') if p.attrib['ref'] not in omit}
|
||||
if pins:
|
||||
nets[frozenset(pins)] = pins
|
||||
return parts, nets
|
||||
|
||||
|
||||
def main():
|
||||
from check_power_paths import main as verify_power_paths
|
||||
verify_power_paths()
|
||||
rows, unconnected, boards = [], [], {}
|
||||
after = OUT / 'verified'
|
||||
after.mkdir(exist_ok=True)
|
||||
for number in (1, 2):
|
||||
b = Board(number)
|
||||
b.compile(CLI, after)
|
||||
added = REFS if number == 2 else set()
|
||||
old_parts, old_nets = snapshot(OUT / 'before' / f'Board{number}.xml')
|
||||
new_parts, new_nets = snapshot(after / f'Board{number}.xml', added)
|
||||
assert old_parts == new_parts, 'Unrelated component metadata changed'
|
||||
assert old_nets.keys() == new_nets.keys(), 'Unapproved connectivity change'
|
||||
assert set(b.parts) - set(old_parts) == added
|
||||
expected_nc = {(ref, pin) for ref, pins in NC[number].items() for pin in pins}
|
||||
changed = set()
|
||||
for members, pins in old_nets.items():
|
||||
for endpoint, attrs in pins.items():
|
||||
expected = dict(attrs)
|
||||
if endpoint in expected_nc:
|
||||
assert len(members) == 1 and 'no_connect' not in attrs['pintype']
|
||||
expected['pintype'] += '+no_connect'
|
||||
assert expected == new_nets[members][endpoint], endpoint
|
||||
if attrs != expected:
|
||||
changed.add(endpoint)
|
||||
assert changed == expected_nc
|
||||
if number == 2:
|
||||
def check(label, good):
|
||||
assert good, label
|
||||
verify_hub_straps(b, check)
|
||||
current_pcb = pcb.LoadBoard(str(PCB))
|
||||
previous_pcb = pcb.LoadBoard(str(OUT / 'before' / PCB.name))
|
||||
footprints = {f.GetReference(): f for f in current_pcb.GetFootprints()}
|
||||
changed_pins = {str(pin) for pin in STRAPS.values()}
|
||||
for old_fp in previous_pcb.GetFootprints():
|
||||
ref = old_fp.GetReference()
|
||||
new_fp = footprints[ref]
|
||||
assert old_fp.GetPosition() == new_fp.GetPosition() and old_fp.GetLayer() == new_fp.GetLayer(), ref
|
||||
assert old_fp.GetOrientationDegrees() == new_fp.GetOrientationDegrees(), ref
|
||||
new_pads = {p.GetNumber(): p for p in new_fp.Pads()}
|
||||
for old_pad in old_fp.Pads():
|
||||
new_pad = new_pads[old_pad.GetNumber()]
|
||||
assert old_pad.GetPosition() == new_pad.GetPosition(), ref
|
||||
if ref != 'U32' or old_pad.GetNumber() not in changed_pins:
|
||||
assert old_pad.GetNetname() == new_pad.GetNetname(), (ref, old_pad.GetNumber())
|
||||
report = json.loads((after / f'Board{number}-erc.json').read_text())
|
||||
before = json.loads((OUT / 'before' / f'Board{number}-erc.json').read_text())
|
||||
for key in ('ignored_checks', 'included_severities'):
|
||||
assert report[key] == before[key]
|
||||
for sheet in report['sheets']:
|
||||
for violation in sheet['violations']:
|
||||
description = violation['items'][0]['description']
|
||||
match = re.search(r'^Symbol (\S+) .*? (\S+) \[', description)
|
||||
assert match, description
|
||||
ref, pin = match.groups()
|
||||
kind = violation['type']
|
||||
net = b.pins.get((ref, pin), '')
|
||||
if ref.startswith('#PWR'):
|
||||
path = b.path.parent / (sheet['path'].strip('/') + '.kicad_sch')
|
||||
instance = next(s for s in children(parse(path.read_text(encoding='utf-8')), 'symbol')
|
||||
if properties(s)['Reference'][2] == ref)
|
||||
net = properties(instance)['Value'][2]
|
||||
status, blocking = 'REVIEW_REQUIRED', True
|
||||
category, action = classify(number, ref, pin)
|
||||
evidence = ''
|
||||
if kind == 'power_pin_not_driven':
|
||||
category = 'supply_model'
|
||||
status, blocking = 'TEMPORARILY_ACCEPTED_FOR_ROUTING', False
|
||||
evidence = 'out/power-path-review/verification.json'
|
||||
action = 'Topology traced; retain ERC until source operation/ratings are reviewed, then add scoped PWR_FLAG'
|
||||
elif category in ('hub_port_power', 'hub_overcurrent'):
|
||||
category = 'hub_power_architecture'
|
||||
evidence = '../五路直供电源板设计.md sections 15.2,21.6; USB2517.pdf pages 10,12'
|
||||
action = 'Intentional external MCU control; verify 5V short-circuit protection and USB port power/OC reporting before release'
|
||||
elif category == 'unused_dac_channel':
|
||||
evidence = 'DAC63204.pdf pin table and power-down/NVM sections; ../firmware/components/drv/dac63204.c'
|
||||
action = 'Runtime keeps unused channels Hi-Z; verify manufactured NVM image and recall/fault state before NC'
|
||||
elif category == 'mcu_recovery':
|
||||
action = 'Add accessible IO0/UART recovery test points; current schematic explicitly says they are not fitted'
|
||||
rows.append({'board': number, 'sheet': sheet['path'], 'reference': ref, 'pin': pin,
|
||||
'erc_type': kind, 'severity': violation['severity'], 'net': net,
|
||||
'category': category, 'disposition': status,
|
||||
'temporary_routing_acceptance': not blocking,
|
||||
'release_blocking': True, 'evidence': evidence, 'next_action': action})
|
||||
unconnected.extend(inventory(b))
|
||||
boards[number] = {'original_components_unchanged': len(old_parts), 'added_components': sorted(added),
|
||||
'original_endpoint_groups_preserved': len(new_nets), 'approved_nc': sorted(changed), 'erc': b.erc}
|
||||
with (OUT / 'erc-disposition.csv').open('w', encoding='utf-8-sig', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
(OUT / 'erc-disposition.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
||||
result = {'status': 'ECO_VERIFIED_NOT_RELEASED', 'boards': boards,
|
||||
'remaining_erc': len(rows), 'temporary_routing_acceptances': sum(r['temporary_routing_acceptance'] for r in rows),
|
||||
'release_review_required': sum(r['release_blocking'] for r in rows),
|
||||
'unconnected': write_inventory(OUT, unconnected)}
|
||||
(OUT / 'verification.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户