Skip to content

Commit 5e0be14

Browse files
authored
Refactor microcontroller dev boards to use composition (#508)
Refactors the ESP32 and iCE40 microcontroller devices to use the new wrapper APIs microcontroller style from #502, #497 Deprecates BaseIoControllerExportable, now that all usages have been refactored out. Add pin filtering to PinMapUtil and implement it in device models. This is needed to restrict automatic allocation in wrapped microcontroller models, otherwise they allocate pins that don't exist on the modules. Add pin filtering to all wrappers. Allow pin names as part of pin_assign specifications; this is needed by wrapper remapping if the pin name and GPIO name don't line up. Fix the ground connection style of wrappers to account for when ground is unneeded in power source mode. Other refactorings: - Cleans up the _export_ios_inner implementation, to use a single dict and to not request() transformed-out IOs - Move the microcontroller wrapper test to the ESP32C3 as a more complex example with fewer pins to deal with - Deletes the owlbot example. It was a misnomer, and it uses the horribly abstraction breaking camera I2C pins which are no longer supported Resolves #389
1 parent 86ac330 commit 5e0be14

59 files changed

Lines changed: 2361 additions & 44253 deletions

Some content is hidden

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

edg/abstract_parts/IoController.py

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from itertools import chain
2-
from typing import List, Dict, Tuple, Type, Optional, Any, Union, Callable, cast
2+
from typing import List, Dict, Tuple, Type, Optional, Any, Union, Callable
33
from typing_extensions import override
44
from deprecated import deprecated
55

@@ -125,24 +125,23 @@ def _export_ios_inner(
125125
assert isinstance(self, GeneratorBlock), "transforms require a GeneratorBlock to work"
126126
assert self._elaboration_state in (BlockElaborationState.generate,), "transforms can only run in generate()"
127127

128-
assigns_raw = self.get(self.pin_assigns)
129-
# mutated in-place during _make_export_*
130-
assigns = cast(List[Optional[str]], assigns_raw.copy())
131-
assign_index_by_name = {assign.split("=")[0]: i for i, assign in enumerate(assigns_raw)}
128+
assigns_dict: Optional[Dict[str, str]] = {}
129+
assert assigns_dict is not None
130+
for assign in self.get(self.pin_assigns):
131+
key, value = assign.split("=")
132+
assigns_dict[key] = value
132133
else:
133-
assigns = None
134-
assign_index_by_name = {}
134+
assigns_dict = None
135135

136-
def connect_port_transformed(self_io: BasePort, inner_io: BasePort, name: str) -> None:
137-
assert transform_fn is not None and assigns is not None
138-
assign_index = assign_index_by_name.get(name)
139-
assign = assigns[assign_index] if assign_index is not None else None
136+
def connect_port_transformed(self_io: BasePort, inner_io: Callable[[], BasePort], name: str) -> None:
137+
assert transform_fn is not None and assigns_dict is not None
138+
assign = assigns_dict.get(name, None)
140139
transform_result = transform_fn(self_io, assign)
141140
if transform_result is not None:
142-
self.connect(transform_result, inner_io)
141+
self.connect(transform_result, inner_io())
143142
else:
144-
if assign_index is not None:
145-
assigns[assign_index] = None
143+
if assign is not None:
144+
del assigns_dict[name]
146145

147146
inner_ios_by_type = {self._type_of_io(io_port): io_port for io_port in inner._io_ports}
148147
for self_io in self._io_ports:
@@ -162,17 +161,17 @@ def connect_port_transformed(self_io: BasePort, inner_io: BasePort, name: str) -
162161
self_io.defined()
163162
for io_requested in self.get(self_io.requested()):
164163
self_io_elt = self_io.append_elt(self_io.elt_type().empty(), io_requested)
165-
connect_port_transformed(self_io_elt, inner_io.request(io_requested), io_requested)
164+
connect_port_transformed(self_io_elt, lambda: inner_io.request(io_requested), io_requested)
166165
elif isinstance(inner_io, Port):
167166
if transform_fn is None:
168167
self.connect(self_io, inner_io)
169168
else:
170-
connect_port_transformed(self_io, inner_io, self_io._name_from(self))
169+
connect_port_transformed(self_io, lambda: inner_io, self_io._name_from(self))
171170
else:
172171
raise NotImplementedError(f"unknown port type {self_io}")
173172

174-
if assigns is not None:
175-
filtered_assigns = [assign for assign in assigns if assign is not None]
173+
if assigns_dict is not None:
174+
filtered_assigns = [f"{key}={value}" for key, value in assigns_dict.items()]
176175
return ArrayStringExpr._to_expr_type(filtered_assigns)
177176
else:
178177
return self.pin_assigns
@@ -267,20 +266,11 @@ def _instantiate_from(
267266
class BaseIoControllerPinmapGenerator(BaseIoController, GeneratorBlock):
268267
"""BaseIoController with generator code to set pin mappings"""
269268

270-
def __init__(self, *args: Any, **kwargs: Any) -> None:
271-
super().__init__(*args, **kwargs)
272-
self.generator_param(self.pin_assigns)
273-
274269
@override
275270
def contents(self) -> None:
276271
super().contents()
277-
for io_port in self._io_ports: # defined in contents() so subclass __init__ can define additional _io_ports
278-
if isinstance(io_port, Vector):
279-
self.generator_param(io_port.requested())
280-
elif isinstance(io_port, Port):
281-
self.generator_param(io_port.is_connected())
282-
else:
283-
raise NotImplementedError(f"unknown port type {io_port}")
272+
self.generator_param(self.pin_assigns)
273+
self._generator_param_all_ios() # defined in contents() so subclass __init__ can define additional _io_ports
284274

285275
def _system_pinmap(self) -> Dict[str, Union[Passive, HasPassivePort]]:
286276
"""Implement me. Defines the fixed pin mappings from pin number to port."""

edg/abstract_parts/IoControllerExportable.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from typing import List, Optional, TypeVar, cast, Any
22

3+
from deprecated import deprecated
34
from typing_extensions import override
45

56
from ..electronics_model import *
67
from .IoController import BaseIoController
78

89

910
@non_library
11+
@deprecated("use explicit BaseIoController._export_ios_inner")
1012
class BaseIoControllerExportable(BaseIoController, GeneratorBlock):
1113
"""BaseIoController wrapper (this is a BaseIoController, which wraps another BaseIoController)
1214
which automatically exports my IOs from the internal IOs in an extensible way (additional connects

edg/abstract_parts/PinMappable.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,36 @@ def resource_pin(resource: BasePinMapResource) -> List[str]:
328328

329329
return PinMapUtil(remapped_resources, self.transforms)
330330

331+
def filter_pins(self, allowed_pins: List[str]) -> "PinMapUtil":
332+
"""Returns a new PinMapUtil with only the specified pins kept.
333+
If the allowed_pins list is empty, returns the input (all pins kept).
334+
allowed_pins may be specified as a pin name or pin number."""
335+
336+
if not allowed_pins:
337+
return self
338+
339+
def filter_resource(resource: BasePinMapResource) -> Optional[BasePinMapResource]:
340+
if isinstance(resource, PinResource):
341+
if resource.pin in allowed_pins or resource.pinname in allowed_pins:
342+
return resource
343+
else:
344+
return None
345+
elif isinstance(resource, PeripheralFixedPin):
346+
# only keep if the entire peripheral can be pinned
347+
for key, pin in resource.inner_allowed_pins.items():
348+
if pin not in allowed_pins and resource.inner_pinnames[key] not in allowed_pins:
349+
return None
350+
return resource
351+
elif isinstance(resource, BaseDelegatingPinMapResource):
352+
return resource
353+
else:
354+
raise NotImplementedError(f"unknown resource {resource}")
355+
356+
filtered_resources_raw = [filter_resource(resource) for resource in self.resources]
357+
filtered_resources = [resource for resource in filtered_resources_raw if resource is not None]
358+
359+
return PinMapUtil(filtered_resources, self.transforms)
360+
331361
@staticmethod
332362
def _resource_port_types(resource: BasePinMapResource) -> List[Type[Port]]:
333363
if isinstance(resource, PinResource):
@@ -340,7 +370,9 @@ def _resource_port_types(resource: BasePinMapResource) -> List[Type[Port]]:
340370
@staticmethod
341371
def _resource_names(resource: BasePinMapResource) -> List[str]:
342372
if isinstance(resource, PinResource):
343-
return [resource.pin] + [resource_name for resource_name, model in resource.name_models.items()]
373+
return [resource.pin, resource.pinname] + [
374+
resource_name for resource_name, model in resource.name_models.items()
375+
]
344376
elif isinstance(resource, (PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource)):
345377
return [resource.name]
346378
else:

edg/abstract_parts/test_pinmappable.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,56 @@ def test_assign_remapped(self) -> None: # fully user-specified
9898
self.assertIn(AllocatedResource(ain_model, "AIO4", "P4", "4"), allocated)
9999
self.assertIn(AllocatedResource(ain_model, "AIO5", "P5", "5"), allocated)
100100

101+
def test_assign_filtered(self) -> None: # fully user-specified
102+
dio_model = DigitalBidir()
103+
allocated = (
104+
PinMapUtil(
105+
[
106+
PinResource("P1", {"PIO1": dio_model}),
107+
PinResource("P2", {"PIO2": dio_model}),
108+
PinResource("P3", {"PIO3": dio_model}),
109+
]
110+
)
111+
.remap_pins(
112+
{
113+
"P1": "1",
114+
"P2": "2",
115+
"P3": "3",
116+
}
117+
)
118+
.filter_pins(["P1", "P3"]) # test both pin name and pin number
119+
.allocate([(DigitalBidir, ["DIO1", "DIO3"])])
120+
)
121+
self.assertIn(AllocatedResource(dio_model, "DIO1", "P1", "1"), allocated)
122+
self.assertIn(AllocatedResource(dio_model, "DIO3", "P3", "3"), allocated)
123+
124+
def test_assign_filtered_empty(self) -> None:
125+
dio_model = DigitalBidir()
126+
allocated = (
127+
PinMapUtil(
128+
[
129+
PinResource("P1", {"PIO1": dio_model}),
130+
PinResource("P2", {"PIO2": dio_model}),
131+
]
132+
)
133+
.filter_pins([])
134+
.allocate([(DigitalBidir, ["DIO1", "DIO2"])])
135+
)
136+
self.assertIn(AllocatedResource(dio_model, "DIO1", "P1", "P1"), allocated)
137+
self.assertIn(AllocatedResource(dio_model, "DIO2", "P2", "P2"), allocated)
138+
139+
def test_assign_filtered_overflow(self) -> None:
140+
dio_model = DigitalBidir()
141+
with self.assertRaises(AutomaticAllocationError):
142+
PinMapUtil(
143+
[
144+
PinResource("1", {"PIO1": dio_model}),
145+
PinResource("3", {"PIO3": dio_model}),
146+
]
147+
).filter_pins(
148+
["1"]
149+
).allocate([(DigitalBidir, ["DIO1", "DIO2"])])
150+
101151
def test_assign_mixed(self) -> None: # mix of user-specified and automatic assignments, assuming greedy algo
102152
dio_model = DigitalBidir()
103153
ain_model = AnalogSink()

0 commit comments

Comments
 (0)