Skip to content
Merged
10 changes: 10 additions & 0 deletions edg/abstract_parts/IoController.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.gnd = self.Port(Ground.empty(), [Common], optional=True)
self.pwr = self.Port(VoltageSink.empty(), [Power], optional=True)

@override
def contents(self) -> None:
super().contents()
# self-powered microcontrollers (like USB dev boards) can be used without GND
# for instance with keyswitches connected to only GPIO
self.require(
self.pwr.is_connected().implies(self.gnd.is_connected()),
"gnd must be connected if pwr connected",
)


@non_library
class IoControllerPowerRequired(IoController):
Expand Down
89 changes: 87 additions & 2 deletions edg/abstract_parts/IoControllerInterfaceMixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any
from typing import Any, Union

from typing_extensions import override

from ..electronics_interfaces import *
from .IoController import BaseIoController, IoController
Expand Down Expand Up @@ -117,16 +119,86 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
doc="Power output port, typically of the device's Vdd or VddIO rail at 3.3v",
)

@override
def contents(self) -> None:
super().contents()
if isinstance(self, IoController):
self.require(
self.pwr_out.is_connected().implies(~self.pwr.is_connected()),
"can only connect one of pwr and pwr_out (same physical pin)",
)
self.require(
self.pwr_out.is_connected().implies(self.gnd.is_connected()),
"gnd must be connected if pwr_out connected",
)

def _generate_gnd_node(self) -> Ground:
"""Helper function that returns a ground node, either directly taking the gnd port if available,
or generating an internal ground node.

Requires self.gnd.is_connected() as a generator param.
"""
assert isinstance(self, IoController)
if self.get(self.gnd.is_connected()): # gnd connected externally
return self.gnd
else:
self.gnd_model = self.Block(DummyGround())
return self.gnd_model.io

def _generate_pwr_node(
self, voltage_out: RangeLike, current_limits: RangeLike
) -> Union[VoltageSink, VoltageSource]:
"""Helper function that returns a power node, either directly taking the pwr port if available,
or generating an internal voltage node and optionally connecting it to pwr_out (if used).

Requires self.pwr.is_connected(), self.pwr_out.is_connected as generator params.
"""
assert isinstance(self, IoController)
if self.get(self.pwr.is_connected()): # power supplied externally
return self.pwr
else:
self.pwr_out_model = self.Block(
DummyVoltageSource(
voltage_out=voltage_out, # tolerance is a guess
current_limits=current_limits,
)
)
if self.get(self.pwr_out.is_connected()):
self.connect(self.pwr_out, self.pwr_out_model.io)
return self.pwr_out_model.io


class IoControllerUsbOut(BlockInterfaceMixin[IoController]):
"""IO controller mixin that provides an output of the IO controller's USB Vbus."""
"""IO controller mixin that provides an output of the IO controller's USB Vbus.

If also used with IoControllerVin, it is assumed that pwr_vin is the same physical pin as vusb_out.
TODO: support devices with separate Vin and Vbus"""

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.vusb_out = self.Port(
VoltageSource.empty(), optional=True, doc="Power output port of the device's Vbus, typically 5v"
)

@override
def contents(self) -> None:
super().contents()
if isinstance(self, IoController):
self.require(
self.vusb_out.is_connected().implies(self.gnd.is_connected()),
"gnd must be connected if vusb_out connected",
)
self.require(
self.vusb_out.is_connected().implies(~self.pwr.is_connected()),
"can't sink logic-level pwr if sourcing power from USB",
)

if isinstance(self, IoControllerVin):
self.require(
self.vusb_out.is_connected().implies(~self.pwr_vin.is_connected()),
"can only connect one of pwr_vin and vusb_out (same physical pin)",
)


class IoControllerVin(BlockInterfaceMixin[IoController]):
"""IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator."""
Expand All @@ -136,3 +208,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.pwr_vin = self.Port(
VoltageSink.empty(), optional=True, doc="Power input pin, typically rated for 5v or a bit beyond."
)

@override
def contents(self) -> None:
super().contents()
if isinstance(self, IoController):
self.require(
self.pwr_vin.is_connected().implies(self.gnd.is_connected()),
"gnd must be connected if pwr_vin connected",
)
self.require(
self.pwr_vin.is_connected().implies(~self.pwr.is_connected()),
"can't sink logic-level pwr if powered from external vin",
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
from typing import *
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union
from typing_extensions import override

from ..electronics_interfaces import *
from .IoController import BaseIoController


class BaseIoControllerModelable(BaseIoController):
"""Base class for a BaseIoController that can (optionally) be used as a (non-physical) model.
This only adds parameters as a standard interface and is not functional.
Subclasses must plumb these parameters."""

def __init__(
self, *args: Any, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [], **kwargs: Any
) -> None:
super().__init__(*args, **kwargs)

self._model = self.ArgParameter(_model)
self._allowed_pins = self.ArgParameter(_allowed_pins)
self.generator_param(self._allowed_pins)


class BaseIoControllerWrapped(BaseIoController):
"""Base class for IoController wrapped blocks, particularly footprints that are used
with an outer WrapperSubboardBlock to implement e.g. a dev board or module around a modeling subcircuit.
Expand Down Expand Up @@ -204,6 +220,9 @@ def _make_model_pinning(self, remapping: Dict[str, str], device_assigns: List[st
remapping is specified as the forward remapping, from pinname to device pinnum.

Requires _generator_param_all_ios, so all the IOs names are available.

In most cases, use _wrap_inner_model_device which provides all the wrapping functionality, though
this may be useful where other logic needs to happen with parameters.
"""
inverse_remapping = {v: k for k, v in remapping.items()}

Expand All @@ -227,3 +246,33 @@ def _make_model_pinning(self, remapping: Dict[str, str], device_assigns: List[st
remapped_assigns.append(assign)

return remapped_assigns

@override
def _wrap_inner(self, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError # use _wrap_inner_model_device instead

def _wrap_inner_model_device(
self, model: BaseIoControllerModelable, device: BaseIoControllerWrapped, remapping: Dict[str, str]
) -> None:
"""A version of _wrap_inner, but for the model (non-physical, directly connected) and device (physical,
export tapped) for a wrapper block.

Both the model and device should have pin_assigns unassigned, since this will assign them.
The model must also have _allowed_pins unassigned.

Export IO transforms are not supported. Where needed, the wrapper should instead represent
the device footprint instead of the application circuit, which should be defined at one level of hierarchy
higher.

Requires _generator_param_all_ios, so all the IOs names are available, and pin_assigns to be a generator param.
"""
assert isinstance(self, GeneratorBlock)

self.assign(model.pin_assigns, self._make_model_pinning(remapping, self.get(self.pin_assigns)))
self.assign(model._allowed_pins, list(remapping.keys()))
self._export_ios_inner(model)
self.assign(self.io_current_draw, model.io_current_draw)

self.assign(device.pin_assigns, model.actual_pin_assigns)
self._export_tap_ios_inner(device)
self.assign(self.actual_pin_assigns, device.actual_pin_assigns)
2 changes: 1 addition & 1 deletion edg/abstract_parts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@

from .IoController import BaseIoController, IoController, IoControllerPowerRequired, BaseIoControllerPinmapGenerator
from .IoControllerExportable import BaseIoControllerExportable
from .BaseIoControllerWrapped import BaseIoControllerWrapped, BaseIoControllerWrapper
from .IoControllerWrapper import BaseIoControllerModelable, BaseIoControllerWrapped, BaseIoControllerWrapper
from .IoControllerInterfaceMixins import (
IoControllerSpiPeripheral,
IoControllerI2cTarget,
Expand Down
2 changes: 1 addition & 1 deletion edg/electronics_interfaces/GroundDummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class DummyGround(BaseDummyBlock[GroundLink]):
def __init__(self) -> None:
super().__init__()
self.io = self.Port(Ground(), [Common, InOut])
self.io: Ground = self.Port(Ground(), [Common, InOut])

def __getattr__(self, item: str) -> Any:
if item == "gnd":
Expand Down
4 changes: 2 additions & 2 deletions edg/electronics_interfaces/VoltageDummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
) -> None:
super().__init__()

self.io = self.Port(
self.io: VoltageSource = self.Port(
VoltageSource(
voltage_out=voltage_out,
current_limits=current_limits,
Expand Down Expand Up @@ -55,7 +55,7 @@ def __init__(
) -> None:
super().__init__()

self.io = self.Port(
self.io: VoltageSink = self.Port(
VoltageSink(
voltage_limits=voltage_limit,
current_draw=current_draw,
Expand Down
61 changes: 22 additions & 39 deletions edg/parts/microcontroller/Esp32.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ class Esp32_Interfaces(


class Esp32_Wroom_32_Device(
Esp32_Interfaces, InternalSubcircuit, BaseIoControllerPinmapGenerator, FootprintBlock, JlcPart
Esp32_Interfaces,
InternalSubcircuit,
BaseIoControllerModelable,
BaseIoControllerPinmapGenerator,
FootprintBlock,
JlcPart,
):
"""ESP32-WROOM-32 module

Expand Down Expand Up @@ -59,12 +64,9 @@ class Esp32_Wroom_32_Device(
"GPIO23": "37",
}

def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [], **kwargs: Any) -> None:
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)

self._allowed_pins = self.ArgParameter(_allowed_pins)
self.generator_param(self._allowed_pins)

self.pwr = self.Port(
VoltageSink(
voltage_limits=(3.0, 3.6) * Volt, # section 5.2, table 14, most restrictive limits
Expand All @@ -84,7 +86,7 @@ def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = []
pullup_capable=True,
pulldown_capable=True,
)
self.chip_pu = self.Port(self._dio_model, optional=_model)
self.chip_pu = self.Port(self._dio_model, optional=self._model)
# section 2.4, table 5: strapping IOs that need a fixed value to boot, TODO currently not allocatable post-boot
self.io0 = self.Port(self._dio_model, optional=True) # default pullup (SPI boot), set low to download boot
self.io2 = self.Port(
Expand Down Expand Up @@ -395,40 +397,21 @@ def generate(self) -> None:
super().generate()

self.model = self.Block(
Esp32_Wroom_32_Device(
pin_assigns=self._make_model_pinning(
Freenove_Esp32_Wrover_Device._PIN_REMAPPING, self.get(self.pin_assigns)
),
_model=True,
_allowed_pins=list(Freenove_Esp32_Wrover_Device._PIN_REMAPPING.keys()),
)
Esp32_Wroom_32_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr())
)
self._export_ios_inner(self.model)
self.device = self.Block(Freenove_Esp32_Wrover_Device(pin_assigns=ArrayStringExpr()), external=True)
self._wrap_inner_model_device(self.model, self.device, Freenove_Esp32_Wrover_Device._PIN_REMAPPING)

self.device = self.Block(Freenove_Esp32_Wrover_Device(pin_assigns=self.model.actual_pin_assigns), external=True)
self._export_tap_ios_inner(self.device)
self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns)
self.connect(self._generate_gnd_node(), self.model.gnd)
self.export_tap(self.gnd, self.device.gnd)

if self.get(self.gnd.is_connected()):
self.connect(self.gnd, self.model.gnd)
self.export_tap(self.gnd, self.device.gnd)
else:
self.gnd_model = self.Block(DummyGround()).connected(self.model.gnd)

if self.get(self.pwr.is_connected()): # power supplied externally
self.connect(self.pwr, self.model.pwr)
self.export_tap(self.pwr.net, self.device.v3v3)
else: # board sources power from USB
self.pwr_out_model = self.Block(
DummyVoltageSource(
voltage_out=3.3 * Volt(tol=0.05), # tolerance is a guess
current_limits=UsbConnector.USB2_CURRENT_LIMITS,
)
)
self.connect(self.pwr_out_model.io, self.model.pwr)
if self.get(self.pwr_out.is_connected()):
self.connect(self.pwr_out, self.pwr_out_model.io)
self.export_tap(self.pwr_out.net, self.device.v3v3)
self.connect(
self._generate_pwr_node(
voltage_out=3.3 * Volt(tol=0.05),
current_limits=UsbConnector.USB2_CURRENT_LIMITS, # tolerance is a guess
),
self.model.pwr,
)
self.export_tap((self.pwr if self.get(self.pwr.is_connected()) else self.pwr_out).net, self.device.v3v3)

if self.get(self.vusb_out.is_connected()):
self.export_tap(self.vusb_out.net, self.device.vcc)
self.export_tap(self.vusb_out.net, self.device.vcc)
Loading
Loading