krishnax12/t113-minimal-linux
Allwinner T113-S3 dual-Cortex-A7 SoC with integrated 128MB DDR3, multi-rail decoupling, 24MHz and 32.768kHz oscillators, filtering, and exposed test points.
- Version
- 0.1.1
- License
- unset
- Stars
- 0
scripts/routing_geometry.py
import json,math,re,collections
from pathlib import Path
from shapely.geometry import Point,LineString,Polygon,box
from shapely.affinity import rotate
from shapely.ops import unary_union
from shapely.strtree import STRtree
LAYERS=['top','inner1','inner2','bottom']
LMAP={'F.Cu':'top','In1.Cu':'inner1','In2.Cu':'inner2','B.Cu':'bottom'}
REMOVABLE_JUMPERS={'JP_UART_RX','JP_UART_TX'}
class Nets:
def __init__(self,data):
self.parent={};self.data=data
# Fitted shunts complete the schematic circuit, but PCB copper must
# leave their contacts isolated when the user removes the shunt.
self.removable={c['source_component_id']:c['name'] for c in self.of('source_component') if c.get('name') in REMOVABLE_JUMPERS}
for t in self.of('source_trace'):self.join(t.get('connected_source_port_ids',[])+t.get('connected_source_net_ids',[]))
for ids in self.permanent_internal_connections():self.join(ids)
self.names={self.find(n['source_net_id']):n['name'] for n in self.of('source_net')}
self.ids={n['name']:n['source_net_id'] for n in self.of('source_net')}
self.ports={p['pcb_port_id']:p for p in self.of('pcb_port')}
self.traces={t['source_trace_id']:t for t in self.of('source_trace')}
def of(self,t):return [x for x in self.data if x['type']==t]
def permanent_internal_connections(self):
return [ids for c in self.of('source_component') if c['source_component_id'] not in self.removable for ids in c.get('internally_connected_source_port_ids',[])] + [c.get('source_port_ids',[]) for c in self.of('source_component_internal_connection') if c.get('source_component_id') not in self.removable]
def find(self,x):
self.parent.setdefault(x,x)
if self.parent[x]!=x:self.parent[x]=self.find(self.parent[x])
return self.parent[x]
def join(self,ids):
for x in ids[1:]:self.parent[self.find(x)]=self.find(ids[0])
def source(self,sid):return self.names.get(self.find(sid),self.find(sid))
def net(self,g):
if g.get('source_net_id'):return self.source(g['source_net_id'])
if g.get('source_trace_id'):
t=self.traces[g['source_trace_id']];ids=t.get('connected_source_port_ids',[])+t.get('connected_source_net_ids',[])
return self.source(ids[0])
if g.get('pcb_port_id') in self.ports:return self.source(self.ports[g['pcb_port_id']]['source_port_id'])
return 'NC_'+str(g.get(g['type']+'_id'))
def geom(g,drill=False):
x,y=g['x'],g['y'];s=g.get('shape',g.get('hole_shape','circle'))
if s=='circle':return Point(x,y).buffer((g.get('hole_diameter') if drill else g.get('outer_diameter',g.get('diameter',g.get('hole_diameter'))))/2,quad_segs=24)
if s=='rect' or s=='circular_hole_with_rect_pad':
if drill:return Point(x,y).buffer(g['hole_diameter']/2,quad_segs=24)
w=g.get('width',g.get('rect_pad_width',g.get('outer_width')));h=g.get('height',g.get('rect_pad_height',g.get('outer_height')))
p=box(x-w/2,y-h/2,x+w/2,y+h/2)
return rotate(p,g.get('ccw_rotation',0),origin=(x,y))
if s=='pill':
w=g['hole_width' if drill else 'outer_width'];h=g['hole_height' if drill else 'outer_height'];r=min(w,h)/2
p=LineString([(x-(w/2-r),y-(h/2-r)),(x+(w/2-r),y+(h/2-r))]).buffer(r,quad_segs=24)
return rotate(p,g.get('ccw_rotation',0),origin=(x,y))
raise ValueError(s)
def copper(data):
nets=Nets(data);items=[]
def add(gid,kind,net,layer,geometry,**kw):items.append(dict(id=gid,kind=kind,net=net,layer=layer,geometry=geometry,**kw))
for g in nets.of('pcb_smtpad')+nets.of('pcb_plated_hole')+nets.of('pcb_via'):
gid=g[g['type']+'_id'];net=nets.net(g);ls=[g['layer']] if g['type']=='pcb_smtpad' else g.get('layers',LAYERS)
for l in ls:add(gid,'pad' if g['type']!='pcb_via' else 'via',net,l,geom(g),source_port_id=nets.ports.get(g.get('pcb_port_id'),{}).get('source_port_id'))
for t in nets.of('pcb_trace'):
net=nets.net(t)
for i,(a,b) in enumerate(zip(t['route'],t['route'][1:])):
layer=width=None
if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:layer,width=a['layer'],max(a['width'],b['width'])
elif a['route_type']=='wire' and b['route_type']=='via' and a['layer']==b['from_layer']:layer,width=a['layer'],a['width']
elif a['route_type']=='via' and b['route_type']=='wire' and a['to_layer']==b['layer']:layer,width=b['layer'],b['width']
if layer and (a['x'],a['y'])!=(b['x'],b['y']):
add(t['pcb_trace_id']+'_'+str(i),'trace',net,layer,LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(width/2,quad_segs=16))
return items,nets
def remove_standalone_plane_islands(data):
"""Remove only plane components with no physical copper contact.
Return (cleaned_records, removed_pour_records), preserving retained records
and IDs. Same-net pours can anchor one another on the same layer. Any pad,
trace, via or plated-hole contact anchors a component regardless of net;
even a floating route must survive for audit() to report. Foreign-net pour
contact also anchors both sides so cleanup cannot conceal a plane short.
"""
contact_tolerance=.0006 # Same physical-contact tolerance as audit().
items,nets=copper(data);pours=nets.of('pcb_copper_pour')
if not pours:return list(data),[]
def pour_geometry(p):
b=p['brep_shape'];points=lambda ring:[(q['x'],q['y']) for q in ring['vertices']]
return Polygon(points(b['outer_ring']),[points(h) for h in b.get('inner_rings',[])])
geometries=[pour_geometry(p) for p in pours]
plane_nets=[nets.net(p) for p in pours]
parent=list(range(len(pours)));anchored=set()
def find(i):
while parent[i]!=i:
parent[i]=parent[parent[i]];i=parent[i]
return i
def join(a,b):parent[find(a)]=find(b)
for layer in LAYERS:
chosen=[i for i,p in enumerate(pours) if p['layer']==layer]
gs=[geometries[i] for i in chosen];plane_tree=STRtree(gs)
nonplanes=[obj['geometry'] for obj in items if obj['layer']==layer]
copper_tree=STRtree(nonplanes)
for idx,i in enumerate(chosen):
shape=gs[idx];nearby=shape.buffer(contact_tolerance)
if any(shape.distance(nonplanes[int(hit)])<contact_tolerance for hit in copper_tree.query(nearby)):
anchored.add(i)
for hit in plane_tree.query(nearby):
k=chosen[int(hit)]
if k<=i or shape.distance(geometries[k])>=contact_tolerance:continue
if plane_nets[i]==plane_nets[k]:join(i,k)
else:anchored.update((i,k))
anchored_components={find(i) for i in anchored}
removed=[p for i,p in enumerate(pours) if find(i) not in anchored_components]
removed_ids={p['pcb_copper_pour_id'] for p in removed}
return [g for g in data if g.get('pcb_copper_pour_id') not in removed_ids],removed
def fill_planes(data,removed_islands=None):
items,nets=copper(data);board=nets.of('pcb_board')[0]
boundary=Polygon([(p['x'],p['y']) for p in board['outline']]).buffer(-0.5)
holes=[geom(h).buffer(.2) for h in nets.of('pcb_hole')]
out=[g for g in data if g['type']!='pcb_copper_pour']
regions=json.loads((Path(__file__).resolve().parent.parent / 'routing' / 'plane-regions.json').read_text())
for region in regions:
layer,net=region['layer'],region['net']
region_shape=Polygon([(p['x'],p['y']) for p in region['outline']]).intersection(boundary) if 'outline' in region else boundary
# Leave 4 micrometres beyond the 0.15 mm clearance requirement.
# A 2 micrometre outline simplification keeps exports compact while
# retaining a positive clearance margin after coordinate rounding.
cut=holes+[i['geometry'].buffer(.154,quad_segs=16) for i in items if i['layer']==layer and i['net']!=net]
filled=region_shape.difference(unary_union(cut)).buffer(0).simplify(.002,preserve_topology=True)
polygons=list(filled.geoms) if filled.geom_type=='MultiPolygon' else [filled]
for p in polygons:
if p.is_empty:continue
def ring(coords):return {'vertices':[{'x':round(x,6),'y':round(y,6)} for x,y in list(coords)[:-1]]}
out.append(dict(type='pcb_copper_pour',pcb_copper_pour_id='pcb_copper_pour_'+str(sum(x['type']=='pcb_copper_pour' for x in out)),shape='brep',layer=layer,source_net_id=nets.ids[net],subcircuit_id=nets.of('pcb_board')[0]['subcircuit_id'],covered_with_solder_mask=True,brep_shape={'outer_ring':ring(p.exterior.coords),'inner_rings':[ring(r.coords) for r in p.interiors]}))
cleaned,removed=remove_standalone_plane_islands(out)
if removed_islands is not None:removed_islands.extend(removed)
return cleaned
def audit(data):
items,nets=copper(data);parent={};issues=[]
def f(i):
parent.setdefault(i,i)
if parent[i]!=i:parent[i]=f(parent[i])
return parent[i]
def j(a,b):parent[f(a)]=f(b)
for p in nets.of('pcb_copper_pour'):
b=p['brep_shape'];pts=lambda r:[(q['x'],q['y']) for q in r['vertices']]
g=Polygon(pts(b['outer_ring']),[pts(r) for r in b.get('inner_rings',[])])
items.append(dict(id=p['pcb_copper_pour_id'],kind='plane',net=nets.net(p),layer=p['layer'],geometry=g))
ids=collections.defaultdict(list);ports=collections.defaultdict(list)
for i,obj in enumerate(items):
ids[obj['id']].append(i)
if obj.get('source_port_id'):ports[obj['source_port_id']].append(i)
for ii in ids.values():
for i in ii[1:]:j(ii[0],i)
required_ports={sid for t in nets.of('source_trace') for sid in t.get('connected_source_port_ids',[])}
for sid in sorted(required_ports):
if not ports.get(sid):issues.append(dict(type='missing_physical_endpoint',source_port_id=sid,net=nets.source(sid)))
for source_ids in nets.permanent_internal_connections():
ii=[i for sid in source_ids for i in ports[sid]]
for i in ii[1:]:j(ii[0],i)
for layer in LAYERS:
chosen=[i for i,x in enumerate(items) if x['layer']==layer];gs=[items[i]['geometry'] for i in chosen];tree=STRtree(gs)
for idx,i in enumerate(chosen):
a=items[i]
for hit in tree.query(gs[idx].buffer(.151)):
k=chosen[int(hit)]
if k<=i:continue
b=items[k];distance=a['geometry'].distance(b['geometry'])
# Actual copper contact conducts even when the net metadata is
# wrong. Keep that physical graph for the open-shunt test.
if distance<.0006:j(i,k)
if a['net']==b['net']:
continue
required=.1 if a['kind']==b['kind']=='pad' else .15
if distance<required-.001:issues.append(dict(type='copper_clearance',layer=layer,a=a['id'],b=b['id'],nets=[a['net'],b['net']],actual=round(distance,5),required=required))
jumper_checks=[]
for component_id,name in sorted(nets.removable.items()):
source_ids=[p['source_port_id'] for p in nets.of('source_port') if p.get('source_component_id')==component_id]
if len(source_ids)!=2:
issues.append(dict(type='removable_jumper_pin_count',name=name,source_port_ids=source_ids))
continue
a,b=source_ids
present=bool(ports.get(a)) and bool(ports.get(b))
bypassed=bool({f(i) for i in ports.get(a,[])} & {f(i) for i in ports.get(b,[])})
physical_nets=[nets.source(a),nets.source(b)]
if physical_nets[0]==physical_nets[1]:issues.append(dict(type='removable_jumper_net_short',name=name,source_port_ids=source_ids))
if bypassed:issues.append(dict(type='removable_jumper_bypassed',name=name,source_port_ids=source_ids))
jumper_checks.append(dict(name=name,source_port_ids=source_ids,physical_nets=physical_nets,isolated_when_open=present and not bypassed and physical_nets[0]!=physical_nets[1]))
groups=collections.defaultdict(lambda:collections.defaultdict(list))
for i,obj in enumerate(items):
if obj['kind']=='pad' and obj.get('source_port_id'):groups[obj['net']][f(i)].append(obj['source_port_id'])
unconnected=[]
for net,g in groups.items():
if len(g)>1:unconnected.append(dict(net=net,groups=[sorted(set(ii)) for ii in g.values()]))
physical_groups=collections.defaultdict(list)
for i,obj in enumerate(items):physical_groups[f(i)].append(obj)
for group in physical_groups.values():
if not any(obj.get('source_port_id') for obj in group) and any(obj['kind'] in ('trace','via') for obj in group):
issues.append(dict(type='floating_copper',net=group[0]['net'],ids=sorted({obj['id'] for obj in group})))
# Nonplated holes remove copper on every layer, including top/bottom tracks.
for hole in nets.of('pcb_hole'):
h=geom(hole,drill=True)
for obj in items:
d=h.distance(obj['geometry'])
if d<.15-.001:issues.append(dict(type='nonplated_hole_clearance',hole=hole['pcb_hole_id'],id=obj['id'],layer=obj['layer'],actual=round(d,5),required=.15))
drilled=nets.of('pcb_plated_hole')+nets.of('pcb_via')+nets.of('pcb_hole');dg=[geom(g,drill=True) for g in drilled];dt=STRtree(dg)
min_drill=100
for i,g in enumerate(dg):
for hit in dt.query(g.buffer(.251)):
k=int(hit)
if k<=i:continue
d=g.distance(dg[k]);required=.15
min_drill=min(min_drill,d)
if d<required-.001:issues.append(dict(type='drill_clearance',a=drilled[i].get(drilled[i]['type']+'_id'),b=drilled[k].get(drilled[k]['type']+'_id'),actual=d,required=required))
board=nets.of('pcb_board')[0];boundary=Polygon([(p['x'],p['y']) for p in board['outline']])
for a in items:
if a['kind'] in ('trace','via') and not boundary.buffer(-.19).covers(a['geometry']):issues.append(dict(type='board_edge_clearance',id=a['id']))
return dict(passed=not issues and not unconnected,copperItems=len(items),pcbTraces=len(nets.of('pcb_trace')),vias=len(nets.of('pcb_via')),unconnectedNets=unconnected,violations=issues,violationCounts=dict(collections.Counter(x['type'] for x in issues)),removableJumperChecks=jumper_checks)