Skip to content

Commit 2c84e6c

Browse files
authored
SVGPCB backend + templates (#346)
Add the SVGPCB backend including running as part of unit tests. Change the default function name to take optional name components from `_svgpcb_fn_name_adds` Add layout templates for: - Charlieplexed LED matrix - NeopixelArray variant with a circular layout - Fixes of keyboard example Other library consistency fixes: - Change charlieplexed LED matrix to nrows / ncols - Remove packed resistors from LED matrix example, to allow layout template generation
1 parent 5066c59 commit 2c84e6c

File tree

18 files changed

+1736
-265
lines changed

18 files changed

+1736
-265
lines changed

edg/BoardCompiler.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Type, Optional, Tuple
55

66
from edg_core import Block, ScalaCompiler, CompiledDesign
7-
from electronics_model import NetlistBackend
7+
from electronics_model import NetlistBackend, SvgPcbBackend
88
from electronics_model.RefdesRefinementPass import RefdesRefinementPass
99
from electronics_model.BomBackend import GenerateBom
1010

@@ -20,6 +20,7 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
2020
netlist_filename = os.path.join(target_dir, f'{target_name}.net')
2121
netlist_refdes_filename = os.path.join(target_dir, f'{target_name}.ref.net')
2222
bom_filename = os.path.join(target_dir, f'{target_name}.csv')
23+
svgpcb_filename = os.path.join(target_dir, f'{target_name}.svgpcb.js')
2324

2425
with suppress(FileNotFoundError):
2526
os.remove(design_filename)
@@ -29,6 +30,8 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
2930
os.remove(netlist_refdes_filename)
3031
with suppress(FileNotFoundError):
3132
os.remove(bom_filename)
33+
with suppress(FileNotFoundError):
34+
os.remove(svgpcb_filename)
3235

3336
compiled = ScalaCompiler.compile(design, ignore_errors=True)
3437
compiled.append_values(RefdesRefinementPass().run(compiled))
@@ -44,6 +47,7 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
4447
netlist_all = NetlistBackend().run(compiled)
4548
netlist_refdes = NetlistBackend().run(compiled, {'RefdesMode': 'refdes'})
4649
bom_all = GenerateBom().run(compiled)
50+
svgpcb_all = SvgPcbBackend().run(compiled)
4751
assert len(netlist_all) == 1
4852

4953
if target_dir_name is not None:
@@ -56,6 +60,10 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
5660
with open(bom_filename, 'w', encoding='utf-8') as bom_file:
5761
bom_file.write(bom_all[0][1])
5862

63+
if svgpcb_all:
64+
with open(svgpcb_filename, 'w', encoding='utf-8') as bom_file:
65+
bom_file.write(svgpcb_all[0][1])
66+
5967
return compiled
6068

6169

electronics_lib/LedMatrix.py

Lines changed: 163 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,165 @@
11
from electronics_abstract_parts import *
2-
from typing import Dict
2+
from typing import Dict, Optional
33

44

5-
class CharlieplexedLedMatrix(Light, GeneratorBlock):
5+
class CharlieplexedLedMatrix(Light, GeneratorBlock, SvgPcbTemplateBlock):
66
"""A LED matrix that saves on IO pins by charlieplexing, only requiring max(rows + 1, cols) GPIOs to control.
77
Requires IOs that can tri-state, and requires scanning through rows (so not all LEDs are simultaneously on).
88
99
Anodes (columns) are directly connected to the IO line, while the cathodes (rows) are connected through a resistor.
1010
A generalization of https://en.wikipedia.org/wiki/Charlieplexing#/media/File:3-pin_Charlieplexing_matrix_with_common_resistors.svg
1111
"""
12+
def _svgpcb_fn_name_adds(self) -> Optional[str]:
13+
return f"{self._svgpcb_get(self.ncols)}_{self._svgpcb_get(self.nrows)}"
14+
15+
def _svgpcb_template(self) -> str:
16+
led_block = self._svgpcb_footprint_block_path_of(['led[0_0]'])
17+
res_block = self._svgpcb_footprint_block_path_of(['res[0]'])
18+
assert led_block is not None and res_block is not None
19+
led_footprint = self._svgpcb_footprint_of(led_block)
20+
led_a_pin = self._svgpcb_pin_of(['led[0_0]'], ['a'], led_block)
21+
led_k_pin = self._svgpcb_pin_of(['led[0_0]'], ['k'], led_block)
22+
res_footprint = self._svgpcb_footprint_of(res_block)
23+
res_a_pin = self._svgpcb_pin_of(['res[0]'], ['a'], res_block)
24+
res_b_pin = self._svgpcb_pin_of(['res[0]'], ['b'], res_block)
25+
assert all([pin is not None for pin in [led_a_pin, led_k_pin, res_a_pin, res_b_pin]])
26+
27+
return f"""\
28+
function {self._svgpcb_fn_name()}(xy, colSpacing=1, rowSpacing=1) {{
29+
const kXCount = {self._svgpcb_get(self.ncols)} // number of columns (x dimension)
30+
const kYCount = {self._svgpcb_get(self.nrows)} // number of rows (y dimension)
31+
32+
// Global params
33+
const traceSize = 0.015
34+
const viaTemplate = via(0.02, 0.035)
35+
36+
// Return object
37+
const obj = {{
38+
footprints: {{}},
39+
pts: {{}}
40+
}}
41+
42+
allLeds = []
43+
allVias = []
44+
lastViasPreResistor = [] // state preserved between rows
45+
for (let yIndex=0; yIndex < kYCount; yIndex++) {{
46+
rowLeds = []
47+
rowVias = []
48+
49+
viasPreResistor = []
50+
viasPostResistor = [] // on the same net as the prior row pre-resistor
51+
52+
for (let xIndex=0; xIndex < kXCount; xIndex++) {{
53+
ledPos = [xy[0] + colSpacing * xIndex, xy[1] + rowSpacing * yIndex]
54+
obj.footprints[`d[${{yIndex}}_${{xIndex}}]`] = led = board.add({led_footprint}, {{
55+
translate: ledPos,
56+
id: `{self._svgpcb_pathname()}_d[${{yIndex}}_${{xIndex}}]`
57+
}})
58+
rowLeds.push(led)
59+
60+
// anode line
61+
thisVia = board.add(viaTemplate, {{
62+
translate: [ledPos[0] + colSpacing*1/3, ledPos[1]]
63+
}})
64+
rowVias.push(thisVia)
65+
board.wire([led.pad("{led_a_pin}"), thisVia.pos], traceSize, "F.Cu")
66+
if (xIndex <= yIndex) {{
67+
viasPreResistor.push(thisVia)
68+
}} else {{
69+
viasPostResistor.push(thisVia)
70+
}}
71+
}}
72+
allLeds.push(rowLeds)
73+
allVias.push(rowVias)
74+
75+
// Wire the anode lines, including the row-crossing one accounting for the diagonal-skip where the resistor is in the schematic matrix
76+
// viasPreResistor guaranteed nonempty
77+
board.wire([viasPreResistor[0].pos, viasPreResistor[viasPreResistor.length - 1].pos], traceSize, "B.Cu")
78+
if (viasPostResistor.length > 0) {{
79+
board.wire([viasPostResistor[0].pos, viasPostResistor[viasPostResistor.length - 1].pos], traceSize, "B.Cu")
80+
}}
81+
82+
// Create the inter-row bridging trace, if applicable
83+
if (viasPostResistor.length > 0 && lastViasPreResistor.length > 0) {{
84+
via1Pos = lastViasPreResistor[lastViasPreResistor.length - 1].pos
85+
via2Pos = viasPostResistor[0].pos
86+
centerY = (via1Pos[1] + via2Pos[1]) / 2
87+
board.wire([via1Pos,
88+
[via1Pos[0], centerY],
89+
[via2Pos[0], centerY],
90+
via2Pos
91+
],
92+
traceSize, "B.Cu")
93+
}}
94+
95+
lastViasPreResistor = viasPreResistor
96+
}}
97+
98+
allResistors = []
99+
for (let xIndex=0; xIndex < kXCount; xIndex++) {{
100+
const resPos = [xy[0] + colSpacing * xIndex, xy[1] + rowSpacing * kYCount]
101+
obj.footprints[`r[${{xIndex + 1}}]`] = res = board.add({res_footprint}, {{
102+
translate: resPos,
103+
id: `{self._svgpcb_pathname()}_r[${{xIndex + 1}}]`
104+
}})
105+
allResistors.push(res)
106+
107+
if (xIndex < allVias.length && xIndex < allVias[xIndex].length - 1) {{
108+
targetVia = allVias[xIndex][xIndex + 1]
109+
thisVia = board.add(viaTemplate, {{
110+
translate: [resPos[0] + colSpacing*2/3, targetVia.pos[1]]
111+
}})
112+
113+
board.wire([
114+
res.pad("{res_b_pin}"),
115+
[resPos[0] + colSpacing*2/3, res.padY("{res_b_pin}")],
116+
thisVia.pos
117+
], traceSize, "F.Cu")
118+
board.wire([
119+
thisVia.pos,
120+
targetVia.pos,
121+
], traceSize, "B.Cu")
122+
}} else if (xIndex <= allVias.length && xIndex < allVias[xIndex - 1].length) {{
123+
// connect the last via
124+
thisVia = board.add(viaTemplate, {{
125+
translate: [resPos[0] + colSpacing*2/3, resPos[1]]
126+
}})
127+
targetVia = allVias[xIndex - 1][xIndex - 1]
128+
board.wire([
129+
res.pad("{res_b_pin}"),
130+
thisVia.pos
131+
], traceSize, "F.Cu")
132+
centerY = targetVia.pos[1] + colSpacing/2
133+
board.wire([
134+
thisVia.pos,
135+
[thisVia.pos[0], centerY],
136+
[targetVia.pos[0], centerY],
137+
targetVia.pos,
138+
], traceSize, "B.Cu")
139+
}}
140+
}}
141+
142+
// create the along-column cathode line
143+
for (let xIndex=0; xIndex < kXCount; xIndex++) {{
144+
colPads = allLeds.flatMap(row => row.length > xIndex ? [row[xIndex].pad("{led_k_pin}")] : [])
145+
if (xIndex < allResistors.length) {{
146+
colPads.push(allResistors[xIndex].pad("{res_a_pin}"))
147+
}}
148+
149+
for (let i=0; i<colPads.length - 1; i++) {{
150+
board.wire([
151+
colPads[i],
152+
colPads[i+1]
153+
], traceSize, "F.Cu")
154+
}}
155+
}}
156+
157+
return obj
158+
}}
159+
"""
160+
12161
@init_in_parent
13-
def __init__(self, rows: IntLike, cols: IntLike,
162+
def __init__(self, nrows: IntLike, ncols: IntLike,
14163
color: LedColorLike = Led.Any, current_draw: RangeLike = (1, 10)*mAmp):
15164
super().__init__()
16165

@@ -20,14 +169,14 @@ def __init__(self, rows: IntLike, cols: IntLike,
20169
# note that IOs supply both the positive and negative
21170
self.ios = self.Port(Vector(DigitalSink.empty()))
22171

23-
self.rows = self.ArgParameter(rows)
24-
self.cols = self.ArgParameter(cols)
25-
self.generator_param(self.rows, self.cols)
172+
self.nrows = self.ArgParameter(nrows)
173+
self.ncols = self.ArgParameter(ncols)
174+
self.generator_param(self.nrows, self.ncols)
26175

27176
def generate(self):
28177
super().generate()
29-
rows = self.get(self.rows)
30-
cols = self.get(self.cols)
178+
nrows = self.get(self.nrows)
179+
ncols = self.get(self.ncols)
31180

32181
io_voltage = self.ios.hull(lambda x: x.link().voltage)
33182
io_voltage_upper = io_voltage.upper()
@@ -52,11 +201,11 @@ def connect_passive_io(index: int, io: Passive):
52201
led_model = Led(color=self.color)
53202

54203
# generate the resistor and LEDs for each column
55-
for col in range(cols):
204+
for col in range(ncols):
56205
# generate the cathode resistor, guaranteed one per column
57206
self.res[str(col)] = res = self.Block(res_model)
58-
connect_passive_io (col, res.b)
59-
for row in range(rows):
207+
connect_passive_io(col, res.b)
208+
for row in range(nrows):
60209
self.led[f"{row}_{col}"] = led = self.Block(led_model)
61210
self.connect(led.k, res.a)
62211
if row >= col: # displaced by resistor
@@ -67,15 +216,15 @@ def connect_passive_io(index: int, io: Passive):
67216
# generate the adapters and connect the internal passive IO to external typed IO
68217
for index, passive_io in passive_ios.items():
69218
# if there is a cathode resistor attached to this index, then include the sunk current
70-
if index < cols:
219+
if index < ncols:
71220
sink_res = self.res[str(index)]
72-
sink_current = -(io_voltage / sink_res.actual_resistance).upper() * cols
221+
sink_current = -(io_voltage / sink_res.actual_resistance).upper() * ncols
73222
else:
74223
sink_current = 0 * mAmp
75224

76225
# then add the maximum of the LED source currents, for the rest of the cathode lines
77226
source_current = 0 * mAmp
78-
for col in range(cols):
227+
for col in range(ncols):
79228
col_res = self.res[str(col)]
80229
source_current = (io_voltage / col_res.actual_resistance).upper().max(source_current)
81230

0 commit comments

Comments
 (0)