Skip to content

Commit

Permalink
SvgPcb layout template backend + netlister refactor (#337)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ducky64 authored Apr 22, 2024
1 parent 8f81485 commit da7a2ea
Show file tree
Hide file tree
Showing 74 changed files with 35,876 additions and 568 deletions.
7 changes: 7 additions & 0 deletions edg/BoardCompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]

design_filename = os.path.join(target_dir, f'{target_name}.edg')
netlist_filename = os.path.join(target_dir, f'{target_name}.net')
netlist_refdes_filename = os.path.join(target_dir, f'{target_name}.ref.net')
bom_filename = os.path.join(target_dir, f'{target_name}.csv')

with suppress(FileNotFoundError):
os.remove(design_filename)
with suppress(FileNotFoundError):
os.remove(netlist_filename)
with suppress(FileNotFoundError):
os.remove(netlist_refdes_filename)
with suppress(FileNotFoundError):
os.remove(bom_filename)

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

netlist_all = NetlistBackend().run(compiled)
netlist_refdes = NetlistBackend().run(compiled, {'RefdesMode': 'refdes'})
bom_all = GenerateBom().run(compiled)
assert len(netlist_all) == 1

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

with open(netlist_refdes_filename, 'w', encoding='utf-8') as net_file:
net_file.write(netlist_refdes[0][1])

with open(bom_filename, 'w', encoding='utf-8') as bom_file:
bom_file.write(bom_all[0][1])

Expand Down
10 changes: 7 additions & 3 deletions edg_core/ScalaCompilerInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ def __init__(self, design: edgir.Design, values: Dict[bytes, edgir.LitTypes], er

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

def append_values(self, values: List[Tuple[edgir.LocalPath, edgir.ValueLit]]):
"""Append solved values to this design, such as from a refinement pass"""
Expand Down
44 changes: 33 additions & 11 deletions edg_core/TransformUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,39 @@ def __repr__(self) -> str:
def empty(cls) -> Path:
return Path((), (), (), ())

def append_block(self, name: str) -> Path:
assert not self.links and not self.ports and not self.params, f"tried to append block {name} to {self}"
return Path(self.blocks + (name, ), self.links, self.ports, self.params)

def append_link(self, name: str) -> Path:
assert not self.ports and not self.params, f"tried to append link {name} to {self}"
return Path(self.blocks, self.links + (name, ), self.ports, self.params)

def append_port(self, name: str) -> Path:
assert not self.params, f"tried to append port {name} to {self}"
return Path(self.blocks, self.links, self.ports + (name, ), self.params)
def startswith(self, prefix: Path) -> bool:
if self.blocks == prefix.blocks: # exact match, check subpaths
if self.links == prefix.links:
if self.ports == prefix.ports:
return len(self.params) >= len(prefix.params) and self.params[:len(prefix.params)] == prefix.params
elif len(self.ports) >= len(prefix.ports) and self.ports[:len(prefix.ports)] == prefix.ports:
return (not self.params) and (not prefix.params)
else:
return False

elif len(self.links) >= len(prefix.links) and self.links[:len(prefix.links)] == prefix.links:
return (not self.ports) and (not prefix.ports) and (not self.params) and (not prefix.params)
else:
return False

elif len(self.blocks) >= len(prefix.blocks) and self.blocks[:len(prefix.blocks)] == prefix.blocks:
# partial match, check subpaths don't exist
return (not self.links) and (not prefix.links) and (not self.ports) and (not prefix.ports) and \
(not self.params) and (not prefix.params)
else: # no match
return False

def append_block(self, *names: str) -> Path:
assert not self.links and not self.ports and not self.params, f"tried to append block {names} to {self}"
return Path(self.blocks + tuple(names), self.links, self.ports, self.params)

def append_link(self, *names: str) -> Path:
assert not self.ports and not self.params, f"tried to append link {names} to {self}"
return Path(self.blocks, self.links + tuple(names), self.ports, self.params)

def append_port(self, *names: str) -> Path:
assert not self.params, f"tried to append port {names} to {self}"
return Path(self.blocks, self.links, self.ports + tuple(names), self.params)

def append_param(self, name: str) -> Path:
return Path(self.blocks, self.links, self.ports, self.params + (name, ))
Expand Down
21 changes: 21 additions & 0 deletions edg_core/test_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import unittest

from .TransformUtil import Path


class PathTestCase(unittest.TestCase):
def test_startswith(self) -> None:
path = Path.empty()
self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a')))
self.assertFalse(path.append_block('a').startswith(path.append_block('a', 'b')))
self.assertTrue(path.append_block('a').startswith(path.append_block('a')))
self.assertTrue(path.append_block('a', 'b').startswith(path.append_block('a', 'b')))

self.assertFalse(path.append_block('a').startswith(path.append_link('a')))

self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a')))
self.assertTrue(path.append_block('a').append_link('b').startswith(path.append_block('a').append_link('b')))
self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b')))
self.assertTrue(path.append_block('a').append_link('b', 'c').startswith(path.append_block('a').append_link('b', 'c')))
self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_link('b')))
self.assertFalse(path.append_block('a').append_link('b').startswith(path.append_block('a', 'b')))
75 changes: 46 additions & 29 deletions electronics_abstract_parts/test_kicad_import_netlist.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# this class lives in electronics_abstract_parts since it requires the Resistor
import unittest

from edg_core import Block, Range, Refinements, InOut
from edg_core import Block, Range, Refinements, InOut, TransformUtil
from electronics_model import FootprintBlock, Passive
from electronics_abstract_parts import Resistor
from electronics_model.test_netlist import NetlistTestCase
from electronics_model.test_netlist import NetlistTestCase, Net, NetPin, NetBlock
from electronics_model.test_kicad_import_blackbox import KiCadBlackboxBlock
from electronics_model.footprint import Pin, Block as FBlock # TODO cleanup naming


class PassiveDummy(Block):
Expand Down Expand Up @@ -41,29 +40,47 @@ def test_netlist(self):
]
))
# note, dut pruned out from paths since it's the only block in the top-level
self.assertEqual(net.nets['pwr'], [
Pin('U1', '1')
])
self.assertEqual(net.nets['gnd'], [
Pin('U1', '3')
])
self.assertEqual(net.nets['node.0'], [
Pin('U1', '2'),
Pin('res', '1')
])
self.assertEqual(net.nets['out'], [
Pin('res', '2')
])
self.assertEqual(net.blocks['U1'], FBlock('Package_TO_SOT_SMD:SOT-23', 'U1',
# expected value is wonky because netlisting combines part and value
'Sensor_Temperature:MCP9700AT-ETT', 'MCP9700AT-ETT',
['dut', 'U1'], ['U1'],
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']))
self.assertEqual(net.blocks['SYM1'], FBlock('Symbol:Symbol_ESD-Logo_CopperTop', 'SYM1',
# expected value is wonky because netlisting combines part and value
'Graphic:SYM_ESD_Small', 'SYM_ESD_Small',
['dut', 'SYM1'], ['SYM1'],
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']))
self.assertEqual(net.blocks['res'], FBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '',
['dut', 'res'], ['res'],
['electronics_abstract_parts.test_kicad_import_netlist.DummyResistor']))
self.assertIn(Net('dut.pwr', [
NetPin(['dut', 'U1'], '1')
], [
TransformUtil.Path.empty().append_block('dut').append_port('pwr'),
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '1'),
TransformUtil.Path.empty().append_block('dummypwr').append_port('port'),
]), net.nets)
self.assertIn(Net('dut.gnd', [
NetPin(['dut', 'U1'], '3')
], [
TransformUtil.Path.empty().append_block('dut').append_port('gnd'),
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '3'),
TransformUtil.Path.empty().append_block('dummygnd').append_port('port'),
]), net.nets)
self.assertIn(Net('dut.node', [
NetPin(['dut', 'U1'], '2'),
NetPin(['dut', 'res'], '1')
], [
TransformUtil.Path.empty().append_block('dut', 'U1').append_port('ports', '2'),
TransformUtil.Path.empty().append_block('dut', 'res').append_port('a'),
]), net.nets)
self.assertIn(Net('dut.out', [
NetPin(['dut', 'res'], '2')
], [
TransformUtil.Path.empty().append_block('dut').append_port('out'),
TransformUtil.Path.empty().append_block('dut', 'res').append_port('b'),
TransformUtil.Path.empty().append_block('dummyout').append_port('port'),
]), net.nets)
self.assertIn(NetBlock('Package_TO_SOT_SMD:SOT-23', 'U1',
# expected value is wonky because netlisting combines part and value
'Sensor_Temperature:MCP9700AT-ETT', 'MCP9700AT-ETT',
['dut', 'U1'], ['U1'],
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']),
net.blocks)
self.assertIn(NetBlock('Symbol:Symbol_ESD-Logo_CopperTop', 'SYM1',
# expected value is wonky because netlisting combines part and value
'Graphic:SYM_ESD_Small', 'SYM_ESD_Small',
['dut', 'SYM1'], ['SYM1'],
['electronics_model.KiCadSchematicBlock.KiCadBlackbox']),
net.blocks)
self.assertIn(NetBlock('Resistor_SMD:R_0603_1608Metric', 'R1', '', '',
['dut', 'res'], ['res'],
['electronics_abstract_parts.test_kicad_import_netlist.DummyResistor']),
net.blocks)
80 changes: 39 additions & 41 deletions electronics_lib/Microcontroller_Esp32c3.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def _io_pinmap(self) -> PinMapUtil:
PeripheralAnyResource('SPI2', spi_model),
PeripheralAnyResource('SPI2_P', spi_peripheral_model), # TODO shared resource w/ SPI controller
PeripheralAnyResource('I2S', I2sController.empty()),
])
]).remap_pins(self.RESOURCE_PIN_REMAP)


@abstract_block
Expand Down Expand Up @@ -132,6 +132,19 @@ class Esp32c3_Wroom02_Device(Esp32c3_Base, FootprintBlock, JlcPart):
Module datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c3-wroom-02_datasheet_en.pdf
"""
RESOURCE_PIN_REMAP = {
'MTMS': '3', # GPIO4
'MTDI': '4', # GPIO5
'MTCK': '5', # GPIO6
'MTDO': '6', # GPIO7
'GPIO10': '10',
'GPIO18': '13',
'GPIO19': '14',
'GPIO3': '15',
'GPIO1': '17',
'GPIO0': '18',
}

def _system_pinmap(self) -> Dict[str, CircuitPort]:
return VariantPinRemapper(super()._system_pinmap()).remap({
'Vdd': '1',
Expand All @@ -144,20 +157,6 @@ def _system_pinmap(self) -> Dict[str, CircuitPort]:
'TXD': '12', # TXD, GPIO21
})

def _io_pinmap(self) -> PinMapUtil:
return super()._io_pinmap().remap_pins({
'MTMS': '3', # GPIO4
'MTDI': '4', # GPIO5
'MTCK': '5', # GPIO6
'MTDO': '6', # GPIO7
'GPIO10': '10',
'GPIO18': '13',
'GPIO19': '14',
'GPIO3': '15',
'GPIO1': '17',
'GPIO0': '18',
})

def generate(self) -> None:
super().generate()

Expand Down Expand Up @@ -216,6 +215,31 @@ class Esp32c3_Device(Esp32c3_Base, FootprintBlock, JlcPart):
"""ESP32C3 with 4MB integrated flash
TODO: support other part numbers, including without integrated flash
"""
RESOURCE_PIN_REMAP = {
'GPIO0': '4',
'GPIO1': '5',
'GPIO3': '8',
'MTMS': '9', # GPIO4
'MTDI': '10', # GPIO5
'MTCK': '12', # GPIO6
'MTDO': '13', # GPIO7
'GPIO10': '16',
'GPIO18': '25',
'GPIO19': '26',
}

def _system_pinmap(self) -> Dict[str, CircuitPort]:
return VariantPinRemapper(super()._system_pinmap()).remap({
'Vdd': ['31', '32'], # VDDA
'Vss': ['33'], # 33 is EP
'GPIO2': '6',
'EN': '7',
'GPIO8': '14',
'GPIO9': '15',
'RXD': '27', # U0RXD, GPIO20
'TXD': '28', # U0TXD, GPIO21
})

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lna_in = self.Port(Passive())
Expand All @@ -238,32 +262,6 @@ def __init__(self, *args, **kwargs):
self.xtal = self.Port(CrystalDriver(frequency_limits=40*MHertz(tol=10e-6),
voltage_out=self.pwr.link().voltage))

def _system_pinmap(self) -> Dict[str, CircuitPort]:
return VariantPinRemapper(super()._system_pinmap()).remap({
'Vdd': ['31', '32'], # VDDA
'Vss': ['33'], # 33 is EP
'GPIO2': '6',
'EN': '7',
'GPIO8': '14',
'GPIO9': '15',
'RXD': '27', # U0RXD, GPIO20
'TXD': '28', # U0TXD, GPIO21
})

def _io_pinmap(self) -> PinMapUtil:
return super()._io_pinmap().remap_pins({
'GPIO0': '4',
'GPIO1': '5',
'GPIO3': '8',
'MTMS': '9', # GPIO4
'MTDI': '10', # GPIO5
'MTCK': '12', # GPIO6
'MTDO': '13', # GPIO7
'GPIO10': '16',
'GPIO18': '25',
'GPIO19': '26',
})

def generate(self) -> None:
super().generate()

Expand Down
Loading

0 comments on commit da7a2ea

Please sign in to comment.