Skip to content

Commit

Permalink
Add Export-with-default utility, improve sensor and IoController port…
Browse files Browse the repository at this point in the history
… naming and docs (#350)
  • Loading branch information
ducky64 authored May 2, 2024
1 parent 9ca4461 commit 6a72e49
Show file tree
Hide file tree
Showing 29 changed files with 317 additions and 278 deletions.
35 changes: 35 additions & 0 deletions edg_core/Generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from deprecated import deprecated

import edgir
from .Ports import BasePort, Port
from .PortTag import PortTag
from .IdentityDict import IdentityDict
from .Binding import InitParamBinding, AllocatedBinding, IsConnectedBinding
from .Blocks import BlockElaborationState, AbstractBlockProperty
Expand Down Expand Up @@ -130,3 +132,36 @@ def _generated_def_to_proto(self, generate_values: Iterable[Tuple[edgir.LocalPat
self._elaboration_state = BlockElaborationState.post_generate

return self._def_to_proto()


class DefaultExportBlock(GeneratorBlock):
"""EXPERIMENTAL UTILITY CLASS. There needs to be a cleaner way to address this eventually,
perhaps as a core compiler construct.
This encapsulates the common pattern of an optional export, which if not externally connected,
connects the internal port to some other default port.
TODO The default can be specified as a port, or a function that returns a port (e.g. to instantiate adapters)."""
def __init__(self):
super().__init__()
self._default_exports: List[Tuple[BasePort, Port, Port]] = [] # internal, exported, default

ExportType = TypeVar('ExportType', bound=BasePort)
def Export(self, port: ExportType, *args, default: Optional[Port] = None, **kwargs) -> ExportType:
"""A generator-only variant of Export that supports an optional default (either internal or external)
to connect the (internal) port being exported to, if the external exported port is not connected."""
if default is None:
new_port = super().Export(port, *args, **kwargs)
else:
assert 'optional' not in kwargs, "optional must not be specified with default"
new_port = super().Export(port, *args, optional=True, _connect=False, **kwargs)
assert isinstance(new_port, Port), "defaults only supported with Port types"
self.generator_param(new_port.is_connected())
self._default_exports.append((port, new_port, default))
return new_port

def generate(self):
super().generate()
for (internal, exported, default) in self._default_exports:
if self.get(exported.is_connected()):
self.connect(internal, exported)
else:
self.connect(internal, default)
7 changes: 5 additions & 2 deletions edg_core/HierarchyBlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,8 @@ def Port(self, tpe: T, tags: Iterable[PortTag]=[], *, optional: bool = False, do
return port # type: ignore

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

self.connect(new_port, port)
if _connect:
self.connect(new_port, port)

return new_port # type: ignore

BlockType = TypeVar('BlockType', bound='Block')
Expand Down
2 changes: 1 addition & 1 deletion edg_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .DesignTop import DesignTop
from .BlockInterfaceMixin import BlockInterfaceMixin
from .HierarchyBlock import Block, ImplicitConnect, init_in_parent, abstract_block, abstract_block_default
from .Generator import GeneratorBlock
from .Generator import GeneratorBlock, DefaultExportBlock
from .MultipackBlock import PackedBlockArray, MultipackBlock
from .PortBlocks import PortBridge, PortAdapter
from .Array import Vector
Expand Down
26 changes: 16 additions & 10 deletions electronics_abstract_parts/IoController.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ class BaseIoController(PinMappable, Block):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True)
self.adc = self.Port(Vector(AnalogSink.empty()), optional=True)

self.spi = self.Port(Vector(SpiController.empty()), optional=True)
self.i2c = self.Port(Vector(I2cController.empty()), optional=True)
self.uart = self.Port(Vector(UartPort.empty()), optional=True)
self.gpio = self.Port(Vector(DigitalBidir.empty()), optional=True,
doc="Microcontroller digital GPIO pins")
self.adc = self.Port(Vector(AnalogSink.empty()), optional=True,
doc="Microcontroller analog input pins")

self.spi = self.Port(Vector(SpiController.empty()), optional=True,
doc="Microcontroller SPI controllers, each element is an independent SPI controller")
self.i2c = self.Port(Vector(I2cController.empty()), optional=True,
doc="Microcontroller I2C controllers, each element is an independent I2C controller")
self.uart = self.Port(Vector(UartPort.empty()), optional=True,
doc="Microcontroller UARTs")

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

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

self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True)
self.gnd = self.Port(Ground.empty(), [Common], optional=True)
self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True)


@non_library
class IoControllerPowerRequired(IoController):
"""IO controller with required power pins."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.require(self.pwr.is_connected())
self.require(self.gnd.is_connected())
self.require(self.pwr.is_connected())
43 changes: 26 additions & 17 deletions electronics_abstract_parts/IoControllerInterfaceMixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,44 @@ class IoControllerSpiPeripheral(BlockInterfaceMixin[BaseIoController]):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

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


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

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


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

self.touch = self.Port(Vector(TouchDriver.empty()), optional=True)
self.touch = self.Port(Vector(TouchDriver.empty()), optional=True,
doc="Microcontroller touch input")
self.implementation(lambda base: base._io_ports.insert(0, self.touch)) # allocate first


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

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


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

self.can = self.Port(Vector(CanControllerPort.empty()), optional=True)
self.can = self.Port(Vector(CanControllerPort.empty()), optional=True,
doc="Microcontroller CAN controller ports")
self.implementation(lambda base: base._io_ports.append(self.can))


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

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


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

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


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


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


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


class IoControllerUsbOut(IoControllerGroundOut):
"""IO controller mixin that provides an output of the IO controller's USB Vbus.
For devices without PD support, this should be 5v. For devices with PD support, this is whatever
Vbus can be."""
class IoControllerUsbOut(IoControllerGroundOut, BlockInterfaceMixin[IoController]):
"""IO controller mixin that provides an output of the IO controller's USB Vbus."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.vusb_out = self.Port(VoltageSource.empty(), optional=True)
self.vusb_out = self.Port(VoltageSource.empty(), optional=True,
doc="Power output port of the device's Vbus, typically 5v; must be used with gnd_out")
56 changes: 29 additions & 27 deletions electronics_lib/Distance_Vl53l0x.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __init__(self) -> None:

gpio_model = self._gpio_model(self.vss, self.vdd)
self.xshut = self.Port(DigitalSink.from_bidir(gpio_model))
self.gpio1 = self.Port(gpio_model, optional=True)
self.gpio1 = self.Port(DigitalSingleSource.low_from_supply(self.vss), optional=True)

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


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

self.pwr = self.Port(VoltageSink.empty(), [Power])
self.gnd = self.Port(Ground.empty(), [Common])
self.pwr = self.Port(VoltageSink.empty(), [Power])

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


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

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

gpio_model = self._gpio_model(self.gnd, self.pwr)

self.connect(self.gpio1, self.conn.pins.request('5').adapt_to(gpio_model))
i2c_io_model = self._i2c_io_model(self.gnd, self.pwr)
self.connect(self.i2c.scl, self.conn.pins.request('3').adapt_to(i2c_io_model))
self.connect(self.i2c.sda, self.conn.pins.request('4').adapt_to(i2c_io_model))
self.i2c.init_from(I2cTarget(DigitalBidir.empty(), []))

gpio_model = self._gpio_model(self.gnd, self.pwr)
if self.get(self.xshut.is_connected()):
self.connect(self.xshut, self.conn.pins.request('6').adapt_to(gpio_model))
if self.get(self.reset.is_connected()):
self.connect(self.reset, self.conn.pins.request('6').adapt_to(gpio_model))
else:
self.connect(self.pwr.as_digital_source(), self.conn.pins.request('6').adapt_to(gpio_model))

if self.get(self.int.is_connected()):
self.connect(self.int, self.conn.pins.request('5').adapt_to(
DigitalSingleSource.low_from_supply(self.gnd)
))


class Vl53l0x(Vl53l0xBase, GeneratorBlock):
"""Board-mount laser ToF sensor"""
"""Time-of-flight laser ranging sensor, up to 2m"""
def contents(self):
super().contents()
self.ic = self.Block(Vl53l0x_Device())
self.connect(self.pwr, self.ic.vdd)
self.connect(self.gnd, self.ic.vss)

self.connect(self.i2c, self.ic.i2c)
self.connect(self.gpio1, self.ic.gpio1)
self.generator_param(self.xshut.is_connected())
self.generator_param(self.reset.is_connected(), self.int.is_connected())

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

def generate(self):
super().generate()
if self.get(self.xshut.is_connected()):
self.connect(self.xshut, self.ic.xshut)
if self.get(self.reset.is_connected()):
self.connect(self.reset, self.ic.xshut)
else:
self.connect(self.pwr.as_digital_source(), self.ic.xshut)

if self.get(self.int.is_connected()):
self.connect(self.int, self.ic.gpio1)

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

self.count = self.ArgParameter(count)
self.first_xshut_fixed = self.ArgParameter(first_xshut_fixed)
self.generator_param(self.count, self.first_xshut_fixed)
self.first_reset_fixed = self.ArgParameter(first_reset_fixed)
self.generator_param(self.count, self.first_reset_fixed)

def generate(self):
super().generate()
Expand All @@ -160,9 +164,7 @@ def generate(self):
self.connect(self.pwr, elt.pwr)
self.connect(self.gnd, elt.gnd)
self.connect(self.i2c, elt.i2c)
if self.get(self.first_xshut_fixed) and elt_i == 0:
self.connect(elt.pwr.as_digital_source(), elt.xshut)
if self.get(self.first_reset_fixed) and elt_i == 0:
self.connect(elt.pwr.as_digital_source(), elt.reset)
else:
self.connect(self.xshut.append_elt(DigitalSink.empty(), str(elt_i)), elt.xshut)

self.connect(self.gpio1.append_elt(DigitalBidir.empty(), str(elt_i)), elt.gpio1)
self.connect(self.reset.append_elt(DigitalSink.empty(), str(elt_i)), elt.reset)
8 changes: 5 additions & 3 deletions electronics_lib/EnvironmentalSensor_Bme680.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ def contents(self) -> None:
self.assign(self.actual_basic_part, False)


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

def contents(self):
Expand Down
Loading

0 comments on commit 6a72e49

Please sign in to comment.