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-kelvin.py
"""Check the built sense copper, including same-net shortcuts that DRC permits."""
import contextlib,io,runpy,json,math
from pathlib import Path
from shapely.geometry import LineString,Point,box
from shapely.ops import unary_union
with contextlib.redirect_stdout(io.StringIO()):
e=runpy.run_path(str(Path(__file__).with_name('audit-copper.py')))
c=e['c'];nodes=e['nodes'];R=e['R'];traces={x['pcb_trace_id']:x for x in c if x['type']=='pcb_trace'}
results=[]
for trace_id,net,start,end in [('motor_sense_positive','LS_RETURN','U7.22','R81.1'),('motor_sense_negative','GND','U7.23','R81.2')]:
t=traces[trace_id];route=t['route'];pads={n['label']:n['shape'] for n in nodes if n['kind']=='pad'}
assert all(q['route_type']=='wire' and q['layer']=='top' and q['width']>=.2 for q in route)
assert pads[start].covers(Point(R.xy(route[0]))) and pads[end].covers(Point(R.xy(route[-1])))
sense=unary_union([n['shape'] for n in nodes if n['id']==trace_id])
# Shared copper is permissible only in the actual shunt land. At the IC there
# must be no other connection to that sensing pin's land or its track.
forbidden_contact=sense.difference(pads[end].buffer(1e-6))
unexpected=[]
for n in nodes:
if 'top' not in n['layers'] or n['id']==trace_id or n['label'] in [start,end]:continue
if forbidden_contact.intersects(n['shape']):
unexpected.append({'kind':n['kind'],'id':n['id'],'net':n['net'],'label':n['label']})
start_pad=pads[start]
for n in nodes:
if 'top' not in n['layers'] or n['id']==trace_id or n['label']==start:continue
if start_pad.intersects(n['shape']):
unexpected.append({'kind':n['kind'],'id':n['id'],'net':n['net'],'atSensePad':True})
results.append({'trace':trace_id,'net':net,'from':start,'to':end,'widthMm':.2,
'lengthMm':LineString([R.xy(q) for q in route]).length,'layer':'top','vias':0,'unexpectedContacts':unexpected,'pass':not unexpected})
# The long common run has matched direction and 0.32–0.4 mm centre spacing (0.12–0.2 gap).
p=LineString([R.xy(q) for q in traces['motor_sense_positive']['route']]);n=LineString([R.xy(q) for q in traces['motor_sense_negative']['route']])
paired=[]
for y in [12.1,12.5,13,13.5,14,14.5,15,16,16.5,17,17.5,18,18.5]:
line=LineString([(-3,y),(-.8,y)]);a=p.intersection(line);b=n.intersection(line)
assert a.geom_type==b.geom_type=='Point'
paired.append({'y':y,'centerSpacingMm':a.distance(b)})
assert all(.3199 <= v['centerSpacingMm'] <= .4001 for v in paired)
out={'sha256':e['out']['sha256'],'sensePaths':results,'pairedTrunk':paired,
'note':'Kelvin isolation in built copper; terminal fanouts diverge to the two ends of the 2512 shunt. Noise performance still requires hardware testing.',
'status':'PASS' if all(x['pass'] for x in results) else 'FAIL'}
(R.BASE/'kelvin-audit.json').write_text(json.dumps(out,indent=2)+'\n')
print(json.dumps(out,indent=2));assert out['status']=='PASS'