From 823d918f700e58809130dfaf7218c14f29452fed Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 21:49:19 -0700 Subject: [PATCH 01/16] wip --- ...BaseIoControllerWrapped.py => IoControllerWrapper.py} | 0 edg/abstract_parts/__init__.py | 2 +- edg/parts/microcontroller/Rp2040.py | 9 ++++----- 3 files changed, 5 insertions(+), 6 deletions(-) rename edg/abstract_parts/{BaseIoControllerWrapped.py => IoControllerWrapper.py} (100%) diff --git a/edg/abstract_parts/BaseIoControllerWrapped.py b/edg/abstract_parts/IoControllerWrapper.py similarity index 100% rename from edg/abstract_parts/BaseIoControllerWrapped.py rename to edg/abstract_parts/IoControllerWrapper.py diff --git a/edg/abstract_parts/__init__.py b/edg/abstract_parts/__init__.py index 6a43150fc..59f6f007c 100644 --- a/edg/abstract_parts/__init__.py +++ b/edg/abstract_parts/__init__.py @@ -129,7 +129,7 @@ from .IoController import BaseIoController, IoController, IoControllerPowerRequired, BaseIoControllerPinmapGenerator from .IoControllerExportable import BaseIoControllerExportable -from .BaseIoControllerWrapped import BaseIoControllerWrapped, BaseIoControllerWrapper +from .IoControllerWrapper import BaseIoControllerWrapped, BaseIoControllerWrapper from .IoControllerInterfaceMixins import ( IoControllerSpiPeripheral, IoControllerI2cTarget, diff --git a/edg/parts/microcontroller/Rp2040.py b/edg/parts/microcontroller/Rp2040.py index 467f3e0e6..83ad77970 100644 --- a/edg/parts/microcontroller/Rp2040.py +++ b/edg/parts/microcontroller/Rp2040.py @@ -56,7 +56,6 @@ def __init__(self, *, _model: BoolLike = False, _allowed_pins: ArrayStringLike = self._allowed_pins = self.ArgParameter(_allowed_pins) self.generator_param(self._allowed_pins) - self._model = self.ArgParameter(_model) self.gnd = self.Port(Ground(), [Common]) self.iovdd = self.Port( @@ -111,10 +110,10 @@ def __init__(self, *, _model: BoolLike = False, _allowed_pins: ArrayStringLike = pulldown_capable=True, ) - self.qspi = self.Port(SpiController(self._dio_std_model), optional=self._model) # TODO actually QSPI - self.qspi_cs = self.Port(self._dio_std_model, optional=self._model) - self.qspi_sd2 = self.Port(self._dio_std_model, optional=self._model) - self.qspi_sd3 = self.Port(self._dio_std_model, optional=self._model) + self.qspi = self.Port(SpiController(self._dio_std_model), optional=_model) # TODO actually QSPI + self.qspi_cs = self.Port(self._dio_std_model, optional=_model) + self.qspi_sd2 = self.Port(self._dio_std_model, optional=_model) + self.qspi_sd3 = self.Port(self._dio_std_model, optional=_model) self.xosc = self.Port( CrystalDriver( From 27bc6164629ed0b5103456c004df46ff37367871 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 22:14:19 -0700 Subject: [PATCH 02/16] centralize power port mutex logic --- edg/abstract_parts/IoController.py | 10 ++++ .../IoControllerInterfaceMixins.py | 52 ++++++++++++++++++- edg/parts/microcontroller/Rp2040.py | 18 ------- edg/parts/microcontroller/Stm32f303.py | 9 ---- edg/parts/microcontroller/nRF52840.py | 9 ---- 5 files changed, 61 insertions(+), 37 deletions(-) diff --git a/edg/abstract_parts/IoController.py b/edg/abstract_parts/IoController.py index 628beda79..1ecb40497 100644 --- a/edg/abstract_parts/IoController.py +++ b/edg/abstract_parts/IoController.py @@ -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 or pwr_out connected", + ) + @non_library class IoControllerPowerRequired(IoController): diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index f9565d196..54edf99b1 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -1,5 +1,7 @@ from typing import Any +from typing_extensions import override + from ..electronics_interfaces import * from .IoController import BaseIoController, IoController @@ -117,6 +119,20 @@ 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.is_connected().implies(~self.pwr_out.is_connected()) + & 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 or pwr_out connected", + ) + class IoControllerUsbOut(BlockInterfaceMixin[IoController]): """IO controller mixin that provides an output of the IO controller's USB Vbus.""" @@ -127,12 +143,46 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: 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 pwr or pwr_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", + ) + class IoControllerVin(BlockInterfaceMixin[IoController]): - """IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator.""" + """IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator. + If also used with IoControllerUsbOut, 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.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 or pwr_out connected", + ) + self.require( + self.vusb_out.is_connected().implies(~self.pwr.is_connected()), + "can't sink logic-level pwr if powered from external vin", + ) + + if isinstance(self, IoControllerUsbOut): + self.require( + self.pwr_vin.is_connected().implies(~self.vusb_out.is_connected()), + "can only connect one of pwr_vin and vusb_out (same physical pin)", + ) diff --git a/edg/parts/microcontroller/Rp2040.py b/edg/parts/microcontroller/Rp2040.py index 83ad77970..9503a5337 100644 --- a/edg/parts/microcontroller/Rp2040.py +++ b/edg/parts/microcontroller/Rp2040.py @@ -464,24 +464,6 @@ def generate(self) -> None: VoltageSource(voltage_out=UsbConnector.USB2_VOLTAGE_RANGE, current_limits=UsbConnector.USB2_CURRENT_LIMITS) ) - self.require( - ~self.pwr_vin.is_connected() | ~self.vusb_out.is_connected(), "cannot use both VUsb out and VUsb in" - ) - self.require( - (self.pwr_vin.is_connected() | self.vusb_out.is_connected()).implies(~self.pwr.is_connected()), - "cannot use 3.3v input if VUsb used", - ) - self.require(~self.pwr_out.is_connected() | ~self.pwr.is_connected(), "cannot use both 3.3v out and 3.3v in") - self.require( - ( - self.pwr_vin.is_connected() - | self.vusb_out.is_connected() - | self.pwr.is_connected() - | self.pwr_out.is_connected() - ).implies(self.gnd.is_connected()), - "ground required if power used", - ) - self.model = self.Block( Rp2040_Device( pin_assigns=self._make_model_pinning(Xiao_Rp2040_Device._PIN_REMAPPING, self.get(self.pin_assigns)), diff --git a/edg/parts/microcontroller/Stm32f303.py b/edg/parts/microcontroller/Stm32f303.py index 7ffad4f5e..0d9a10771 100644 --- a/edg/parts/microcontroller/Stm32f303.py +++ b/edg/parts/microcontroller/Stm32f303.py @@ -87,15 +87,6 @@ def __init__(self) -> None: def generate(self) -> None: super().generate() - self.require( - self.pwr.is_connected().implies(~self.vusb_out.is_connected()), - "can't source USB power if power input connected", - ) - self.require( - self.pwr.is_connected().implies(~self.pwr_out.is_connected()), - "can't source 3v3 power if power input connected", - ) - self.footprint( "U", "edg:Nucleo32", diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index cc599e6c3..2622ac5e0 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -683,15 +683,6 @@ def __init__(self, **kwargs: Any) -> None: def generate(self) -> None: super().generate() - self.require( - self.pwr.is_connected().implies(~self.vusb_out.is_connected()), - "can't source USB power if power input connected", - ) - self.require( - self.pwr.is_connected().implies(~self.pwr_out.is_connected()), - "can't source 3v3 power if power input connected", - ) - self.model = self.Block( Mdbt50q_1mv2_Device( pin_assigns=self._make_model_pinning( From a0395f1030608b14a4b8f14618d80b30a30eeb90 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 22:16:03 -0700 Subject: [PATCH 03/16] fix duplicate constr naming --- edg/abstract_parts/IoController.py | 2 +- edg/abstract_parts/IoControllerInterfaceMixins.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/edg/abstract_parts/IoController.py b/edg/abstract_parts/IoController.py index 1ecb40497..0a4506349 100644 --- a/edg/abstract_parts/IoController.py +++ b/edg/abstract_parts/IoController.py @@ -337,7 +337,7 @@ def contents(self) -> None: # 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 or pwr_out connected", + "gnd must be connected if pwr connected", ) diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index 54edf99b1..40e464b63 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -130,7 +130,7 @@ def contents(self) -> None: ) self.require( self.pwr_out.is_connected().implies(self.gnd.is_connected()), - "gnd must be connected if pwr or pwr_out connected", + "gnd must be connected if pwr_out connected", ) @@ -149,7 +149,7 @@ def contents(self) -> None: if isinstance(self, IoController): self.require( self.vusb_out.is_connected().implies(self.gnd.is_connected()), - "gnd must be connected if pwr or pwr_out connected", + "gnd must be connected if vusb_out connected", ) self.require( self.vusb_out.is_connected().implies(~self.pwr.is_connected()), @@ -174,7 +174,7 @@ def contents(self) -> None: if isinstance(self, IoController): self.require( self.pwr_vin.is_connected().implies(self.gnd.is_connected()), - "gnd must be connected if pwr or pwr_out connected", + "gnd must be connected if pwr_vin connected", ) self.require( self.vusb_out.is_connected().implies(~self.pwr.is_connected()), From 107746084b230d15eb4e9e8abe1c01a28eade894 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 22:57:32 -0700 Subject: [PATCH 04/16] refactor out model/device wrapper --- edg/abstract_parts/IoControllerWrapper.py | 29 +++++++++++++++++++++++ edg/parts/microcontroller/Esp32c3.py | 19 ++++----------- edg/parts/microcontroller/Esp32s3.py | 13 +++------- edg/parts/microcontroller/Rp2040.py | 9 +++---- edg/parts/microcontroller/nRF52840.py | 11 +++------ 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index a0abb4186..ce65b60a4 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -204,6 +204,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()} @@ -227,3 +230,29 @@ 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: BaseIoController, 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. + + 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. + """ + self.assign(self.model.pin_assigns, self._make_model_pinning(remapping, self.get(self.pin_assigns))) + self._export_ios_inner(self.model) + self.assign(self.io_current_draw, self.model.io_current_draw) + + self.assgin(self.device.pin_assigns, self.model.actual_pin_assigns) + self._export_tap_ios_inner(self.device) + self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns) diff --git a/edg/parts/microcontroller/Esp32c3.py b/edg/parts/microcontroller/Esp32c3.py index 0f6407e8d..22f8acc1c 100644 --- a/edg/parts/microcontroller/Esp32c3.py +++ b/edg/parts/microcontroller/Esp32c3.py @@ -399,15 +399,9 @@ def __init__(self, **kwargs: Any) -> None: def generate(self) -> None: super().generate() - self._export_ios_inner(self.model) - self.assign( - self.model.pin_assigns, - self._make_model_pinning(Esp32c3_Wroom02_Footprint._PIN_REMAPPING, self.get(self.pin_assigns)), - ) + self.device = self.Block(Esp32c3_Wroom02_Footprint(pin_assigns=ArrayStringExpr()), external=True) + self._wrap_inner_model_device(self.model, self.device, Esp32c3_Wroom02_Footprint._PIN_REMAPPING) - self.device = self.Block(Esp32c3_Wroom02_Footprint(pin_assigns=self.model.actual_pin_assigns), external=True) - self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns) - self._export_tap_ios_inner(self.device) self.export_tap(self.gnd, self.device.gnd) self.export_tap(self.v3v3, self.device.v3v3) self.export_tap(self.en, self.device.en) @@ -566,16 +560,13 @@ def generate(self) -> None: self.model = self.Block( Esp32c3_Device( - pin_assigns=self._make_model_pinning(Xiao_Esp32c3_Device._PIN_REMAPPING, self.get(self.pin_assigns)), + pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=list(Xiao_Esp32c3_Device._PIN_REMAPPING.keys()), ) ) - self._export_ios_inner(self.model) - - self.device = self.Block(Xiao_Esp32c3_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.device = self.Block(Xiao_Esp32c3_Device(pin_assigns=ArrayStringExpr()), external=True) + self._wrap_inner_model_device(self.model, self.device, Xiao_Esp32c3_Device._PIN_REMAPPING) if self.get(self.gnd.is_connected()): self.connect(self.gnd, self.model.gnd) diff --git a/edg/parts/microcontroller/Esp32s3.py b/edg/parts/microcontroller/Esp32s3.py index 80d50ce71..03190be17 100644 --- a/edg/parts/microcontroller/Esp32s3.py +++ b/edg/parts/microcontroller/Esp32s3.py @@ -374,20 +374,13 @@ def generate(self) -> None: self.model = self.Block( Esp32s3_Wroom_1_Device( - pin_assigns=self._make_model_pinning( - Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING, self.get(self.pin_assigns) - ), + pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=list(Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING.keys()), ) ) - self._export_ios_inner(self.model) - - self.device = self.Block( - Freenove_Esp32s3_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.device = self.Block(Freenove_Esp32s3_Wrover_Device(pin_assigns=ArrayStringExpr()), external=True) + self._wrap_inner_model_device(self.model, self.device, Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING) if self.get(self.gnd.is_connected()): self.connect(self.gnd, self.model.gnd) diff --git a/edg/parts/microcontroller/Rp2040.py b/edg/parts/microcontroller/Rp2040.py index 9503a5337..f08900833 100644 --- a/edg/parts/microcontroller/Rp2040.py +++ b/edg/parts/microcontroller/Rp2040.py @@ -466,16 +466,13 @@ def generate(self) -> None: self.model = self.Block( Rp2040_Device( - pin_assigns=self._make_model_pinning(Xiao_Rp2040_Device._PIN_REMAPPING, self.get(self.pin_assigns)), + pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=list(Xiao_Rp2040_Device._PIN_REMAPPING.keys()), ) ) - self._export_ios_inner(self.model) - - self.device = self.Block(Xiao_Rp2040_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.device = self.Block(Xiao_Rp2040_Device(pin_assigns=ArrayStringExpr()), external=True) + self._wrap_inner_model_device(self.model, self.device, Xiao_Rp2040_Device._PIN_REMAPPING) if self.get(self.gnd.is_connected()): self.connect(self.gnd, self.model.gnd) diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index 2622ac5e0..6267131dc 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -685,17 +685,12 @@ def generate(self) -> None: self.model = self.Block( Mdbt50q_1mv2_Device( - pin_assigns=self._make_model_pinning( - Feather_Nrf52840_Device._PIN_REMAPPING, self.get(self.pin_assigns) - ), + pin_assigns=ArrayStringExpr(), _allowed_pins=list(Feather_Nrf52840_Device._PIN_REMAPPING.keys()), ) ) - self._export_ios_inner(self.model) - - self.device = self.Block(Feather_Nrf52840_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.device = self.Block(Feather_Nrf52840_Device(pin_assigns=ArrayStringExpr()), external=True) + self._wrap_inner_model_device(self.model, self.device, Feather_Nrf52840_Device._PIN_REMAPPING) if self.get(self.gnd.is_connected()): self.connect(self.gnd, self.model.gnd) From 6278c6b78f4418177ab45c98c680e2adf7b82fe1 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 22:58:20 -0700 Subject: [PATCH 05/16] lol --- edg/abstract_parts/IoControllerWrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index ce65b60a4..351d58b7c 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -253,6 +253,6 @@ def _wrap_inner_model_device( self._export_ios_inner(self.model) self.assign(self.io_current_draw, self.model.io_current_draw) - self.assgin(self.device.pin_assigns, self.model.actual_pin_assigns) + self.assign(self.device.pin_assigns, self.model.actual_pin_assigns) self._export_tap_ios_inner(self.device) self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns) From 44b8fa621b689580934ba1a279ac68e2bf782d50 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:24:58 -0700 Subject: [PATCH 06/16] wip --- .../IoControllerInterfaceMixins.py | 37 +++++++++++++++- edg/electronics_interfaces/GroundDummy.py | 2 +- edg/electronics_interfaces/VoltageDummy.py | 4 +- edg/parts/microcontroller/Esp32.py | 43 ++++++------------- edg/parts/microcontroller/Esp32c3.py | 34 ++++++--------- edg/parts/microcontroller/Esp32s3.py | 34 ++++++--------- edg/parts/microcontroller/Rp2040.py | 36 ++++++---------- edg/parts/microcontroller/nRF52840.py | 36 +++++----------- edg/parts/microcontroller/test_mcu_wrapper.py | 11 +++++ 9 files changed, 110 insertions(+), 127 deletions(-) diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index 40e464b63..12fabd497 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Union from typing_extensions import override @@ -133,6 +133,41 @@ def contents(self) -> None: "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.""" diff --git a/edg/electronics_interfaces/GroundDummy.py b/edg/electronics_interfaces/GroundDummy.py index 746fc5ac2..e55e68736 100644 --- a/edg/electronics_interfaces/GroundDummy.py +++ b/edg/electronics_interfaces/GroundDummy.py @@ -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": diff --git a/edg/electronics_interfaces/VoltageDummy.py b/edg/electronics_interfaces/VoltageDummy.py index 61c39a4b8..3bead0901 100644 --- a/edg/electronics_interfaces/VoltageDummy.py +++ b/edg/electronics_interfaces/VoltageDummy.py @@ -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, @@ -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, diff --git a/edg/parts/microcontroller/Esp32.py b/edg/parts/microcontroller/Esp32.py index 1aaee3090..b0dc2b403 100644 --- a/edg/parts/microcontroller/Esp32.py +++ b/edg/parts/microcontroller/Esp32.py @@ -396,39 +396,24 @@ def generate(self) -> None: 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) - ), + pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=list(Freenove_Esp32_Wrover_Device._PIN_REMAPPING.keys()), ) ) - 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.vusb) diff --git a/edg/parts/microcontroller/Esp32c3.py b/edg/parts/microcontroller/Esp32c3.py index 22f8acc1c..c0e82f194 100644 --- a/edg/parts/microcontroller/Esp32c3.py +++ b/edg/parts/microcontroller/Esp32c3.py @@ -568,29 +568,19 @@ def generate(self) -> None: self.device = self.Block(Xiao_Esp32c3_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Xiao_Esp32c3_Device._PIN_REMAPPING) - 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) + self.connect(self._generate_gnd_node(), self.model.gnd) + self.export_tap(self.gnd, self.device.gnd) - self.connect( + model_pwr = self.connect( self.model.vdda, self.model.vdd3p3, self.model.vdd3p3_rtc, self.model.vdd3p3_cpu, self.model.vdd_spi ) - if self.get(self.pwr.is_connected()): # power supplied externally - self.connect(self.pwr, self.model.vdd3p3) - 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.vdd3p3) - 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 + ), + 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.vusb) + self.export_tap(self.vusb_out.net, self.device.vusb) diff --git a/edg/parts/microcontroller/Esp32s3.py b/edg/parts/microcontroller/Esp32s3.py index 03190be17..830712442 100644 --- a/edg/parts/microcontroller/Esp32s3.py +++ b/edg/parts/microcontroller/Esp32s3.py @@ -382,26 +382,16 @@ def generate(self) -> None: self.device = self.Block(Freenove_Esp32s3_Wrover_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING) - 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_gnd_node(), self.model.gnd) + self.export_tap(self.gnd, self.device.gnd) + + 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.vusb) + self.export_tap(self.vusb_out.net, self.device.vusb) diff --git a/edg/parts/microcontroller/Rp2040.py b/edg/parts/microcontroller/Rp2040.py index f08900833..de749052e 100644 --- a/edg/parts/microcontroller/Rp2040.py +++ b/edg/parts/microcontroller/Rp2040.py @@ -474,30 +474,18 @@ def generate(self) -> None: self.device = self.Block(Xiao_Rp2040_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Xiao_Rp2040_Device._PIN_REMAPPING) - 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) + self.connect(self._generate_gnd_node(), self.model.gnd) + self.export_tap(self.gnd, self.device.gnd) self.connect(self.model.vreg_vout, self.model.dvdd) model_pwr = self.connect(self.model.iovdd, self.model.vreg_vin, self.model.adc_avdd, self.model.usb_vdd) - if self.get(self.pwr.is_connected()): # power supplied externally - self.connect(self.pwr, 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, 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) - - if self.get(self.pwr_vin.is_connected()): - self.export_tap(self.pwr_vin.net, self.device.vcc) - if self.get(self.vusb_out.is_connected()): - self.export_tap(self.vusb_out.net, self.device.vcc) + self.connect( + self._generate_pwr_node( + voltage_out=3.3 * Volt(tol=0.05), + current_limits=UsbConnector.USB2_CURRENT_LIMITS, # tolerance is a guess + ), + model_pwr, + ) + self.export_tap((self.pwr if self.get(self.pwr.is_connected()) else self.pwr_out).net, self.device.v3v3) + + self.export_tap((self.pwr_vin if self.get(self.pwr_vin.is_connected()) else self.vusb_out).net, self.device.vcc) diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index 6267131dc..2de47c22a 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -692,29 +692,13 @@ def generate(self) -> None: self.device = self.Block(Feather_Nrf52840_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Feather_Nrf52840_Device._PIN_REMAPPING) - 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.pwr) - else: # board sources power from USB - self.pwr_out_model = self.Block( - DummyVoltageSource(voltage_out=self._AP2112_3V3_OUT, 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.pwr) - - if self.get(self.vusb_out.is_connected()): - self.vusb_out.init_from( - VoltageSource( - voltage_out=UsbConnector.USB2_VOLTAGE_RANGE - self._MBR120_DROP, - current_limits=UsbConnector.USB2_CURRENT_LIMITS, - ) - ) - self.export_tap(self.vusb_out.net, self.device.vusb) + self.connect(self._generate_gnd_node(), self.model.gnd) + self.export_tap(self.gnd, self.device.gnd) + + self.connect( + self._generate_pwr_node(voltage_out=self._AP2112_3V3_OUT, current_limits=UsbConnector.USB2_CURRENT_LIMITS), + self.model.pwr, + ) + self.export_tap((self.pwr if self.get(self.pwr.is_connected()) else self.pwr_out).net, self.device.pwr) + + self.export_tap(self.vusb_out.net, self.device.vusb) diff --git a/edg/parts/microcontroller/test_mcu_wrapper.py b/edg/parts/microcontroller/test_mcu_wrapper.py index 59860f72b..4891d4d52 100644 --- a/edg/parts/microcontroller/test_mcu_wrapper.py +++ b/edg/parts/microcontroller/test_mcu_wrapper.py @@ -41,6 +41,14 @@ def __init__(self) -> None: self.ios[i] = self.Block(DummyDigitalSource()).connected(self.dut.gpio.request(str(i))) +class UnpoweredMcuTest(DesignTop): + # tests a wrapper without ground or power connections + def __init__(self) -> None: + super().__init__() + self.dut = self.Block(Xiao_Esp32c3()) + self.io = self.Block(DummyDigitalSource()).connected(self.dut.gpio.request()) + + class BaseMcuTest(DesignTop): def __init__(self) -> None: super().__init__() @@ -105,6 +113,9 @@ def test_overallocate(self) -> None: with self.assertRaises(CompilerCheckError): ScalaCompiler.compile(OverallocateTest) + def test_unpowered_mcu(self) -> None: + ScalaCompiler.compile(UnpoweredMcuTest) + def test_auto_pins(self) -> None: ScalaCompiler.compile(FullMcuTest) # check that it compiles without error From 2dbabc724d0f9feca4777257997a3e18ccd7fc58 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:34:11 -0700 Subject: [PATCH 07/16] wip netlists --- examples/BasicKeyboard/BasicKeyboard.net.ref | 8 ++++---- examples/BasicKeyboard/BasicKeyboard.svgpcb.js | 4 ++-- examples/TestBlinkyBasic/TestBlinkyBasic.net.ref | 2 +- examples/TestBlinkyBasic/TestBlinkyBasic.svgpcb.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/BasicKeyboard/BasicKeyboard.net.ref b/examples/BasicKeyboard/BasicKeyboard.net.ref index 8592cbd83..eaf128f3f 100644 --- a/examples/BasicKeyboard/BasicKeyboard.net.ref +++ b/examples/BasicKeyboard/BasicKeyboard.net.ref @@ -179,11 +179,11 @@ (node (ref U1) (pin 10)) (node (ref D3) (pin 2)) (node (ref D6) (pin 2))) -(net (code 6) (name "mcu.pwr_out") - (node (ref U1) (pin 12))) -(net (code 7) (name "mcu.device.gnd") +(net (code 6) (name "mcu.gnd") (node (ref U1) (pin 13))) -(net (code 8) (name "mcu.device.vcc") +(net (code 7) (name "mcu.pwr_out") + (node (ref U1) (pin 12))) +(net (code 8) (name "mcu.vusb_out") (node (ref U1) (pin 14))) (net (code 9) (name "sw.sw[0,0].sw") (node (ref SW1) (pin 1)) diff --git a/examples/BasicKeyboard/BasicKeyboard.svgpcb.js b/examples/BasicKeyboard/BasicKeyboard.svgpcb.js index ffd2be7cd..4505f0510 100644 --- a/examples/BasicKeyboard/BasicKeyboard.svgpcb.js +++ b/examples/BasicKeyboard/BasicKeyboard.svgpcb.js @@ -13,9 +13,9 @@ board.setNetlist([ {name: "mcu.gpio.1_0", pads: [["U1", "9"], ["D1", "2"], ["D4", "2"]]}, {name: "mcu.gpio.1_1", pads: [["U1", "11"], ["D2", "2"], ["D5", "2"]]}, {name: "mcu.gpio.1_2", pads: [["U1", "10"], ["D3", "2"], ["D6", "2"]]}, + {name: "mcu.gnd", pads: [["U1", "13"]]}, {name: "mcu.pwr_out", pads: [["U1", "12"]]}, - {name: "mcu.device.gnd", pads: [["U1", "13"]]}, - {name: "mcu.device.vcc", pads: [["U1", "14"]]}, + {name: "mcu.vusb_out", pads: [["U1", "14"]]}, {name: "sw.sw[0,0].sw", pads: [["SW1", "1"], ["D1", "1"]]}, {name: "sw.sw[0,1].sw", pads: [["SW2", "1"], ["D2", "1"]]}, {name: "sw.sw[0,2].sw", pads: [["SW3", "1"], ["D3", "1"]]}, diff --git a/examples/TestBlinkyBasic/TestBlinkyBasic.net.ref b/examples/TestBlinkyBasic/TestBlinkyBasic.net.ref index 27af5ce41..32b62551b 100644 --- a/examples/TestBlinkyBasic/TestBlinkyBasic.net.ref +++ b/examples/TestBlinkyBasic/TestBlinkyBasic.net.ref @@ -42,7 +42,7 @@ (node (ref R1) (pin 2))) (net (code 2) (name "mcu.pwr_out") (node (ref U1) (pin 12))) -(net (code 3) (name "mcu.device.vcc") +(net (code 3) (name "mcu.vusb_out") (node (ref U1) (pin 14))) (net (code 4) (name "led.signal") (node (ref U1) (pin 7)) diff --git a/examples/TestBlinkyBasic/TestBlinkyBasic.svgpcb.js b/examples/TestBlinkyBasic/TestBlinkyBasic.svgpcb.js index ca7adcd27..9981014ec 100644 --- a/examples/TestBlinkyBasic/TestBlinkyBasic.svgpcb.js +++ b/examples/TestBlinkyBasic/TestBlinkyBasic.svgpcb.js @@ -19,7 +19,7 @@ const R1 = board.add(R_0603_1608Metric, { board.setNetlist([ {name: "mcu.gnd", pads: [["U1", "13"], ["R1", "2"]]}, {name: "mcu.pwr_out", pads: [["U1", "12"]]}, - {name: "mcu.device.vcc", pads: [["U1", "14"]]}, + {name: "mcu.vusb_out", pads: [["U1", "14"]]}, {name: "led.signal", pads: [["U1", "7"], ["D1", "2"]]}, {name: "led.package.k", pads: [["D1", "1"], ["R1", "1"]]} ]) From f8b8dd503ca6515075bc666e4d04c8b4b8818922 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:36:17 -0700 Subject: [PATCH 08/16] add back in vusb out --- edg/parts/microcontroller/nRF52840.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index 2de47c22a..cb33ac170 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -701,4 +701,10 @@ def generate(self) -> None: ) self.export_tap((self.pwr if self.get(self.pwr.is_connected()) else self.pwr_out).net, self.device.pwr) + self.vusb_out.init_from( + VoltageSource( + voltage_out=UsbConnector.USB2_VOLTAGE_RANGE - self._MBR120_DROP, + current_limits=UsbConnector.USB2_CURRENT_LIMITS, + ) + ) self.export_tap(self.vusb_out.net, self.device.vusb) From 2c8b20be3e68e4ea33728778a96b4edef80cf416 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:39:42 -0700 Subject: [PATCH 09/16] Update IoControllerInterfaceMixins.py --- edg/abstract_parts/IoControllerInterfaceMixins.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index 12fabd497..7bc42a6bb 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -124,8 +124,7 @@ def contents(self) -> None: super().contents() if isinstance(self, IoController): self.require( - self.pwr.is_connected().implies(~self.pwr_out.is_connected()) - & self.pwr_out.is_connected().implies(~self.pwr.is_connected()), + self.pwr_out.is_connected().implies(~self.pwr.is_connected()), "can only connect one of pwr and pwr_out (same physical pin)", ) self.require( @@ -212,12 +211,12 @@ def contents(self) -> None: "gnd must be connected if pwr_vin connected", ) self.require( - self.vusb_out.is_connected().implies(~self.pwr.is_connected()), + self.pwr_vin.is_connected().implies(~self.pwr.is_connected()), "can't sink logic-level pwr if powered from external vin", ) if isinstance(self, IoControllerUsbOut): self.require( - self.pwr_vin.is_connected().implies(~self.vusb_out.is_connected()), + self.vusb_out.is_connected().implies(~self.pwr_vin.is_connected()), "can only connect one of pwr_vin and vusb_out (same physical pin)", ) From e2b5e61333f25c1ddaed5bb0d314da6d6e766fe0 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:41:13 -0700 Subject: [PATCH 10/16] Update IoControllerInterfaceMixins.py --- .../IoControllerInterfaceMixins.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/edg/abstract_parts/IoControllerInterfaceMixins.py b/edg/abstract_parts/IoControllerInterfaceMixins.py index 7bc42a6bb..0ad1f4031 100644 --- a/edg/abstract_parts/IoControllerInterfaceMixins.py +++ b/edg/abstract_parts/IoControllerInterfaceMixins.py @@ -169,7 +169,10 @@ def _generate_pwr_node( 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) @@ -190,11 +193,15 @@ def contents(self) -> None: "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. - If also used with IoControllerUsbOut, it is assumed that pwr_vin is the same physical pin as vusb_out. - TODO: support devices with separate Vin and Vbus""" + """IO controller mixin that provides a >=5v input to the device, typically upstream of the Vbus-to-3.3 regulator.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -214,9 +221,3 @@ def contents(self) -> None: self.pwr_vin.is_connected().implies(~self.pwr.is_connected()), "can't sink logic-level pwr if powered from external vin", ) - - if isinstance(self, IoControllerUsbOut): - 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)", - ) From e0f1fa1eabf1e15ca122f3c7044c2affeb91a8a6 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:46:36 -0700 Subject: [PATCH 11/16] Update IoControllerWrapper.py --- edg/abstract_parts/IoControllerWrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index 351d58b7c..42524b444 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -1,4 +1,5 @@ -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 From b56a96a20b33624f2c3d5c0d4c4f3c5d5e1f512c Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:53:09 -0700 Subject: [PATCH 12/16] Update Esp32.py --- edg/parts/microcontroller/Esp32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edg/parts/microcontroller/Esp32.py b/edg/parts/microcontroller/Esp32.py index b0dc2b403..8e1eaa6c4 100644 --- a/edg/parts/microcontroller/Esp32.py +++ b/edg/parts/microcontroller/Esp32.py @@ -416,4 +416,4 @@ def generate(self) -> None: ) self.export_tap((self.pwr if self.get(self.pwr.is_connected()) else self.pwr_out).net, self.device.v3v3) - self.export_tap(self.vusb_out.net, self.device.vusb) + self.export_tap(self.vusb_out.net, self.device.vcc) From fe5d61f79921437a19d9bf3895813635bbe819e7 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Mon, 1 Jun 2026 23:54:36 -0700 Subject: [PATCH 13/16] Update IoControllerWrapper.py --- edg/abstract_parts/IoControllerWrapper.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index 42524b444..6ca1752ca 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -250,10 +250,10 @@ def _wrap_inner_model_device( Requires _generator_param_all_ios, so all the IOs names are available, and pin_assigns to be a generator param. """ - self.assign(self.model.pin_assigns, self._make_model_pinning(remapping, self.get(self.pin_assigns))) - self._export_ios_inner(self.model) - self.assign(self.io_current_draw, self.model.io_current_draw) + self.assign(model.pin_assigns, self._make_model_pinning(remapping, self.get(self.pin_assigns))) + self._export_ios_inner(model) + self.assign(self.io_current_draw, model.io_current_draw) - self.assign(self.device.pin_assigns, self.model.actual_pin_assigns) - self._export_tap_ios_inner(self.device) - self.assign(self.actual_pin_assigns, self.device.actual_pin_assigns) + 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) From 156d47b40f75dfb5a2b51fc797922484c294f3ba Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Tue, 2 Jun 2026 00:12:15 -0700 Subject: [PATCH 14/16] add modelable base class --- edg/abstract_parts/IoControllerWrapper.py | 19 +++++++++++++- edg/abstract_parts/__init__.py | 2 +- edg/parts/microcontroller/Esp32.py | 20 +++++++-------- edg/parts/microcontroller/Esp32c3.py | 30 +++++++++++------------ edg/parts/microcontroller/Esp32s3.py | 20 +++++++-------- edg/parts/microcontroller/Rp2040.py | 27 ++++++++++---------- edg/parts/microcontroller/nRF52840.py | 18 +++++++------- 7 files changed, 74 insertions(+), 62 deletions(-) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index 6ca1752ca..aca03de68 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -5,6 +5,21 @@ 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. @@ -237,12 +252,13 @@ def _wrap_inner(self, *args: Any, **kwargs: Any) -> None: raise NotImplementedError # use _wrap_inner_model_device instead def _wrap_inner_model_device( - self, model: BaseIoController, device: BaseIoControllerWrapped, remapping: Dict[str, str] + 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 @@ -251,6 +267,7 @@ def _wrap_inner_model_device( Requires _generator_param_all_ios, so all the IOs names are available, and pin_assigns to be a generator param. """ 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) diff --git a/edg/abstract_parts/__init__.py b/edg/abstract_parts/__init__.py index 59f6f007c..9448e81f1 100644 --- a/edg/abstract_parts/__init__.py +++ b/edg/abstract_parts/__init__.py @@ -129,7 +129,7 @@ from .IoController import BaseIoController, IoController, IoControllerPowerRequired, BaseIoControllerPinmapGenerator from .IoControllerExportable import BaseIoControllerExportable -from .IoControllerWrapper import BaseIoControllerWrapped, BaseIoControllerWrapper +from .IoControllerWrapper import BaseIoControllerModelable, BaseIoControllerWrapped, BaseIoControllerWrapper from .IoControllerInterfaceMixins import ( IoControllerSpiPeripheral, IoControllerI2cTarget, diff --git a/edg/parts/microcontroller/Esp32.py b/edg/parts/microcontroller/Esp32.py index 8e1eaa6c4..518401468 100644 --- a/edg/parts/microcontroller/Esp32.py +++ b/edg/parts/microcontroller/Esp32.py @@ -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 @@ -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 @@ -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( @@ -395,11 +397,7 @@ def generate(self) -> None: super().generate() self.model = self.Block( - Esp32_Wroom_32_Device( - pin_assigns=ArrayStringExpr(), - _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.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) diff --git a/edg/parts/microcontroller/Esp32c3.py b/edg/parts/microcontroller/Esp32c3.py index c0e82f194..da44a6269 100644 --- a/edg/parts/microcontroller/Esp32c3.py +++ b/edg/parts/microcontroller/Esp32c3.py @@ -20,7 +20,14 @@ class Esp32c3_Interfaces( """Defines base interfaces for ESP32C3 microcontrollers""" -class Esp32c3_Device(Esp32c3_Interfaces, BaseIoControllerPinmapGenerator, InternalSubcircuit, FootprintBlock, JlcPart): +class Esp32c3_Device( + Esp32c3_Interfaces, + BaseIoControllerModelable, + BaseIoControllerPinmapGenerator, + InternalSubcircuit, + FootprintBlock, + JlcPart, +): RESOURCE_PIN_REMAP = { "GPIO0": "4", @@ -55,12 +62,9 @@ def _system_pinmap(self) -> Mapping[Union[Iterable[str], str], Union[Passive, Ha "29": self.xtal.xtal_out, } - 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.gnd = self.Port(Ground(), [Common]) self.vdda = self.Port( # models total current draw VoltageSink( @@ -93,7 +97,7 @@ def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [] # 10ppm requirement from ESP32-C3-WROOM schematic, and in ESP32 hardware design guidelines self.xtal = self.Port( # vdda domain assumed CrystalDriver(frequency_limits=40 * MHertz(tol=10e-6), voltage_out=self.vdda.link().voltage), - optional=_model, + optional=self._model, ) # section 2.4: strapping IOs that need a fixed value to boot, and currently can't be allocated as GPIO @@ -107,9 +111,9 @@ def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [] pullup_capable=True, pulldown_capable=True, ) - self.en = self.Port(DigitalSink.from_bidir(self._dio_model), optional=_model) # needs external pullup - self.io2 = self.Port(self._dio_model, optional=_model) # needs external pullup; affects IO glitching on boot - self.io8 = self.Port(self._dio_model, optional=_model) # needs external pullup, required for download boot + self.en = self.Port(DigitalSink.from_bidir(self._dio_model), optional=self._model) # needs external pullup + self.io2 = self.Port(self._dio_model, optional=self._model) # needs external pullup; affects boot IO glitching + self.io8 = self.Port(self._dio_model, optional=self._model) # needs external pullup, required for download boot self.io9 = self.Port( self._dio_model, optional=True ) # internally pulled up for SPI boot, connect to GND for download @@ -117,7 +121,7 @@ def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [] # similarly, the programming UART is fixed and allocated separately self.uart0 = self.Port(UartPort(self._dio_model), optional=True) - self.lna_in = self.Port(Passive(), optional=_model) + self.lna_in = self.Port(Passive(), optional=self._model) @override def generate(self) -> None: @@ -559,11 +563,7 @@ def generate(self) -> None: super().generate() self.model = self.Block( - Esp32c3_Device( - pin_assigns=ArrayStringExpr(), - _model=True, - _allowed_pins=list(Xiao_Esp32c3_Device._PIN_REMAPPING.keys()), - ) + Esp32c3_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr()) ) self.device = self.Block(Xiao_Esp32c3_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Xiao_Esp32c3_Device._PIN_REMAPPING) diff --git a/edg/parts/microcontroller/Esp32s3.py b/edg/parts/microcontroller/Esp32s3.py index 830712442..a3d69b530 100644 --- a/edg/parts/microcontroller/Esp32s3.py +++ b/edg/parts/microcontroller/Esp32s3.py @@ -24,7 +24,12 @@ class Esp32s3_Interfaces( class Esp32s3_Wroom_1_Device( - Esp32s3_Interfaces, BaseIoControllerPinmapGenerator, InternalSubcircuit, FootprintBlock, JlcPart + Esp32s3_Interfaces, + BaseIoControllerModelable, + BaseIoControllerPinmapGenerator, + InternalSubcircuit, + FootprintBlock, + JlcPart, ): _PIN_MAPPING = { "GPIO4": "4", @@ -62,12 +67,9 @@ class Esp32s3_Wroom_1_Device( "GPIO1": "39", } - 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( # assumes single-rail module voltage_limits=(3.0, 3.6) * Volt, # table 4-2 @@ -88,7 +90,7 @@ def __init__(self, _model: BoolLike = False, _allowed_pins: ArrayStringLike = [] pulldown_capable=True, ) - self.chip_pu = self.Port(self._dio_model, optional=_model) + self.chip_pu = self.Port(self._dio_model, optional=self._model) self.io0 = self.Port( self._dio_model, optional=True ) # table 2-11, default pullup (SPI boot), set low to download boot @@ -373,11 +375,7 @@ def generate(self) -> None: super().generate() self.model = self.Block( - Esp32s3_Wroom_1_Device( - pin_assigns=ArrayStringExpr(), - _model=True, - _allowed_pins=list(Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING.keys()), - ) + Esp32s3_Wroom_1_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr()) ) self.device = self.Block(Freenove_Esp32s3_Wrover_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Freenove_Esp32s3_Wrover_Device._PIN_REMAPPING) diff --git a/edg/parts/microcontroller/Rp2040.py b/edg/parts/microcontroller/Rp2040.py index de749052e..c99b0239d 100644 --- a/edg/parts/microcontroller/Rp2040.py +++ b/edg/parts/microcontroller/Rp2040.py @@ -12,7 +12,13 @@ class Rp2040_Interfaces(IoControllerI2cTarget, IoControllerUsb, BaseIoController class Rp2040_Device( - Rp2040_Interfaces, BaseIoControllerPinmapGenerator, InternalSubcircuit, GeneratorBlock, JlcPart, FootprintBlock + Rp2040_Interfaces, + BaseIoControllerModelable, + BaseIoControllerPinmapGenerator, + InternalSubcircuit, + GeneratorBlock, + JlcPart, + FootprintBlock, ): _PIN_MAPPING = { "GPIO0": "2", @@ -51,12 +57,9 @@ class Rp2040_Device( "SWCLK": "24", } - 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.gnd = self.Port(Ground(), [Common]) self.iovdd = self.Port( VoltageSink( @@ -110,10 +113,10 @@ def __init__(self, *, _model: BoolLike = False, _allowed_pins: ArrayStringLike = pulldown_capable=True, ) - self.qspi = self.Port(SpiController(self._dio_std_model), optional=_model) # TODO actually QSPI - self.qspi_cs = self.Port(self._dio_std_model, optional=_model) - self.qspi_sd2 = self.Port(self._dio_std_model, optional=_model) - self.qspi_sd3 = self.Port(self._dio_std_model, optional=_model) + self.qspi = self.Port(SpiController(self._dio_std_model), optional=self._model) # TODO actually QSPI + self.qspi_cs = self.Port(self._dio_std_model, optional=self._model) + self.qspi_sd2 = self.Port(self._dio_std_model, optional=self._model) + self.qspi_sd3 = self.Port(self._dio_std_model, optional=self._model) self.xosc = self.Port( CrystalDriver( @@ -465,11 +468,7 @@ def generate(self) -> None: ) self.model = self.Block( - Rp2040_Device( - pin_assigns=ArrayStringExpr(), - _model=True, - _allowed_pins=list(Xiao_Rp2040_Device._PIN_REMAPPING.keys()), - ) + Rp2040_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr()) ) self.device = self.Block(Xiao_Rp2040_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Xiao_Rp2040_Device._PIN_REMAPPING) diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index cb33ac170..34575f10a 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -15,7 +15,13 @@ class Nrf52840_Interfaces( class Mdbt50q_1mv2_Device( - Nrf52840_Interfaces, BaseIoControllerPinmapGenerator, InternalSubcircuit, JlcPart, GeneratorBlock, FootprintBlock + Nrf52840_Interfaces, + BaseIoControllerModelable, + BaseIoControllerPinmapGenerator, + InternalSubcircuit, + JlcPart, + GeneratorBlock, + FootprintBlock, ): # in the absence of a chip-level subcircuit, this is used as the authoritative base device model # that other modules should wrap @@ -74,12 +80,9 @@ class Mdbt50q_1mv2_Device( "P1.01": "61", } - def __init__(self, _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.gnd = self.Port(Ground(), [Common]) self.pwr = self.Port( VoltageSink( @@ -684,10 +687,7 @@ def generate(self) -> None: super().generate() self.model = self.Block( - Mdbt50q_1mv2_Device( - pin_assigns=ArrayStringExpr(), - _allowed_pins=list(Feather_Nrf52840_Device._PIN_REMAPPING.keys()), - ) + Mdbt50q_1mv2_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr()) ) self.device = self.Block(Feather_Nrf52840_Device(pin_assigns=ArrayStringExpr()), external=True) self._wrap_inner_model_device(self.model, self.device, Feather_Nrf52840_Device._PIN_REMAPPING) From 9390679a854482695f6bc1a141a04d431ee390f3 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Tue, 2 Jun 2026 00:18:20 -0700 Subject: [PATCH 15/16] Update Esp32c3.py --- edg/parts/microcontroller/Esp32c3.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/edg/parts/microcontroller/Esp32c3.py b/edg/parts/microcontroller/Esp32c3.py index da44a6269..d74efa553 100644 --- a/edg/parts/microcontroller/Esp32c3.py +++ b/edg/parts/microcontroller/Esp32c3.py @@ -382,11 +382,7 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.model = self.Block( - Esp32c3_Device( - pin_assigns=ArrayStringExpr(), - _model=True, - _allowed_pins=list(Esp32c3_Wroom02_Footprint._PIN_REMAPPING.keys()), - ) + Esp32c3_Device(pin_assigns=ArrayStringExpr(), _model=True, _allowed_pins=ArrayStringExpr()) ) self.gnd = self.Export(self.model.gnd, [Common]) self.v3v3 = self.Export(self.model.vdd3p3, [Power]) From 14823c302553e1a56fa35a508356894c0b9c3033 Mon Sep 17 00:00:00 2001 From: Richard Lin Date: Tue, 2 Jun 2026 00:26:38 -0700 Subject: [PATCH 16/16] Update IoControllerWrapper.py --- edg/abstract_parts/IoControllerWrapper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edg/abstract_parts/IoControllerWrapper.py b/edg/abstract_parts/IoControllerWrapper.py index aca03de68..f9d2b3ff5 100644 --- a/edg/abstract_parts/IoControllerWrapper.py +++ b/edg/abstract_parts/IoControllerWrapper.py @@ -266,6 +266,8 @@ def _wrap_inner_model_device( 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)