diff --git a/edg/abstract_parts/Jumper.py b/edg/abstract_parts/Jumper.py index e8982cc5c..b2843e47e 100644 --- a/edg/abstract_parts/Jumper.py +++ b/edg/abstract_parts/Jumper.py @@ -67,3 +67,20 @@ def contents(self) -> None: self.assign(self.input.current_draw, self.output.link().current_draw) # for model purposes, treat as connected self.connect(self.input.net, self.device.a) self.connect(self.output.net, self.device.b) + + +class PoeJumper(TypedJumper, Block): + def __init__(self) -> None: + super().__init__() + self.jack = self.Port(PoeDevicePort(), [Input]) # jack-facing, device-presenting port + self.device = self.Port(PoePowerPort(), [Output]) # device-facing, power-presenting port + + @override + def contents(self) -> None: + super().contents() + self.pos = self.Block(Jumper()) + self.connect(self.jack.pos, self.pos.a) + self.connect(self.device.pos, self.pos.b) + self.neg = self.Block(Jumper()) + self.connect(self.jack.neg, self.neg.a) + self.connect(self.device.neg, self.neg.b) diff --git a/edg/abstract_parts/__init__.py b/edg/abstract_parts/__init__.py index 92e92b7a0..e151122e2 100644 --- a/edg/abstract_parts/__init__.py +++ b/edg/abstract_parts/__init__.py @@ -120,7 +120,7 @@ CanDiffTestPoint, ) from .TestPoint import AnalogCoaxTestPoint -from .Jumper import Jumper, GroundJumper, VoltageJumper, DigitalJumper +from .Jumper import Jumper, GroundJumper, VoltageJumper, DigitalJumper, PoeJumper from .PassiveConnector import PassiveConnector, FootprintPassiveConnector from .UsbConnectors import UsbConnector, UsbHostConnector, UsbDeviceConnector, UsbEsdDiode diff --git a/edg/electronics_interfaces/EthernetPort.py b/edg/electronics_interfaces/EthernetPort.py new file mode 100644 index 000000000..2ed3e094c --- /dev/null +++ b/edg/electronics_interfaces/EthernetPort.py @@ -0,0 +1,116 @@ +from typing_extensions import override + +from ..electronics_model import * + + +class EthernetMdiPairLink(Link): + """Single pair ethernet twisted-pair MDI connection, between the PHY and magnetics.""" + + def __init__(self) -> None: + super().__init__() + self.phy = self.Port(EthernetMdiPhyPairPort.empty()) + self.mag = self.Port(EthernetMdiMagPairPort.empty()) + + @override + def contents(self) -> None: + # KiCad diffpair-friendly naming + self.dp_P = self.connect(self.phy.pos, self.mag.pos) + self.dp_N = self.connect(self.phy.neg, self.mag.neg) + self.center = self.connect(self.phy.center, self.mag.center) + + +class EthernetMdiPhyPairPort(Port[EthernetMdiPairLink]): + """PHY-side port of an ethernet twisted-pair MDI connection""" + + link_type = EthernetMdiPairLink + + def __init__(self) -> None: + super().__init__() + self.pos = self.Port(Passive()) + self.neg = self.Port(Passive()) + self.center = self.Port(Passive()) + + +class EthernetMdiMagPairPort(Port[EthernetMdiPairLink]): + """Magnetics-side port of a twisted-pair MDI connection""" + + link_type = EthernetMdiPairLink + + def __init__(self) -> None: + super().__init__() + self.pos = self.Port(Passive()) + self.neg = self.Port(Passive()) + self.center = self.Port(Passive()) + + +class EthernetMdiLink(Link): + """Full (multi-pair) connection for ethernet twisted-pair MDI connection, between the PHY and magnetics. + Currently supports only 10/100Mbps (100BASE-TX) connections with TX/RX pairs.""" + + def __init__(self) -> None: + super().__init__() + self.phy = self.Port(EthernetMdi100BaseTxPhyPort.empty()) + self.mag = self.Port(EthernetMdi100BaseTxMagPort.empty()) + + @override + def contents(self) -> None: + self.tx = self.connect(self.phy.tx, self.mag.tx) + self.rx = self.connect(self.phy.rx, self.mag.rx) + + +class EthernetMdi100BaseTxPhyPort(Port[EthernetMdiLink]): + """PHY-side MDI port for 100BASE-TX / Fast Ethernet""" + + link_type = EthernetMdiLink + + def __init__(self) -> None: + super().__init__() + self.tx = self.Port(EthernetMdiPhyPairPort()) + self.rx = self.Port(EthernetMdiPhyPairPort()) + + +class EthernetMdi100BaseTxMagPort(Port[EthernetMdiLink]): + """Magnetics-side MDI port for 100BASE-TX / Fast Ethernet""" + + link_type = EthernetMdiLink + + def __init__(self) -> None: + super().__init__() + self.tx = self.Port(EthernetMdiMagPairPort()) + self.rx = self.Port(EthernetMdiMagPairPort()) + + +class PoeLink(Link): + """Power over Ethernet connection between the powered device and the post-rectification jack-facing circuit.""" + + def __init__(self) -> None: + super().__init__() + self.jack = self.Port(PoePowerPort.empty()) + self.poe = self.Port(PoeDevicePort.empty()) + + @override + def contents(self) -> None: + self.connect(self.jack.pos, self.poe.pos) + self.connect(self.jack.neg, self.poe.neg) + + +class PoePowerPort(Port[PoeLink]): + """Jack side port for Power over Ethernet, post-rectification.""" + + link_type = PoeLink + + def __init__(self) -> None: + super().__init__() + self.pos = self.Port(Passive()) + self.neg = self.Port(Passive()) + + +class PoeDevicePort(Port[PoeLink]): + """Powered device side port for Power over Ethernet. Generally exposed by a PoE controller subcircuit""" + + link_type = PoeLink + + def __init__(self) -> None: + super().__init__() + self.pos = self.Port(Passive()) + self.neg = self.Port(Passive()) diff --git a/edg/electronics_interfaces/UsbPort.py b/edg/electronics_interfaces/UsbPort.py index c6b6252c9..4307382e7 100644 --- a/edg/electronics_interfaces/UsbPort.py +++ b/edg/electronics_interfaces/UsbPort.py @@ -1,9 +1,6 @@ -from typing import * - from typing_extensions import override from ..electronics_model import * -from .DigitalPorts import DigitalBidir from ..electronics_model.PassivePort import PassiveBridge diff --git a/edg/electronics_interfaces/__init__.py b/edg/electronics_interfaces/__init__.py index 68c8e86fd..976cd806e 100644 --- a/edg/electronics_interfaces/__init__.py +++ b/edg/electronics_interfaces/__init__.py @@ -18,6 +18,9 @@ from .UsbPort import UsbHostPort, UsbDevicePort, UsbPassivePort, UsbCcPort, UsbLink from .DvpPort import Dvp8Host, Dvp8Camera, Dvp8Link from .I2sPort import I2sController, I2sTargetReceiver, I2sLink +from .EthernetPort import EthernetMdiPairLink, EthernetMdiPhyPairPort, EthernetMdiMagPairPort +from .EthernetPort import EthernetMdiLink, EthernetMdi100BaseTxPhyPort, EthernetMdi100BaseTxMagPort +from .EthernetPort import PoeLink, PoePowerPort, PoeDevicePort # model-breaking constructs, including for unit testing from .GroundDummy import DummyGround diff --git a/edg/parts/connector/Ethernet.py b/edg/parts/connector/Ethernet.py new file mode 100644 index 000000000..9f0ac8020 --- /dev/null +++ b/edg/parts/connector/Ethernet.py @@ -0,0 +1,134 @@ +from typing_extensions import override + +from ...circuits import * +from ...vendor_parts.jlc.JlcPart import JlcPart + + +class Hy931147c_Device(InternalSubcircuit, FootprintBlock, JlcPart): + def __init__(self) -> None: + super().__init__() + + self.eth = self.Port(EthernetMdi100BaseTxMagPort.empty(), optional=True) + self.poe = self.Port(PoePowerPort.empty(), optional=True) + + self.led_grn_anode = self.Port(Passive(), optional=True) + self.led_grn_cathode = self.Port(Passive(), optional=True) + + self.led_yel_anode = self.Port(Passive(), optional=True) + self.led_yel_cathode = self.Port(Passive(), optional=True) + + self.shield = self.Port(Passive()) + + @override + def contents(self) -> None: + super().contents() + + self.require(self.led_grn_anode.is_connected() == self.led_grn_cathode.is_connected()) + self.require(self.led_yel_anode.is_connected() == self.led_yel_cathode.is_connected()) + + self.footprint( + "J", + "Connector_RJ:RJ45_Wuerth_7499111446_Horizontal", + { + "1": self.eth.rx.pos, + "2": self.eth.rx.neg, + "3": self.eth.rx.center, + "6": self.eth.tx.neg, + "5": self.eth.tx.pos, + "4": self.eth.tx.center, + "9": self.poe.pos, + "10": self.poe.neg, + "11": self.led_yel_anode, + "12": self.led_yel_cathode, + "13": self.led_grn_anode, + "14": self.led_grn_cathode, + "SH": self.shield, + }, + "Hanrun", + "HY931147C", + pnp_rot=90, + pnp_offset=(5.6, 6.4), + ) + self.assign(self.lcsc_part, "C91754") + self.assign(self.actual_basic_part, False) + + +class Hy931147c(Connector, GeneratorBlock): + """Commonly available RJ45 magjack with PoE support. + Footprint and pin-compatible with Wuerth 7499211121A. + + This uses the footprint for the Wuerth 7499111446, which shares the same pattern + but is not functionally compatible. + + TODO should define and implement an abstract base class, EthernetConnector, which defines the + magnetics-side ports and can also be implemented by DiscreteMagneticsEthernetConnector, + which has a passive-typed RJ45, discrete magnetics, and optional PoE diode bridge generator. + + TODO: allow LEDs to be driven in source mode + + TODO: support LED connection by multipacking""" + + _LED_CURRENT_LIMITS = (0, 20) * mAmp + + def __init__(self, *, led_target_current: RangeLike = (1, 10) * mAmp) -> None: + super().__init__() + self.led_target_current = self.ArgParameter(led_target_current) + + self.conn = self.Block(Hy931147c_Device()) + + self.eth = self.Export(self.conn.eth, optional=True) + self.poe = self.Export(self.conn.poe, optional=True) + + self.gnd = self.Port(Ground()) # for termination + self.pwr_led = self.Port(VoltageSink(), optional=True) # for LED power + self.led_yel_sink = self.Port( + DigitalSink(current_draw=RangeExpr()), optional=True, doc="Yellow LED cathode connection" + ) + self.led_grn_sink = self.Port( + DigitalSink(current_draw=RangeExpr()), optional=True, doc="Green LED cathode connection" + ) + self.generator_param(self.led_yel_sink.is_connected(), self.led_grn_sink.is_connected()) + + @override + def generate(self) -> None: + super().generate() + + self.require(self.eth.is_connected() | self.poe.is_connected(), "must use ethernet or PoE") + + self.require( + (self.led_yel_sink.is_connected() | self.led_grn_sink.is_connected()).implies(self.pwr_led.is_connected()), + "power required when LEDs used", + ) + if self.get(self.led_yel_sink.is_connected()): + self.led_yel_res = self.Block( + Resistor( + (1 / self.led_target_current).shrink_multiply( + self.pwr_led.link().voltage - self.led_yel_sink.link().output_thresholds.lower() + ) + ) + ) + self.connect(self.pwr_led.net, self.conn.led_yel_anode) + self.connect(self.conn.led_yel_cathode, self.led_yel_res.a) + self.connect(self.led_yel_res.b, self.led_yel_sink.net) + self.assign( + self.led_yel_sink.current_draw, -self.pwr_led.link().voltage / self.led_yel_res.actual_resistance + ) + + if self.get(self.led_grn_sink.is_connected()): + self.led_grn_res = self.Block( + Resistor( + (1 / self.led_target_current).shrink_multiply( + self.pwr_led.link().voltage - self.led_grn_sink.link().output_thresholds.lower() + ) + ) + ) + self.connect(self.pwr_led.net, self.conn.led_grn_anode) + self.connect(self.conn.led_grn_cathode, self.led_grn_res.a) + self.connect(self.led_grn_res.b, self.led_grn_sink.net) + self.assign( + self.led_grn_sink.current_draw, -self.pwr_led.link().voltage / self.led_grn_res.actual_resistance + ) + + self.cap = self.Block(Capacitor(1 * nFarad(tol=0.2), voltage=(0, 1000) * Volt)) # termination + self.connect(self.cap.neg, self.gnd.net) + self.connect(self.cap.pos, self.conn.shield) diff --git a/edg/parts/connector/__init__.py b/edg/parts/connector/__init__.py index 45be55da6..51182dd7b 100644 --- a/edg/parts/connector/__init__.py +++ b/edg/parts/connector/__init__.py @@ -53,6 +53,8 @@ from .FanConnector import CpuFanConnector, CpuFanPwmControl from .Connectors import PowerBarrelJack, Pj_102ah, Pj_036ah, LipoConnector, QwiicTarget +from .Ethernet import Hy931147c + from .UsbPorts import UsbAReceptacle, UsbCReceptacle, UsbMicroBReceptacle from .UsbPorts import Tpd2e009, Pesd5v0x1bt, Pgb102st23 diff --git a/edg/parts/interface/Ethernet_W5500.py b/edg/parts/interface/Ethernet_W5500.py new file mode 100644 index 000000000..bc78380e2 --- /dev/null +++ b/edg/parts/interface/Ethernet_W5500.py @@ -0,0 +1,196 @@ +from typing_extensions import override + +from ...circuits import * +from ...vendor_parts.jlc.JlcPart import JlcPart + + +class W5500_Device(InternalSubcircuit, FootprintBlock, JlcPart): + def __init__(self) -> None: + super().__init__() + + self.agnd = self.Port(Ground()) + self.gnd = self.Port(Ground()) + + self.avdd = self.Port( + VoltageSink( + voltage_limits=(2.97, 3.63) * Volt, + current_draw=(13, 132) * mAmp, # power down to 100M link, arbitrarily lumped into avdd + ) + ) + self.vdd = self.Port(VoltageSink(voltage_limits=(2.97, 3.63) * Volt)) + + self.v1v20 = self.Port(VoltageSource(voltage=1.2 * Volt(tol=0), current_limits=0 * Amp(tol=0))) + self.tocap = self.Port(VoltageSource(voltage=self.avdd.link().voltage)) # assumed, not documented + self.exres1 = self.Port(AnalogSource.from_supply(self.gnd, self.avdd)) # assumed, not documented + + self.crystal = self.Port(CrystalDriver(frequency_limits=25 * MHertz(tol=30e-6))) # TODO also support CLKIN + + self.txp = self.Port(Passive()) + self.txn = self.Port(Passive()) + self.rxp = self.Port(Passive()) + self.rxn = self.Port(Passive()) + + dio_model = DigitalBidir.from_supply( + self.gnd, + self.vdd, + voltage_limit_abs=(-0.3, 5.5) * Volt, + input_threshold_abs=(0.8, 2.0) * Volt, + current_limits=(-5, 5) * mAmp, # absolute max rating for DC input current + ) + dio_pu_model = DigitalSink.from_supply( + self.gnd, + self.vdd, + voltage_limit_abs=(-0.3, 5.5) * Volt, + input_threshold_abs=(0.8, 2.0) * Volt, + pullup_capable=True, + ) + + self.spi = self.Port(SpiPeripheral(dio_model)) + self.scsn = self.Port(dio_pu_model) + # according to some internet forum posts, a reset pulse is not needed + self.rstn = self.Port(dio_pu_model, optional=True) + self.intn = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) + + # PMODE[0..2] internally pulled up, defaulting to auto-negotiation + self.pmode0 = self.Port(dio_pu_model, optional=True) + self.pmode1 = self.Port(dio_pu_model, optional=True) + self.pmode2 = self.Port(dio_pu_model, optional=True) + + # TODO add LEDs + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "U", + "Package_QFP:LQFP-48_7x7mm_P0.5mm", + { + "1": self.txn, + "2": self.txp, + ("3", "9", "14", "16", "19", "48"): self.agnd, + ("4", "8", "11", "15", "17", "21"): self.avdd, + "5": self.rxn, + "6": self.rxp, + # "7": DNC + "10": self.exres1, + # ("12", "13"): NC + # "18": VBG, "must be left floating" + "20": self.tocap, + "22": self.v1v20, + "23": self.gnd, # RSVD, "must be tied to GND" + # "24": self.spdled, + # "25": self.linkled, + # "26": self.dupled, + # "27": self.actled, + "28": self.vdd, + "29": self.gnd, + "30": self.crystal.xtal_in, + "31": self.crystal.xtal_out, + "32": self.scsn, + "33": self.spi.sck, + "34": self.spi.miso, + "35": self.spi.mosi, + "36": self.intn, + "37": self.rstn, + # ("38", "39", "40", "41", "42"): NC + "43": self.pmode2, + "44": self.pmode1, + "45": self.pmode0, + # ("46", "47"): NC + }, + "Wiznet", + "W5500", + ) + self.assign(self.lcsc_part, "C32843") + self.assign(self.actual_basic_part, False) + + +class W5500(Resettable, Interface, Block): + """SPI Ethernet controller supporting 10/100Mbps ethernet and onboard TCP/IP stack.""" + + def __init__(self, *, damping_resistance: RangeLike = 33 * Ohm(tol=0.05)) -> None: + super().__init__() + self.damping_resistance = self.ArgParameter(damping_resistance) + + self.ic = self.Block(W5500_Device()) + self.gnd = self.Export(self.ic.gnd, [Common]) + self.pwr = self.Export(self.ic.vdd, [Power]) + + self.eth = self.Port(EthernetMdi100BaseTxPhyPort.empty()) + self.spi = self.Export(self.ic.spi) + self.cs = self.Export(self.ic.scsn) + self.int = self.Export(self.ic.intn, optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.connect(self.reset, self.ic.rstn) + self.connect(self.gnd, self.ic.agnd) + self.l = self.Block(SeriesPowerFerriteBead(hf_impedance=(100, 2000) * Ohm)).connected(self.pwr, self.ic.avdd) + + self.crystal = self.Block(OscillatorReference(frequency=25 * MHertz(tol=30e-6))) + self.connect(self.crystal.gnd, self.gnd) + self.connect(self.crystal.crystal, self.ic.crystal) + + with self.implicit_connect(ImplicitConnect(self.gnd, [Common])) as imp: + self.exres1 = imp.Block(AnalogSetpointResistor(12.4 * kOhm(tol=0.01))).connected(io=self.ic.exres1) + self.c1v20 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))).connected(pwr=self.ic.v1v20) + self.tocap = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(pwr=self.ic.tocap) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.ic.vdd, [Power]), + ) as imp: + self.vdd_cap0 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.vdd_cap1 = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + + with self.implicit_connect( + ImplicitConnect(self.gnd, [Common]), + ImplicitConnect(self.ic.avdd, [Power]), + ) as imp: + self.avdd_caps = ElementDict[DecouplingCapacitor]() + for i in range(6): + self.avdd_caps[str(i)] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) + self.avdd_caps[6] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) + + # TODO parameterize PMODE configuration + self.connect(self.ic.pmode0, self.ic.pmode1, self.ic.pmode2, self.pwr.as_digital_source()) + + # optional damping resistors for EMI reduction + damp_resistor_model = Resistor(self.damping_resistance) + self.txp_damp = self.Block(damp_resistor_model) + self.txn_damp = self.Block(damp_resistor_model) + self.connect(self.txp_damp.a, self.ic.txp) + self.connect(self.txn_damp.a, self.ic.txn) + self.rxp_damp = self.Block(damp_resistor_model) + self.rxn_damp = self.Block(damp_resistor_model) + self.connect(self.rxp_damp.a, self.ic.rxp) + self.connect(self.rxn_damp.a, self.ic.rxn) + + # Ethernet termination circuit + bias_resistor_model = Resistor(49.9 * Ohm(tol=0.01)) + self.txp_bias = self.Block(bias_resistor_model) + self.txn_bias = self.Block(bias_resistor_model) + self.txc_bias = self.Block(Resistor(10 * Ohm(tol=0.01))) + self.connect(self.txp_bias.a, self.txn_bias.a, self.txc_bias.a) + self.connect(self.txc_bias.a.adapt_to(VoltageSink()), self.ic.avdd) + self.connect(self.txp_damp.b, self.txp_bias.b, self.eth.tx.pos) + self.connect(self.txn_damp.b, self.txn_bias.b, self.eth.tx.neg) + self.txc_cap = self.Block(Capacitor(22 * nFarad(tol=0.2), voltage=(0, 5) * Volt)) + self.connect(self.txc_bias.b, self.txc_cap.pos, self.eth.tx.center) + self.connect(self.txc_cap.neg.adapt_to(Ground()), self.gnd) + + ac_cap_model = Capacitor(6.8 * nFarad(tol=0.2), voltage=(0, 5) * Volt) + self.rxp_ac = self.Block(ac_cap_model) + self.rxn_ac = self.Block(ac_cap_model) + self.connect(self.rxp_ac.pos, self.eth.rx.pos) + self.connect(self.rxn_ac.pos, self.eth.rx.neg) + self.rxp_bias = self.Block(bias_resistor_model) + self.rxn_bias = self.Block(bias_resistor_model) + self.connect(self.rxp_damp.b, self.rxp_bias.a, self.rxp_ac.neg) + self.connect(self.rxn_damp.b, self.rxn_bias.a, self.rxn_ac.neg) + self.rxc_cap = self.Block(Capacitor(10 * nFarad(tol=0.2), voltage=(0, 5) * Volt)) + self.connect(self.rxc_cap.pos, self.eth.rx.center, self.rxp_bias.b, self.rxn_bias.b) + self.connect(self.rxc_cap.neg.adapt_to(Ground()), self.gnd) diff --git a/edg/parts/interface/Poe_Tps2378.py b/edg/parts/interface/Poe_Tps2378.py new file mode 100644 index 000000000..c8ca03e34 --- /dev/null +++ b/edg/parts/interface/Poe_Tps2378.py @@ -0,0 +1,118 @@ +from typing_extensions import override + +from ...circuits import * +from ...vendor_parts.jlc.JlcPart import JlcPart + + +class Tps2378_Device(InternalSubcircuit, FootprintBlock, JlcPart): + def __init__(self) -> None: + super().__init__() + + self.vss = self.Port(Ground()) + self.vdd = self.Port( + VoltageSink.from_gnd(self.vss, voltage_limits=(0, 57) * Volt, current_draw=(285, 500) * uAmp) + ) + self.den = self.Port(Passive()) # AnalogSink + self.cls = self.Port(AnalogSource()) + + self.rtn = self.Port(Ground()) + self.cdb = self.Port(DigitalSource.low_from_supply(self.rtn), optional=True) # -0.3 - 100v standoff limit + self.t2p = self.Port(DigitalSource.low_from_supply(self.rtn), optional=True) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( + "U", + "Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm_ThermalVias", + { + "1": self.vdd, + "2": self.den, + "3": self.cls, + "4": self.vss, + "5": self.rtn, + "6": self.cdb, + "7": self.t2p, + "8": self.rtn, # APD, connect to RTN if unused + "9": self.vss, + # ("4", "5", "6", "7", "8"): NC + }, + mfr="Texas Instruments", + part="TPS2378", + datasheet="https://www.ti.com/lit/ds/symlink/tps2378.pdf", + pnp_rot=-90, + ) + self.assign(self.lcsc_part, "C337500") + self.assign(self.actual_basic_part, False) + + +class Tps2378(Interface, GeneratorBlock): + def __init__(self, poe_class: IntLike = 0) -> None: + super().__init__() + self.poe_class = self.ArgParameter(poe_class) + self.generator_param(self.poe_class) + + self.ic = self.Block(Tps2378_Device()) + self.gnd = self.Export(self.ic.rtn, [Common]) + self.pwr_out = self.Port(VoltageSource.empty(), [Output]) + + self.poe = self.Port(PoeDevicePort(), [Input], doc="PoE input") + + self.cdb = self.Export( + self.ic.cdb, + doc="active-low output when the in inrush limiting, intended to disable a downstream converter", + optional=True, + ) + self.t2p = self.Export(self.ic.t2p, doc="active-low output indicating type-2 PSE", optional=True) + + @override + def generate(self) -> None: + super().generate() + + POE_VOUT_MIN = 37 + POE_VOUT_MAX = 57 + + poe_class = self.get(self.poe_class) + if poe_class == 0: + cls_res = 270 * Ohm(tol=0.05) + output_power_max = 12.95 + elif poe_class == 1: + cls_res = 243 * Ohm(tol=0.05) + output_power_max = 3.84 + elif poe_class == 2: + cls_res = 137 * Ohm(tol=0.05) + output_power_max = 6.49 + elif poe_class == 3: + cls_res = 90.9 * Ohm(tol=0.05) + output_power_max = 12.95 + elif poe_class == 4: + cls_res = 63.4 * Ohm(tol=0.05) + output_power_max = 25.5 + self.require(self.t2p.is_connected(), "class 4 devices must use T2P to draw >13W") + else: + raise ValueError(f"unsupported PoE class {poe_class}") + + self.cls = self.Block(AnalogSetpointResistor(cls_res)).connected(self.ic.vss, self.ic.cls) + + self.den = self.Block(Resistor(24.9 * kOhm(tol=0.01))) + self.connect(self.den.a, self.poe.pos) + self.connect(self.den.b, self.ic.den) + + self.connect( + self.poe.pos.adapt_to( + VoltageSource( + voltage=(POE_VOUT_MIN, POE_VOUT_MAX) * Volt, current_limits=(0, output_power_max / POE_VOUT_MAX) + ) + ), + self.ic.vdd, + self.pwr_out, + ) + self.connect(self.poe.neg.adapt_to(Ground()), self.ic.vss) + + with self.implicit_connect( + ImplicitConnect(self.ic.vss, [Common]), + ImplicitConnect(self.ic.vdd, [Power]), + ) as imp: + self.vdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.1))) + self.prot = imp.Block(ProtectionZenerDiode((57, 66) * Volt)) # based on SMAJ58A as recommended in datasheet diff --git a/edg/parts/interface/__init__.py b/edg/parts/interface/__init__.py index a74d2628a..af8d05dfe 100644 --- a/edg/parts/interface/__init__.py +++ b/edg/parts/interface/__init__.py @@ -7,6 +7,9 @@ from .UsbUart_Cp2102 import Cp2102 from .UsbInterface_Ft232h import Ft232hl +from .Ethernet_W5500 import W5500 +from .Poe_Tps2378 import Tps2378 + from .Isolator_Cbmud1200 import Cbmud1200l # Expanders diff --git a/examples/IotThermalCamera/IotThermalCamera.net.ref b/examples/IotThermalCamera/IotThermalCamera.net.ref index 8ace1d26b..34886635b 100644 --- a/examples/IotThermalCamera/IotThermalCamera.net.ref +++ b/examples/IotThermalCamera/IotThermalCamera.net.ref @@ -64,7 +64,7 @@ (value "eth.conn") (footprint "Connector_RJ:RJ45_Wuerth_7499111446_Horizontal") (property (name "Sheetname") (value "eth")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Hy931147c")) + (property (name "Sheetfile") (value "edg.parts.connector.Ethernet.Hy931147c")) (property (name "edg_path") (value "eth.conn")) (property (name "edg_short_path") (value "eth.conn")) (property (name "edg_refdes") (value "TJ2")) @@ -76,7 +76,7 @@ (value "eth.led_yel_res") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "eth")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Hy931147c")) + (property (name "Sheetfile") (value "edg.parts.connector.Ethernet.Hy931147c")) (property (name "edg_path") (value "eth.led_yel_res")) (property (name "edg_short_path") (value "eth.led_yel_res")) (property (name "edg_refdes") (value "TR1")) @@ -88,7 +88,7 @@ (value "eth.led_grn_res") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "eth")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Hy931147c")) + (property (name "Sheetfile") (value "edg.parts.connector.Ethernet.Hy931147c")) (property (name "edg_path") (value "eth.led_grn_res")) (property (name "edg_short_path") (value "eth.led_grn_res")) (property (name "edg_refdes") (value "TR2")) @@ -100,7 +100,7 @@ (value "eth.cap") (footprint "Capacitor_SMD:C_1206_3216Metric") (property (name "Sheetname") (value "eth")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Hy931147c")) + (property (name "Sheetfile") (value "edg.parts.connector.Ethernet.Hy931147c")) (property (name "edg_path") (value "eth.cap")) (property (name "edg_short_path") (value "eth.cap")) (property (name "edg_refdes") (value "TC1")) @@ -112,7 +112,7 @@ (value "poe.ic") (footprint "Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm_ThermalVias") (property (name "Sheetname") (value "poe")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Tps2378")) + (property (name "Sheetfile") (value "edg.parts.interface.Poe_Tps2378.Tps2378")) (property (name "edg_path") (value "poe.ic")) (property (name "edg_short_path") (value "poe.ic")) (property (name "edg_refdes") (value "TU2")) @@ -124,7 +124,7 @@ (value "poe.cls") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "poe")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Tps2378")) + (property (name "Sheetfile") (value "edg.parts.interface.Poe_Tps2378.Tps2378")) (property (name "edg_path") (value "poe.cls.res")) (property (name "edg_short_path") (value "poe.cls")) (property (name "edg_refdes") (value "TR3")) @@ -136,7 +136,7 @@ (value "poe.den") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "poe")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Tps2378")) + (property (name "Sheetfile") (value "edg.parts.interface.Poe_Tps2378.Tps2378")) (property (name "edg_path") (value "poe.den")) (property (name "edg_short_path") (value "poe.den")) (property (name "edg_refdes") (value "TR4")) @@ -148,7 +148,7 @@ (value "poe.vdd_cap") (footprint "Capacitor_SMD:C_0805_2012Metric") (property (name "Sheetname") (value "poe")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Tps2378")) + (property (name "Sheetfile") (value "edg.parts.interface.Poe_Tps2378.Tps2378")) (property (name "edg_path") (value "poe.vdd_cap.cap")) (property (name "edg_short_path") (value "poe.vdd_cap")) (property (name "edg_refdes") (value "TC2")) @@ -160,7 +160,7 @@ (value "poe.prot") (footprint "Diode_SMD:D_SMA") (property (name "Sheetname") (value "poe")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.Tps2378")) + (property (name "Sheetfile") (value "edg.parts.interface.Poe_Tps2378.Tps2378")) (property (name "edg_path") (value "poe.prot.diode")) (property (name "edg_short_path") (value "poe.prot")) (property (name "edg_refdes") (value "TD1")) @@ -172,7 +172,7 @@ (value "poe_jmp.pos") (footprint "Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm") (property (name "Sheetname") (value "poe_jmp")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.PoeJumper")) + (property (name "Sheetfile") (value "edg.abstract_parts.Jumper.PoeJumper")) (property (name "edg_path") (value "poe_jmp.pos")) (property (name "edg_short_path") (value "poe_jmp.pos")) (property (name "edg_refdes") (value "TJP1")) @@ -184,7 +184,7 @@ (value "poe_jmp.neg") (footprint "Jumper:SolderJumper-2_P1.3mm_Open_TrianglePad1.0x1.5mm") (property (name "Sheetname") (value "poe_jmp")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.PoeJumper")) + (property (name "Sheetfile") (value "edg.abstract_parts.Jumper.PoeJumper")) (property (name "edg_path") (value "poe_jmp.neg")) (property (name "edg_short_path") (value "poe_jmp.neg")) (property (name "edg_refdes") (value "TJP2")) @@ -712,7 +712,7 @@ (value "phy.ic") (footprint "Package_QFP:LQFP-48_7x7mm_P0.5mm") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.ic")) (property (name "edg_short_path") (value "phy.ic")) (property (name "edg_refdes") (value "TU9")) @@ -724,7 +724,7 @@ (value "phy.l") (footprint "Inductor_SMD:L_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.l.fb")) (property (name "edg_short_path") (value "phy.l")) (property (name "edg_refdes") (value "TFB2")) @@ -772,7 +772,7 @@ (value "phy.exres1") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.exres1.res")) (property (name "edg_short_path") (value "phy.exres1")) (property (name "edg_refdes") (value "TR13")) @@ -784,7 +784,7 @@ (value "phy.c1v20") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.c1v20.cap")) (property (name "edg_short_path") (value "phy.c1v20")) (property (name "edg_refdes") (value "TC23")) @@ -796,7 +796,7 @@ (value "phy.tocap") (footprint "Capacitor_SMD:C_0805_2012Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.tocap.cap")) (property (name "edg_short_path") (value "phy.tocap")) (property (name "edg_refdes") (value "TC24")) @@ -808,7 +808,7 @@ (value "phy.vdd_cap0") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.vdd_cap0.cap")) (property (name "edg_short_path") (value "phy.vdd_cap0")) (property (name "edg_refdes") (value "TC25")) @@ -820,7 +820,7 @@ (value "phy.vdd_cap1") (footprint "Capacitor_SMD:C_0805_2012Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.vdd_cap1.cap")) (property (name "edg_short_path") (value "phy.vdd_cap1")) (property (name "edg_refdes") (value "TC26")) @@ -832,7 +832,7 @@ (value "phy.avdd_caps[0]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[0].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[0]")) (property (name "edg_refdes") (value "TC27")) @@ -844,7 +844,7 @@ (value "phy.avdd_caps[1]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[1].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[1]")) (property (name "edg_refdes") (value "TC28")) @@ -856,7 +856,7 @@ (value "phy.avdd_caps[2]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[2].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[2]")) (property (name "edg_refdes") (value "TC29")) @@ -868,7 +868,7 @@ (value "phy.avdd_caps[3]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[3].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[3]")) (property (name "edg_refdes") (value "TC30")) @@ -880,7 +880,7 @@ (value "phy.avdd_caps[4]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[4].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[4]")) (property (name "edg_refdes") (value "TC31")) @@ -892,7 +892,7 @@ (value "phy.avdd_caps[5]") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[5].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[5]")) (property (name "edg_refdes") (value "TC32")) @@ -904,7 +904,7 @@ (value "phy.avdd_caps[6]") (footprint "Capacitor_SMD:C_0805_2012Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.avdd_caps[6].cap")) (property (name "edg_short_path") (value "phy.avdd_caps[6]")) (property (name "edg_refdes") (value "TC33")) @@ -916,7 +916,7 @@ (value "phy.txp_damp") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txp_damp")) (property (name "edg_short_path") (value "phy.txp_damp")) (property (name "edg_refdes") (value "TR14")) @@ -928,7 +928,7 @@ (value "phy.txn_damp") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txn_damp")) (property (name "edg_short_path") (value "phy.txn_damp")) (property (name "edg_refdes") (value "TR15")) @@ -940,7 +940,7 @@ (value "phy.rxp_damp") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxp_damp")) (property (name "edg_short_path") (value "phy.rxp_damp")) (property (name "edg_refdes") (value "TR16")) @@ -952,7 +952,7 @@ (value "phy.rxn_damp") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxn_damp")) (property (name "edg_short_path") (value "phy.rxn_damp")) (property (name "edg_refdes") (value "TR17")) @@ -964,7 +964,7 @@ (value "phy.txp_bias") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txp_bias")) (property (name "edg_short_path") (value "phy.txp_bias")) (property (name "edg_refdes") (value "TR18")) @@ -976,7 +976,7 @@ (value "phy.txn_bias") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txn_bias")) (property (name "edg_short_path") (value "phy.txn_bias")) (property (name "edg_refdes") (value "TR19")) @@ -988,7 +988,7 @@ (value "phy.txc_bias") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txc_bias")) (property (name "edg_short_path") (value "phy.txc_bias")) (property (name "edg_refdes") (value "TR20")) @@ -1000,7 +1000,7 @@ (value "phy.txc_cap") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.txc_cap")) (property (name "edg_short_path") (value "phy.txc_cap")) (property (name "edg_refdes") (value "TC34")) @@ -1012,7 +1012,7 @@ (value "phy.rxp_ac") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxp_ac")) (property (name "edg_short_path") (value "phy.rxp_ac")) (property (name "edg_refdes") (value "TC35")) @@ -1024,7 +1024,7 @@ (value "phy.rxn_ac") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxn_ac")) (property (name "edg_short_path") (value "phy.rxn_ac")) (property (name "edg_refdes") (value "TC36")) @@ -1036,7 +1036,7 @@ (value "phy.rxp_bias") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxp_bias")) (property (name "edg_short_path") (value "phy.rxp_bias")) (property (name "edg_refdes") (value "TR21")) @@ -1048,7 +1048,7 @@ (value "phy.rxn_bias") (footprint "Resistor_SMD:R_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxn_bias")) (property (name "edg_short_path") (value "phy.rxn_bias")) (property (name "edg_refdes") (value "TR22")) @@ -1060,7 +1060,7 @@ (value "phy.rxc_cap") (footprint "Capacitor_SMD:C_0603_1608Metric") (property (name "Sheetname") (value "phy")) - (property (name "Sheetfile") (value "examples.test_iot_thermal_camera.W5500")) + (property (name "Sheetfile") (value "edg.parts.interface.Ethernet_W5500.W5500")) (property (name "edg_path") (value "phy.rxc_cap")) (property (name "edg_short_path") (value "phy.rxc_cap")) (property (name "edg_refdes") (value "TC37")) diff --git a/examples/test_iot_thermal_camera.py b/examples/test_iot_thermal_camera.py index d5e232815..8f0f00a65 100644 --- a/examples/test_iot_thermal_camera.py +++ b/examples/test_iot_thermal_camera.py @@ -5,574 +5,6 @@ from edg import * from .util import run_test_board -# these libraries live in this example until it gets fabbed out and tested - - -class EthernetMdiPairLink(Link): - """Single pair ethernet twisted-pair MDI connection, between the PHY and magnetics.""" - - def __init__(self) -> None: - super().__init__() - self.phy = self.Port(EthernetMdiPhyPairPort.empty()) - self.mag = self.Port(EthernetMdiMagPairPort.empty()) - - @override - def contents(self) -> None: - # KiCad diffpair-friendly naming - self.dp_P = self.connect(self.phy.pos, self.mag.pos) - self.dp_N = self.connect(self.phy.neg, self.mag.neg) - self.center = self.connect(self.phy.center, self.mag.center) - - -class EthernetMdiPhyPairPort(Port[EthernetMdiPairLink]): - """PHY-side port of an ethernet twisted-pair MDI connection""" - - link_type = EthernetMdiPairLink - - def __init__(self) -> None: - super().__init__() - self.pos = self.Port(Passive()) - self.neg = self.Port(Passive()) - self.center = self.Port(Passive()) - - -class EthernetMdiMagPairPort(Port[EthernetMdiPairLink]): - """Magnetics-side port of a twisted-pair MDI connection""" - - link_type = EthernetMdiPairLink - - def __init__(self) -> None: - super().__init__() - self.pos = self.Port(Passive()) - self.neg = self.Port(Passive()) - self.center = self.Port(Passive()) - - -class EthernetMdiLink(Link): - """Full (multi-pair) connection for ethernet twisted-pair MDI connection, between the PHY and magnetics. - Currently supports only 10/100Mbps (100BASE-TX) connections with TX/RX pairs.""" - - def __init__(self) -> None: - super().__init__() - self.phy = self.Port(EthernetMdi100BaseTxPhyPort.empty()) - self.mag = self.Port(EthernetMdi100BaseTxMagPort.empty()) - - @override - def contents(self) -> None: - self.tx = self.connect(self.phy.tx, self.mag.tx) - self.rx = self.connect(self.phy.rx, self.mag.rx) - - -class EthernetMdi100BaseTxPhyPort(Port[EthernetMdiLink]): - """PHY-side MDI port for 100BASE-TX / Fast Ethernet""" - - link_type = EthernetMdiLink - - def __init__(self) -> None: - super().__init__() - self.tx = self.Port(EthernetMdiPhyPairPort()) - self.rx = self.Port(EthernetMdiPhyPairPort()) - - -class EthernetMdi100BaseTxMagPort(Port[EthernetMdiLink]): - """Magnetics-side MDI port for 100BASE-TX / Fast Ethernet""" - - link_type = EthernetMdiLink - - def __init__(self) -> None: - super().__init__() - self.tx = self.Port(EthernetMdiMagPairPort()) - self.rx = self.Port(EthernetMdiMagPairPort()) - - -class PoeLink(Link): - """Power over Ethernet connection between the powered device and the post-rectification jack-facing circuit.""" - - def __init__(self) -> None: - super().__init__() - self.jack = self.Port(PoePowerPort.empty()) - self.poe = self.Port(PoeDevicePort.empty()) - - @override - def contents(self) -> None: - self.connect(self.jack.pos, self.poe.pos) - self.connect(self.jack.neg, self.poe.neg) - - -class PoePowerPort(Port[PoeLink]): - """Jack side port for Power over Ethernet, post-rectification.""" - - link_type = PoeLink - - def __init__(self) -> None: - super().__init__() - self.pos = self.Port(Passive()) - self.neg = self.Port(Passive()) - - -class PoeDevicePort(Port[PoeLink]): - """Powered device side port for Power over Ethernet. Generally exposed by a PoE controller subcircuit""" - - link_type = PoeLink - - def __init__(self) -> None: - super().__init__() - self.pos = self.Port(Passive()) - self.neg = self.Port(Passive()) - - -class PoeJumper(TypedJumper, Block): - def __init__(self) -> None: - super().__init__() - self.jack = self.Port(PoeDevicePort(), [Input]) # jack-facing, device-presenting port - self.device = self.Port(PoePowerPort(), [Output]) # device-facing, power-presenting port - - @override - def contents(self) -> None: - super().contents() - self.pos = self.Block(Jumper()) - self.connect(self.jack.pos, self.pos.a) - self.connect(self.device.pos, self.pos.b) - self.neg = self.Block(Jumper()) - self.connect(self.jack.neg, self.neg.a) - self.connect(self.device.neg, self.neg.b) - - -class Hy931147c_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.eth = self.Port(EthernetMdi100BaseTxMagPort.empty(), optional=True) - self.poe = self.Port(PoePowerPort.empty(), optional=True) - - self.led_grn_anode = self.Port(Passive(), optional=True) - self.led_grn_cathode = self.Port(Passive(), optional=True) - - self.led_yel_anode = self.Port(Passive(), optional=True) - self.led_yel_cathode = self.Port(Passive(), optional=True) - - self.shield = self.Port(Passive()) - - @override - def contents(self) -> None: - super().contents() - - self.require(self.led_grn_anode.is_connected() == self.led_grn_cathode.is_connected()) - self.require(self.led_yel_anode.is_connected() == self.led_yel_cathode.is_connected()) - - self.footprint( - "J", - "Connector_RJ:RJ45_Wuerth_7499111446_Horizontal", - { - "1": self.eth.rx.pos, - "2": self.eth.rx.neg, - "3": self.eth.rx.center, - "6": self.eth.tx.neg, - "5": self.eth.tx.pos, - "4": self.eth.tx.center, - "9": self.poe.pos, - "10": self.poe.neg, - "11": self.led_yel_anode, - "12": self.led_yel_cathode, - "13": self.led_grn_anode, - "14": self.led_grn_cathode, - "SH": self.shield, - }, - "Hanrun", - "HY931147C", - pnp_rot=90, - pnp_offset=(5.6, 6.4), - ) - self.assign(self.lcsc_part, "C91754") - self.assign(self.actual_basic_part, False) - - -class Hy931147c(Connector, GeneratorBlock): - """Commonly available RJ45 magjack with PoE support. - Footprint and pin-compatible with Wuerth 7499211121A. - - This uses the footprint for the Wuerth 7499111446, which shares the same pattern - but is not functionally compatible. - - TODO should define and implement an abstract base class, EthernetConnector, which defines the - magnetics-side ports and can also be implemented by DiscreteMagneticsEthernetConnector, - which has a passive-typed RJ45, discrete magnetics, and optional PoE diode bridge generator. - - TODO: allow LEDs to be driven in source mode - - TODO: support LED connection by multipacking""" - - _LED_CURRENT_LIMITS = (0, 20) * mAmp - - def __init__(self, *, led_target_current: RangeLike = (1, 10) * mAmp) -> None: - super().__init__() - self.led_target_current = self.ArgParameter(led_target_current) - - self.conn = self.Block(Hy931147c_Device()) - - self.eth = self.Export(self.conn.eth, optional=True) - self.poe = self.Export(self.conn.poe, optional=True) - - self.gnd = self.Port(Ground()) # for termination - self.pwr_led = self.Port(VoltageSink(), optional=True) # for LED power - self.led_yel_sink = self.Port( - DigitalSink(current_draw=RangeExpr()), optional=True, doc="Yellow LED cathode connection" - ) - self.led_grn_sink = self.Port( - DigitalSink(current_draw=RangeExpr()), optional=True, doc="Green LED cathode connection" - ) - self.generator_param(self.led_yel_sink.is_connected(), self.led_grn_sink.is_connected()) - - @override - def generate(self) -> None: - super().generate() - - self.require(self.eth.is_connected() | self.poe.is_connected(), "must use ethernet or PoE") - - self.require( - (self.led_yel_sink.is_connected() | self.led_grn_sink.is_connected()).implies(self.pwr_led.is_connected()), - "power required when LEDs used", - ) - if self.get(self.led_yel_sink.is_connected()): - self.led_yel_res = self.Block( - Resistor( - (1 / self.led_target_current).shrink_multiply( - self.pwr_led.link().voltage - self.led_yel_sink.link().output_thresholds.lower() - ) - ) - ) - self.connect(self.pwr_led.net, self.conn.led_yel_anode) - self.connect(self.conn.led_yel_cathode, self.led_yel_res.a) - self.connect(self.led_yel_res.b, self.led_yel_sink.net) - self.assign( - self.led_yel_sink.current_draw, -self.pwr_led.link().voltage / self.led_yel_res.actual_resistance - ) - - if self.get(self.led_grn_sink.is_connected()): - self.led_grn_res = self.Block( - Resistor( - (1 / self.led_target_current).shrink_multiply( - self.pwr_led.link().voltage - self.led_grn_sink.link().output_thresholds.lower() - ) - ) - ) - self.connect(self.pwr_led.net, self.conn.led_grn_anode) - self.connect(self.conn.led_grn_cathode, self.led_grn_res.a) - self.connect(self.led_grn_res.b, self.led_grn_sink.net) - self.assign( - self.led_grn_sink.current_draw, -self.pwr_led.link().voltage / self.led_grn_res.actual_resistance - ) - - self.cap = self.Block(Capacitor(1 * nFarad(tol=0.2), voltage=(0, 1000) * Volt)) # termination - self.connect(self.cap.neg, self.gnd.net) - self.connect(self.cap.pos, self.conn.shield) - - -class W5500_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.agnd = self.Port(Ground()) - self.gnd = self.Port(Ground()) - - self.avdd = self.Port( - VoltageSink( - voltage_limits=(2.97, 3.63) * Volt, - current_draw=(13, 132) * mAmp, # power down to 100M link, arbitrarily lumped into avdd - ) - ) - self.vdd = self.Port(VoltageSink(voltage_limits=(2.97, 3.63) * Volt)) - - self.v1v20 = self.Port(VoltageSource(voltage=1.2 * Volt(tol=0), current_limits=0 * Amp(tol=0))) - self.tocap = self.Port(VoltageSource(voltage=self.avdd.link().voltage)) # assumed, not documented - self.exres1 = self.Port(AnalogSource.from_supply(self.gnd, self.avdd)) # assumed, not documented - - self.crystal = self.Port(CrystalDriver(frequency_limits=25 * MHertz(tol=30e-6))) # TODO also support CLKIN - - self.txp = self.Port(Passive()) - self.txn = self.Port(Passive()) - self.rxp = self.Port(Passive()) - self.rxn = self.Port(Passive()) - - dio_model = DigitalBidir.from_supply( - self.gnd, - self.vdd, - voltage_limit_abs=(-0.3, 5.5) * Volt, - input_threshold_abs=(0.8, 2.0) * Volt, - current_limits=(-5, 5) * mAmp, # absolute max rating for DC input current - ) - dio_pu_model = DigitalSink.from_supply( - self.gnd, - self.vdd, - voltage_limit_abs=(-0.3, 5.5) * Volt, - input_threshold_abs=(0.8, 2.0) * Volt, - pullup_capable=True, - ) - - self.spi = self.Port(SpiPeripheral(dio_model)) - self.scsn = self.Port(dio_pu_model) - # according to some internet forum posts, a reset pulse is not needed - self.rstn = self.Port(dio_pu_model, optional=True) - self.intn = self.Port(DigitalSource.low_from_supply(self.gnd), optional=True) - - # PMODE[0..2] internally pulled up, defaulting to auto-negotiation - self.pmode0 = self.Port(dio_pu_model, optional=True) - self.pmode1 = self.Port(dio_pu_model, optional=True) - self.pmode2 = self.Port(dio_pu_model, optional=True) - - # TODO add LEDs - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - "U", - "Package_QFP:LQFP-48_7x7mm_P0.5mm", - { - "1": self.txn, - "2": self.txp, - ("3", "9", "14", "16", "19", "48"): self.agnd, - ("4", "8", "11", "15", "17", "21"): self.avdd, - "5": self.rxn, - "6": self.rxp, - # "7": DNC - "10": self.exres1, - # ("12", "13"): NC - # "18": VBG, "must be left floating" - "20": self.tocap, - "22": self.v1v20, - "23": self.gnd, # RSVD, "must be tied to GND" - # "24": self.spdled, - # "25": self.linkled, - # "26": self.dupled, - # "27": self.actled, - "28": self.vdd, - "29": self.gnd, - "30": self.crystal.xtal_in, - "31": self.crystal.xtal_out, - "32": self.scsn, - "33": self.spi.sck, - "34": self.spi.miso, - "35": self.spi.mosi, - "36": self.intn, - "37": self.rstn, - # ("38", "39", "40", "41", "42"): NC - "43": self.pmode2, - "44": self.pmode1, - "45": self.pmode0, - # ("46", "47"): NC - }, - "Wiznet", - "W5500", - ) - self.assign(self.lcsc_part, "C32843") - self.assign(self.actual_basic_part, False) - - -class W5500(Resettable, Interface, Block): - """SPI Ethernet controller supporting 10/100Mbps ethernet and onboard TCP/IP stack.""" - - def __init__(self, *, damping_resistance: RangeLike = 33 * Ohm(tol=0.05)) -> None: - super().__init__() - self.damping_resistance = self.ArgParameter(damping_resistance) - - self.ic = self.Block(W5500_Device()) - self.gnd = self.Export(self.ic.gnd, [Common]) - self.pwr = self.Export(self.ic.vdd, [Power]) - - self.eth = self.Port(EthernetMdi100BaseTxPhyPort.empty()) - self.spi = self.Export(self.ic.spi) - self.cs = self.Export(self.ic.scsn) - self.int = self.Export(self.ic.intn, optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.connect(self.reset, self.ic.rstn) - self.connect(self.gnd, self.ic.agnd) - self.l = self.Block(SeriesPowerFerriteBead(hf_impedance=(100, 2000) * Ohm)).connected(self.pwr, self.ic.avdd) - - self.crystal = self.Block(OscillatorReference(frequency=25 * MHertz(tol=30e-6))) - self.connect(self.crystal.gnd, self.gnd) - self.connect(self.crystal.crystal, self.ic.crystal) - - with self.implicit_connect(ImplicitConnect(self.gnd, [Common])) as imp: - self.exres1 = imp.Block(AnalogSetpointResistor(12.4 * kOhm(tol=0.01))).connected(io=self.ic.exres1) - self.c1v20 = imp.Block(DecouplingCapacitor(10 * nFarad(tol=0.2))).connected(pwr=self.ic.v1v20) - self.tocap = imp.Block(DecouplingCapacitor(4.7 * uFarad(tol=0.2))).connected(pwr=self.ic.tocap) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ImplicitConnect(self.ic.vdd, [Power]), - ) as imp: - self.vdd_cap0 = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.vdd_cap1 = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - - with self.implicit_connect( - ImplicitConnect(self.gnd, [Common]), - ImplicitConnect(self.ic.avdd, [Power]), - ) as imp: - self.avdd_caps = ElementDict[DecouplingCapacitor]() - for i in range(6): - self.avdd_caps[str(i)] = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.2))) - self.avdd_caps[6] = imp.Block(DecouplingCapacitor(10 * uFarad(tol=0.2))) - - # TODO parameterize PMODE configuration - self.connect(self.ic.pmode0, self.ic.pmode1, self.ic.pmode2, self.pwr.as_digital_source()) - - # optional damping resistors for EMI reduction - damp_resistor_model = Resistor(self.damping_resistance) - self.txp_damp = self.Block(damp_resistor_model) - self.txn_damp = self.Block(damp_resistor_model) - self.connect(self.txp_damp.a, self.ic.txp) - self.connect(self.txn_damp.a, self.ic.txn) - self.rxp_damp = self.Block(damp_resistor_model) - self.rxn_damp = self.Block(damp_resistor_model) - self.connect(self.rxp_damp.a, self.ic.rxp) - self.connect(self.rxn_damp.a, self.ic.rxn) - - # Ethernet termination circuit - bias_resistor_model = Resistor(49.9 * Ohm(tol=0.01)) - self.txp_bias = self.Block(bias_resistor_model) - self.txn_bias = self.Block(bias_resistor_model) - self.txc_bias = self.Block(Resistor(10 * Ohm(tol=0.01))) - self.connect(self.txp_bias.a, self.txn_bias.a, self.txc_bias.a) - self.connect(self.txc_bias.a.adapt_to(VoltageSink()), self.ic.avdd) - self.connect(self.txp_damp.b, self.txp_bias.b, self.eth.tx.pos) - self.connect(self.txn_damp.b, self.txn_bias.b, self.eth.tx.neg) - self.txc_cap = self.Block(Capacitor(22 * nFarad(tol=0.2), voltage=(0, 5) * Volt)) - self.connect(self.txc_bias.b, self.txc_cap.pos, self.eth.tx.center) - self.connect(self.txc_cap.neg.adapt_to(Ground()), self.gnd) - - ac_cap_model = Capacitor(6.8 * nFarad(tol=0.2), voltage=(0, 5) * Volt) - self.rxp_ac = self.Block(ac_cap_model) - self.rxn_ac = self.Block(ac_cap_model) - self.connect(self.rxp_ac.pos, self.eth.rx.pos) - self.connect(self.rxn_ac.pos, self.eth.rx.neg) - self.rxp_bias = self.Block(bias_resistor_model) - self.rxn_bias = self.Block(bias_resistor_model) - self.connect(self.rxp_damp.b, self.rxp_bias.a, self.rxp_ac.neg) - self.connect(self.rxn_damp.b, self.rxn_bias.a, self.rxn_ac.neg) - self.rxc_cap = self.Block(Capacitor(10 * nFarad(tol=0.2), voltage=(0, 5) * Volt)) - self.connect(self.rxc_cap.pos, self.eth.rx.center, self.rxp_bias.b, self.rxn_bias.b) - self.connect(self.rxc_cap.neg.adapt_to(Ground()), self.gnd) - - -class Tps2378_Device(InternalSubcircuit, FootprintBlock, JlcPart): - def __init__(self) -> None: - super().__init__() - - self.vss = self.Port(Ground()) - self.vdd = self.Port( - VoltageSink.from_gnd(self.vss, voltage_limits=(0, 57) * Volt, current_draw=(285, 500) * uAmp) - ) - self.den = self.Port(Passive()) # AnalogSink - self.cls = self.Port(AnalogSource()) - - self.rtn = self.Port(Ground()) - self.cdb = self.Port(DigitalSource.low_from_supply(self.rtn), optional=True) # -0.3 - 100v standoff limit - self.t2p = self.Port(DigitalSource.low_from_supply(self.rtn), optional=True) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( - "U", - "Package_SO:HSOP-8-1EP_3.9x4.9mm_P1.27mm_EP2.41x3.1mm_ThermalVias", - { - "1": self.vdd, - "2": self.den, - "3": self.cls, - "4": self.vss, - "5": self.rtn, - "6": self.cdb, - "7": self.t2p, - "8": self.rtn, # APD, connect to RTN if unused - "9": self.vss, - # ("4", "5", "6", "7", "8"): NC - }, - mfr="Texas Instruments", - part="TPS2378", - datasheet="https://www.ti.com/lit/ds/symlink/tps2378.pdf", - pnp_rot=-90, - ) - self.assign(self.lcsc_part, "C337500") - self.assign(self.actual_basic_part, False) - - -class Tps2378(Interface, GeneratorBlock): - def __init__(self, poe_class: IntLike = 0) -> None: - super().__init__() - self.poe_class = self.ArgParameter(poe_class) - self.generator_param(self.poe_class) - - self.ic = self.Block(Tps2378_Device()) - self.gnd = self.Export(self.ic.rtn, [Common]) - self.pwr_out = self.Port(VoltageSource.empty(), [Output]) - - self.poe = self.Port(PoeDevicePort(), [Input], doc="PoE input") - - self.cdb = self.Export( - self.ic.cdb, - doc="active-low output when the in inrush limiting, intended to disable a downstream converter", - optional=True, - ) - self.t2p = self.Export(self.ic.t2p, doc="active-low output indicating type-2 PSE", optional=True) - - @override - def generate(self) -> None: - super().generate() - - POE_VOUT_MIN = 37 - POE_VOUT_MAX = 57 - - poe_class = self.get(self.poe_class) - if poe_class == 0: - cls_res = 270 * Ohm(tol=0.05) - output_power_max = 12.95 - elif poe_class == 1: - cls_res = 243 * Ohm(tol=0.05) - output_power_max = 3.84 - elif poe_class == 2: - cls_res = 137 * Ohm(tol=0.05) - output_power_max = 6.49 - elif poe_class == 3: - cls_res = 90.9 * Ohm(tol=0.05) - output_power_max = 12.95 - elif poe_class == 4: - cls_res = 63.4 * Ohm(tol=0.05) - output_power_max = 25.5 - self.require(self.t2p.is_connected(), "class 4 devices must use T2P to draw >13W") - else: - raise ValueError(f"unsupported PoE class {poe_class}") - - self.cls = self.Block(AnalogSetpointResistor(cls_res)).connected(self.ic.vss, self.ic.cls) - - self.den = self.Block(Resistor(24.9 * kOhm(tol=0.01))) - self.connect(self.den.a, self.poe.pos) - self.connect(self.den.b, self.ic.den) - - self.connect( - self.poe.pos.adapt_to( - VoltageSource( - voltage=(POE_VOUT_MIN, POE_VOUT_MAX) * Volt, current_limits=(0, output_power_max / POE_VOUT_MAX) - ) - ), - self.ic.vdd, - self.pwr_out, - ) - self.connect(self.poe.neg.adapt_to(Ground()), self.ic.vss) - - with self.implicit_connect( - ImplicitConnect(self.ic.vss, [Common]), - ImplicitConnect(self.ic.vdd, [Power]), - ) as imp: - self.vdd_cap = imp.Block(DecouplingCapacitor(0.1 * uFarad(tol=0.1))) - self.prot = imp.Block(ProtectionZenerDiode((57, 66) * Volt)) # based on SMAJ58A as recommended in datasheet - class IotThermalCamera(JlcBoardTop): """Dual-mode IR and RGB camera board with ESP32 and ethernet PoE"""