文件
phonerail/kicad/scripts/fan_revision_checks.py

129 行
8.3 KiB
Python

"""Connectivity and electrical assumptions for the four-wire fan prototype."""
import itertools
import hashlib
import json
import xml.etree.ElementTree as ET
from schematic_maintenance import ROOT, parse, children, child, properties
def verify_fans(board, check):
a = board
selection = json.loads((ROOT/'fan-selection.json').read_text(encoding='utf-8'))
def exact(name, *pins):
expected = {(r, str(p)) for r, p in pins}
actual = a.net_members.get(a.pins.get(next(iter(expected)), ''), set())
check('Fan ' + name, actual == expected, str(sorted(actual)))
for ref in ['J26', 'J27']:
check(f'Fan {ref} pin 1 fixed GND', a.net(ref, 1) == 'GND')
check(f'Fan {ref} four-position screw terminal',
a.parts[ref]['Footprint'] == selection['connector']['footprint']
and a.parts[ref]['MPN'] == selection['connector']['mpn']
and a.parts[ref]['LCSC'] == selection['connector']['lcsc'])
check(f'Fan {ref} selected external fan', a.parts[ref].get('Fan_MPN') == selection['mpn']
and a.parts[ref].get('Fan_LCSC') == selection['lcsc']
and a.parts[ref].get('Fan_Device_UUID') == selection['easyeda_device_uuid'])
exact('switched power', ('Q7', 3), ('J26', 2), ('J27', 2), ('C738', 1), ('D2', 1), ('R744', 2), ('R748', 2))
exact('PWM open drain', ('Q6', 3), ('J26', 4), ('J27', 4))
exact('PWM gate', ('Q6', 1), ('R733', 2), ('R734', 2))
exact('PWM GPIO', ('R733', 1), ('U20', 11))
exact('enable GPIO', ('R742', 1), ('U20', 5))
exact('enable gate', ('Q8', 1), ('R742', 2), ('R743', 2))
exact('high-side gate clamp', ('Q7', 1), ('DZ3', 2), ('R740', 1), ('R741', 2))
exact('high-side gate sink', ('Q8', 3), ('R741', 1))
for ref, pin in [('Q7', 2), ('DZ3', 1), ('R740', 2)]:
check(f'Fan {ref}.{pin} +12V', a.net(ref, pin) == '+12V')
for ref, pin in [('Q6', 2), ('Q8', 2), ('Q9', 2), ('Q10', 2), ('D2', 2),
('C738', 2), ('R734', 1), ('R743', 1), ('R746', 1), ('R750', 1)]:
check(f'Fan {ref}.{pin} GND', a.net(ref, pin) == 'GND')
check('Fan enable GPIO function', a.functions[('U20', '5')] in {'IO5', 'IO5_5'} and a.net('U20', 5) == 'FAN_EN')
for ref, resistance in {'R733':1000, 'R734':100000, 'R740':100000,
'R741':1000, 'R742':1000, 'R743':100000,
**{f'R{n}':15000 if n in [745, 749] else 10000 for n in range(744, 752)}}.items():
check(f'Fan {ref} resistance', a.resistor(ref) == resistance)
check('Fan PMOS part', a.parts['Q7']['MPN'] == 'AO3401A')
check('Fan gate clamp part', a.parts['DZ3']['MPN'] == 'BZT52C8V2-7-F')
check('Fan bulk capacitance part', a.parts['C738']['MPN'] == 'CL31A226KAHNNNE')
check('Fan rail clamp part', a.parts['D2']['MPN'] == 'SS34')
for q in ['Q6', 'Q8', 'Q9', 'Q10']:
check(f'Fan {q} NMOS part', a.parts[q]['MPN'] == 'AO3400A')
for idx, (q, j, mcu) in enumerate([('Q9', 'J26', 9), ('Q10', 'J27', 4)]):
rp, rs, rg, ro = [f'R{744 + idx*4 + n}' for n in range(4)]
exact(j + ' TACH raw', (j, 3), (rp, 1), (rs, 1))
exact(j + ' TACH gate divider', (rs, 2), (rg, 2), (q, 1))
exact(j + ' TACH MCU output', (ro, 1), (q, 3), ('U20', mcu))
check(f'Fan {j} MCU pullup +3V3', a.net(ro, 2) == '+3V3')
# 0.6V is the vendor's family example, not exact-model certification.
# High-state leakage <=1uA and a 0.2V supply-path drop are design assumptions.
high, low, sink = [], [], []
for supply, p, s, g in itertools.product([10.6, 13.2], *[[v*.99, v*1.01] for v in [a.resistor(rp), a.resistor(rs), a.resistor(rg)]]):
high.append((supply - 1e-6*p)*g/(p+s+g))
low.append(.6*g/(s+g))
sink.append(supply/p)
check(f'Fan {j} conditional gate high', min(high) > 2.5,
f'{min(high):.4f}..{max(high):.4f}V; assumes TACH leakage <=1uA')
check(f'Fan {j} conditional gate low', max(low) < .30,
f'<= {max(low):.4f}V; family example TACH low <=0.6V; exact model and hot NMOS off-state still require verification')
check(f'Fan {j} conditional TACH sink', max(sink) < .005,
f'<= {1000*max(sink):.4f}mA; must fit vendor sink rating')
for ref in ['Q6', 'Q7', 'Q8', 'Q9', 'Q10']:
for p, function, kind in [('1', 'G', 'input'), ('2', 'S', 'passive'), ('3', 'D', 'passive')]:
check(f'Fan {ref}.{p} pin identity', a.functions[(ref, p)] in {function, function+'_'+p} and a.pin_types[(ref, p)] == kind)
# Local footprint files are part of this project, including the two copied
# standard footprints. Match every placed fan symbol to actual pad numbers.
src = (ROOT/'PhoneRail-Board1/P6-FAN-EDL.kicad_sch').read_text(encoding='utf-8')
root = parse(src)
libs = {n[1]:n for n in children(child(root, 'lib_symbols'), 'symbol')}
for sym in children(root, 'symbol'):
p = properties(sym)
ref = p['Reference'][2]
lib, name = p['Footprint'][2].split(':')
path = ROOT / f'lib/{lib}.pretty/{name}.kicad_mod'
fp = parse(path.read_text(encoding='utf-8'))
pads = {n[1] for n in children(fp, 'pad')}
expected = {child(n, 'number')[1] for u in children(libs[child(sym, 'lib_id')[1]], 'symbol') for n in children(u, 'pin')}
check(f'Fan {ref} footprint pad numbers', expected <= pads)
check('Fan archived model datasheet', hashlib.sha256((ROOT/selection['datasheet']).read_bytes()).hexdigest() == selection['datasheet_sha256'])
check('Fan archived connector datasheet', hashlib.sha256((ROOT/selection['connector']['datasheet']).read_bytes()).hexdigest() == selection['connector']['datasheet_sha256'])
device = json.loads((ROOT/selection['catalog_record']).read_text(encoding='utf-8'))['response']['result']
check('Fan live EasyEDA selection provenance', device['property']['manufacturerId'] == selection['mpn']
and device['property']['supplierId'] == selection['lcsc'])
check('Fan documented voltage covers system bus', selection['operating_voltage_v'][0] <= 10.6
and selection['operating_voltage_v'][1] >= 13.2,
'Includes a provisional 0.2V low-side-of-range path-drop budget; actual drop needs validation')
check('Fan documented PWM and stop behavior', selection['pwm_frequency_hz'] == 25000
and selection['pwm_zero_stops'] is False and selection['rated_at_pwm_0']['rpm'] == 3300)
connector_fp = parse((ROOT/'lib/PhoneRail-Fan.pretty'/
(selection['connector']['footprint'].split(':')[1]+'.kicad_mod')).read_text(encoding='utf-8'))
pads = {p[1]:p for p in children(connector_fp, 'pad')}
check('Fan terminal pitch and drill match datasheet',
all(abs(float(child(pads[str(n)], 'at')[1]) - 2.54*(n-1)) < .001
and abs(float(child(pads[str(n)], 'drill')[1])-1.1) < .001 for n in range(1,5)))
# The supplier's family examples explicitly say that specifications vary.
check('Fan exact-model PWM TACH limits verified',
selection['family_interface_examples']['applicability_to_exact_model_confirmed'],
'Family PWM/TACH examples archived; exact-model limits, leakage and startup/locked-rotor current not published in model sheet')
check('Fan assembly and startup protection verified', False,
'Connector drawing checked; Q7 SOA/inrush, fuse coordination, power budget and installed harness/hot-plug remain open; see FAN_CIRCUIT.md')
def verify_unrelated(check, out=ROOT/'out'):
def groups(path, refs=(), pins=()):
return {frozenset((p.get('ref'), p.get('pin')) for p in n.findall('node')
if p.get('ref') not in refs and (p.get('ref'), p.get('pin')) not in pins)
for n in ET.parse(path).findall('nets/net')} - {frozenset()}
old = ROOT/'out/before-four-wire'
fan_refs = {'Q6','Q7','Q8','Q9','Q10','DZ3','D2','C738','J26','J27','R733','R734',
*{f'R{n}' for n in range(740, 752)}}
pins = {('U20', '4'), ('U20', '5'), ('U20', '9'), ('U20', '11')}
check('Four-wire revision: unrelated Board1 networks preserved',
groups(old/'Board1.xml', fan_refs, pins) == groups(out/'Board1.xml', fan_refs, pins))
from hub_strap_checks import REFS
check('Four-wire revision: Board2 networks preserved apart from verified Hub strap additions',
groups(old/'Board2.xml') == groups(out/'Board2.xml', REFS))