diff --git a/edg/core/Blocks.py b/edg/core/Blocks.py index 71e7fcfaa..d1a7420ca 100644 --- a/edg/core/Blocks.py +++ b/edg/core/Blocks.py @@ -492,12 +492,12 @@ def check_subexpr(expr: Union[ConstraintExpr, BasePort]) -> None: # TODO rewrit for subexpr in constraint._get_exprs(): check_subexpr(subexpr) - def require(self, constraint: BoolLike, name: Optional[str] = None, *, unchecked: bool = False) -> BoolExpr: + def require(self, constraint: BoolLike, name: Optional[str] = None, *, _unchecked: bool = False) -> BoolExpr: constraint_typed = BoolExpr._to_expr_type(constraint) if not isinstance(name, (str, type(None))): raise EdgTypeError(f"require(...) name", name, (str, None)) - if not unchecked: # before we have const prop need to manually set nested params + if not _unchecked: # allow breaking structural / abstraction rules, used in some unit tests self._check_constraint(constraint_typed) self._constraints.register(constraint_typed) diff --git a/edg/electronics_interfaces/CanPort.py b/edg/electronics_interfaces/CanPort.py index aa1d7feb2..dc523e521 100644 --- a/edg/electronics_interfaces/CanPort.py +++ b/edg/electronics_interfaces/CanPort.py @@ -16,13 +16,12 @@ def __init__(self) -> None: self.transceiver = self.Port(CanTransceiverPort(DigitalBidir.empty())) self.passive = self.Port(Vector(CanPassivePort(DigitalBidir.empty())), optional=True) - # TODO write custom top level digital constraints - # TODO model frequency ... somewhere + # 0-1 Mbit/s are standard CANbus, 1-8 Mbit/s imply CAN-FD + self.bitrate_limit = self.Parameter(RangeExpr()) @override def contents(self) -> None: super().contents() - # TODO future: digital constraints through link inference self.txd = self.connect( self.controller.txd, self.transceiver.txd, self.passive.map_extract(lambda port: port.txd), flatten=True @@ -31,13 +30,14 @@ def contents(self) -> None: self.controller.rxd, self.transceiver.rxd, self.passive.map_extract(lambda port: port.rxd), flatten=True ) + self.assign(self.bitrate_limit, self.controller.bitrate_limit.intersect(self.transceiver.bitrate_limit)) + self.require(self.bitrate_limit != RangeExpr.EMPTY, "no compatible bitrate between devices") + class CanControllerPort(Port[CanLogicLink]): link_type = CanLogicLink def __init__(self, model: Optional[DigitalBidir] = None, *, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: - # 0-1 Mbit/s are standard CANbus - # 1-8 Mbit/s imply CAN-FD super().__init__() if model is None: # ideal by default model = DigitalBidir() @@ -49,12 +49,13 @@ def __init__(self, model: Optional[DigitalBidir] = None, *, bitrate_limit: Range class CanTransceiverPort(Port[CanLogicLink]): link_type = CanLogicLink - def __init__(self, model: Optional[DigitalBidir] = None) -> None: + def __init__(self, model: Optional[DigitalBidir] = None, *, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: super().__init__() if model is None: # ideal by default model = DigitalBidir() self.txd = self.Port(DigitalSink.from_bidir(model)) self.rxd = self.Port(DigitalSource.from_bidir(model)) + self.bitrate_limit = self.Parameter(RangeExpr(bitrate_limit)) class CanPassivePort(Port[CanLogicLink]): @@ -75,8 +76,7 @@ def __init__(self) -> None: super().__init__() self.nodes = self.Port(Vector(CanDiffPort.empty())) # TODO mark as required - # TODO write custom top level digital constraints - # TODO future: digital constraints through link inference + self.bitrate_limit = self.Parameter(RangeExpr()) @override def contents(self) -> None: @@ -85,13 +85,16 @@ def contents(self) -> None: self.canh = self.connect(self.nodes.map_extract(lambda node: node.canh), flatten=True) self.canl = self.connect(self.nodes.map_extract(lambda node: node.canl), flatten=True) + self.assign(self.bitrate_limit, self.nodes.intersection(lambda x: x.bitrate_limit)) + self.require(self.bitrate_limit != RangeExpr.EMPTY, "no compatible bitrate between devices") + class CanDiffBridge(PortBridge): def __init__(self) -> None: super().__init__() self.outer_port = self.Port(CanDiffPort.empty()) - self.inner_link = self.Port(CanDiffPort.empty()) + self.inner_link = self.Port(CanDiffPort()) @override def contents(self) -> None: @@ -105,12 +108,16 @@ def contents(self) -> None: self.connect(self.outer_port.canl, self.canl_bridge.outer_port) self.connect(self.canl_bridge.inner_link, self.inner_link.canl) + self.assign(self.outer_port.bitrate_limit, self.inner_link.link().bitrate_limit) + class CanDiffPort(Port[CanDiffLink]): link_type = CanDiffLink bridge_type = CanDiffBridge - def __init__(self) -> None: + def __init__(self, *, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: super().__init__() self.canh = self.Port(Passive()) self.canl = self.Port(Passive()) + + self.bitrate_limit = self.Parameter(RangeExpr(bitrate_limit)) diff --git a/edg/electronics_interfaces/I2cPort.py b/edg/electronics_interfaces/I2cPort.py index 20a496ff5..bd0a29265 100644 --- a/edg/electronics_interfaces/I2cPort.py +++ b/edg/electronics_interfaces/I2cPort.py @@ -7,7 +7,7 @@ class I2cLink(Link): - """I2C connection, using terminology from the auhtoritative NXP specification at + """I2C connection, using terminology from the authoritative NXP specification at https://www.nxp.com/docs/en/user-guide/UM10204.pdf. """ @@ -25,7 +25,7 @@ def __init__(self) -> None: self.addresses = self.Parameter( ArrayIntExpr(self.targets.flatten(lambda x: x.addresses).concat(self.controller.addresses)) ) - self.frequency = self.Parameter(RangeExpr()) + self.frequency_limit = self.Parameter(RangeExpr()) self.has_pull = self.Parameter(BoolExpr(self.pull.any_connected())) @@ -38,9 +38,10 @@ def contents(self) -> None: self.require(self.addresses.all_unique(), "conflicting addresses on I2C bus") self.assign( - self.frequency, + self.frequency_limit, self.controller.frequency_limit.intersect(self.targets.intersection(lambda x: x.frequency_limit)), ) + self.require(self.frequency_limit != RangeExpr.EMPTY, "no compatible frequency between devices") self.scl = self.connect( self.pull.map_extract(lambda device: device.scl), @@ -82,6 +83,7 @@ def contents(self) -> None: DigitalBidir.empty(), has_pullup=self.inner_link.link().has_pull, addresses=self.inner_link.link().addresses, + frequency_limit=self.inner_link.link().frequency_limit, ) ) @@ -128,7 +130,13 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - self.outer_port.init_from(I2cTarget(DigitalBidir.empty(), addresses=self.inner_link.link().addresses)) + self.outer_port.init_from( + I2cTarget( + DigitalBidir.empty(), + addresses=self.inner_link.link().addresses, + frequency_limit=self.inner_link.link().frequency_limit, + ) + ) self.scl_bridge = self.Block(DigitalSinkBridge()) self.connect(self.outer_port.scl, self.scl_bridge.outer_port) diff --git a/edg/electronics_interfaces/I2sPort.py b/edg/electronics_interfaces/I2sPort.py index 6d3ff6221..7687c40c7 100644 --- a/edg/electronics_interfaces/I2sPort.py +++ b/edg/electronics_interfaces/I2sPort.py @@ -13,6 +13,9 @@ def __init__(self) -> None: self.target_receiver = self.Port(I2sTargetReceiver(DigitalSink.empty())) # TODO: multiple receivers, target transmitters, eg microphones + self.sample_rate_limit = self.Parameter(RangeExpr()) + self.bit_limit = self.Parameter(RangeExpr()) + @override def contents(self) -> None: super().contents() @@ -20,13 +23,26 @@ def contents(self) -> None: self.ws = self.connect(self.controller.ws, self.target_receiver.ws) self.sd = self.connect(self.controller.sd, self.target_receiver.sd) + self.assign( + self.sample_rate_limit, self.controller.sample_rate_limit.intersect(self.target_receiver.sample_rate_limit) + ) + self.require(self.sample_rate_limit != RangeExpr.EMPTY, "no compatible sample rate between devices") + self.assign(self.bit_limit, self.controller.bit_limit.intersect(self.target_receiver.bit_limit)) + self.require(self.bit_limit != RangeExpr.EMPTY, "no compatible sample bits (resolution) between devices") + class I2sController(Port[I2sLink]): """Controller is both controller (drives SCK and WS lines) and transmitter (SD is output)""" link_type = I2sLink - def __init__(self, model: Optional[DigitalBidir] = None, *, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: + def __init__( + self, + model: Optional[DigitalBidir] = None, + *, + sample_rate_limit: RangeLike = RangeExpr.ALL, + bit_limit: RangeLike = RangeExpr.ALL, + ) -> None: super().__init__() if model is None: model = DigitalBidir() # ideal by default @@ -34,7 +50,8 @@ def __init__(self, model: Optional[DigitalBidir] = None, *, bitrate_limit: Range self.ws = self.Port(DigitalSource.from_bidir(model)) self.sd = self.Port(model) # bidirectional - self.bitrate_limit = self.Parameter(RangeExpr(bitrate_limit)) # bitrate + self.sample_rate_limit = self.Parameter(RangeExpr(sample_rate_limit)) + self.bit_limit = self.Parameter(RangeExpr(bit_limit)) class I2sTargetReceiver(Port[I2sLink]): @@ -42,10 +59,19 @@ class I2sTargetReceiver(Port[I2sLink]): link_type = I2sLink - def __init__(self, model: Optional[DigitalSink] = None) -> None: + def __init__( + self, + model: Optional[DigitalSink] = None, + *, + sample_rate_limit: RangeLike = RangeExpr.ALL, + bit_limit: RangeLike = RangeExpr.ALL, + ) -> None: super().__init__() if model is None: model = DigitalSink() # ideal by default self.sck = self.Port(model) self.ws = self.Port(model) self.sd = self.Port(model) + + self.sample_rate_limit = self.Parameter(RangeExpr(sample_rate_limit)) + self.bit_limit = self.Parameter(RangeExpr(bit_limit)) diff --git a/edg/electronics_interfaces/MergedBlocks.py b/edg/electronics_interfaces/MergedBlocks.py index a621694bd..5c8aee1e0 100644 --- a/edg/electronics_interfaces/MergedBlocks.py +++ b/edg/electronics_interfaces/MergedBlocks.py @@ -3,7 +3,7 @@ from ..electronics_model import * from .VoltagePorts import VoltageSource, VoltageSink, VoltageLink -from .DigitalPorts import DigitalSource, DigitalSink, DigitalLink +from .DigitalPorts import DigitalSource, DigitalSink, DigitalLink, DigitalBidir from .AnalogPort import AnalogSource, AnalogSink, AnalogLink from .SpiPort import SpiController, SpiPeripheral, SpiLink @@ -125,11 +125,13 @@ def generate(self) -> None: self.ins.defined() for in_request in self.get(self.ins.requested()): - in_port = self.ins.append_elt(SpiPeripheral.empty(), in_request) + in_port = self.ins.append_elt(SpiPeripheral(DigitalBidir.empty()), in_request) self.connect(miso_net, in_port.miso) self.connect(self.sck_merge.ins.request(in_request), in_port.sck) self.connect(self.mosi_merge.ins.request(in_request), in_port.mosi) + self.assign(self.out.frequency_limit, self.ins.hull(lambda x: x.link().frequency_limit)) + def connected_from(self, *ins: Port[SpiLink]) -> "MergedSpiController": for in_port in ins: builder.block().connect(in_port, self.ins.request()) diff --git a/edg/electronics_interfaces/SpiPort.py b/edg/electronics_interfaces/SpiPort.py index 58017cf34..3c3a8d4f8 100644 --- a/edg/electronics_interfaces/SpiPort.py +++ b/edg/electronics_interfaces/SpiPort.py @@ -21,6 +21,8 @@ def __init__(self) -> None: self.controller = self.Port(SpiController(DigitalBidir.empty())) self.peripherals = self.Port(Vector(SpiPeripheral(DigitalBidir.empty()))) + self.frequency_limit = self.Parameter(RangeExpr()) + @override def contents(self) -> None: super().contents() @@ -33,6 +35,11 @@ def contents(self) -> None: self.mosi = self.connect( self.controller.mosi, self.peripherals.map_extract(lambda device: device.mosi), flatten=True ) + self.assign( + self.frequency_limit, + self.controller.frequency_limit.intersect(self.peripherals.hull(lambda x: x.frequency_limit)), + ) + self.require(self.frequency_limit != RangeExpr.EMPTY, "no compatible frequency between devices") class SpiController(Port[SpiLink]): diff --git a/edg/electronics_interfaces/UartPort.py b/edg/electronics_interfaces/UartPort.py index 826979fda..2af9efe8c 100644 --- a/edg/electronics_interfaces/UartPort.py +++ b/edg/electronics_interfaces/UartPort.py @@ -11,6 +11,8 @@ def __init__(self) -> None: self.a = self.Port(UartPort(DigitalBidir.empty())) self.b = self.Port(UartPort(DigitalBidir.empty())) + self.baud_limit = self.Parameter(RangeExpr()) + @override def contents(self) -> None: super().contents() @@ -18,6 +20,9 @@ def contents(self) -> None: self.a_tx = self.connect(self.a.tx, self.b.rx) self.b_tx = self.connect(self.b.tx, self.a.rx) + self.assign(self.baud_limit, self.a.baud_limit.intersect(self.b.baud_limit)) + self.require(self.baud_limit != RangeExpr.EMPTY, "no compatible baud rate between devices") + class UartPort(Port[UartLink]): link_type = UartLink diff --git a/edg/electronics_interfaces/test_can_link.py b/edg/electronics_interfaces/test_can_link.py new file mode 100644 index 000000000..4c5a8be35 --- /dev/null +++ b/edg/electronics_interfaces/test_can_link.py @@ -0,0 +1,149 @@ +import unittest + +from ..core.HdlUserExceptions import UnconnectableError +from ..electronics_model import * +from .DigitalPorts import DigitalBidir +from .CanPort import CanControllerPort, CanTransceiverPort, CanDiffPort + + +class CanControllerBlock(Block): + def __init__(self, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(CanControllerPort(DigitalBidir(), bitrate_limit=bitrate_limit)) + + +class CanTransceiverBlock(Block): + def __init__(self, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(CanTransceiverPort(DigitalBidir(), bitrate_limit=bitrate_limit)) + + +class CanLogicTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(CanControllerBlock()) + self.transceiver = self.Block(CanTransceiverBlock()) + self.connect(self.controller.port, self.transceiver.port) + + +class CanLogicOverconnectTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(CanControllerBlock()) + self.transceiver1 = self.Block(CanTransceiverBlock()) + self.transceiver2 = self.Block(CanTransceiverBlock()) + self.connect(self.controller.port, self.transceiver1.port, self.transceiver2.port) + + +class CanLogicFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(CanControllerBlock(bitrate_limit=(0, 1) * MHertz)) + self.transceiver = self.Block(CanTransceiverBlock(bitrate_limit=(0.5, 1) * MHertz)) + self.connect(self.controller.port, self.transceiver.port) + self.require(self.controller.port.link().bitrate_limit == (0.5, 1) * MHertz, _unchecked=True) + + +class CanLogicFrequencyInvalidTest(DesignTop): + def __init__(self) -> None: + super().__init__() + # this generally isn't plausible in practice + self.controller = self.Block(CanControllerBlock(bitrate_limit=(0, 0.25) * MHertz)) + self.transceiver = self.Block(CanTransceiverBlock(bitrate_limit=(0.5, 1) * MHertz)) + self.connect(self.controller.port, self.transceiver.port) + + +class CanDiffBlock(Block): + def __init__(self, bitrate_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(CanDiffPort(bitrate_limit=bitrate_limit)) + + +class CanNestedDiffBlock(Block): + def __init__( + self, node1_bitrate_limit: RangeLike = RangeExpr.ALL, node2_bitrate_limit: RangeLike = RangeExpr.ALL + ) -> None: + super().__init__() + self.port = self.Port(CanDiffPort.empty()) + self.node1 = self.Block(CanDiffBlock(node1_bitrate_limit)) + self.node2 = self.Block(CanDiffBlock(node2_bitrate_limit)) + self.connect(self.node1.port, self.node2.port, self.port) + + +class CanDiffTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.node1 = self.Block(CanDiffBlock()) + self.node2 = self.Block(CanDiffBlock()) + self.node3 = self.Block(CanDiffBlock()) + self.connect(self.node1.port, self.node2.port, self.node3.port) + + +class CanBadConnectTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(CanControllerBlock()) + self.diff = self.Block(CanDiffBlock()) + self.connect(self.controller.port, self.diff.port) + + +class CanDiffFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.node1 = self.Block(CanDiffBlock(bitrate_limit=(0, 1) * MHertz)) + self.node2 = self.Block(CanDiffBlock(bitrate_limit=(0.5, 1) * MHertz)) + self.node3 = self.Block(CanDiffBlock(bitrate_limit=(0.25, 0.5) * MHertz)) + self.connect(self.node1.port, self.node2.port, self.node3.port) + self.require(self.node1.port.link().bitrate_limit == (0.5, 0.5) * MHertz, _unchecked=True) + + +class CanDiffFrequencyInvalidTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.node1 = self.Block(CanDiffBlock(bitrate_limit=(0, 0.25) * MHertz)) + self.node2 = self.Block(CanDiffBlock(bitrate_limit=(0.5, 1) * MHertz)) + self.connect(self.node1.port, self.node2.port) + + +class CanDiffNestedFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.node1 = self.Block(CanDiffBlock(bitrate_limit=(0, 1) * MHertz)) + self.nodes = self.Block( + CanNestedDiffBlock(node1_bitrate_limit=(0.5, 1) * MHertz, node2_bitrate_limit=(0.25, 0.5) * MHertz) + ) + self.connect(self.node1.port, self.nodes.port) + self.require(self.node1.port.link().bitrate_limit == (0.5, 0.5) * MHertz, _unchecked=True) + + +class CanTestCase(unittest.TestCase): + def test_logic_link(self) -> None: + ScalaCompiler.compile(CanLogicTest) + + def test_logic_overconnect(self) -> None: + with self.assertRaises(UnconnectableError): + ScalaCompiler.compile(CanLogicOverconnectTest) + + def test_logic_frequency(self) -> None: + ScalaCompiler.compile(CanLogicFrequencyTest) + + def test_logic_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(CanLogicFrequencyInvalidTest) + + def test_diff_link(self) -> None: + ScalaCompiler.compile(CanDiffTest) + + def test_bad_connect(self) -> None: + with self.assertRaises(UnconnectableError): + ScalaCompiler.compile(CanBadConnectTest) + + def test_diff_frequency(self) -> None: + ScalaCompiler.compile(CanDiffFrequencyTest) + + def test_diff_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(CanDiffFrequencyInvalidTest) + + def test_diff_nested_frequency(self) -> None: + ScalaCompiler.compile(CanDiffNestedFrequencyTest) diff --git a/edg/electronics_interfaces/test_i2c_link.py b/edg/electronics_interfaces/test_i2c_link.py index cf30e60bb..7e6f7bbf1 100644 --- a/edg/electronics_interfaces/test_i2c_link.py +++ b/edg/electronics_interfaces/test_i2c_link.py @@ -6,9 +6,9 @@ class I2cControllerBlock(Block): - def __init__(self) -> None: + def __init__(self, frequency_limit: RangeLike = RangeExpr.ALL) -> None: super().__init__() - self.port = self.Port(I2cController()) + self.port = self.Port(I2cController(frequency_limit=frequency_limit)) class I2cPullupBlock(Block): @@ -18,9 +18,9 @@ def __init__(self) -> None: class I2cTargetBlock(Block): - def __init__(self, address: IntLike): + def __init__(self, address: IntLike, frequency_limit: RangeLike = RangeExpr.ALL) -> None: super().__init__() - self.port = self.Port(I2cTarget(DigitalBidir(), addresses=[address])) + self.port = self.Port(I2cTarget(DigitalBidir(), addresses=[address], frequency_limit=frequency_limit)) class I2cTest(DesignTop): @@ -32,7 +32,7 @@ def __init__(self) -> None: self.device2 = self.Block(I2cTargetBlock(2)) self.link = self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) - self.require(self.controller.port.link().addresses == [1, 2], unchecked=True) + self.require(self.controller.port.link().addresses == [1, 2], _unchecked=True) class I2cNoPullTest(DesignTop): @@ -64,7 +64,22 @@ def __init__(self) -> None: self.link = self.connect(self.port, self.controller.port, self.pull.port, self.device1.port, self.device2.port) -class I2cNestedTest(DesignTop): +class I2cNestedBlock(Block): + def __init__( + self, + dev1_addr: IntLike = 0, + dev1_freq_limit: RangeLike = RangeExpr.ALL, + dev2_addr: IntLike = 1, + dev2_freq_limit: RangeLike = RangeExpr.ALL, + ) -> None: + super().__init__() + self.port = self.Port(I2cTarget.empty()) + self.device1 = self.Block(I2cTargetBlock(dev1_addr, frequency_limit=dev1_freq_limit)) + self.device2 = self.Block(I2cTargetBlock(dev2_addr, frequency_limit=dev2_freq_limit)) + self.link = self.connect(self.port, self.device1.port, self.device2.port) + + +class I2cControllerNestedTest(DesignTop): def __init__(self) -> None: super().__init__() self.controller = self.Block(I2cControllerNestedBlock()) @@ -83,6 +98,59 @@ def __init__(self) -> None: self.link = self.connect(self.controller.port, self.pull.port, self.device.port) +class I2cNestedTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock()) + self.pull = self.Block(I2cPullupBlock()) + self.devices = self.Block(I2cNestedBlock(dev1_addr=1, dev2_addr=2)) + self.link = self.connect(self.controller.port, self.pull.port, self.devices.port) + self.require(self.controller.port.link().addresses == [1, 2], _unchecked=True) + + +class I2cNestedConflictTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock()) + self.pull = self.Block(I2cPullupBlock()) + self.devices = self.Block(I2cNestedBlock(dev1_addr=1, dev2_addr=1)) + self.link = self.connect(self.controller.port, self.pull.port, self.devices.port) + + +class I2cFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock(frequency_limit=(0, 400) * kHertz)) + self.pull = self.Block(I2cPullupBlock()) + self.device1 = self.Block(I2cTargetBlock(1, frequency_limit=(0, 100) * kHertz)) + self.device2 = self.Block(I2cTargetBlock(2, frequency_limit=(10, 400) * kHertz)) + self.connect(self.controller.port, self.pull.port, self.device1.port, self.device2.port) + # whole bus must run within all devices' limits + self.require(self.controller.port.link().frequency_limit == (10, 100) * kHertz, _unchecked=True) + + +class I2cFrequencyInvalidTest(DesignTop): + """Checks that non-overlapping frequencies are an error. + This generally doesn't happen in practice, since devices tend to support low speeds""" + + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock(frequency_limit=(400, 400) * kHertz)) + self.pull = self.Block(I2cPullupBlock()) + self.device = self.Block(I2cTargetBlock(1, frequency_limit=(100, 100) * kHertz)) + self.connect(self.controller.port, self.pull.port, self.device.port) + + +class I2cFrequencyNestedTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2cControllerBlock(frequency_limit=(0, 400) * kHertz)) + self.pull = self.Block(I2cPullupBlock()) + self.devices = self.Block(I2cNestedBlock(dev1_freq_limit=(0, 100) * kHertz, dev2_freq_limit=(10, 400) * kHertz)) + self.connect(self.controller.port, self.pull.port, self.devices.port) + self.require(self.controller.port.link().frequency_limit == (10, 100) * kHertz, _unchecked=True) + + class I2cTestCase(unittest.TestCase): def test_i2c(self) -> None: ScalaCompiler.compile(I2cTest) @@ -95,9 +163,26 @@ def test_i2c_conflict(self) -> None: with self.assertRaises(CompilerCheckError): ScalaCompiler.compile(I2cConflictTest) - def test_i2c_nested(self) -> None: - ScalaCompiler.compile(I2cNestedTest) + def test_i2c_nested_controller(self) -> None: + ScalaCompiler.compile(I2cControllerNestedTest) def test_i2c_nested_extrapull(self) -> None: with self.assertRaises(CompilerCheckError): ScalaCompiler.compile(I2cNestedExtraPullTest) + + def test_i2c_nested(self) -> None: + ScalaCompiler.compile(I2cNestedTest) + + def test_i2c_nested_conflict(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2cNestedConflictTest) + + def test_i2c_frequency(self) -> None: + ScalaCompiler.compile(I2cFrequencyTest) + + def test_i2c_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2cFrequencyInvalidTest) + + def test_i2c_frequency_nested(self) -> None: + ScalaCompiler.compile(I2cFrequencyNestedTest) diff --git a/edg/electronics_interfaces/test_i2s_link.py b/edg/electronics_interfaces/test_i2s_link.py new file mode 100644 index 000000000..19723302e --- /dev/null +++ b/edg/electronics_interfaces/test_i2s_link.py @@ -0,0 +1,94 @@ +import unittest + +from ..core.HdlUserExceptions import UnconnectableError +from ..electronics_model import * +from .DigitalPorts import DigitalBidir, DigitalSink +from .I2sPort import I2sController, I2sTargetReceiver + + +class I2sControllerBlock(Block): + def __init__(self, sample_rate_limit: RangeLike = RangeExpr.ALL, bit_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(I2sController(DigitalBidir(), sample_rate_limit=sample_rate_limit, bit_limit=bit_limit)) + + +class I2sTargetReceiverBlock(Block): + def __init__(self, sample_rate_limit: RangeLike = RangeExpr.ALL, bit_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port( + I2sTargetReceiver(DigitalSink(), sample_rate_limit=sample_rate_limit, bit_limit=bit_limit) + ) + + +class I2sTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock()) + self.target = self.Block(I2sTargetReceiverBlock()) + self.connect(self.controller.port, self.target.port) + + +class I2sOverconnectTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock()) + self.target1 = self.Block(I2sTargetReceiverBlock()) + self.target2 = self.Block(I2sTargetReceiverBlock()) + self.connect(self.controller.port, self.target1.port, self.target2.port) + + +class I2sSampleRateTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock(sample_rate_limit=(0, 48) * kHertz)) + self.target = self.Block(I2sTargetReceiverBlock(sample_rate_limit=(8, 96) * kHertz)) + self.connect(self.controller.port, self.target.port) + self.require(self.controller.port.link().sample_rate_limit == (8, 48) * kHertz, _unchecked=True) + + +class I2sSampleRateInvalidTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock(sample_rate_limit=(0, 16) * kHertz)) + self.target = self.Block(I2sTargetReceiverBlock(sample_rate_limit=(48, 96) * kHertz)) + self.connect(self.controller.port, self.target.port) + + +class I2sBitsTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock(bit_limit=(8, 16) * Bit)) + self.target = self.Block(I2sTargetReceiverBlock(bit_limit=(16, 32) * Bit)) + self.connect(self.controller.port, self.target.port) + self.require(self.controller.port.link().bit_limit == (16, 16) * Bit, _unchecked=True) + + +class I2sBitsInvalidTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(I2sControllerBlock(bit_limit=(0, 8) * Bit)) + self.target = self.Block(I2sTargetReceiverBlock(bit_limit=(16, 16) * Bit)) + self.connect(self.controller.port, self.target.port) + + +class I2sTestCase(unittest.TestCase): + def test_link(self) -> None: + ScalaCompiler.compile(I2sTest) + + def test_overconnect(self) -> None: + with self.assertRaises(UnconnectableError): + ScalaCompiler.compile(I2sOverconnectTest) + + def test_frequency(self) -> None: + ScalaCompiler.compile(I2sSampleRateTest) + + def test_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2sSampleRateInvalidTest) + + def test_bits(self) -> None: + ScalaCompiler.compile(I2sBitsTest) + + def test_bits_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(I2sBitsInvalidTest) diff --git a/edg/electronics_interfaces/test_spi_link.py b/edg/electronics_interfaces/test_spi_link.py new file mode 100644 index 000000000..e4ff45001 --- /dev/null +++ b/edg/electronics_interfaces/test_spi_link.py @@ -0,0 +1,60 @@ +import unittest + +from ..electronics_model import * +from .DigitalPorts import DigitalBidir +from .SpiPort import SpiController, SpiPeripheral + + +class SpiControllerBlock(Block): + def __init__(self, frequency_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(SpiController(DigitalBidir(), frequency_limit=frequency_limit)) + + +class SpiPeripheralBlock(Block): + def __init__(self, frequency_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(SpiPeripheral(DigitalBidir(), frequency_limit=frequency_limit)) + + +class SpiTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(SpiControllerBlock()) + self.device1 = self.Block(SpiPeripheralBlock()) + self.device2 = self.Block(SpiPeripheralBlock()) + self.connect(self.controller.port, self.device1.port, self.device2.port) + + +class SpiFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(SpiControllerBlock(frequency_limit=(1, 10) * MHertz)) + self.device1 = self.Block(SpiPeripheralBlock(frequency_limit=(2, 8) * MHertz)) + self.device2 = self.Block(SpiPeripheralBlock(frequency_limit=(0.5, 5) * MHertz)) + self.connect(self.controller.port, self.device1.port, self.device2.port) + # bus may run at any frequency the controller and any peripheral is capable of + self.require(self.controller.port.link().frequency_limit == (1, 8) * MHertz, _unchecked=True) + + +class SpiFrequencyInvalidTest(DesignTop): + """Invalid connection with no overlapping frequency range, e.g. fixed-frequency non-programmable + controller with incompatible peripheral""" + + def __init__(self) -> None: + super().__init__() + self.controller = self.Block(SpiControllerBlock(frequency_limit=(1, 1) * MHertz)) + self.device = self.Block(SpiPeripheralBlock(frequency_limit=(0.01, 0.1) * MHertz)) + self.connect(self.controller.port, self.device.port) + + +class SpiTestCase(unittest.TestCase): + def test_link(self) -> None: + ScalaCompiler.compile(SpiTest) + + def test_frequency(self) -> None: + ScalaCompiler.compile(SpiFrequencyTest) + + def test_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(SpiFrequencyInvalidTest) diff --git a/edg/electronics_interfaces/test_uart_link.py b/edg/electronics_interfaces/test_uart_link.py new file mode 100644 index 000000000..ee9f22a55 --- /dev/null +++ b/edg/electronics_interfaces/test_uart_link.py @@ -0,0 +1,62 @@ +import unittest + +from ..core.HdlUserExceptions import UnconnectableError +from ..electronics_model import * +from .DigitalPorts import DigitalBidir +from .UartPort import UartPort + + +class UartBlock(Block): + def __init__(self, baud_limit: RangeLike = RangeExpr.ALL) -> None: + super().__init__() + self.port = self.Port(UartPort(DigitalBidir(), baud_limit=baud_limit)) + + +class UartTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.device1 = self.Block(UartBlock()) + self.device2 = self.Block(UartBlock()) + self.connect(self.device1.port, self.device2.port) + + +class UartOverconnectTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.device1 = self.Block(UartBlock()) + self.device2 = self.Block(UartBlock()) + self.device3 = self.Block(UartBlock()) + self.connect(self.device1.port, self.device2.port, self.device3.port) + + +class UartFrequencyTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.device1 = self.Block(UartBlock(baud_limit=(9600, 115200) * Hertz)) + self.device2 = self.Block(UartBlock(baud_limit=(38400, 921600) * Hertz)) + self.connect(self.device1.port, self.device2.port) + self.require(self.device1.port.link().baud_limit == (38400, 115200) * Hertz, _unchecked=True) + + +class UartFrequencyInvalidTest(DesignTop): + def __init__(self) -> None: + super().__init__() + self.device1 = self.Block(UartBlock(baud_limit=(9600, 38400) * Hertz)) + self.device2 = self.Block(UartBlock(baud_limit=(115200, 921600) * Hertz)) + self.connect(self.device1.port, self.device2.port) + + +class UartTestCase(unittest.TestCase): + def test_link(self) -> None: + ScalaCompiler.compile(UartTest) + + def test_overconnect(self) -> None: + with self.assertRaises(UnconnectableError): + ScalaCompiler.compile(UartOverconnectTest) + + def test_frequency(self) -> None: + ScalaCompiler.compile(UartFrequencyTest) + + def test_frequency_invalid(self) -> None: + with self.assertRaises(CompilerCheckError): + ScalaCompiler.compile(UartFrequencyInvalidTest) diff --git a/edg/parts/display/oled/Er_Oled_096_1c.py b/edg/parts/display/oled/Er_Oled_096_1c.py index e490ad4ca..5004066a5 100644 --- a/edg/parts/display/oled/Er_Oled_096_1c.py +++ b/edg/parts/display/oled/Er_Oled_096_1c.py @@ -115,10 +115,12 @@ def __init__(self) -> None: self.gnd = self.Export(self.device.vss, [Common]) self.vcc = self.Export(self.device.vcc) # device power self.pwr = self.Export(self.device.vdd) # logic power - self.spi = self.Port(SpiSlave.empty(), optional=True) + self.spi = self.Port(SpiPeripheral(DigitalBidir.empty(), frequency_limit=(0, 10) * MHertz), optional=True) self.cs = self.Port(DigitalSink.empty(), optional=True) self.dc = self.Port(DigitalSink.empty(), optional=True) - self.i2c = self.Port(I2cSlave.empty(), optional=True) + self.i2c = self.Port( + I2cTarget(DigitalBidir.empty(), addresses=[0x3C], frequency_limit=(0, 400) * kHertz), optional=True + ) self.generator_param(self.spi.is_connected(), self.dc.is_connected(), self.i2c.is_connected()) @override @@ -171,7 +173,6 @@ def generate(self) -> None: self.connect(self.device.dc, gnd_digital) # addr, TODO support I2C addr self.connect(self.device.cs, gnd_digital) self.require(~self.spi.is_connected() & ~self.cs.is_connected() & ~self.dc.is_connected()) - self.assign(self.i2c.addresses, [0x3C]) elif self.get(self.spi.is_connected()): self.connect(self.device.bs1, gnd_digital) self.connect(self.spi.sck, self.device.d0) diff --git a/edg/parts/human_interface/SpeakerDriver_Max98357a.py b/edg/parts/human_interface/SpeakerDriver_Max98357a.py index eb7e8e28a..eb6d5cb65 100644 --- a/edg/parts/human_interface/SpeakerDriver_Max98357a.py +++ b/edg/parts/human_interface/SpeakerDriver_Max98357a.py @@ -26,7 +26,7 @@ def __init__(self, *, part: StringLike = "") -> None: voltage_limits=(-0.3, 6) * Volt, # abs max ratings input_thresholds=(0.6, 0.6) * Volt, # only input low voltage given ) - self.i2s = self.Port(I2sTargetReceiver(din_model)) + self.i2s = self.Port(I2sTargetReceiver(din_model, sample_rate_limit=(8, 96) * kHertz, bit_limit=(16, 32) * Bit)) self.out = self.Port(SpeakerDriverPort(AnalogSource()), [Output]) diff --git a/edg/parts/interface/CanTransceiver_Iso1050.py b/edg/parts/interface/CanTransceiver_Iso1050.py index 8c852f8bd..ad1528a1f 100644 --- a/edg/parts/interface/CanTransceiver_Iso1050.py +++ b/edg/parts/interface/CanTransceiver_Iso1050.py @@ -12,6 +12,7 @@ def __init__(self) -> None: self.gnd1 = self.Port(Ground()) self.gnd2 = self.Port(Ground()) + # dominant timeout with maximum successive bits sets minimum rate self.controller = self.Port( CanTransceiverPort( DigitalBidir( @@ -20,11 +21,12 @@ def __init__(self) -> None: current_limits=(-5, 5) * uAmp, input_thresholds=(0.8, 2) * Volt, output_thresholds=(0 * Volt, self.vcc1.link().voltage.lower()), - ) + ), + bitrate_limit=(0.000916, 1) * MHertz, ) ) - self.can = self.Port(CanDiffPort()) + self.can = self.Port(CanDiffPort(bitrate_limit=self.controller.link().bitrate_limit)) @override def contents(self) -> None: diff --git a/edg/parts/interface/CanTransceiver_Sn65hvd230.py b/edg/parts/interface/CanTransceiver_Sn65hvd230.py index 94d13c325..a19eb7855 100644 --- a/edg/parts/interface/CanTransceiver_Sn65hvd230.py +++ b/edg/parts/interface/CanTransceiver_Sn65hvd230.py @@ -18,11 +18,12 @@ def __init__(self) -> None: current_limits=(-8, 8) * mAmp, # driver pin actually -40-48mA input_thresholds=(0.8, 2) * Volt, output_thresholds=(0 * Volt, self.vcc.link().voltage.lower()), - ) + ), + bitrate_limit=(0, 1) * MHertz, ) ) - self.can = self.Port(CanDiffPort()) + self.can = self.Port(CanDiffPort(bitrate_limit=self.controller.link().bitrate_limit)) @override def contents(self) -> None: diff --git a/edg/parts/interface/UsbUart_Cp2102.py b/edg/parts/interface/UsbUart_Cp2102.py index a4eb7c20e..66373ee69 100644 --- a/edg/parts/interface/UsbUart_Cp2102.py +++ b/edg/parts/interface/UsbUart_Cp2102.py @@ -43,7 +43,7 @@ def __init__(self) -> None: self.suspend = self.Port(dout_model, optional=True) self.nsuspend = self.Port(dout_model, optional=True) - self.uart = self.Port(UartPort(dio_model), optional=True) # baud up to 921600bps + self.uart = self.Port(UartPort(dio_model, baud_limit=(0, 921600) * Hertz), optional=True) self.ri = self.Port(din_model, optional=True) self.dcd = self.Port(din_model, optional=True) self.dtr = self.Port(dout_model, optional=True) diff --git a/edg/parts/microcontroller/Esp32c3.py b/edg/parts/microcontroller/Esp32c3.py index 17df39fee..9aa70f1bd 100644 --- a/edg/parts/microcontroller/Esp32c3.py +++ b/edg/parts/microcontroller/Esp32c3.py @@ -157,7 +157,8 @@ def _io_pinmap(self) -> PinMapUtil: spi_peripheral_model = SpiPeripheral(DigitalBidir.empty(), frequency_limit=(0, 60) * MHertz) i2c_model = I2cController(DigitalBidir.empty(), frequency_limit=(100, 800) * kHertz) # section 3.4.4 i2c_target_model = I2cTarget(DigitalBidir.empty(), frequency_limit=(100, 800) * kHertz) - i2s_model = I2sController(DigitalBidir.empty(), bitrate_limit=(0.01, 40) * MHertz) + # specified up to 40 Mbps, sample rate backed out with 8 bits and two channels + i2s_model = I2sController(DigitalBidir.empty(), sample_rate_limit=(0, 2.5) * MHertz, bit_limit=(8, 32) * Bit) can_model = CanControllerPort(DigitalBidir.empty(), bitrate_limit=(1, 1000) * kHertz) # aka TWAI return ( diff --git a/edg/parts/microcontroller/Esp32s3.py b/edg/parts/microcontroller/Esp32s3.py index a8fa2b974..095c3a2ca 100644 --- a/edg/parts/microcontroller/Esp32s3.py +++ b/edg/parts/microcontroller/Esp32s3.py @@ -139,7 +139,8 @@ def _io_pinmap(self) -> PinMapUtil: i2c_target_model = I2cTarget(DigitalBidir.empty(), frequency_limit=(100, 800) * kHertz) touch_model = TouchDriver() can_model = CanControllerPort(DigitalBidir.empty(), bitrate_limit=(1, 1000) * kHertz) - i2s_model = I2sController(DigitalBidir.empty(), bitrate_limit=(0.01, 40) * MHertz) + # specified up to 40 Mbps, sample rate backed out with 8 bits and two channels + i2s_model = I2sController(DigitalBidir.empty(), sample_rate_limit=(0, 2.5) * MHertz, bit_limit=(8, 32) * Bit) dvp8_model = Dvp8Host(DigitalBidir.empty()) return ( diff --git a/edg/parts/microcontroller/Stm32g031.py b/edg/parts/microcontroller/Stm32g031.py index bb3909563..4a76bbbb7 100644 --- a/edg/parts/microcontroller/Stm32g031.py +++ b/edg/parts/microcontroller/Stm32g031.py @@ -109,6 +109,8 @@ def _io_pinmap(self) -> PinMapUtil: # TODO SPI peripherals, which have fixed-pin CS lines i2c_model = I2cController(DigitalBidir.empty(), frequency_limit=(0, 1) * MHertz) i2c_target_model = I2cTarget(DigitalBidir.empty(), frequency_limit=(0, 1) * MHertz) + # 192kHz specified sample rate * 2 channels * 32 bits data resolution + i2s_model = I2sController(DigitalBidir.empty(), sample_rate_limit=(0, 192) * kHertz, bit_limit=(16, 32) * Bit) return PinMapUtil( [ # Table 12, partial table for up to 32-pin only @@ -159,7 +161,7 @@ def _io_pinmap(self) -> PinMapUtil: ), PeripheralFixedResource( "I2S1", - I2sController(DigitalBidir.empty()), + i2s_model, {"sck": ["PA1", "PA5", "PB3"], "ws": ["PA4", "PB0", "PA15"], "sd": ["PA2", "PA7", "PA12", "PB5"]}, ), PeripheralFixedResource("USART2", uart_model, {"tx": ["PA2", "PA14"], "rx": ["PA3", "PA15"]}), diff --git a/edg/parts/microcontroller/Stm32g431.py b/edg/parts/microcontroller/Stm32g431.py index 7739c6ad9..c3ff898cd 100644 --- a/edg/parts/microcontroller/Stm32g431.py +++ b/edg/parts/microcontroller/Stm32g431.py @@ -140,6 +140,7 @@ def _io_pinmap(self) -> PinMapUtil: i2c_model = I2cController(DigitalBidir.empty(), frequency_limit=(0, 1) * MHertz) i2c_target_model = I2cTarget(DigitalBidir.empty(), frequency_limit=(0, 1) * MHertz) fdcan_model = CanControllerPort(DigitalBidir.empty(), bitrate_limit=(0, 8) * MHertz) + i2s_model = I2sController(DigitalBidir.empty(), sample_rate_limit=(0, 192) * kHertz, bit_limit=(16, 32) * Bit) return PinMapUtil( [ # for 32 pins only for now @@ -176,12 +177,10 @@ def _io_pinmap(self) -> PinMapUtil: PeripheralFixedResource("SPI3", spi_model, {"sck": ["PB3"], "miso": ["PB4"], "mosi": ["PB5"]}), PeripheralFixedResource( "I2S2", - I2sController(DigitalBidir.empty()), + i2s_model, {"sck": ["PB13", "PF1"], "ws": ["PB12", "PF0"], "sd": ["PA11", "PB15"]}, ), - PeripheralFixedResource( - "I2S3", I2sController(DigitalBidir.empty()), {"sck": ["PB3"], "ws": ["PA4", "PA15"], "sd": ["PB5"]} - ), + PeripheralFixedResource("I2S3", i2s_model, {"sck": ["PB3"], "ws": ["PA4", "PA15"], "sd": ["PB5"]}), PeripheralFixedResource("USART1", uart_model, {"tx": ["PA9", "PB6"], "rx": ["PA10", "PB7"]}), PeripheralFixedResource( "USART2", uart_model, {"tx": ["PA2", "PA14", "PB3"], "rx": ["PA3", "PA15", "PB4"]} diff --git a/edg/parts/microcontroller/nRF52840.py b/edg/parts/microcontroller/nRF52840.py index cb5b74cd4..6a3573bff 100644 --- a/edg/parts/microcontroller/nRF52840.py +++ b/edg/parts/microcontroller/nRF52840.py @@ -161,7 +161,8 @@ def _io_pinmap(self) -> PinMapUtil: ) # tristated by CS pin i2c_model = I2cController(DigitalBidir.empty(), frequency_limit=(100, 400) * kHertz) i2c_target_model = I2cTarget(DigitalBidir.empty(), frequency_limit=(100, 400) * kHertz) - i2s_model = I2sController(DigitalBidir.empty(), bitrate_limit=(0, 2000) * kHertz) + # 8-24 bits, with 24 is sign-extended to 32 on the wire + i2s_model = I2sController(DigitalBidir.empty(), sample_rate_limit=(0, 48) * kHertz, bit_limit=(8, 32) * Bit) hf_io_pins = [ "P0.00", diff --git a/examples/test_deskcontroller.py b/examples/test_deskcontroller.py index 328638817..032101ccd 100644 --- a/examples/test_deskcontroller.py +++ b/examples/test_deskcontroller.py @@ -16,7 +16,7 @@ def __init__(self) -> None: self.pwr = self.Port( VoltageSource(voltage=5 * Volt(tol=0), current_limits=(0, 300) * mAmp) ) # reportedly drives at least 300mA - self.uart = self.Port(UartPort(DigitalBidir.from_supply(self.gnd, self.pwr))) + self.uart = self.Port(UartPort(DigitalBidir.from_supply(self.gnd, self.pwr), baud_limit=9600 * Hertz(tol=0))) self.conn = self.Block(PassiveConnector(length=6)).connected( { @@ -40,9 +40,10 @@ def __init__( ) -> None: super().__init__() self.lv_pwr = self.Port(VoltageSink.empty()) - self.lv_uart = self.Port(UartPort.empty()) + self.lv_uart = self.Port(UartPort(DigitalBidir.empty(), baud_limit=RangeExpr.ALL)) self.hv_pwr = self.Port(VoltageSink.empty()) - self.hv_uart = self.Port(UartPort.empty()) + # arbitrarily treated as outer for dataflow purposes + self.hv_uart = self.Port(UartPort(DigitalBidir.empty(), baud_limit=self.lv_uart.link().baud_limit)) self.hv_tx_shift = self.Block(BidirectionalLevelShifter(lv_res=lv_res, hv_res=hv_res, src_hint="hv")) self.lv_tx_shift = self.Block(BidirectionalLevelShifter(lv_res=lv_res, hv_res=hv_res, src_hint="lv"))