Skip to content

Commit 36fa559

Browse files
authored
Wrapper Footprint Blocks (#385)
Wrapper footprint blocks replace its internal contents with a footprint during netlisting. That is, it can have internal blocks for electronics model purposes, but when generating a board only its footprint shows up. This is implemented as board scopes in the netlister. Potential pre-feature for #367, daughterboard / multi-board assembly support. Adds two examples, a new Pololu A4988 stepper driver board, and refactor the existing VL53L0x connector to use this (instead of the weird superclass stuff). In the future, perhaps the microcontrollers can also be refactored to use this, though might need changes to the pin remapping layer. There are in two styles, with different trade-offs: - The A4988 is a custom block that only instantiates the internal IC using it for modeling. It doesn't create a bunch of ghost supporting passives (if it has extended / instantiated the application circuit), but duplicates a lot of code. Note, had this extended the application circuit, the internal resistor would create a basic-part error. - The VL53L0x extends the application circuit, which makes it a valid refinement target and duplicates no code, but generates phantom passives that don't get netlisted. Inheritance is stylistically ugly here. - Unclear what the long-term solution should be. Other changes: - Empty nets are pruned during generation (this changes a bunch of netlists) - Refdesing is scope aware - Example cleanups for the VL53L0x change
1 parent 275b102 commit 36fa559

25 files changed

+1265
-687
lines changed

edg/electronics_model/CircuitBlock.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ def footprint(self, refdes: StringLike, footprint: StringLike, pinning: Mapping[
9191
self.assign(self.fp_datasheet, '')
9292

9393

94+
class WrapperFootprintBlock(FootprintBlock):
95+
"""Block that has a footprint and optional internal contents, but the netlister ignores internal components.
96+
Useful for, for example, a breakout board where the modelling details are provided by internal chip blocks,
97+
but needs to show up as only a carrier board footprint.
98+
EXPERIMENTAL - API SUBJECT TO CHANGE."""
99+
def __init__(self, *args, **kwargs):
100+
super().__init__(*args, **kwargs)
101+
self.fp_is_wrapper = self.Metadata("A") # TODO replace with not metadata, eg superclass inspection
102+
103+
94104
@abstract_block
95105
class NetBlock(InternalBlock, NetBaseBlock, Block):
96106
def contents(self):

edg/electronics_model/NetlistGenerator.py

Lines changed: 94 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,23 @@ class Netlist(NamedTuple):
3737
nets: List[Net]
3838

3939

40-
Blocks = Dict[TransformUtil.Path, NetBlock] # Path -> Block
41-
Edges = Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths
42-
AssertConnected = List[Tuple[TransformUtil.Path, TransformUtil.Path]]
40+
# The concept of board scopes is footprints associated with a scope (defined as a path) that is a board,
41+
# to support multi-board assemblies and virtual components.
42+
# The default (top-level) board has TransformUtil.Path.empty().
43+
# None board scope means the footprints do not exist (virtual component), eg the associated blocks were for modeling.
44+
class BoardScope(NamedTuple):
45+
path: TransformUtil.Path # root path
46+
footprints: Dict[TransformUtil.Path, NetBlock] # Path -> Block footprint
47+
edges: Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths
48+
pins: Dict[TransformUtil.Path, List[NetPin]] # mapping from Port to pad
49+
assert_connected: List[Tuple[TransformUtil.Path, TransformUtil.Path]]
50+
51+
@classmethod
52+
def empty(cls, path: TransformUtil.Path): # returns a fresh, empty BordScope
53+
return BoardScope(path, {}, {}, {}, [])
54+
55+
56+
Scopes = Dict[TransformUtil.Path, Optional[BoardScope]] # Block -> board scope (reference, aliased across entries)
4357
ClassPaths = Dict[TransformUtil.Path, List[edgir.LibraryPath]] # Path -> class names corresponding to shortened path name
4458
class NetlistTransform(TransformUtil.Transform):
4559
@staticmethod
@@ -53,28 +67,38 @@ def flatten_port(path: TransformUtil.Path, port: edgir.PortLike) -> Iterable[Tra
5367
raise ValueError(f"don't know how to flatten netlistable port {port}")
5468

5569
def __init__(self, design: CompiledDesign):
56-
self.blocks: Blocks = {}
57-
self.edges: Edges = {} # as port Paths, including intermediates
58-
self.pins: Dict[TransformUtil.Path, List[NetPin]] = {} # mapping from Port to pad
59-
self.assert_connected: AssertConnected = []
70+
self.all_scopes = [BoardScope.empty(TransformUtil.Path.empty())] # list of unique scopes
71+
self.scopes: Scopes = {TransformUtil.Path.empty(): self.all_scopes[0]}
72+
6073
self.short_paths: Dict[TransformUtil.Path, List[str]] = {TransformUtil.Path.empty(): []} # seed root
6174
self.class_paths: ClassPaths = {TransformUtil.Path.empty(): []} # seed root
6275

6376
self.design = design
6477

6578
def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, edgir.LinkArray, edgir.HierarchyBlock]) -> None:
79+
# TODO may need rethought to support multi-board assemblies
80+
scope = self.scopes[path] # including footprint and exports, and everything within a link
81+
internal_scope = scope # for internal blocks
82+
6683
if isinstance(block, edgir.HierarchyBlock):
84+
if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored
85+
internal_scope = None
86+
for block_pair in block.blocks:
87+
self.scopes[path.append_block(block_pair.name)] = internal_scope
88+
for link_pair in block.links: # links considered to be the same scope as self
89+
self.scopes[path.append_link(link_pair.name)] = scope
90+
6791
# generate short paths for children first, for Blocks only
6892
main_internal_blocks: Dict[str, edgir.BlockLike] = {}
6993
other_internal_blocks: Dict[str, edgir.BlockLike] = {}
70-
if isinstance(block, edgir.HierarchyBlock):
71-
for block_pair in block.blocks:
72-
subblock = block_pair.value
73-
# ignore pseudoblocks like bridges and adapters that have no internals
74-
if not subblock.hierarchy.blocks and 'fp_is_footprint' not in subblock.hierarchy.meta.members.node:
75-
other_internal_blocks[block_pair.name] = block_pair.value
76-
else:
77-
main_internal_blocks[block_pair.name] = block_pair.value
94+
95+
for block_pair in block.blocks:
96+
subblock = block_pair.value
97+
# ignore pseudoblocks like bridges and adapters that have no internals
98+
if not subblock.hierarchy.blocks and 'fp_is_footprint' not in subblock.hierarchy.meta.members.node:
99+
other_internal_blocks[block_pair.name] = block_pair.value
100+
else:
101+
main_internal_blocks[block_pair.name] = block_pair.value
78102

79103
short_path = self.short_paths[path]
80104
class_path = self.class_paths[path]
@@ -91,17 +115,20 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
91115
for (name, subblock) in other_internal_blocks.items():
92116
self.short_paths[path.append_block(name)] = short_path + [name]
93117
self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class]
118+
elif isinstance(block, (edgir.Link, edgir.LinkArray)):
119+
for link_pair in block.links:
120+
self.scopes[path.append_link(link_pair.name)] = scope
94121

95-
if 'nets' in block.meta.members.node:
122+
if 'nets' in block.meta.members.node and scope is not None:
96123
# add self as a net
97124
# list conversion to deal with iterable-once
98125
flat_ports = list(chain(*[self.flatten_port(path.append_port(port_pair.name), port_pair.value)
99126
for port_pair in block.ports]))
100-
self.edges.setdefault(path, []).extend(flat_ports)
127+
scope.edges.setdefault(path, []).extend(flat_ports)
101128
for port_path in flat_ports:
102-
self.edges.setdefault(port_path, []).append(path)
129+
scope.edges.setdefault(port_path, []).append(path)
103130

104-
if 'nets_packed' in block.meta.members.node:
131+
if 'nets_packed' in block.meta.members.node and scope is not None:
105132
# this connects the first source to all destinations, then asserts all the sources are equal
106133
# this leaves the sources unconnected, to be connected externally and checked at the end
107134
src_port_name = block.meta.members.node['nets_packed'].members.node['src'].text_leaf
@@ -110,13 +137,13 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
110137
flat_dsts = list(self.flatten_port(path.append_port(dst_port_name), edgir.pair_get(block.ports, dst_port_name)))
111138
assert flat_srcs, "missing source port(s) for packed net"
112139
for dst_path in flat_dsts:
113-
self.edges.setdefault(flat_srcs[0], []).append(dst_path)
114-
self.edges.setdefault(dst_path, []).append(flat_srcs[0])
140+
scope.edges.setdefault(flat_srcs[0], []).append(dst_path)
141+
scope.edges.setdefault(dst_path, []).append(flat_srcs[0])
115142
for src_path in flat_srcs: # assert all sources connected
116143
for dst_path in flat_srcs:
117-
self.assert_connected.append((src_path, dst_path))
144+
scope.assert_connected.append((src_path, dst_path))
118145

119-
if 'fp_is_footprint' in block.meta.members.node:
146+
if 'fp_is_footprint' in block.meta.members.node and scope is not None:
120147
footprint_name = self.design.get_value(path.to_tuple() + ('fp_footprint',))
121148
footprint_pinning = self.design.get_value(path.to_tuple() + ('fp_pinning',))
122149
mfr = self.design.get_value(path.to_tuple() + ('fp_mfr',))
@@ -139,14 +166,14 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
139166
]
140167
part_str = " ".join(filter(None, part_comps))
141168
value_str = value if value else (part if part else '')
142-
self.blocks[path] = NetBlock(
169+
scope.footprints[path] = NetBlock(
143170
footprint_name,
144171
refdes,
145172
part_str,
146173
value_str,
147174
path,
148175
self.short_paths[path],
149-
self.class_paths[path],
176+
self.class_paths[path]
150177
)
151178

152179
for pin_spec in footprint_pinning:
@@ -157,56 +184,63 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
157184
pin_port_path = edgir.LocalPathList(pin_spec_split[1].split('.'))
158185

159186
src_path = path.follow(pin_port_path, block)[0]
160-
self.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named
161-
self.pins.setdefault(src_path, []).append(NetPin(path, pin_name))
187+
scope.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named
188+
scope.pins.setdefault(src_path, []).append(NetPin(path, pin_name))
162189

163190
for constraint_pair in block.constraints:
164-
if constraint_pair.value.HasField('connected'):
165-
self.process_connected(path, block, constraint_pair.value.connected)
166-
elif constraint_pair.value.HasField('exported'):
167-
self.process_exported(path, block, constraint_pair.value.exported)
168-
elif constraint_pair.value.HasField('exportedTunnel'):
169-
self.process_exported(path, block, constraint_pair.value.exportedTunnel)
170-
elif constraint_pair.value.HasField('connectedArray'):
171-
for expanded_connect in constraint_pair.value.connectedArray.expanded:
172-
self.process_connected(path, block, expanded_connect)
173-
elif constraint_pair.value.HasField('exportedArray'):
174-
for expanded_export in constraint_pair.value.exportedArray.expanded:
175-
self.process_exported(path, block, expanded_export)
176-
177-
def process_connected(self, path: TransformUtil.Path, current: edgir.EltTypes, constraint: edgir.ConnectedExpr) -> None:
191+
if scope is not None:
192+
if constraint_pair.value.HasField('connected'):
193+
self.process_connected(path, block, scope, constraint_pair.value.connected)
194+
elif constraint_pair.value.HasField('connectedArray'):
195+
for expanded_connect in constraint_pair.value.connectedArray.expanded:
196+
self.process_connected(path, block, scope, expanded_connect)
197+
elif constraint_pair.value.HasField('exported'):
198+
self.process_exported(path, block, scope, constraint_pair.value.exported)
199+
elif constraint_pair.value.HasField('exportedArray'):
200+
for expanded_export in constraint_pair.value.exportedArray.expanded:
201+
self.process_exported(path, block, scope, expanded_export)
202+
elif constraint_pair.value.HasField('exportedTunnel'):
203+
self.process_exported(path, block, scope, constraint_pair.value.exportedTunnel)
204+
205+
def process_connected(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope,
206+
constraint: edgir.ConnectedExpr) -> None:
178207
if constraint.expanded:
179208
assert len(constraint.expanded) == 1
180-
self.process_connected(path, current, constraint.expanded[0])
209+
self.process_connected(path, current, scope, constraint.expanded[0])
181210
return
182211
assert constraint.block_port.HasField('ref')
183212
assert constraint.link_port.HasField('ref')
184213
self.connect_ports(
214+
scope,
185215
path.follow(constraint.block_port.ref, current),
186216
path.follow(constraint.link_port.ref, current))
187217

188-
def process_exported(self, path: TransformUtil.Path, current: edgir.EltTypes, constraint: edgir.ExportedExpr) -> None:
218+
def process_exported(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope,
219+
constraint: edgir.ExportedExpr) -> None:
189220
if constraint.expanded:
190221
assert len(constraint.expanded) == 1
191-
self.process_exported(path, current, constraint.expanded[0])
222+
self.process_exported(path, current, scope, constraint.expanded[0])
192223
return
193224
assert constraint.internal_block_port.HasField('ref')
194225
assert constraint.exterior_port.HasField('ref')
195226
self.connect_ports(
227+
scope,
196228
path.follow(constraint.internal_block_port.ref, current),
197229
path.follow(constraint.exterior_port.ref, current))
198230

199-
def connect_ports(self, elt1: Tuple[TransformUtil.Path, edgir.EltTypes], elt2: Tuple[TransformUtil.Path, edgir.EltTypes]) -> None:
231+
def connect_ports(self, scope: BoardScope, elt1: Tuple[TransformUtil.Path, edgir.EltTypes],
232+
elt2: Tuple[TransformUtil.Path, edgir.EltTypes]) -> None:
200233
"""Recursively connect ports as applicable"""
201234
if isinstance(elt1[1], edgir.Port) and isinstance(elt2[1], edgir.Port):
202-
self.edges.setdefault(elt1[0], []).append(elt2[0])
203-
self.edges.setdefault(elt2[0], []).append(elt1[0])
235+
scope.edges.setdefault(elt1[0], []).append(elt2[0])
236+
scope.edges.setdefault(elt2[0], []).append(elt1[0])
204237
elif isinstance(elt1[1], edgir.Bundle) and isinstance(elt2[1], edgir.Bundle):
205238
elt1_names = list(map(lambda pair: pair.name, elt1[1].ports))
206239
elt2_names = list(map(lambda pair: pair.name, elt2[1].ports))
207240
assert elt1_names == elt2_names, f"mismatched bundle types {elt1}, {elt2}"
208241
for key in elt2_names:
209242
self.connect_ports(
243+
scope,
210244
(elt1[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt1[1].ports, key))),
211245
(elt2[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt2[1].ports, key))))
212246
# don't need to create the bundle connect, since Bundles can't be CircuitPorts
@@ -255,14 +289,12 @@ def pin_name_goodness(pin1: TransformUtil.Path, pin2: TransformUtil.Path) -> int
255289

256290
return net_prefix + str(best_path)
257291

258-
def run(self) -> Netlist:
259-
self.transform_design(self.design.design)
260-
292+
def scope_to_netlist(self, scope: BoardScope) -> Netlist:
261293
# Convert to the netlist format
262294
seen: Set[TransformUtil.Path] = set()
263295
nets: List[List[TransformUtil.Path]] = [] # lists preserve ordering
264296

265-
for port, conns in self.edges.items():
297+
for port, conns in scope.edges.items():
266298
if port not in seen:
267299
curr_net: List[TransformUtil.Path] = []
268300
frontier: List[TransformUtil.Path] = [port] # use BFS to maintain ordering instead of simpler DFS
@@ -271,15 +303,15 @@ def run(self) -> Netlist:
271303
if pin not in seen:
272304
seen.add(pin)
273305
curr_net.append(pin)
274-
frontier.extend(self.edges[pin])
306+
frontier.extend(scope.edges[pin])
275307
nets.append(curr_net)
276308

277309
pin_to_net: Dict[TransformUtil.Path, List[TransformUtil.Path]] = {} # values share reference to nets
278310
for net in nets:
279311
for pin in net:
280312
pin_to_net[pin] = net
281313

282-
for (connected1, connected2) in self.assert_connected:
314+
for (connected1, connected2) in scope.assert_connected:
283315
if pin_to_net[connected1] is not pin_to_net[connected2]:
284316
raise InvalidPackingException(f"packed pins {connected1}, {connected2} not connected")
285317

@@ -294,10 +326,16 @@ def run(self) -> Netlist:
294326
def port_ignored_paths(path: TransformUtil.Path) -> bool: # ignore link ports for netlisting
295327
return bool(path.links) or any([block.startswith('(adapter)') or block.startswith('(bridge)') for block in path.blocks])
296328

297-
netlist_blocks = [block for path, block in self.blocks.items()]
329+
netlist_footprints = [footprint for path, footprint in scope.footprints.items()]
298330
netlist_nets = [Net(name,
299-
list(chain(*[self.pins[port] for port in net if port in self.pins])),
331+
list(chain(*[scope.pins[port] for port in net if port in scope.pins])),
300332
[port for port in net if not port_ignored_paths(port)])
301333
for name, net in named_nets.items()]
334+
netlist_nets = [net for net in netlist_nets if net.pins] # prune empty nets
335+
336+
return Netlist(netlist_footprints, netlist_nets)
337+
338+
def run(self) -> Netlist:
339+
self.transform_design(self.design.design)
302340

303-
return Netlist(netlist_blocks, netlist_nets)
341+
return self.scope_to_netlist(self.all_scopes[0]) # TODO support multiple scopes

edg/electronics_model/RefdesRefinementPass.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Tuple, Dict, Set
1+
from typing import List, Tuple, Dict, Set, Optional
22

33
from .. import edgir
44
from ..core import CompiledDesign, TransformUtil
@@ -23,18 +23,28 @@ def __init__(self, design: CompiledDesign):
2323
assert isinstance(board_refdes_prefix, str)
2424
self.board_refdes_prefix = board_refdes_prefix
2525

26+
self.scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = {
27+
TransformUtil.Path.empty(): TransformUtil.Path.empty()
28+
}
29+
2630
self.block_refdes_list: List[Tuple[TransformUtil.Path, str]] = [] # populated in traversal order
27-
self.seen_blocks: Set[TransformUtil.Path] = set()
28-
self.refdes_last: Dict[str, int] = {}
31+
self.refdes_last: Dict[Tuple[TransformUtil.Path, str], int] = {} # (scope, prefix) -> num
2932

3033
def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None:
31-
if 'fp_is_footprint' in block.meta.members.node:
34+
scope = self.scopes[context.path]
35+
internal_scope = scope
36+
if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored
37+
internal_scope = None
38+
39+
for block_pair in block.blocks:
40+
self.scopes[context.path.append_block(block_pair.name)] = internal_scope
41+
42+
if 'fp_is_footprint' in block.meta.members.node and scope is not None:
3243
refdes_prefix = self.design.get_value(context.path.to_tuple() + ('fp_refdes_prefix',))
3344
assert isinstance(refdes_prefix, str)
3445

35-
refdes_id = self.refdes_last.get(refdes_prefix, 0) + 1
36-
self.refdes_last[refdes_prefix] = refdes_id
37-
assert context.path not in self.seen_blocks
46+
refdes_id = self.refdes_last.get((scope, refdes_prefix), 0) + 1
47+
self.refdes_last[(scope, refdes_prefix)] = refdes_id
3848
self.block_refdes_list.append((context.path, self.board_refdes_prefix + refdes_prefix + str(refdes_id)))
3949

4050
def run(self) -> List[Tuple[TransformUtil.Path, str]]:

edg/electronics_model/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from ..core import *
22

3-
from .CircuitBlock import FootprintBlock, CircuitPortBridge, CircuitPortAdapter, NetBlock
3+
from .CircuitBlock import FootprintBlock, WrapperFootprintBlock, CircuitPortBridge, CircuitPortAdapter, NetBlock
44
from .VoltagePorts import CircuitPort
55

66
from .Units import Farad, uFarad, nFarad, pFarad, MOhm, kOhm, Ohm, mOhm, Henry, uHenry, nHenry

0 commit comments

Comments
 (0)