129 行
8.3 KiB
Python
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))
|