Skip to content

Commit 6a72e49

Browse files
authored
Add Export-with-default utility, improve sensor and IoController port naming and docs (#350)
1 parent 9ca4461 commit 6a72e49

29 files changed

+317
-278
lines changed

edg_core/Generator.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from deprecated import deprecated
66

77
import edgir
8+
from .Ports import BasePort, Port
9+
from .PortTag import PortTag
810
from .IdentityDict import IdentityDict
911
from .Binding import InitParamBinding, AllocatedBinding, IsConnectedBinding
1012
from .Blocks import BlockElaborationState, AbstractBlockProperty
@@ -130,3 +132,36 @@ def _generated_def_to_proto(self, generate_values: Iterable[Tuple[edgir.LocalPat
130132
self._elaboration_state = BlockElaborationState.post_generate
131133

132134
return self._def_to_proto()
135+
136+
137+
class DefaultExportBlock(GeneratorBlock):
138+
"""EXPERIMENTAL UTILITY CLASS. There needs to be a cleaner way to address this eventually,
139+
perhaps as a core compiler construct.
140+
This encapsulates the common pattern of an optional export, which if not externally connected,
141+
connects the internal port to some other default port.
142+
TODO The default can be specified as a port, or a function that returns a port (e.g. to instantiate adapters)."""
143+
def __init__(self):
144+
super().__init__()
145+
self._default_exports: List[Tuple[BasePort, Port, Port]] = [] # internal, exported, default
146+
147+
ExportType = TypeVar('ExportType', bound=BasePort)
148+
def Export(self, port: ExportType, *args, default: Optional[Port] = None, **kwargs) -> ExportType:
149+
"""A generator-only variant of Export that supports an optional default (either internal or external)
150+
to connect the (internal) port being exported to, if the external exported port is not connected."""
151+
if default is None:
152+
new_port = super().Export(port, *args, **kwargs)
153+
else:
154+
assert 'optional' not in kwargs, "optional must not be specified with default"
155+
new_port = super().Export(port, *args, optional=True, _connect=False, **kwargs)
156+
assert isinstance(new_port, Port), "defaults only supported with Port types"
157+
self.generator_param(new_port.is_connected())
158+
self._default_exports.append((port, new_port, default))
159+
return new_port
160+
161+
def generate(self):
162+
super().generate()
163+
for (internal, exported, default) in self._default_exports:
164+
if self.get(exported.is_connected()):
165+
self.connect(internal, exported)
166+
else:
167+
self.connect(internal, default)

edg_core/HierarchyBlock.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,8 @@ def Port(self, tpe: T, tags: Iterable[PortTag]=[], *, optional: bool = False, do
504504
return port # type: ignore
505505

506506
ExportType = TypeVar('ExportType', bound=BasePort)
507-
def Export(self, port: ExportType, tags: Iterable[PortTag]=[], *, optional: bool = False, doc: Optional[str] = None) -> ExportType:
507+
def Export(self, port: ExportType, tags: Iterable[PortTag]=[], *, optional: bool = False, doc: Optional[str] = None,
508+
_connect = True) -> ExportType:
508509
"""Exports a port of a child block, but does not propagate tags or optional."""
509510
assert port._is_bound(), "can only export bound type"
510511
port_parent = port._block_parent()
@@ -521,7 +522,9 @@ def Export(self, port: ExportType, tags: Iterable[PortTag]=[], *, optional: bool
521522
else:
522523
raise NotImplementedError(f"unknown exported port type {port}")
523524

524-
self.connect(new_port, port)
525+
if _connect:
526+
self.connect(new_port, port)
527+
525528
return new_port # type: ignore
526529

527530
BlockType = TypeVar('BlockType', bound='Block')

edg_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .DesignTop import DesignTop
88
from .BlockInterfaceMixin import BlockInterfaceMixin
99
from .HierarchyBlock import Block, ImplicitConnect, init_in_parent, abstract_block, abstract_block_default
10-
from .Generator import GeneratorBlock
10+
from .Generator import GeneratorBlock, DefaultExportBlock
1111
from .MultipackBlock import PackedBlockArray, MultipackBlock
1212
from .PortBlocks import PortBridge, PortAdapter
1313
from .Array import Vector

electronics_abstract_parts/IoController.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,21 @@ class BaseIoController(PinMappable, Block):
1818
def __init__(self, *args, **kwargs) -> None:
1919
super().__init__(*args, **kwargs)
2020

21-
self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True)
22-
self.adc = self.Port(Vector(AnalogSink.empty()), optional=True)
23-
24-
self.spi = self.Port(Vector(SpiController.empty()), optional=True)
25-
self.i2c = self.Port(Vector(I2cController.empty()), optional=True)
26-
self.uart = self.Port(Vector(UartPort.empty()), optional=True)
21+
self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True,
22+
doc="Microcontroller digital GPIO pins")
23+
self.adc = self.Port(Vector(AnalogSink.empty()), optional=True,
24+
doc="Microcontroller analog input pins")
25+
26+
self.spi = self.Port(Vector(SpiController.empty()), optional=True,
27+
doc="Microcontroller SPI controllers, each element is an independent SPI controller")
28+
self.i2c = self.Port(Vector(I2cController.empty()), optional=True,
29+
doc="Microcontroller I2C controllers, each element is an independent I2C controller")
30+
self.uart = self.Port(Vector(UartPort.empty()), optional=True,
31+
doc="Microcontroller UARTs")
2732

2833
# USB should be a mixin, but because it's probably common, it's in base until mixins have GUI support
29-
self.usb = self.Port(Vector(UsbDevicePort.empty()), optional=True)
34+
self.usb = self.Port(Vector(UsbDevicePort.empty()), optional=True,
35+
doc="Microcontroller USB device ports")
3036

3137
# CAN is now mixins, but automatically materialized for compatibility
3238
# In new code, explicit mixin syntax should be used.
@@ -199,19 +205,19 @@ class IoController(ProgrammableController, BaseIoController):
199205
Less common peripheral types like CAN and DAC can be added with mixins.
200206
201207
This defines a power input port that powers the device, though the IoControllerPowerOut mixin can be used
202-
for a controller that provides power, for example a development board powered from onboard USB.
208+
for a controller that provides power (like USB-powered dev boards).
203209
"""
204210
def __init__(self, *awgs, **kwargs) -> None:
205211
super().__init__(*awgs, **kwargs)
206212

207-
self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True)
208213
self.gnd = self.Port(Ground.empty(), [Common], optional=True)
214+
self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True)
209215

210216

211217
@non_library
212218
class IoControllerPowerRequired(IoController):
213219
"""IO controller with required power pins."""
214220
def __init__(self, *args, **kwargs) -> None:
215221
super().__init__(*args, **kwargs)
216-
self.require(self.pwr.is_connected())
217222
self.require(self.gnd.is_connected())
223+
self.require(self.pwr.is_connected())

electronics_abstract_parts/IoControllerInterfaceMixins.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,44 @@ class IoControllerSpiPeripheral(BlockInterfaceMixin[BaseIoController]):
66
def __init__(self, *args, **kwargs) -> None:
77
super().__init__(*args, **kwargs)
88

9-
self.spi_peripheral = self.Port(Vector(SpiPeripheral.empty()), optional=True)
9+
self.spi_peripheral = self.Port(Vector(SpiPeripheral.empty()), optional=True,
10+
doc="Microcontroller SPI peripherals (excluding CS pin, which must be handled separately), each element is an independent SPI peripheral")
1011
self.implementation(lambda base: base._io_ports.append(self.spi_peripheral))
1112

1213

1314
class IoControllerI2cTarget(BlockInterfaceMixin[BaseIoController]):
1415
def __init__(self, *args, **kwargs) -> None:
1516
super().__init__(*args, **kwargs)
1617

17-
self.i2c_target = self.Port(Vector(I2cTarget.empty()), optional=True)
18+
self.i2c_target = self.Port(Vector(I2cTarget.empty()), optional=True,
19+
doc="Microcontroller I2C targets, each element is an independent I2C target")
1820
self.implementation(lambda base: base._io_ports.append(self.i2c_target))
1921

2022

2123
class IoControllerTouchDriver(BlockInterfaceMixin[BaseIoController]):
2224
def __init__(self, *args, **kwargs) -> None:
2325
super().__init__(*args, **kwargs)
2426

25-
self.touch = self.Port(Vector(TouchDriver.empty()), optional=True)
27+
self.touch = self.Port(Vector(TouchDriver.empty()), optional=True,
28+
doc="Microcontroller touch input")
2629
self.implementation(lambda base: base._io_ports.insert(0, self.touch)) # allocate first
2730

2831

2932
class IoControllerDac(BlockInterfaceMixin[BaseIoController]):
3033
def __init__(self, *args, **kwargs) -> None:
3134
super().__init__(*args, **kwargs)
3235

33-
self.dac = self.Port(Vector(AnalogSource.empty()), optional=True)
36+
self.dac = self.Port(Vector(AnalogSource.empty()), optional=True,
37+
doc="Microcontroller analog output pins")
3438
self.implementation(lambda base: base._io_ports.insert(0, self.dac)) # allocate first
3539

3640

3741
class IoControllerCan(BlockInterfaceMixin[BaseIoController]):
3842
def __init__(self, *args, **kwargs) -> None:
3943
super().__init__(*args, **kwargs)
4044

41-
self.can = self.Port(Vector(CanControllerPort.empty()), optional=True)
45+
self.can = self.Port(Vector(CanControllerPort.empty()), optional=True,
46+
doc="Microcontroller CAN controller ports")
4247
self.implementation(lambda base: base._io_ports.append(self.can))
4348

4449

@@ -55,15 +60,17 @@ class IoControllerI2s(BlockInterfaceMixin[BaseIoController]):
5560
def __init__(self, *args, **kwargs) -> None:
5661
super().__init__(*args, **kwargs)
5762

58-
self.i2s = self.Port(Vector(I2sController.empty()), optional=True)
63+
self.i2s = self.Port(Vector(I2sController.empty()), optional=True,
64+
doc="Microcontroller I2S controller ports, each element is an independent I2S controller")
5965
self.implementation(lambda base: base._io_ports.append(self.i2s))
6066

6167

6268
class IoControllerDvp8(BlockInterfaceMixin[BaseIoController]):
6369
def __init__(self, *args, **kwargs) -> None:
6470
super().__init__(*args, **kwargs)
6571

66-
self.dvp8 = self.Port(Vector(Dvp8Host.empty()), optional=True)
72+
self.dvp8 = self.Port(Vector(Dvp8Host.empty()), optional=True,
73+
doc="Microcontroller 8-bit DVP digital video ports")
6774
self.implementation(lambda base: base._io_ports.append(self.dvp8))
6875

6976

@@ -79,26 +86,28 @@ class IoControllerBle(BlockInterfaceMixin[BaseIoController]):
7986
"""Mixin indicating this IoController has programmable Bluetooth LE. Does not expose any ports."""
8087

8188

89+
@non_library
8290
class IoControllerGroundOut(BlockInterfaceMixin[IoController]):
83-
"""Base class for an IO controller that can act as a power output (e.g. dev boards),
84-
this only provides the ground source pin. Subclasses can define output power pins.
91+
"""Base mixin for an IoController that can act as a power output (e.g. dev boards),
92+
this only provides the ground source pin. Subclasses can define output power pins.
8593
Multiple power pin mixins can be used on the same class, but only one gnd_out can be connected."""
8694
def __init__(self, *args, **kwargs) -> None:
8795
super().__init__(*args, **kwargs)
88-
self.gnd_out = self.Port(GroundSource.empty(), optional=True)
96+
self.gnd_out = self.Port(GroundSource.empty(), optional=True,
97+
doc="Ground for power output ports, when the device is acting as a power source")
8998

9099

91-
class IoControllerPowerOut(IoControllerGroundOut):
100+
class IoControllerPowerOut(IoControllerGroundOut, BlockInterfaceMixin[IoController]):
92101
"""IO controller mixin that provides an output of the IO controller's VddIO rail, commonly 3.3v."""
93102
def __init__(self, *args, **kwargs) -> None:
94103
super().__init__(*args, **kwargs)
95-
self.pwr_out = self.Port(VoltageSource.empty(), optional=True)
104+
self.pwr_out = self.Port(VoltageSource.empty(), optional=True,
105+
doc="Power output port, typically of the device's Vdd or VddIO rail; must be used with gnd_out")
96106

97107

98-
class IoControllerUsbOut(IoControllerGroundOut):
99-
"""IO controller mixin that provides an output of the IO controller's USB Vbus.
100-
For devices without PD support, this should be 5v. For devices with PD support, this is whatever
101-
Vbus can be."""
108+
class IoControllerUsbOut(IoControllerGroundOut, BlockInterfaceMixin[IoController]):
109+
"""IO controller mixin that provides an output of the IO controller's USB Vbus."""
102110
def __init__(self, *args, **kwargs) -> None:
103111
super().__init__(*args, **kwargs)
104-
self.vusb_out = self.Port(VoltageSource.empty(), optional=True)
112+
self.vusb_out = self.Port(VoltageSource.empty(), optional=True,
113+
doc="Power output port of the device's Vbus, typically 5v; must be used with gnd_out")

electronics_lib/Distance_Vl53l0x.py

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(self) -> None:
3939

4040
gpio_model = self._gpio_model(self.vss, self.vdd)
4141
self.xshut = self.Port(DigitalSink.from_bidir(gpio_model))
42-
self.gpio1 = self.Port(gpio_model, optional=True)
42+
self.gpio1 = self.Port(DigitalSingleSource.low_from_supply(self.vss), optional=True)
4343

4444
# TODO: support addresses, the default is 0x29 though it's software remappable
4545
self.i2c = self.Port(I2cTarget(self._i2c_io_model(self.vss, self.vdd)), [Output])
@@ -70,17 +70,17 @@ def contents(self):
7070

7171

7272
@abstract_block_default(lambda: Vl53l0x)
73-
class Vl53l0xBase(DistanceSensor, Block):
74-
"""Abstract base class for VL53L0x application circuits"""
73+
class Vl53l0xBase(Resettable, DistanceSensor, Block):
74+
"""Abstract base class for VL53L0x devices"""
7575
def __init__(self) -> None:
7676
super().__init__()
7777

78-
self.pwr = self.Port(VoltageSink.empty(), [Power])
7978
self.gnd = self.Port(Ground.empty(), [Common])
79+
self.pwr = self.Port(VoltageSink.empty(), [Power])
8080

8181
self.i2c = self.Port(I2cTarget.empty())
82-
self.xshut = self.Port(DigitalSink.empty(), optional=True)
83-
self.gpio1 = self.Port(DigitalBidir.empty(), optional=True)
82+
self.int = self.Port(DigitalSingleSource.empty(), optional=True,
83+
doc="Interrupt output for new data available")
8484

8585

8686
class Vl53l0xConnector(Vl53l0x_DeviceBase, Vl53l0xBase, GeneratorBlock):
@@ -89,40 +89,41 @@ class Vl53l0xConnector(Vl53l0x_DeviceBase, Vl53l0xBase, GeneratorBlock):
8989
This has an onboard 2.8v regulator, but thankfully the IO tolerance is not referenced to Vdd"""
9090
def contents(self):
9191
super().contents()
92-
self.generator_param(self.xshut.is_connected())
92+
self.generator_param(self.reset.is_connected(), self.int.is_connected())
9393

9494
def generate(self):
9595
super().generate()
9696
self.conn = self.Block(PassiveConnector(length=6))
9797
self.connect(self.pwr, self.conn.pins.request('1').adapt_to(self._vdd_model()))
9898
self.connect(self.gnd, self.conn.pins.request('2').adapt_to(Ground()))
9999

100-
gpio_model = self._gpio_model(self.gnd, self.pwr)
101-
102-
self.connect(self.gpio1, self.conn.pins.request('5').adapt_to(gpio_model))
103100
i2c_io_model = self._i2c_io_model(self.gnd, self.pwr)
104101
self.connect(self.i2c.scl, self.conn.pins.request('3').adapt_to(i2c_io_model))
105102
self.connect(self.i2c.sda, self.conn.pins.request('4').adapt_to(i2c_io_model))
106103
self.i2c.init_from(I2cTarget(DigitalBidir.empty(), []))
107104

108105
gpio_model = self._gpio_model(self.gnd, self.pwr)
109-
if self.get(self.xshut.is_connected()):
110-
self.connect(self.xshut, self.conn.pins.request('6').adapt_to(gpio_model))
106+
if self.get(self.reset.is_connected()):
107+
self.connect(self.reset, self.conn.pins.request('6').adapt_to(gpio_model))
111108
else:
112109
self.connect(self.pwr.as_digital_source(), self.conn.pins.request('6').adapt_to(gpio_model))
113110

111+
if self.get(self.int.is_connected()):
112+
self.connect(self.int, self.conn.pins.request('5').adapt_to(
113+
DigitalSingleSource.low_from_supply(self.gnd)
114+
))
115+
114116

115117
class Vl53l0x(Vl53l0xBase, GeneratorBlock):
116-
"""Board-mount laser ToF sensor"""
118+
"""Time-of-flight laser ranging sensor, up to 2m"""
117119
def contents(self):
118120
super().contents()
119121
self.ic = self.Block(Vl53l0x_Device())
120122
self.connect(self.pwr, self.ic.vdd)
121123
self.connect(self.gnd, self.ic.vss)
122124

123125
self.connect(self.i2c, self.ic.i2c)
124-
self.connect(self.gpio1, self.ic.gpio1)
125-
self.generator_param(self.xshut.is_connected())
126+
self.generator_param(self.reset.is_connected(), self.int.is_connected())
126127

127128
# Datasheet Figure 3, two decoupling capacitors
128129
self.vdd_cap = ElementDict[DecouplingCapacitor]()
@@ -131,26 +132,29 @@ def contents(self):
131132

132133
def generate(self):
133134
super().generate()
134-
if self.get(self.xshut.is_connected()):
135-
self.connect(self.xshut, self.ic.xshut)
135+
if self.get(self.reset.is_connected()):
136+
self.connect(self.reset, self.ic.xshut)
136137
else:
137138
self.connect(self.pwr.as_digital_source(), self.ic.xshut)
138139

140+
if self.get(self.int.is_connected()):
141+
self.connect(self.int, self.ic.gpio1)
139142

140143
class Vl53l0xArray(DistanceSensor, GeneratorBlock):
141144
"""Array of Vl53l0x with common I2C but individually exposed XSHUT pins and optionally GPIO1 (interrupt)."""
142145
@init_in_parent
143-
def __init__(self, count: IntLike, *, first_xshut_fixed: BoolLike = False):
146+
def __init__(self, count: IntLike, *, first_reset_fixed: BoolLike = False):
144147
super().__init__()
145148
self.pwr = self.Port(VoltageSink.empty(), [Power])
146149
self.gnd = self.Port(Ground.empty(), [Common])
147150
self.i2c = self.Port(I2cTarget.empty())
148-
self.xshut = self.Port(Vector(DigitalSink.empty()))
149-
self.gpio1 = self.Port(Vector(DigitalBidir.empty()), optional=True)
151+
self.reset = self.Port(Vector(DigitalSink.empty()))
152+
# TODO better support for optional vectors so the inner doesn't connect if the outer doesn't connect
153+
# self.int = self.Port(Vector(DigitalSingleSource.empty()), optional=True)
150154

151155
self.count = self.ArgParameter(count)
152-
self.first_xshut_fixed = self.ArgParameter(first_xshut_fixed)
153-
self.generator_param(self.count, self.first_xshut_fixed)
156+
self.first_reset_fixed = self.ArgParameter(first_reset_fixed)
157+
self.generator_param(self.count, self.first_reset_fixed)
154158

155159
def generate(self):
156160
super().generate()
@@ -160,9 +164,7 @@ def generate(self):
160164
self.connect(self.pwr, elt.pwr)
161165
self.connect(self.gnd, elt.gnd)
162166
self.connect(self.i2c, elt.i2c)
163-
if self.get(self.first_xshut_fixed) and elt_i == 0:
164-
self.connect(elt.pwr.as_digital_source(), elt.xshut)
167+
if self.get(self.first_reset_fixed) and elt_i == 0:
168+
self.connect(elt.pwr.as_digital_source(), elt.reset)
165169
else:
166-
self.connect(self.xshut.append_elt(DigitalSink.empty(), str(elt_i)), elt.xshut)
167-
168-
self.connect(self.gpio1.append_elt(DigitalBidir.empty(), str(elt_i)), elt.gpio1)
170+
self.connect(self.reset.append_elt(DigitalSink.empty(), str(elt_i)), elt.reset)

electronics_lib/EnvironmentalSensor_Bme680.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ def contents(self) -> None:
4141
self.assign(self.actual_basic_part, False)
4242

4343

44-
class Bme680(EnvironmentalSensor, Block):
44+
class Bme680(EnvironmentalSensor, DefaultExportBlock):
45+
"""Gas (indoor air quality), pressure, temperature, and humidity sensor.
46+
Humidity accuracy /-3% RH, pressure noise 0.12 Pa, temperature accuracy +/-0.5 C @ 25C"""
4547
def __init__(self):
4648
super().__init__()
4749
self.ic = self.Block(Bme680_Device())
48-
self.vdd = self.Export(self.ic.vdd, [Power])
49-
self.vddio = self.Export(self.ic.vddio, [Power])
5050
self.gnd = self.Export(self.ic.gnd, [Common])
51+
self.pwr = self.Export(self.ic.vdd, [Power])
52+
self.pwr_io = self.Export(self.ic.vddio, default=self.pwr, doc="IO supply voltage")
5153
self.i2c = self.Export(self.ic.i2c, [InOut])
5254

5355
def contents(self):

0 commit comments

Comments
 (0)