Skip to content

Commit da7a2ea

Browse files
authored
SvgPcb layout template backend + netlister refactor (#337)
- Add EXPERIMENTAL (alpha stage) layout template generator for svg-pcb, with example for SwitchMatrix. - Components with a defined template (extends `SvgPcbTemplateBlock` and defines `_svgpcb_template`) generates custom layout templates - Other components (not part of a component above or subtree) generate into footprints from the netlister - Add refdes netlists to test suite - Add barebones keyboard example - Netlister refactor - Move refdes parsing into the Backend, and how it's handled into the netlist file generator (instead of embedded in the NetlistTransform) - Move netlist data structures (block, net, pin) into the NetlistGenerator (instead of the file generator), clean up the data structures to use more structured types (instead of strings) where possible - Pins treated differently from ports, netlister only considers IR-level connections - Clean up and improve net naming priority (in particular prioritizing link names), this simplifies some names - Fix XIAO-ESP32C3 definition - TransformUtil.Path improvements - `.append_*` supports multiple paths - `.starts_with` prefix check w/ unit tests - Lots of cleanup of unit tests
1 parent 8f81485 commit da7a2ea

File tree

74 files changed

+35876
-568
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+35876
-568
lines changed

edg/BoardCompiler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
1818

1919
design_filename = os.path.join(target_dir, f'{target_name}.edg')
2020
netlist_filename = os.path.join(target_dir, f'{target_name}.net')
21+
netlist_refdes_filename = os.path.join(target_dir, f'{target_name}.ref.net')
2122
bom_filename = os.path.join(target_dir, f'{target_name}.csv')
2223

2324
with suppress(FileNotFoundError):
2425
os.remove(design_filename)
2526
with suppress(FileNotFoundError):
2627
os.remove(netlist_filename)
28+
with suppress(FileNotFoundError):
29+
os.remove(netlist_refdes_filename)
2730
with suppress(FileNotFoundError):
2831
os.remove(bom_filename)
2932

@@ -39,13 +42,17 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
3942
raise edg_core.ScalaCompilerInterface.CompilerCheckError(f"error during compilation: \n{compiled.error}")
4043

4144
netlist_all = NetlistBackend().run(compiled)
45+
netlist_refdes = NetlistBackend().run(compiled, {'RefdesMode': 'refdes'})
4246
bom_all = GenerateBom().run(compiled)
4347
assert len(netlist_all) == 1
4448

4549
if target_dir_name is not None:
4650
with open(netlist_filename, 'w', encoding='utf-8') as net_file:
4751
net_file.write(netlist_all[0][1])
4852

53+
with open(netlist_refdes_filename, 'w', encoding='utf-8') as net_file:
54+
net_file.write(netlist_refdes[0][1])
55+
4956
with open(bom_filename, 'w', encoding='utf-8') as bom_file:
5057
bom_file.write(bom_all[0][1])
5158

edg_core/ScalaCompilerInterface.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], er
3838

3939
# Reserved.V is a string because it doesn't load properly at runtime
4040
# Serialized strings are used since proto objects are mutable and unhashable
41-
def get_value(self, path: Iterable[Union[str, 'edgir.Reserved.V']]) -> Optional[edgir.LitTypes]:
42-
path_key = edgir.LocalPathList(path).SerializeToString()
43-
return self._values.get(path_key, None)
41+
def get_value(self, path: Union[edgir.LocalPath, Iterable[Union[str, 'edgir.Reserved.V']]]) ->\
42+
Optional[edgir.LitTypes]:
43+
if isinstance(path, edgir.LocalPath):
44+
localpath = path
45+
else:
46+
localpath = edgir.LocalPathList(path)
47+
return self._values.get(localpath.SerializeToString(), None)
4448

4549
def append_values(self, values: List[Tuple[edgir.LocalPath, edgir.ValueLit]]):
4650
"""Append solved values to this design, such as from a refinement pass"""

edg_core/TransformUtil.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,39 @@ def __repr__(self) -> str:
3232
def empty(cls) -> Path:
3333
return Path((), (), (), ())
3434

35-
def append_block(self, name: str) -> Path:
36-
assert not self.links and not self.ports and not self.params, f"tried to append block {name} to {self}"
37-
return Path(self.blocks + (name, ), self.links, self.ports, self.params)
38-
39-
def append_link(self, name: str) -> Path:
40-
assert not self.ports and not self.params, f"tried to append link {name} to {self}"
41-
return Path(self.blocks, self.links + (name, ), self.ports, self.params)
42-
43-
def append_port(self, name: str) -> Path:
44-
assert not self.params, f"tried to append port {name} to {self}"
45-
return Path(self.blocks, self.links, self.ports + (name, ), self.params)
35+
def startswith(self, prefix: Path) -> bool:
36+
if self.blocks == prefix.blocks: # exact match, check subpaths
37+
if self.links == prefix.links:
38+
if self.ports == prefix.ports:
39+
return len(self.params) >= len(prefix.params) and self.params[:len(prefix.params)] == prefix.params
40+
elif len(self.ports) >= len(prefix.ports) and self.ports[:len(prefix.ports)] == prefix.ports:
41+
return (not self.params) and (not prefix.params)
42+
else:
43+
return False
44+
45+
elif len(self.links) >= len(prefix.links) and self.links[:len(prefix.links)] == prefix.links:
46+
return (not self.ports) and (not prefix.ports) and (not self.params) and (not prefix.params)
47+
else:
48+
return False
49+
50+
elif len(self.blocks) >= len(prefix.blocks) and self.blocks[:len(prefix.blocks)] == prefix.blocks:
51+
# partial match, check subpaths don't exist
52+
return (not self.links) and (not prefix.links) and (not self.ports) and (not prefix.ports) and \
53+
(not self.params) and (not prefix.params)
54+
else: # no match
55+
return False
56+
57+
def append_block(self, *names: str) -> Path:
58+
assert not self.links and not self.ports and not self.params, f"tried to append block {names} to {self}"
59+
return Path(self.blocks + tuple(names), self.links, self.ports, self.params)
60+
61+
def append_link(self, *names: str) -> Path:
62+
assert not self.ports and not self.params, f"tried to append link {names} to {self}"
63+
return Path(self.blocks, self.links + tuple(names), self.ports, self.params)
64+
65+
def append_port(self, *names: str) -> Path:
66+
assert not self.params, f"tried to append port {names} to {self}"
67+
return Path(self.blocks, self.links, self.ports + tuple(names), self.params)
4668

4769
def append_param(self, name: str) -> Path:
4870
return Path(self.blocks, self.links, self.ports, self.params + (name, ))

edg_core/test_path.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import unittest
2+
3+
from .TransformUtil import Path
4+
5+
6+
class PathTestCase(unittest.TestCase):
7+
def test_startswith(self) -> None:
8+
path = Path.empty()
9+
self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a')))
10+
self.assertFalse(path.append_block('a').startswith(path.append_block('a', 'b')))
11+
self.assertTrue(path.append_block('a').startswith(path.append_block('a')))
12+
self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a', 'b')))
13+
14+
self.assertFalse(path.append_block('a').startswith(path.append_link('a')))
15+
16+
self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a')))
17+
self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a').append_link('b')))
18+
self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b')))
19+
self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b', 'c')))
20+
self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_link('b')))
21+
self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_block('a', 'b')))
Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# this class lives in electronics_abstract_parts since it requires the Resistor
22
import unittest
33

4-
from edg_core import Block, Range, Refinements, InOut
4+
from edg_core import Block, Range, Refinements, InOut, TransformUtil
55
from electronics_model import FootprintBlock, Passive
66
from electronics_abstract_parts import Resistor
7-
from electronics_model.test_netlist import NetlistTestCase
7+
from electronics_model.test_netlist import NetlistTestCase, Net, NetPin, NetBlock
88
from electronics_model.test_kicad_import_blackbox import KiCadBlackboxBlock
9-
from electronics_model.footprint import Pin, Block as FBlock # TODO cleanup naming
109

1110

1211
class PassiveDummy(Block):
@@ -41,29 +40,47 @@ def test_netlist(self):
4140
]
4241
))
4342
# note, dut pruned out from paths since it's the only block in the top-level
44-
self.assertEqual(net.nets['pwr'], [
45-
Pin('U1', '1')
46-
])
47-
self.assertEqual(net.nets['gnd'], [
48-
Pin('U1', '3')
49-
])
50-
self.assertEqual(net.nets['node.0'], [
51-
Pin('U1', '2'),
52-
Pin('res', '1')
53-
])
54-
self.assertEqual(net.nets['out'], [
55-
Pin('res', '2')
56-
])
57-
self.assertEqual(net.blocks['U1'], FBlock('Package_TO_SOT_SMD:SOT-23', 'U1',
58-
# expected value is wonky because netlisting combines part and value
59-
'Sensor_Temperature:MCP9700AT-ETT', 'MCP9700AT-ETT',
60-
['dut', 'U1'], ['U1'],
61-
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']))
62-
self.assertEqual(net.blocks['SYM1'], FBlock('Symbol:Symbol_ESD-Logo_CopperTop', 'SYM1',
63-
# expected value is wonky because netlisting combines part and value
64-
'Graphic:SYM_ESD_Small', 'SYM_ESD_Small',
65-
['dut', 'SYM1'], ['SYM1'],
66-
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']))
67-
self.assertEqual(net.blocks['res'], FBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '',
68-
['dut', 'res'], ['res'],
69-
['electronics_abstract_parts.test_kicad_import_netlist.DummyResistor']))
43+
self.assertIn(Net('dut.pwr', [
44+
NetPin(['dut', 'U1'], '1')
45+
], [
46+
TransformUtil.Path.empty().append_block('dut').append_port('pwr'),
47+
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '1'),
48+
TransformUtil.Path.empty().append_block('dummypwr').append_port('port'),
49+
]), net.nets)
50+
self.assertIn(Net('dut.gnd', [
51+
NetPin(['dut', 'U1'], '3')
52+
], [
53+
TransformUtil.Path.empty().append_block('dut').append_port('gnd'),
54+
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '3'),
55+
TransformUtil.Path.empty().append_block('dummygnd').append_port('port'),
56+
]), net.nets)
57+
self.assertIn(Net('dut.node', [
58+
NetPin(['dut', 'U1'], '2'),
59+
NetPin(['dut', 'res'], '1')
60+
], [
61+
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '2'),
62+
TransformUtil.Path.empty().append_block('dut', 'res').append_port('a'),
63+
]), net.nets)
64+
self.assertIn(Net('dut.out', [
65+
NetPin(['dut', 'res'], '2')
66+
], [
67+
TransformUtil.Path.empty().append_block('dut').append_port('out'),
68+
TransformUtil.Path.empty().append_block('dut', 'res').append_port('b'),
69+
TransformUtil.Path.empty().append_block('dummyout').append_port('port'),
70+
]), net.nets)
71+
self.assertIn(NetBlock('Package_TO_SOT_SMD:SOT-23', 'U1',
72+
# expected value is wonky because netlisting combines part and value
73+
'Sensor_Temperature:MCP9700AT-ETT', 'MCP9700AT-ETT',
74+
['dut', 'U1'], ['U1'],
75+
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']),
76+
net.blocks)
77+
self.assertIn(NetBlock('Symbol:Symbol_ESD-Logo_CopperTop', 'SYM1',
78+
# expected value is wonky because netlisting combines part and value
79+
'Graphic:SYM_ESD_Small', 'SYM_ESD_Small',
80+
['dut', 'SYM1'], ['SYM1'],
81+
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']),
82+
net.blocks)
83+
self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '',
84+
['dut', 'res'], ['res'],
85+
['electronics_abstract_parts.test_kicad_import_netlist.DummyResistor']),
86+
net.blocks)

electronics_lib/Microcontroller_Esp32c3.py

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _io_pinmap(self) -> PinMapUtil:
8383
PeripheralAnyResource('SPI2', spi_model),
8484
PeripheralAnyResource('SPI2_P', spi_peripheral_model), # TODO shared resource w/ SPI controller
8585
PeripheralAnyResource('I2S', I2sController.empty()),
86-
])
86+
]).remap_pins(self.RESOURCE_PIN_REMAP)
8787

8888

8989
@abstract_block
@@ -132,6 +132,19 @@ class Esp32c3_Wroom02_Device(Esp32c3_Base, FootprintBlock, JlcPart):
132132
133133
Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf
134134
"""
135+
RESOURCE_PIN_REMAP = {
136+
'MTMS': '3', # GPIO4
137+
'MTDI': '4', # GPIO5
138+
'MTCK': '5', # GPIO6
139+
'MTDO': '6', # GPIO7
140+
'GPIO10': '10',
141+
'GPIO18': '13',
142+
'GPIO19': '14',
143+
'GPIO3': '15',
144+
'GPIO1': '17',
145+
'GPIO0': '18',
146+
}
147+
135148
def _system_pinmap(self) -> Dict[str, CircuitPort]:
136149
return VariantPinRemapper(super()._system_pinmap()).remap({
137150
'Vdd': '1',
@@ -144,20 +157,6 @@ def _system_pinmap(self) -> Dict[str, CircuitPort]:
144157
'TXD': '12', # TXD, GPIO21
145158
})
146159

147-
def _io_pinmap(self) -> PinMapUtil:
148-
return super()._io_pinmap().remap_pins({
149-
'MTMS': '3', # GPIO4
150-
'MTDI': '4', # GPIO5
151-
'MTCK': '5', # GPIO6
152-
'MTDO': '6', # GPIO7
153-
'GPIO10': '10',
154-
'GPIO18': '13',
155-
'GPIO19': '14',
156-
'GPIO3': '15',
157-
'GPIO1': '17',
158-
'GPIO0': '18',
159-
})
160-
161160
def generate(self) -> None:
162161
super().generate()
163162

@@ -216,6 +215,31 @@ class Esp32c3_Device(Esp32c3_Base, FootprintBlock, JlcPart):
216215
"""ESP32C3 with 4MB integrated flash
217216
TODO: support other part numbers, including without integrated flash
218217
"""
218+
RESOURCE_PIN_REMAP = {
219+
'GPIO0': '4',
220+
'GPIO1': '5',
221+
'GPIO3': '8',
222+
'MTMS': '9', # GPIO4
223+
'MTDI': '10', # GPIO5
224+
'MTCK': '12', # GPIO6
225+
'MTDO': '13', # GPIO7
226+
'GPIO10': '16',
227+
'GPIO18': '25',
228+
'GPIO19': '26',
229+
}
230+
231+
def _system_pinmap(self) -> Dict[str, CircuitPort]:
232+
return VariantPinRemapper(super()._system_pinmap()).remap({
233+
'Vdd': ['31', '32'], # VDDA
234+
'Vss': ['33'], # 33 is EP
235+
'GPIO2': '6',
236+
'EN': '7',
237+
'GPIO8': '14',
238+
'GPIO9': '15',
239+
'RXD': '27', # U0RXD, GPIO20
240+
'TXD': '28', # U0TXD, GPIO21
241+
})
242+
219243
def __init__(self, *args, **kwargs):
220244
super().__init__(*args, **kwargs)
221245
self.lna_in = self.Port(Passive())
@@ -238,32 +262,6 @@ def __init__(self, *args, **kwargs):
238262
self.xtal = self.Port(CrystalDriver(frequency_limits=40*MHertz(tol=10e-6),
239263
voltage_out=self.pwr.link().voltage))
240264

241-
def _system_pinmap(self) -> Dict[str, CircuitPort]:
242-
return VariantPinRemapper(super()._system_pinmap()).remap({
243-
'Vdd': ['31', '32'], # VDDA
244-
'Vss': ['33'], # 33 is EP
245-
'GPIO2': '6',
246-
'EN': '7',
247-
'GPIO8': '14',
248-
'GPIO9': '15',
249-
'RXD': '27', # U0RXD, GPIO20
250-
'TXD': '28', # U0TXD, GPIO21
251-
})
252-
253-
def _io_pinmap(self) -> PinMapUtil:
254-
return super()._io_pinmap().remap_pins({
255-
'GPIO0': '4',
256-
'GPIO1': '5',
257-
'GPIO3': '8',
258-
'MTMS': '9', # GPIO4
259-
'MTDI': '10', # GPIO5
260-
'MTCK': '12', # GPIO6
261-
'MTDO': '13', # GPIO7
262-
'GPIO10': '16',
263-
'GPIO18': '25',
264-
'GPIO19': '26',
265-
})
266-
267265
def generate(self) -> None:
268266
super().generate()
269267

0 commit comments

Comments
 (0)