Skip to content

Commit cbe46eb

Browse files
authored
Connector pairs for sub-boards (#488)
Adds the SubboardConnectorPair class, which is meant to support re-usable connector-pair (one external connector in the parent board, one internal connector in the sub-board) libraries. Implements connector pairs for 2.54mm pin socket/headers and 0.50mm FPCs with all combinations of cables and contact sides. Implementation-wise, this is handled by the netlister as taking on the parent and self-scopes of its container, recursively (SubboardConnectorPairs should be arbitrarily directly-nestable). Since it shares an API with SubboardBlock (in terms of external / internal boards and export-tap), refactors that into a common HasSubooardBlockApi internal class. Refactors the BLE joystick demo board to use this to move the joystick off the main board using a 0.50mm FPC. The example will be more thoroughly redone later. Refactors the test infrastructure to support checking multiple generated netlists. Opportunistic cleanup: deletes an extraneous reference netlist. Resolves #367
1 parent e5ad241 commit cbe46eb

14 files changed

Lines changed: 508 additions & 1699 deletions

edg/electronics_model/BoardScopedTransform.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ def visit_linkarray_scoped(
3939
def visit_block(self, context: TransformContext, block: edgir.HierarchyBlock) -> None:
4040
parent_scope = self._board_parent_scopes[context.path]
4141

42-
if "fp_subboard" in block.meta.members.node:
42+
if "fp_subboard" in block.meta.members.node or "fp_subboard_connector_pair" in block.meta.members.node:
4343
fp_external_blocks = self._design.get_value(context.path.to_tuple() + ("fp_external_blocks",))
4444
assert isinstance(fp_external_blocks, list)
4545
external_blocks: Optional[List[str]] = cast(List[str], fp_external_blocks)
4646
if "fp_subblocks_ignored" in block.meta.members.node:
4747
internal_scope = None
48+
elif "fp_subboard_connector_pair" in block.meta.members.node:
49+
internal_scope = self._board_scopes[TransformUtil.Path.empty().append_block(*context.path.blocks[:-1])]
4850
else:
4951
internal_scope = context.path
5052
else:

edg/electronics_model/NetlistGenerator.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,9 @@ def process_blocklike(
9898
else:
9999
scope_obj = None
100100

101-
if isinstance(block, edgir.HierarchyBlock) and "fp_subboard" in block.meta.members.node:
102-
# only valid for sub-board blocks, where some things happen in the parent scope
101+
if isinstance(block, edgir.HierarchyBlock):
103102
parent_scope = self._board_parent_scopes[path]
104-
if parent_scope is not None:
103+
if parent_scope is not None and parent_scope != scope:
105104
parent_scope_obj: Optional[BoardScope] = self.scopes[parent_scope]
106105
else:
107106
parent_scope_obj = None

edg/electronics_model/SubboardBlock.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,14 @@
77
from .. import edgir
88

99

10-
class SubboardBlock(Block):
11-
"""A block that is a sub-board, where all its blocks not marked external are part of a different board.
12-
Provides the export_tap construct to tack connectors onto ports without breaking modeling.
13-
14-
IMPORTANT: pseudo-blocks like bridges and adapters are considered internal blocks and do not affect
15-
netlisting in the exterior board. In general, external blocks should only be connected via export-tap
16-
and not direct connections where they may generate pseudo-blocks that end up in the wrong board."""
10+
@non_library
11+
class HasSubboardBlockApi(Block):
12+
"""Base class that provides the subboard construction API."""
1713

1814
def __init__(self) -> None:
1915
super().__init__()
2016
self._external_blocks: List[Block] = []
2117
self._export_taps: List[Tuple[BasePort, BasePort]] = []
22-
23-
self.fp_subboard = self.Metadata("A") # dummy distinct value
2418
self.fp_external_blocks = self.Parameter(ArrayStringExpr()) # names of all external blocks
2519

2620
BlockType = TypeVar("BlockType", bound=Block)
@@ -70,10 +64,44 @@ def _populate_def_proto_hierarchy(self, pb: edgir.HierarchyBlock, ref_map: Refab
7064
constraint_pb.exported.tap = True
7165

7266

67+
class SubboardBlock(HasSubboardBlockApi, Block):
68+
"""A block that is a sub-board, where all its blocks not marked external are part of a different board.
69+
Provides the export_tap construct to tack connectors onto ports without breaking modeling.
70+
71+
IMPORTANT: pseudo-blocks like bridges and adapters are considered internal blocks and do not affect
72+
netlisting in the exterior board. In general, external blocks should only be connected via export-tap
73+
and not direct connections where they may generate pseudo-blocks that end up in the wrong board."""
74+
75+
def __init__(self) -> None:
76+
super().__init__()
77+
self.fp_subboard = self.Metadata("A") # dummy distinct value
78+
79+
7380
class WrapperSubboardBlock(SubboardBlock):
7481
"""A wrapper block where the internal blocks are skipped for netlisting and used for modeling only.
7582
Useful for eg, dev boards that only generate a connector or socket but re-use modeling from the raw subcircuit."""
7683

7784
def __init__(self) -> None:
7885
super().__init__()
7986
self.fp_subblocks_ignored = self.Metadata("B") # dummy distinct value
87+
88+
89+
class SubboardConnectorPair(HasSubboardBlockApi, Block):
90+
"""A block meant for a connector pair, one in the exterior and one in the interior, of a SubboardBlock.
91+
When in a SubboardBlock scope and marked external, this inherits the parent and self board scope of its container,
92+
so inner Blocks marked external are part of the SubboardBlock's parent scope, while internal Blocks
93+
are part of the SubboardBlock's inner scope.
94+
95+
Inner SubboardConnectorPairs marked external similarly inherit board scopes of its containing
96+
SubboardConnectorPair.
97+
98+
Recommended convention, similar to SubboardBlock, is to directly export ports from the internal Block
99+
while export-tapping ports from the external Block. The external Block should be generated first
100+
for refdes ordering. This block's pin numbering should correspond to the external Block.
101+
102+
These should not be instantiated outside a SubboardBlock or SubboardConnectorPair. Bad things can happen.
103+
"""
104+
105+
def __init__(self) -> None:
106+
super().__init__()
107+
self.fp_subboard_connector_pair = self.Metadata("C") # dummy distinct value

edg/electronics_model/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
NetBlock,
88
CircuitPort,
99
)
10-
from .SubboardBlock import SubboardBlock, WrapperSubboardBlock
10+
from .SubboardBlock import SubboardBlock, WrapperSubboardBlock, SubboardConnectorPair
1111

1212
from .Units import Farad, uFarad, nFarad, pFarad, MOhm, kOhm, Ohm, mOhm, Henry, uHenry, nHenry
1313
from .Units import Volt, mVolt, Watt, Amp, mAmp, uAmp, nAmp, pAmp
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import unittest
2+
3+
from typing_extensions import override
4+
5+
from .NetlistGenerator import NetlistTransform
6+
from .. import FootprintBlock, DesignTop, ScalaCompiler, RefdesRefinementPass, SubboardConnectorPair
7+
from ..core import TransformUtil
8+
from .test_netlist import TestFakeSource, TestFakeSink, NetBlock, Net, NetPin
9+
from . import SubboardBlock, VoltageSink, Passive
10+
11+
12+
class SinkExteriorConnector(FootprintBlock):
13+
def __init__(self) -> None:
14+
super().__init__()
15+
16+
self.pos = self.Port(Passive.empty()) # must remain empty
17+
self.neg = self.Port(Passive.empty())
18+
19+
@override
20+
def contents(self) -> None:
21+
super().contents()
22+
23+
self.footprint( # only this footprint shows up
24+
"J",
25+
"Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical",
26+
{"1": self.pos, "2": self.neg},
27+
)
28+
29+
30+
class SinkInternalConnector(FootprintBlock):
31+
def __init__(self) -> None:
32+
super().__init__()
33+
34+
self.pos = self.Port(Passive.empty()) # must remain empty
35+
self.neg = self.Port(Passive.empty())
36+
37+
@override
38+
def contents(self) -> None:
39+
super().contents()
40+
41+
self.footprint( # only this footprint shows up
42+
"J",
43+
"Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
44+
{"1": self.pos, "2": self.neg},
45+
)
46+
47+
48+
class SinkConnectorPair(SubboardConnectorPair):
49+
def __init__(self) -> None:
50+
super().__init__()
51+
52+
self.ext = self.Block(SinkExteriorConnector(), external=True)
53+
self.int = self.Block(SinkInternalConnector())
54+
self.pos = self.Export(self.int.pos)
55+
self.neg = self.Export(self.int.neg)
56+
self.export_tap(self.pos, self.ext.pos)
57+
self.export_tap(self.neg, self.ext.neg)
58+
59+
60+
class SinkConnectorPairBlock(SubboardBlock):
61+
"""Subboard block with a connector pair and internal circuits."""
62+
63+
def __init__(self) -> None:
64+
super().__init__()
65+
66+
self.pos = self.Port(VoltageSink.empty())
67+
self.neg = self.Port(VoltageSink.empty())
68+
69+
@override
70+
def contents(self) -> None:
71+
super().contents()
72+
73+
# these blocks are part of the sub-board
74+
self.inner1 = self.Block(TestFakeSink())
75+
self.inner2 = self.Block(TestFakeSink())
76+
self.vpos = self.connect(self.pos, self.inner1.pos, self.inner2.pos)
77+
self.gnd = self.connect(self.neg, self.inner1.neg, self.inner2.neg)
78+
79+
# these define the external interface block
80+
self.conn = self.Block(SinkConnectorPair(), external=True)
81+
self.export_tap(self.pos.net, self.conn.pos)
82+
self.export_tap(self.neg.net, self.conn.neg)
83+
84+
85+
class TestConnectorPairCircuit(DesignTop):
86+
@override
87+
def contents(self) -> None:
88+
super().contents()
89+
90+
self.source = self.Block(TestFakeSource())
91+
self.sink = self.Block(SinkConnectorPairBlock())
92+
93+
self.vpos = self.connect(self.source.pos, self.sink.pos)
94+
self.gnd = self.connect(self.source.neg, self.sink.neg)
95+
96+
97+
class NetlistConnectorPairTestCase(unittest.TestCase):
98+
def test_subboard_netlist(self) -> None:
99+
compiled = ScalaCompiler.compile(TestConnectorPairCircuit)
100+
compiled.append_values(RefdesRefinementPass().run(compiled))
101+
board_netlists = NetlistTransform(compiled).run()
102+
103+
top_net = board_netlists[TransformUtil.Path.empty()]
104+
105+
self.assertIn(
106+
NetBlock(
107+
"Connector_PinSocket_2.54mm:PinSocket_1x02_P2.54mm_Vertical",
108+
"J1",
109+
"",
110+
"",
111+
["sink", "conn", "ext"],
112+
[
113+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPairBlock",
114+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPair",
115+
"edg.electronics_model.test_netlist_connector_pair.SinkExteriorConnector",
116+
],
117+
),
118+
top_net.blocks,
119+
)
120+
self.assertEqual(len(top_net.blocks), 2) # should only generate top-level source and sink
121+
122+
self.assertIn(
123+
Net(
124+
"vpos",
125+
[
126+
NetPin(["source"], "1"),
127+
NetPin(["sink", "conn", "ext"], "1"),
128+
],
129+
[
130+
TransformUtil.Path.empty().append_block("source").append_port("pos", "net"),
131+
TransformUtil.Path.empty().append_block("sink").append_port("pos", "net"),
132+
TransformUtil.Path.empty().append_block("sink", "conn").append_port("pos"),
133+
TransformUtil.Path.empty().append_block("sink", "conn", "int").append_port("pos"),
134+
TransformUtil.Path.empty().append_block("sink", "conn", "ext").append_port("pos"),
135+
],
136+
),
137+
top_net.nets,
138+
)
139+
self.assertIn(
140+
Net(
141+
"gnd",
142+
[NetPin(["source"], "2"), NetPin(["sink", "conn", "ext"], "2")],
143+
[
144+
TransformUtil.Path.empty().append_block("source").append_port("neg", "net"),
145+
TransformUtil.Path.empty().append_block("sink").append_port("neg", "net"),
146+
TransformUtil.Path.empty().append_block("sink", "conn").append_port("neg"),
147+
TransformUtil.Path.empty().append_block("sink", "conn", "int").append_port("neg"),
148+
TransformUtil.Path.empty().append_block("sink", "conn", "ext").append_port("neg"),
149+
],
150+
),
151+
top_net.nets,
152+
)
153+
self.assertEqual(len(top_net.nets), 2) # ensure empty nets pruned
154+
155+
inner_net = board_netlists[TransformUtil.Path.empty().append_block("sink")]
156+
self.assertIn(
157+
NetBlock(
158+
"Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
159+
"J2",
160+
"",
161+
"",
162+
["sink", "conn", "int"],
163+
[
164+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPairBlock",
165+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPair",
166+
"edg.electronics_model.test_netlist_connector_pair.SinkInternalConnector",
167+
],
168+
),
169+
inner_net.blocks,
170+
)
171+
self.assertIn(
172+
NetBlock(
173+
"Resistor_SMD:R_0603_1608Metric",
174+
"R1",
175+
"",
176+
"1k",
177+
["sink", "inner1"],
178+
[
179+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPairBlock",
180+
"edg.electronics_model.test_netlist.TestFakeSink",
181+
],
182+
),
183+
inner_net.blocks,
184+
)
185+
self.assertIn(
186+
NetBlock(
187+
"Resistor_SMD:R_0603_1608Metric",
188+
"R2",
189+
"",
190+
"1k",
191+
["sink", "inner2"],
192+
[
193+
"edg.electronics_model.test_netlist_connector_pair.SinkConnectorPairBlock",
194+
"edg.electronics_model.test_netlist.TestFakeSink",
195+
],
196+
),
197+
inner_net.blocks,
198+
)
199+
self.assertEqual(len(inner_net.blocks), 3)
200+
201+
self.assertIn(
202+
Net(
203+
"sink.vpos",
204+
[
205+
NetPin(["sink", "inner1"], "1"),
206+
NetPin(["sink", "inner2"], "1"),
207+
NetPin(["sink", "conn", "int"], "1"),
208+
],
209+
[
210+
TransformUtil.Path.empty().append_block("sink").append_port("pos", "net"),
211+
TransformUtil.Path.empty().append_block("sink", "conn").append_port("pos"),
212+
TransformUtil.Path.empty().append_block("sink", "conn", "int").append_port("pos"),
213+
TransformUtil.Path.empty().append_block("sink", "conn", "ext").append_port("pos"),
214+
TransformUtil.Path.empty().append_block("sink", "inner1").append_port("pos", "net"),
215+
TransformUtil.Path.empty().append_block("sink", "inner2").append_port("pos", "net"),
216+
],
217+
),
218+
inner_net.nets,
219+
)
220+
self.assertIn(
221+
Net(
222+
"sink.gnd",
223+
[
224+
NetPin(["sink", "inner1"], "2"),
225+
NetPin(["sink", "inner2"], "2"),
226+
NetPin(["sink", "conn", "int"], "2"),
227+
],
228+
[
229+
TransformUtil.Path.empty().append_block("sink").append_port("neg", "net"),
230+
TransformUtil.Path.empty().append_block("sink", "conn").append_port("neg"),
231+
TransformUtil.Path.empty().append_block("sink", "conn", "int").append_port("neg"),
232+
TransformUtil.Path.empty().append_block("sink", "conn", "ext").append_port("neg"),
233+
TransformUtil.Path.empty().append_block("sink", "inner1").append_port("neg", "net"),
234+
TransformUtil.Path.empty().append_block("sink", "inner2").append_port("neg", "net"),
235+
],
236+
),
237+
inner_net.nets,
238+
)
239+
self.assertEqual(len(inner_net.nets), 2) # ensure empty nets pruned

0 commit comments

Comments
 (0)