shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/prepare-bulk-top.py
"""Prepare the bounded bulk-capacitor relocation without relaxing DRC."""
from route_geometry import *
from collections import defaultdict
from shapely.strtree import STRtree
baseline=Path("../../checkpoints/nema17-before-rishabh-review-v053-DuFOGi/restore-verified")
c=load_circuit("routing/review-placement.circuit.json");old=load_circuit(str(baseline/"dist/index/circuit.json"))
assert not [e for e in c if e["type"].endswith("_error")],"Resolve native placement errors before routing"
root=connectivity(c);mapping=json.loads(Path("routing/net-map.json").read_text())["roots"]
sources={e["source_trace_id"]:e for e in c if e["type"]=="source_trace"}
named={e.get("name"):e for e in sources.values()}
parts={e["source_component_id"]:e["name"] for e in c if e["type"]=="source_component"}
pcnames={e["pcb_component_id"]:parts[e["source_component_id"]] for e in c if e["type"]=="pcb_component"}
ports={e["pcb_port_id"]:e for e in c if e["type"]=="pcb_port"}
by_source={p["source_port_id"]:p for p in ports.values()}
moved=set(json.loads(Path("review-placement.json").read_text()))
newpads=[]
for e in c:
if e["type"] in ["pcb_smtpad","pcb_plated_hole"] and pcnames[e["pcb_component_id"]] in moved:
p=ports.get(e.get("pcb_port_id"))
n=mapping[root(p["source_port_id"])] if p else "NC:"+e[e["type"]+"_id"]
newpads.append((geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else ""),e.get("layers",[e.get("layer")]),n))
def net(t):
s=sources[t["source_trace_id"]]
return mapping[root((s.get("connected_source_port_ids",[])+s.get("connected_source_net_ids",[]))[0])]
original=json.loads((baseline/"routing/complete-routes.json").read_text())
raw_points=[(20.65,-1.85),(20.65,-6.6),(12.4,-6.6),(12.4,-6.1),
(10.4,-6.1),(10.2,-6.6),(9.95,-6.9),(6.4,-9.3)]
raw_shape=LineString(raw_points).buffer(.25)
ground_routes=[
('top',[(18.4,-3.147581),(18.2,-4.8),(18.2,-5.6)]),
('bottom',[(18.2,-5.0),(18.2,-6.0),(18.35,-6.5),(18.35,-9.3),(16.65,-10.4)]),
]
ground_vias=[(18.2,-5.0),(18.2,-5.6)]
motor_routes=[
('top',[(14.592402,-5),(13.4,-5),(13.4,-5.85)]),
('bottom',[(13.4,-5.85),(13.4,-6.7),(10.8,-6.7),(10.8,-7.09)]),
('top',[(10.8,-7.09),(10.8,-10.4),(9.5,-10.9)]),
('bottom',[(9.5,-10.9),(9.5,-12.2)]),
]
motor_vias=[(13.4,-5.85),(10.8,-7.09),(9.5,-10.9)]
fused_routes=[
('top',.3,[(12.6,-8.85),(14.1,-8.9)]),
('bottom',.3,[(14.1,-8.9),(14.7,-8.5),(14.7,-7.3)]),
('bottom',.25,[(14.7,-7.3),(14.8232,-7.1),(14.8232,-6.0)]),
('bottom',.3,[(14.8232,-6.0),(14.62,-5.8),(14.62,-3.74),(13.0,-3.74)]),
('bottom',.25,[(13.0,-3.74),(11.207598,-3.74)]),
('bottom',.3,[(11.207598,-3.74),(11.207598,-4.0)]),
('top',.3,[(11.207598,-4.0),(11.207598,-5.0)]),
]
fused_vias=[(14.1,-8.9),(11.207598,-4.0)]
txn_points=[(15.2232,-5.4495),(15.65,-5.9),(15.9,-5.95)]
txn_via=(15.9,-5.95)
adc_points=[(17.275078,-1.870962),(17.2751,-2.7),(17.35,-3.0),(17.45,-3.5),(17.45,-5.0)]
adc_via=(17.45,-5.0)
filter_ground_routes=[('top',[(15.11,-11.0),(15.6,-10.7),(14.61,-10.35)]),
('bottom',[(14.61,-10.35),(16.65,-10.4)])]
filter_ground_via=(14.61,-10.35)
swd_points=[(19.8,-10.2),(20.7,-10.2)]
swd_via=(20.7,-10.2)
clock_points=[(6.83,-6.0),(7.65,-6.6)]
clock_via=(7.65,-6.6)
ldo_ground_routes=[('bottom',[(16.65,-4.55),(16.15,-4.55),(15.8,-3.85)]),
('top',[(15.8,-3.85),(18.4,-3.147581)])]
ldo_ground_via=(15.8,-3.85)
ldo_input_points=[(16.65,-5.5),(17.4,-5.95)]
ldo_input_via=(17.4,-5.95)
manual=[('N101','top',raw_shape)]+[('N62',l,LineString(points).buffer(.4)) for l,points in ground_routes]
manual += [('N62',l,Point(x,y).buffer(.225)) for x,y in ground_vias for l in LAYERS]
manual += [('N179',l,LineString(points).buffer(.4)) for l,points in motor_routes]
manual += [('N179',l,Point(x,y).buffer(.225)) for x,y in motor_vias for l in LAYERS]
manual += [('N178',l,LineString(points).buffer(w/2)) for l,w,points in fused_routes]
manual += [('N178',l,Point(x,y).buffer(.225)) for x,y in fused_vias for l in LAYERS]
manual += [('N152','bottom',LineString(txn_points).buffer(.1))]
manual += [('N152',l,Point(*txn_via).buffer(.225)) for l in LAYERS]
manual += [('N166','bottom',LineString(adc_points).buffer(.075))]
manual += [('N166',l,Point(*adc_via).buffer(.225)) for l in LAYERS]
manual += [('N62',l,LineString(points).buffer(.075)) for l,points in filter_ground_routes]
manual += [('N62',l,Point(*filter_ground_via).buffer(.225)) for l in LAYERS]
manual += [('N160','bottom',LineString(swd_points).buffer(.075))]
manual += [('N160',l,Point(*swd_via).buffer(.225)) for l in LAYERS]
manual += [('N31','top',LineString(clock_points).buffer(.06))]
manual += [('N31',l,Point(*clock_via).buffer(.225)) for l in LAYERS]
manual += [('N62',l,LineString(points).buffer(.1)) for l,points in ldo_ground_routes]
manual += [('N62',l,Point(*ldo_ground_via).buffer(.225)) for l in LAYERS]
manual += [('N57','bottom',LineString(ldo_input_points).buffer(.1))]
manual += [('N57',l,Point(*ldo_input_via).buffer(.225)) for l in LAYERS]
bad_vias=set()
for t in original:
for p in t["route"]:
if p["route_type"]=="via" and (any(g.distance(Point(p["x"],p["y"]))<.351 for g,ls,n in newpads)
or any(net(t)!=n and g.distance(Point(p['x'],p['y']))<.3251 for n,l,g in manual)):
bad_vias.add((round(p["x"],6),round(p["y"],6)))
retained=[];removed=[];seen=set()
for t in original:
n=net(t)
for p in t["route"]:
if p["route_type"]!="via":continue
key=round(p["x"],6),round(p["y"],6)
if key in bad_vias:continue
k=(n,"via",key)
if k in seen:continue
seen.add(k)
retained.append({**t,"route":[p]})
for a,b in zip(t["route"],t["route"][1:]):
if a["route_type"]!=b["route_type"] or a["route_type"]!="wire" or a["layer"]!=b["layer"]:continue
if math.dist((a["x"],a["y"]),(b["x"],b["y"]))<1e-8:continue
width=max(a["width"],b["width"]);g=LineString([(a["x"],a["y"]),(b["x"],b["y"])]).buffer(width/2)
key=n,a["layer"],tuple(sorted([(a["x"],a["y"]),(b["x"],b["y"])])),width
if key in seen:continue
seen.add(key)
if (any(nn!=n and a["layer"] in ls and g.distance(h)<.1001 for h,ls,nn in newpads)
or any(n!=nn and a['layer']==l and g.distance(h)<.1001 for nn,l,h in manual)):
removed.append(dict(source=t["source_trace_id"],net=n,layer=a["layer"],width=width,points=[(a["x"],a["y"]),(b["x"],b["y"])]))
continue
route=[dict(a),dict(b)]
for p in route:
p["width"]=width
for field in ["start_pcb_port_id","end_pcb_port_id"]:
q=ports.get(p.get(field))
if q and (p["layer"] not in q["layers"] or math.dist((p["x"],p["y"]),(q["x"],q["y"]))>1e-6):p.pop(field)
retained.append({**t,"route":route})
# Remove only obsolete, unanchored leaf copper left by the face changes.
# Stop at a real pad, through-via, junction or another touching same-net wire.
# This opens the new VBUS escape without deleting a working rail/ground tree.
allpads=[]
for e in c:
if e["type"] in ["pcb_smtpad","pcb_plated_hole"]:
p=ports.get(e.get("pcb_port_id"));nn=mapping[root(p["source_port_id"])] if p else None
allpads.append((nn,e.get("layers",[e.get("layer")]),geometry(e,"outer_" if e["type"]=="pcb_plated_hole" else "")))
cut_anchors={(w['net'],w['layer'],round(x,6),round(y,6)) for w in removed for x,y in w['points']}
pruned=0
for iteration in range(100):
wire_rows=[(i,t) for i,t in enumerate(retained) if len(t["route"])==2]
degree=defaultdict(list);shapes=[]
for i,t in wire_rows:
a,b=t["route"];nn=net(t);l=a["layer"]
shapes.append(LineString([(a["x"],a["y"]),(b["x"],b["y"])]).buffer(a["width"]/2))
for p in [a,b]:degree[(nn,l,round(p["x"],6),round(p["y"],6))].append(i)
tree=STRtree(shapes);vias={(net(t),round(p["x"],6),round(p["y"],6)) for t in retained for p in t["route"] if p["route_type"]=="via"}
remove=set()
for (nn,l,x,y),ids in degree.items():
if len(ids)!=1 or (nn,x,y) in vias:continue
if (nn,l,x,y) in cut_anchors:continue
if not (-2<=x<=20.9 and -14<=y<=5):continue
i=ids[0];t=retained[i];point=Point(x,y);allow=t["route"][0]["width"]/2+1e-5
if any(n==nn and l in ls and g.distance(point)<=allow for n,ls,g in allpads):continue
if any(wire_rows[int(j)][0]!=i and net(wire_rows[int(j)][1])==nn and wire_rows[int(j)][1]["route"][0]["layer"]==l and shapes[int(j)].distance(point)<1e-6 for j in tree.query(point)):continue
remove.add(i)
if not remove:break
retained=[t for i,t in enumerate(retained) if i not in remove];pruned+=len(remove)
assert iteration<99
retained.append(dict(type="pcb_trace",source_trace_id=named['BUCK_IN_CAP']['source_trace_id'],
route=[dict(route_type='wire',layer='top',width=.5,x=x,y=y) for x,y in raw_points]))
for l,points in ground_routes:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_BULK_GND']['source_trace_id'],
route=[dict(route_type='wire',layer=l,width=.8,x=x,y=y) for x,y in points]))
for x,y in ground_vias:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_BULK_GND']['source_trace_id'],
route=[dict(route_type='via',x=x,y=y,from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
for l,points in motor_routes:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_PROTECTED']['source_trace_id'],
route=[dict(route_type='wire',layer=l,width=.8,x=x,y=y) for x,y in points]))
for x,y in motor_vias:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_PROTECTED']['source_trace_id'],
route=[dict(route_type='via',x=x,y=y,from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
for l,w,points in fused_routes:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_FUSED']['source_trace_id'],
route=[dict(route_type='wire',layer=l,width=w,x=x,y=y) for x,y in points]))
for x,y in fused_vias:
retained.append(dict(type='pcb_trace',source_trace_id=named['VM_FUSED']['source_trace_id'],
route=[dict(route_type='via',x=x,y=y,from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
retained.append(dict(type='pcb_trace',source_trace_id=named['ETH_TXN_TERM']['source_trace_id'],
route=[dict(route_type='wire',layer='bottom',width=.2,x=x,y=y) for x,y in txn_points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['ETH_TXN_TERM']['source_trace_id'],
route=[dict(route_type='via',x=txn_via[0],y=txn_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
retained.append(dict(type='pcb_trace',source_trace_id=named['CURRENT_A_FILTER']['source_trace_id'],
route=[dict(route_type='wire',layer='bottom',width=.15,x=x,y=y) for x,y in adc_points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['CURRENT_A_FILTER']['source_trace_id'],
route=[dict(route_type='via',x=adc_via[0],y=adc_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
for l,points in filter_ground_routes:
retained.append(dict(type='pcb_trace',source_trace_id=named['CURRENT_A_FILTER_GND']['source_trace_id'],
route=[dict(route_type='wire',layer=l,width=.15,x=x,y=y) for x,y in points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['CURRENT_A_FILTER_GND']['source_trace_id'],
route=[dict(route_type='via',x=filter_ground_via[0],y=filter_ground_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
retained.append(dict(type='pcb_trace',source_trace_id=named['MOTION_TP_SWDIO']['source_trace_id'],
route=[dict(route_type='wire',layer='bottom',width=.15,x=x,y=y) for x,y in swd_points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['MOTION_TP_SWDIO']['source_trace_id'],
route=[dict(route_type='via',x=swd_via[0],y=swd_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
retained.append(dict(type='pcb_trace',source_trace_id=named['ETH_SCLK_SOC_LINK']['source_trace_id'],
route=[dict(route_type='wire',layer='top',width=.12,x=x,y=y) for x,y in clock_points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['ETH_SCLK_SOC_LINK']['source_trace_id'],
route=[dict(route_type='via',x=clock_via[0],y=clock_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
for l,points in ldo_ground_routes:
retained.append(dict(type='pcb_trace',source_trace_id=named['U_2V8_LOCAL_GND']['source_trace_id'],
route=[dict(route_type='wire',layer=l,width=.2,x=x,y=y) for x,y in points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['U_2V8_LOCAL_GND']['source_trace_id'],
route=[dict(route_type='via',x=ldo_ground_via[0],y=ldo_ground_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
retained.append(dict(type='pcb_trace',source_trace_id=named['U_2V8_INPUT']['source_trace_id'],
route=[dict(route_type='wire',layer='bottom',width=.2,x=x,y=y) for x,y in ldo_input_points]))
retained.append(dict(type='pcb_trace',source_trace_id=named['U_2V8_INPUT']['source_trace_id'],
route=[dict(route_type='via',x=ldo_input_via[0],y=ldo_input_via[1],from_layer='top',to_layer='bottom',hole_diameter=.3,outer_diameter=.45)]))
# Connect changed terminals to an actual, stationary copper tree. The entire
# board is reimported from emitted copper afterwards; logical net membership
# alone is never accepted as continuity.
from collections import Counter
anchor_preference={"U_MOTION":0,"U_MOTOR":1,"U_CURRENT_A":2,"U_CURRENT_B":2,"F_VM":3,"U1":4}
def port_net(p):return mapping[root(p["source_port_id"])]
fixed=[p for p in ports.values() if pcnames[p["pcb_component_id"]] not in moved]
def anchor(p,source_name):
choices=[q for q in fixed if q['pcb_port_id']!=p['pcb_port_id'] and port_net(q)==port_net(p)]
assert choices,("No stationary electrical anchor",p)
if source_name=="VM_BULK_GND":
tvs=next(e["source_port_id"] for e in c if e["type"]=="source_port" and parts[e["source_component_id"]]=="D_VM_TVS" and e.get("pin_number")==2)
return next(q for q in ports.values() if q["source_port_id"]==tvs)
return min(choices,key=lambda q:(anchor_preference.get(pcnames[q["pcb_component_id"]],10),math.dist((p["x"],p["y"]),(q["x"],q["y"]))))
def endpoint(p):return {k:p[k] for k in ["x","y","layers","pcb_port_id"]}
jobs=[];seen_ports=set()
def add_job(p,s):
if p["pcb_port_id"] in seen_ports:return
seen_ports.add(p["pcb_port_id"]);name=s.get("name","")
constraints={}
if name in ["VM_BULK","VM_BULK_GND","VM_PROTECTED","VM_TVS_GND","DRV_EP_GND"]:
constraints={"min_trace_thickness":.8,"allowed_layers":["top","bottom"]}
elif name=="VM_FUSED":
constraints={"min_trace_thickness":.3,"allowed_layers":["top","bottom"]}
elif net(s)=="N101":
constraints={"min_trace_thickness":.5,"allowed_layers":["top","bottom"]}
start=endpoint(p)
if name=='BUCK_IN_CAP' and pcnames[p['pcb_component_id']]=='Q_VBUS':
start=dict(x=20.65,y=-1.85,layers=['top'],pcb_port_id='retained_raw_pd_trunk')
if name=='BUCK_IN_CAP' and pcnames[p['pcb_component_id']]=='U_BUCK':
start=dict(x=6.4,y=-12.2,layers=['top','bottom'],pcb_port_id='retained_buck_feed')
if name=='ETH_TXN_TERM' and pcnames[p['pcb_component_id']]=='R_ETH_B_TX':
start=dict(x=txn_via[0],y=txn_via[1],layers=LAYERS,pcb_port_id='authored_ethernet_txn_stitch')
if name=='MOTION_TP_SWDIO' and pcnames[p['pcb_component_id']]=='TP_MOTION_SWDIO':
start=dict(x=swd_via[0],y=swd_via[1],layers=LAYERS,pcb_port_id='authored_swdio_stitch')
if name=='ETH_SCLK_SOC_LINK' and pcnames[p['pcb_component_id']]=='R_ETH_SCLK':
start=dict(x=clock_via[0],y=clock_via[1],layers=LAYERS,pcb_port_id='authored_clock_stitch')
if name=='U_2V8_INPUT' and pcnames[p['pcb_component_id']]=='U_2V8':
start=dict(x=ldo_input_via[0],y=ldo_input_via[1],layers=LAYERS,pcb_port_id='authored_ldo_supply_stitch')
jobs.append(dict(source=s["source_trace_id"],start=start,target={**endpoint(anchor(p,name)),"connected_tree":True},constraints=constraints))
for p in ports.values():
if pcnames[p["pcb_component_id"]] not in moved:continue
candidates=[s for s in sources.values() if p["source_port_id"] in s.get("connected_source_port_ids",[])]
assert candidates,("Unconnected changed terminal",p)
add_job(p,candidates[0])
for sid in sorted({w["source"] for w in removed}):
s=sources[sid]
for pid in s.get("connected_source_port_ids",[]):
if pid in by_source:add_job(by_source[pid],s)
jobs.sort(key=lambda j:(-j.get("constraints",{}).get("min_trace_thickness",.12),
0 if sources[j["source"]].get("name","").startswith("VM_BULK") else 1))
def write(p,v):Path(p).write_text(json.dumps(v,indent=2)+"\n")
for i,t in enumerate(retained):t["pcb_trace_id"]=f"bulk_top_retained_{i}"
write("routing/bulk-top-retained.json",retained)
write("routing/bulk-top-plan.json",dict(prefix="bulk-top",placement="routing/review-placement.circuit.json",retained="routing/bulk-top-retained.json",extent=[-20.9,-20.9,20.9,20.9],jobs=jobs))
authored_wires=[dict(net='N101',layer='top',width=.5,points=raw_points)]
for nn,ww,rr in [('N62',.8,ground_routes),('N179',.8,motor_routes),('N62',.15,filter_ground_routes),('N62',.2,ldo_ground_routes)]:
authored_wires += [dict(net=nn,layer=l,width=ww,points=points) for l,points in rr]
authored_wires += [dict(net='N178',layer=l,width=w,points=points) for l,w,points in fused_routes]
authored_wires += [dict(net=n,layer=l,width=w,points=points) for n,l,w,points in [
('N152','bottom',.2,txn_points),('N166','bottom',.15,adc_points),
('N160','bottom',.15,swd_points),('N31','top',.12,clock_points),('N57','bottom',.2,ldo_input_points)]]
authored_vias=[dict(net=n,x=x,y=y) for n,points in [
('N62',ground_vias),('N179',motor_vias),('N178',fused_vias),('N152',[txn_via]),
('N166',[adc_via]),('N62',[filter_ground_via]),('N160',[swd_via]),
('N31',[clock_via]),('N62',[ldo_ground_via]),('N57',[ldo_input_via])] for x,y in points]
write("reports/bulk-top-preparation.json",dict(moved=sorted(moved),removedSegments=removed,prunedLeaves=pruned,removedVias=sorted(bad_vias),retainedSegments=len(retained),jobs=jobs,
authoredWires=authored_wires,authoredVias=authored_vias))
print(json.dumps(dict(retained=len(retained),removed=len(removed),removedVias=len(bad_vias),prunedLeaves=pruned,jobs=len(jobs),affectedSources=dict(Counter(sources[w["source"]].get("name") for w in removed)))))