226 行
11 KiB
Python
226 行
11 KiB
Python
"""Guarded Hub strap pulldown ECO and reviewed USB/LED NC markers."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import shutil
|
|
import uuid
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import pcbnew as pcb
|
|
|
|
from bootstrap_pcb import native_net_names, xy
|
|
from check_hardware import Board
|
|
from optimize_pcb_layout import Layout, box_mm, expand, point, place_silkscreen
|
|
from schematic_maintenance import ROOT, child, children, parse, properties
|
|
from hub_strap_checks import STRAPS
|
|
|
|
OUT = ROOT / 'out/hub-straps-r1'
|
|
CLI = 'D:/KiCad/bin/kicad-cli.exe'
|
|
NC = {1: {'J22': ['A8', 'B8', 'A4B9', 'B4A9']},
|
|
2: {'J23': ['A8', 'B8'], 'U32': ['14', '16', '18', '32']}}
|
|
SHEETS = [ROOT / 'PhoneRail-Board1/P3-CONTROL.kicad_sch',
|
|
ROOT / 'PhoneRail-Board2/C2-HUB.kicad_sch']
|
|
PCB = ROOT / 'PhoneRail-Board2/PhoneRail-Board2.kicad_pcb'
|
|
|
|
|
|
def uid(key):
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, 'PhoneRail/hub-straps-r1/' + key))
|
|
|
|
|
|
def pin_position(root, instance, number):
|
|
assert child(instance, 'mirror') is None, 'Mirror requires explicit geometry review'
|
|
lib = next(s for s in children(child(root, 'lib_symbols'), 'symbol')
|
|
if s[1] == child(instance, 'lib_id')[1])
|
|
pin = next(p for u in children(lib, 'symbol') for p in children(u, 'pin')
|
|
if child(p, 'number')[1] == str(number))
|
|
x, y, angle = map(float, child(instance, 'at')[1:])
|
|
px, py = map(float, child(pin, 'at')[1:3])
|
|
a = math.radians(angle)
|
|
return round(x + px*math.cos(a)-py*math.sin(a), 5), round(y-px*math.sin(a)-py*math.cos(a), 5)
|
|
|
|
|
|
def label(name, x, y, key, global_label=False, angle=0):
|
|
kind = 'global_label' if global_label else 'label'
|
|
shape = '(shape input)' if global_label else ''
|
|
return (f'({kind} "{name}" {shape} (at {x} {y} {angle}) '
|
|
f'(effects (font (size 1 1)) (justify {"left" if angle == 0 else "right"} bottom)) (uuid "{uid(key)}"))')
|
|
|
|
|
|
def layout_notes(source):
|
|
root = parse(source)
|
|
for note in sorted(children(root, 'text'), key=lambda n: n.start, reverse=True):
|
|
source = source[:note.start] + source[note.end:]
|
|
notes = [
|
|
'CONFIG: CFG_SEL[2:0] = 000; internal defaults + straps.',
|
|
'ROM 06h = 9Bh: self-powered operation.',
|
|
'PRTPWR / OCS open by existing architecture.',
|
|
'5V short-circuit / port OC response: REVIEW OPEN.',
|
|
'R900-R909: 100k explicit strap-low (Fig 7-3).',
|
|
'PRT_SWP[7:1] = 0: normal D+/D- polarity.',
|
|
'BOOST[1:0] = 00; GANG_EN = 0.',
|
|
'Ports 6/7: R714/R715 10k to +3V3_HUB.',
|
|
'Port-disable / enumeration: verify on prototype.',
|
|
'RBIAS: 12.0k 1%; Y1: 24MHz.',
|
|
'VDD18/VDD18PLL: >=1uF, ESR <0.1 ohm.',
|
|
'VBUS_DET: R709/R710 = 100k/100k.',
|
|
'UP_VBUS_DET: R721/R722 = 10k/20k.',
|
|
'NC: USB2-only SBU and four unused LED outputs.',
|
|
]
|
|
additions = []
|
|
for index, text in enumerate(notes):
|
|
additions.append(f'(text "{text}" (at 310 {45+index*7} 0) '
|
|
'(effects (font (size 1.27 1.27)) (justify left bottom)) '
|
|
f'(uuid "{uid("review-note-"+str(index))}"))')
|
|
additions.append('(text "R900-R909: USB2517 strap-low pulldowns, 100k each" (at 25.4 216 0) '
|
|
'(effects (font (size 1.27 1.27)) (justify left bottom)) '
|
|
f'(uuid "{uid("bank-note")}"))')
|
|
root = parse(source)
|
|
return source[:root.end-1] + '\n'.join(additions) + '\n' + source[root.end-1:]
|
|
|
|
|
|
def edit_sheets():
|
|
for number, path in enumerate(SHEETS, 1):
|
|
source = path.read_text(encoding='utf-8')
|
|
root = parse(source)
|
|
instances = {properties(s)['Reference'][2]: s for s in children(root, 'symbol')}
|
|
additions = []
|
|
for ref, pins in NC[number].items():
|
|
for pin in pins:
|
|
x, y = pin_position(root, instances[ref], pin)
|
|
additions.append(f'(no_connect (at {x} {y}) (uuid "{uid(ref+pin)}"))')
|
|
if number == 2:
|
|
template_source = (ROOT / 'PhoneRail-Board2/C4-SITE1.kicad_sch').read_text(encoding='utf-8')
|
|
template_root = parse(template_source)
|
|
template = next(s for s in children(template_root, 'symbol') if properties(s)['Reference'][2] == 'R741')
|
|
props = {k: p[2] for k, p in properties(template).items()}
|
|
lib_id = child(template, 'lib_id')[1]
|
|
library = child(root, 'lib_symbols')
|
|
if not any(s[1] == lib_id for s in children(library, 'symbol')):
|
|
lib = next(s for s in children(child(template_root, 'lib_symbols'), 'symbol') if s[1] == lib_id)
|
|
source = source[:library.end-1] + template_source[lib.start:lib.end] + '\n' + source[library.end-1:]
|
|
root = parse(source)
|
|
sheet_path = child(child(child(instances['R711'], 'instances'), 'project'), 'path')[1]
|
|
for index, (refnum, pin) in enumerate(STRAPS.items()):
|
|
ref, name = f'R{refnum}', f'STRAP_{pin}'
|
|
x, y = 25.4 + index*30.48, 236.22
|
|
fields = []
|
|
for key, value in {**props, 'Reference': ref}.items():
|
|
visible = key in ('Reference', 'Value')
|
|
fx, fy = (x+2.54, y+(2.54 if key == 'Value' else 0)) if visible else (x, y)
|
|
fields.append(f'(property {json.dumps(key)} {json.dumps(value, ensure_ascii=False)} '
|
|
f'(at {fx} {fy} 270) (effects (font (size 1.27 1.27)) '
|
|
f'(justify left bottom) {"" if visible else "(hide yes)"}))')
|
|
additions.append(f'(symbol (lib_id "{lib_id}") (at {x} {y} 90) (unit 1) '
|
|
'(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) '
|
|
f'(uuid "{uid(ref)}") ' + ' '.join(fields) +
|
|
f' (instances (project "PhoneRail-Board2" (path "{sheet_path}" '
|
|
f'(reference "{ref}") (unit 1)))))')
|
|
# The existing resistor symbol has pins at local x = +/-5.08.
|
|
new_instance = parse(additions[-1])
|
|
rx1, ry1 = pin_position(root, new_instance, 1)
|
|
rx2, ry2 = pin_position(root, new_instance, 2)
|
|
additions += [label('GND', rx1, ry1, ref+'gnd', True),
|
|
label(name, rx2, ry2, ref+'net')]
|
|
hx, hy = pin_position(root, instances['U32'], pin)
|
|
additions.append(label(name, hx, hy, 'hub'+str(pin), angle=180 if hx < 128 else 0))
|
|
additions.append('(text "R900-R909: 100k strap-low; USB2517 DS00001598C Fig 7-3." '
|
|
'(at 25.4 216 0) (effects (font (size 1.27 1.27)) (justify left bottom)) '
|
|
f'(uuid "{uid("note")}"))')
|
|
source = source[:root.end-1] + '\n' + '\n'.join(additions) + '\n' + source[root.end-1:]
|
|
if number == 2:
|
|
source = layout_notes(source)
|
|
path.write_text(source, encoding='utf-8')
|
|
|
|
|
|
def sync_pcb(source):
|
|
board = pcb.LoadBoard(str(PCB))
|
|
names = native_net_names(source)
|
|
nets = {n.GetNetname(): n for n in board.GetNetsByName().values()}
|
|
for name in names.values():
|
|
if name not in nets:
|
|
net = pcb.NETINFO_ITEM(board, name)
|
|
board.Add(net)
|
|
nets[name] = net
|
|
existing = {f.GetReference(): f for f in board.GetFootprints()}
|
|
components = {c.attrib['ref']: c for c in ET.parse(OUT / 'after/Board2.xml').findall('components/comp')}
|
|
root_uuid = child(parse(source.path.read_text(encoding='utf-8')), 'uuid')[1]
|
|
layout = Layout.__new__(Layout)
|
|
layout.number, layout.board, layout.parts = 2, board, existing
|
|
layout.done, layout.placements = set(existing), {}
|
|
layout.obstacles = [(r, expand(box_mm(f), .35),
|
|
-1 if any(p.GetAttribute() in (pcb.PAD_ATTRIB_PTH, pcb.PAD_ATTRIB_NPTH)
|
|
for p in f.Pads()) else f.GetLayer()) for r, f in existing.items()]
|
|
for z in board.Zones():
|
|
if z.GetIsRuleArea() and z.GetDoNotAllowFootprints():
|
|
layout.obstacles.append((z.GetZoneName(), expand(box_mm(z), .25),
|
|
-1 if z.GetLayerSet().Contains(pcb.F_Cu) else pcb.B_Cu))
|
|
center = point(existing['U32'])
|
|
for refnum, pin in STRAPS.items():
|
|
ref = f'R{refnum}'
|
|
c = components[ref]
|
|
fp = pcb.FootprintLoad(str(ROOT / 'lib/PhoneRail.pretty'), 'R0402')
|
|
fp.SetFPIDAsString('PhoneRail:R0402')
|
|
fp.SetReference(ref)
|
|
fp.SetValue(c.findtext('value'))
|
|
fp.SetSheetname('C2-HUB')
|
|
fp.SetSheetfile('C2-HUB.kicad_sch')
|
|
for field in c.findall('fields/field'):
|
|
if field.get('name') not in {'Reference', 'Value', 'Footprint'}:
|
|
fp.SetField(field.get('name'), field.text or '')
|
|
path = pcb.KIID_PATH()
|
|
for key in [root_uuid, *c.find('sheetpath').get('tstamps').strip('/').split('/'), c.findtext('tstamps')]:
|
|
path.push_back(pcb.KIID(key))
|
|
fp.SetPath(path)
|
|
board.Add(fp)
|
|
existing[ref] = fp
|
|
pad = next(p for p in existing['U32'].Pads() if p.GetNumber() == str(pin))
|
|
px, py = point(pad)
|
|
dx, dy = px-center[0], py-center[1]
|
|
norm = math.hypot(dx, dy)
|
|
layout.put(ref, (px+dx/norm*2, py+dy/norm*2), rotation=(0,90,180,270), radius=12,
|
|
reason=f'100k pull-down for U32.{pin}; DS00001598C Fig 7-3')
|
|
fp.Reference().SetLayer(pcb.B_Fab)
|
|
fp.SetField('ReferenceOnFabOnly', 'yes')
|
|
for field in fp.GetFields():
|
|
field.SetVisible(field.GetName() == 'Reference')
|
|
field.SetMirrored(True)
|
|
for ref, fp in existing.items():
|
|
for pad in fp.Pads():
|
|
endpoint = (ref, pad.GetNumber())
|
|
if endpoint in source.pins:
|
|
pad.SetNet(nets[names[source.pins[endpoint]]])
|
|
place_silkscreen(board, {'C745', 'C747', 'C748', 'R709', 'R714', 'R719'})
|
|
pcb.SaveBoard(str(PCB), board)
|
|
(OUT / 'placement.json').write_text(json.dumps(layout.placements, indent=2) + '\n')
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument('--apply', action='store_true')
|
|
args = ap.parse_args()
|
|
if not args.apply:
|
|
raise SystemExit('Use --apply once; validate with verify_erc_disposition.py afterwards')
|
|
before = OUT / 'before'
|
|
if before.exists():
|
|
raise RuntimeError('Existing ECO baseline; refusing to repeat mutations')
|
|
before.mkdir(parents=True)
|
|
for path in [*SHEETS, PCB]:
|
|
shutil.copy2(path, before / path.name)
|
|
for number in (1, 2):
|
|
Board(number).compile(CLI, before)
|
|
edit_sheets()
|
|
after = OUT / 'after'
|
|
after.mkdir()
|
|
for number in (1, 2):
|
|
board = Board(number)
|
|
board.compile(CLI, after)
|
|
if number == 2:
|
|
sync_pcb(board)
|
|
hashes = {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in [*SHEETS, PCB]}
|
|
(OUT / 'applied.json').write_text(json.dumps(hashes, indent=2) + '\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|