MustafaMulla29/rp2040-bldc-motor-controller-new
A USB‑C PD–powered RP2040 BLDC motor controller PCB with 3.3 V logic, MCT8329A sensorless commutation, three MOSFET half-bridges, protected power switching, USB data, controls, and motor connectors.
- Version
- 0.3.82
- License
- unset
- Stars
- 0
scripts/pcb-audit/audit-widths.py
import contextlib,io,runpy,collections,json,math
from pathlib import Path
with contextlib.redirect_stdout(io.StringIO()):e=runpy.run_path(str(Path(__file__).with_name('audit-copper.py')))
R=e['R'];nodes=e['nodes'];c=e['c'];B=R.BASE
by=collections.defaultdict(list)
for i,n in enumerate(nodes):by[n['net']].append(i)
# Actual physical contact graph, retaining layers and widths.
adj=[set() for _ in nodes]
for (net,l),ids in e['bylayer'].items():
tree=e['STRtree']([nodes[i]['shape'] for i in ids])
for i in ids:
for j0 in tree.query(nodes[i]['shape'].buffer(1e-6)):
j=ids[int(j0)]
if j!=i and nodes[i]['shape'].distance(nodes[j]['shape'])<=1e-6:adj[i].add(j)
def test(net,a,b,width,paired=False):
ids=by[net];ok=lambda i:(nodes[i]['width'] is None or nodes[i]['width']>=width-1e-6) and (not paired or nodes[i]['kind']!='via' or i in goodvias)
starts=[i for i in ids if (nodes[i]['id']==a[1:] if a.startswith('#') else nodes[i]['label']==a)];targets={i for i in ids if (nodes[i]['id']==b[1:] if b.startswith('#') else nodes[i]['label']==b)};assert starts and targets,(net,a,b)
seen=set(starts);q=collections.deque(starts)
while q and not seen&targets:
for j in adj[q.popleft()]:
if j not in seen and ok(j):seen.add(j);q.append(j)
return bool(seen&targets)
# A parallel pair must contact a common broad-copper island on two layers.
goodvias=set();pairresults=[]
for net,ids in by.items():
vs=[i for i in ids if nodes[i]['kind']=='via'];copper=[i for i in ids if nodes[i]['kind'] in ['pad','pour'] or nodes[i]['kind']=='trace' and nodes[i]['width']>=.35-1e-7]
if len(vs)<2:continue
for k,i in enumerate(vs):
for j in vs[:k]:
a=nodes[i]['shape'].centroid;b=nodes[j]['shape'].centroid;d=a.distance(b)
if not .6-1e-7<=d<=1.3:continue
layers=[]
for l in R.LAYERS:
nearby=[n for n in copper if l in nodes[n]['layers'] and nodes[n]['shape'].distance(a)<2 and nodes[n]['shape'].distance(b)<2]
if not nearby:continue
u=__import__('shapely').union_all([nodes[n]['shape'] for n in nearby]);polys=list(u.geoms) if hasattr(u,'geoms') else [u]
if any(p.distance(nodes[i]['shape'])<1e-6 and p.distance(nodes[j]['shape'])<1e-6 for p in polys):layers.append(l)
if len(layers)>=2:
goodvias.update([i,j]);pairresults.append({'net':net,'vias':[nodes[i]['id'],nodes[j]['id']],'positions':[[a.x,a.y],[b.x,b.y]],'sharedCopperLayers':layers,'spacingMm':d})
requirements=[]
def add(net,a,b,w,pair=False):requirements.append((net,a,b,w,pair))
for gate,n,pin,fet in [('GHA',90,11,10),('GLA',92,12,11),('GHB',94,15,12),('GLB',96,16,13),('GHC',98,19,14),('GLC',100,20,15)]:
add(gate,f'U7.{pin}',f'R{n}.1',.2);add(gate,f'#driver_anchor_{pin}',f'R{n}.1',.4);add(gate+'_GATE',f'R{n}.2',f'Q{fet}.1',.4)
for net,a,b,w in [('LS_RETURN','U7.21','#driver_anchor_21',.2),('LS_RETURN','#driver_anchor_21','R81.1',.4),('MCT_DVDD','U7.36','C78.1',.2),('V3V3','U7.2','C80.1',.2),('GND','U7.25','C77.2',.2),('VMOTOR','U7.5','C70.1',.2),('LOGIC_SW','U3.5','L1.1',.6)]:add(net,a,b,w)
# The 0.4 mm-pitch QFN needs short 0.18–0.22 mm escapes, followed by 0.4 mm runs.
for i,pin in enumerate([9,13,17]):
net='MCT_BST'+chr(65+i);add(net,f'U7.{pin}',f'#driver_anchor_{pin}',.18 if pin==9 else .2);add(net,f'#driver_anchor_{pin}',f'C{74+i}.1',.4)
# PD internal-regulator bypass paths stay >=0.2 mm after the capacitor swap.
add('PD_1V2','U1.21','C3.1',.2)
add('PD_2V7','U1.23','C2.1',.2)
# Local supply and charge-pump targets: continuous >=0.20 mm copper.
# These are project layout margins, not manufacturer current ratings.
for net,a,b in [
('V1V1','U5.45','C48.1'), ('V1V1','U5.50','C47.1'),
('V1V1','U5.23','C46.1'), ('V1V1','U5.45','U5.23'),
('MCT_CPL','U7.6','C72.2'), ('MCT_CPH','U7.7','C72.1'),
('ADC_AVDD','R40.2','C50.1'), ('ADC_AVDD','U5.43','C50.1'),
('LOGIC_IN','U3.2','C10.1'),
]:add(net,a,b,.2)
# The relocated power-switch bypass retains its original supply/return widths.
add('PD_LOAD','U11.8','C140.1',.3)
add('GND','C140.2','U11.15',.2)
# Check every local half-bridge bypass, including the relocated C90.
for i in range(3):
add('VMOTOR',f'Q{10+2*i}.2',f'C{90+i}.1',.6)
add('GND',f'C{90+i}.2','C93.2',.6,True)
add('GND','C93.2','R81.2',1.2,True)
for i,p in enumerate('UVW'):
for net,a,b,w in [('VMOTOR','#power_output_bank_top',f'Q{10+2*i}.2',1),('LS_RETURN','R81.1',f'Q{11+2*i}.3',1.2),('PHASE_'+p,f'Q{10+2*i}.3',f'Q{11+2*i}.2',1),('PHASE_'+p,f'Q{10+2*i}.3',f'J{10+i}.1',1),('PHASE_'+p,f'Q{10+2*i}.3',f'J{10+i}.2',1)]:add(net,a,b,w,True)
for net,a,b,w in [('VMOTOR','Q10.2','C93.1',1),('VBUS_RAW','J19.15','Q1.5',.3),('VBUS_RAW','J19.16','Q1.8',.3),('PD_COMMON_S','Q1.1','Q2.1',.8),('PD_SWITCHED','Q2.5','R13.1',.8),('PD9','R13.2','F1.1',1),('PD9_FUSED','F1.2','Q7.1',1),('PD_LOAD','Q7.5','D6.2',1),('PD_LOAD','D4.1','D6.2',1),('GND','D4.2','C93.2',1)]:add(net,a,b,w,True)
for pin in ['C20.1','C21.1','C22.1']:add('PD9','R13.2',pin,.6)
# Power switch: validate both wide buses and every parallel IC-pin escape.
add('PD_LOAD','Q7.5','#power_input_bus',1,True)
add('VMOTOR','D6.1','C93.1',1,True)
for pin in [5,6,7]:add('VMOTOR',f'U11.{pin}','#power_output_bank_top',.3)
for pin in [8,9,10]:add('PD_LOAD',f'U11.{pin}','#power_input_bus',.3)
add('GND','U11.15','C93.2',.6,True)
# Added ideal-diode pins and close charge-pump/bypass paths.
for pin in [2,3]:add('PD9_FUSED','Q7.1',f'Q7.{pin}',.4)
for net,a,b in [('IDEAL_GATE','U12.5','Q7.4'),('IDEAL_VCAP','U12.1','C145.1'),('PD9_FUSED','U12.6','Q7.1'),('PD9_FUSED','U12.3','Q7.1'),('PD_LOAD','U12.4','Q7.5'),('GND','U12.2','C146.2'),('PD9_FUSED','C145.2','Q7.1'),('PD9_FUSED','C146.1','Q7.1')]:add(net,a,b,.2)
out=[]
for net,a,b,w,p in requirements:
basic=test(net,a,b,w);full=test(net,a,b,w,p);out.append({'net':net,'from':a,'to':b,'width':w,'requireParallelPowerVias':p,'widthContinuity':basic,'pairedViaContinuity':full,'pass':full})
report={'sha256':e['out']['sha256'],'scope':'Physical copper continuity at stated trace-width thresholds and geometric parallel-via check; not current or thermal certification.','paths':out,'parallelPairs':pairresults}
(B/'width-audit.json').write_text(json.dumps(report,indent=2));print('Path checks',sum(r['pass'] for r in out),'/',len(out),'parallel pairs',len(pairresults))
for r in out:
if not r['pass']:print('FAIL',r)