Skip to content

Commit

Permalink
Nondirectioned Ground refactor (#364)
Browse files Browse the repository at this point in the history
This introduces a dedicated non-directioned Ground type, with an
optional GroundReference which can specify the voltage level. Ground can
optionally specify a voltage tolerance.

Prior, Ground was actually just a VoltageSink in disguise, which had a
few problems stemming from the VoltageSink/VoltageSource directionality
that this fixes:
- Avoids needing explicit VoltageSource merges when connecting the
ground of multiple power supplies, eg USB + battery
- Avoids a confusing gnd and gnd_src for devices that can source or sink
power
- Eliminates unused current_draw parameter

This refactors a lot of internal libraries. Notable structural changes:
- BLDC driver current-sense resistors moved into the block, generating
if the sense pins are used.
- Ditto with the soldering iron connector
- Gate driver switch node is now a Ground
- Unify gnd and gnd_src for blocks that have both
- DecouplingCapacitor defined in terms of gnd/voltage instead of pos/neg

This change is breaking and requires some changes to top-level designs:
- Voltage-source merges for grounds for multiple power sources must be
removed
- GroundSource should be replaced with Ground (remapped with a
deprecation)
- VoltageTestPoint must be replaced with GroundTestPoint, now that the
types are different
- In general, ground-side current sense resistors are more difficult
now, the move so far is to move them into the associated block

Library cleanup to support offset grounds is needed in the future, to
make sure all devices use VoltageSink.from_gnd to use a ground-relative
voltage instead of assuming ground is 0v.

Also some internal cleanup.

Notes for the future:
- Some things that might need further thought
- Is there a clean unified way to get the voltage of a port accounting
for connected-ness and directionality? Currently, we have
`GroundLink._voltage_range` and `VoltageLink,_voltage_range`, though
code that doesn't need to be as robust uses port.link().voltage
  • Loading branch information
ducky64 authored Jul 13, 2024
1 parent 8390382 commit becfb8f
Show file tree
Hide file tree
Showing 96 changed files with 1,113 additions and 1,012 deletions.
9 changes: 5 additions & 4 deletions edg/abstract_parts/AbstractCapacitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,17 +328,18 @@ def __init__(self, capacitance: RangeLike, *, exact_capacitance: BoolLike = Fals
super().__init__()

self.cap = self.Block(Capacitor(capacitance, voltage=RangeExpr(), exact_capacitance=exact_capacitance))
self.gnd = self.Export(self.cap.neg.adapt_to(VoltageSink()), [Common])
self.pwr = self.Export(self.cap.pos.adapt_to(VoltageSink(
voltage_limits=(self.cap.actual_voltage_rating + self.gnd.link().voltage).hull(self.gnd.link().voltage),
self.gnd = self.Export(self.cap.neg.adapt_to(Ground()), [Common])
self.pwr = self.Export(self.cap.pos.adapt_to(VoltageSink.from_gnd(
self.gnd,
voltage_limits=self.cap.actual_voltage_rating,
current_draw=0*Amp(tol=0)
)), [Power, InOut])

self.assign(self.cap.voltage, self.pwr.link().voltage - self.gnd.link().voltage)

# TODO there should be a way to forward the description string of the inner element

def connected(self, gnd: Optional[Port[VoltageLink]] = None, pwr: Optional[Port[VoltageLink]] = None) -> \
def connected(self, gnd: Optional[Port[GroundLink]] = None, pwr: Optional[Port[VoltageLink]] = None) -> \
'DecouplingCapacitor':
"""Convenience function to connect both ports, returning this object so it can still be given a name."""
if gnd is not None:
Expand Down
2 changes: 1 addition & 1 deletion edg/abstract_parts/AbstractDevices.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def __init__(self, voltage: RangeLike,
super().__init__()

self.pwr = self.Port(VoltageSource.empty()) # set by subclasses
self.gnd = self.Port(GroundSource.empty())
self.gnd = self.Port(Ground.empty())

self.voltage = self.ArgParameter(voltage)
self.capacity = self.ArgParameter(capacity)
Expand Down
2 changes: 1 addition & 1 deletion edg/abstract_parts/AbstractLed.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def __init__(self, count: IntLike, color: LedColorLike = Led.Any, *,
current_draw: RangeLike = (1, 10) * mAmp):
super().__init__()
self.signals = self.Port(Vector(DigitalSink.empty()), [InOut])
self.gnd = self.Port(VoltageSink.empty(), [Common])
self.gnd = self.Port(Ground.empty(), [Common])

self.color = self.ArgParameter(color)
self.current_draw = self.ArgParameter(current_draw)
Expand Down
8 changes: 4 additions & 4 deletions edg/abstract_parts/AbstractOpamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(self):
class MultipackOpampGenerator(MultipackOpamp, GeneratorBlock):
"""Skeleton base class that provides scaffolding for common packed opamp definitions"""
class OpampPorts(NamedTuple):
gnd: VoltageSink
gnd: Ground
pwr: VoltageSink
amps: List[Tuple[AnalogSink, AnalogSink, AnalogSource]] # amp-, amp+, out

Expand All @@ -68,17 +68,17 @@ def generate(self):
super().generate()
amp_ports = self._make_multipack_opamp()

self.gnd_merge = self.Block(PackedVoltageSource())
self.gnd_merge = self.Block(PackedGround())
self.pwr_merge = self.Block(PackedVoltageSource())
self.connect(self.gnd_merge.pwr_out, amp_ports.gnd)
self.connect(self.gnd_merge.gnd_out, amp_ports.gnd)
self.connect(self.pwr_merge.pwr_out, amp_ports.pwr)

requested = self.get(self.pwr.requested())
assert self.get(self.gnd.requested()) == self.get(self.inp.requested()) == \
self.get(self.inn.requested()) == self.get(self.out.requested()) == requested
for i, (amp_neg, amp_pos, amp_out) in zip(requested, amp_ports.amps):
self.connect(self.pwr.append_elt(VoltageSink.empty(), i), self.pwr_merge.pwr_ins.request(i))
self.connect(self.gnd.append_elt(VoltageSink.empty(), i), self.gnd_merge.pwr_ins.request(i))
self.connect(self.gnd.append_elt(Ground.empty(), i), self.gnd_merge.gnd_ins.request(i))
self.connect(self.inn.append_elt(AnalogSink.empty(), i), amp_neg)
self.connect(self.inp.append_elt(AnalogSink.empty(), i), amp_pos)
self.connect(self.out.append_elt(AnalogSource.empty(), i), amp_out)
2 changes: 1 addition & 1 deletion edg/abstract_parts/AbstractResistor.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class PulldownResistorArray(TypedTestPoint, GeneratorBlock):
@init_in_parent
def __init__(self, resistance: RangeLike):
super().__init__()
self.gnd = self.Port(VoltageSink.empty(), [Common])
self.gnd = self.Port(Ground.empty(), [Common])
self.io = self.Port(Vector(DigitalSingleSource.empty()), [InOut])
self.generator_param(self.io.requested())
self.resistance = self.ArgParameter(resistance)
Expand Down
12 changes: 12 additions & 0 deletions edg/abstract_parts/AbstractTestPoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ def contents(self):
self.assign(conn_tp.tp_name, (self.tp_name == "").then_else(self.io.link().name(), self.tp_name))


class GroundTestPoint(BaseTypedTestPoint, Block):
"""Test point with a VoltageSink port."""
def __init__(self, *args):
super().__init__(*args)
self.io = self.Port(Ground.empty(), [InOut])
self.connect(self.io, self.tp.io.adapt_to(Ground()))

def connected(self, io: Port[GroundLink]) -> 'GroundTestPoint':
cast(Block, builder.get_enclosing_block()).connect(io, self.io)
return self


class VoltageTestPoint(BaseTypedTestPoint, Block):
"""Test point with a VoltageSink port."""
def __init__(self, *args):
Expand Down
6 changes: 6 additions & 0 deletions edg/abstract_parts/DummyDevices.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ def __init__(self) -> None:
self.io = self.Port(Passive(), [InOut])


class DummyGround(DummyDevice):
def __init__(self) -> None:
super().__init__()
self.gnd = self.Port(Ground(), [Common, InOut])


class DummyVoltageSource(DummyDevice):
@init_in_parent
def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO,
Expand Down
2 changes: 1 addition & 1 deletion edg/abstract_parts/GateDrivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, has_boot_diode: BoolLike):
self.low_out = self.Port(DigitalSource.empty()) # referenced to main gnd

self.high_pwr = self.Port(VoltageSink.empty(), optional=True) # not used with internal boot diode
self.high_gnd = self.Port(VoltageSink.empty()) # this encodes the voltage limit from gnd
self.high_gnd = self.Port(Ground.empty()) # a separate constraint needs to encode voltage limits
self.high_out = self.Port(DigitalSource.empty()) # referenced to high_pwr and high_gnd


Expand Down
15 changes: 2 additions & 13 deletions edg/abstract_parts/IoControllerInterfaceMixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,15 @@ class IoControllerBle(BlockInterfaceMixin[BaseIoController]):
"""Mixin indicating this IoController has programmable Bluetooth LE. Does not expose any ports."""


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


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


class IoControllerUsbOut(IoControllerGroundOut, BlockInterfaceMixin[IoController]):
class IoControllerUsbOut(BlockInterfaceMixin[IoController]):
"""IO controller mixin that provides an output of the IO controller's USB Vbus."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
Expand Down
6 changes: 3 additions & 3 deletions edg/abstract_parts/PowerCircuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ def contents(self):
self.connect(self.high_gate_res.b, self.high_fet.gate)

# to avoid tolerance stackup, model the switch node as a static voltage
self.connect(self.low_fet.drain, self.high_fet.source)
self.connect(self.low_fet.drain.adapt_to(VoltageSource(
voltage_out=self.pwr.link().voltage)),
self.high_fet.source.adapt_to(VoltageSink()),
self.driver.high_gnd,
voltage_out=self.pwr.link().voltage)),
self.out)
self.connect(self.out.as_ground((0, 0)*Amp), self.driver.high_gnd) # TODO model driver current


class FetHalfBridgeIndependent(FetHalfBridge, HalfBridgeIndependent):
Expand Down
2 changes: 1 addition & 1 deletion edg/abstract_parts/UsbConnectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UsbDeviceConnector(UsbConnector, PowerSource):
def __init__(self) -> None:
super().__init__()
self.pwr = self.Port(VoltageSource.empty(), optional=True)
self.gnd = self.Port(GroundSource.empty())
self.gnd = self.Port(Ground.empty())

self.usb = self.Port(UsbHostPort.empty(), optional=True)

Expand Down
8 changes: 4 additions & 4 deletions edg/abstract_parts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
from .AbstractOscillator import Oscillator, TableOscillator
from .AbstractDebugHeaders import SwdCortexTargetConnector, SwdCortexTargetConnectorReset, \
SwdCortexTargetConnectorSwo, SwdCortexTargetConnectorTdi
from .AbstractTestPoint import TestPoint, VoltageTestPoint, DigitalTestPoint, DigitalArrayTestPoint, AnalogTestPoint, \
I2cTestPoint, SpiTestPoint, CanControllerTestPoint
from .AbstractTestPoint import TestPoint, GroundTestPoint, VoltageTestPoint, DigitalTestPoint, DigitalArrayTestPoint, \
AnalogTestPoint, I2cTestPoint, SpiTestPoint, CanControllerTestPoint
from .AbstractTestPoint import AnalogRfTestPoint
from .AbstractJumper import Jumper, DigitalJumper
from .PassiveConnector import PassiveConnector, FootprintPassiveConnector
Expand All @@ -99,8 +99,8 @@
from .PinMappable import PinResource, PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource
from .VariantPinRemapper import VariantPinRemapper

from .DummyDevices import DummyPassive, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, DummyAnalogSource, \
DummyAnalogSink
from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, \
DummyAnalogSource, DummyAnalogSink
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltage, ForcedVoltageCurrent, ForcedAnalogVoltage,\
ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController
4 changes: 2 additions & 2 deletions edg/abstract_parts/test_ideal_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
class IdealCircuitTestTop(Block):
def __init__(self):
super().__init__()
self.gnd = self.Block(DummyVoltageSource(0*Volt(tol=0)))
self.gnd = self.Block(DummyGround())
self.pwr = self.Block(DummyVoltageSource(5*Volt(tol=0)))
with self.implicit_connect(
ImplicitConnect(self.gnd.pwr, [Common]),
ImplicitConnect(self.gnd.gnd, [Common]),
) as imp:
self.reg = imp.Block(LinearRegulator(2*Volt(tol=0)))
self.connect(self.reg.pwr_in, self.pwr.pwr)
Expand Down
2 changes: 1 addition & 1 deletion edg/abstract_parts/test_opamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self):
(self.dummyref, ), _ = self.chain(self.dut.reference, self.Block(AnalogSourceDummy()))
(self.dummyout, ), _ = self.chain(self.dut.output, self.Block(DummyAnalogSink()))
(self.dummypwr, ), _ = self.chain(self.dut.pwr, self.Block(DummyVoltageSource()))
(self.dummygnd, ), _ = self.chain(self.dut.gnd, self.Block(DummyVoltageSource()))
(self.dummygnd, ), _ = self.chain(self.dut.gnd, self.Block(DummyGround()))


class OpampCircuitTest(unittest.TestCase):
Expand Down
5 changes: 3 additions & 2 deletions edg/electronics_model/AnalogPort.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from ..core import *
from .CircuitBlock import CircuitLink
from .GroundPort import GroundLink
from .VoltagePorts import CircuitPort, CircuitPortBridge, VoltageLink


Expand Down Expand Up @@ -116,7 +117,7 @@ class AnalogSink(AnalogBase):
bridge_type = AnalogSinkBridge

@staticmethod
def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink], *,
def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *,
voltage_limit_tolerance: Optional[RangeLike] = None,
voltage_limit_abs: Optional[RangeLike] = None,
signal_limit_tolerance: Optional[RangeLike] = None,
Expand Down Expand Up @@ -172,7 +173,7 @@ class AnalogSource(AnalogBase):
bridge_type = AnalogSourceBridge

@staticmethod
def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink], *,
def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *,
signal_out_bound: Optional[Tuple[FloatLike, FloatLike]] = None,
current_limits: RangeLike = RangeExpr.ALL,
impedance: RangeLike = RangeExpr.ZERO):
Expand Down
7 changes: 5 additions & 2 deletions edg/electronics_model/CircuitBlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
from ..core.ConstraintExpr import Refable
from .KiCadImportableBlock import KiCadImportableBlock

if TYPE_CHECKING:
from .VoltagePorts import CircuitPort

CircuitLinkType = TypeVar('CircuitLinkType', bound=Link)
class CircuitPort(Port[CircuitLinkType], Generic[CircuitLinkType]):
"""Electrical connection that represents a single port into a single copper net"""
pass


T = TypeVar('T', bound=BasePort)
Expand Down
23 changes: 23 additions & 0 deletions edg/electronics_model/CircuitPackingBlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ..core import *
from .PassivePort import Passive
from .GroundPort import Ground, GroundReference
from .VoltagePorts import VoltageSource, VoltageSink


Expand Down Expand Up @@ -30,6 +31,28 @@ def generate(self):
self.elts.append_elt(Passive(), request)


class PackedGround(NetPackingBlock, GeneratorBlock):
"""Takes in several Ground connections that are of the same net (asserted in netlister),
and provides a single Ground."""
def __init__(self):
super().__init__()
self.gnd_ins = self.Port(Vector(Ground.empty()))
self.gnd_out = self.Port(GroundReference(
voltage_out=RangeExpr(),
))
self.generator_param(self.gnd_ins.requested())
self.packed(self.gnd_ins, self.gnd_out)

def generate(self):
super().generate()
self.gnd_ins.defined()
for in_request in self.get(self.gnd_ins.requested()):
self.gnd_ins.append_elt(Ground(), in_request)

self.assign(self.gnd_out.voltage_out,
self.gnd_ins.hull(lambda x: x.link().voltage))


class PackedVoltageSource(NetPackingBlock, GeneratorBlock):
"""Takes in several VoltageSink connections that are of the same net (asserted in netlister),
and provides a single VoltageSource. Distributes the current draw from the VoltageSource
Expand Down
22 changes: 12 additions & 10 deletions edg/electronics_model/DigitalPorts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Optional, Tuple
from ..core import *
from .CircuitBlock import CircuitLink, CircuitPortBridge, CircuitPortAdapter
from .GroundPort import GroundLink
from .VoltagePorts import CircuitPort, VoltageLink, VoltageSource
from .Units import Volt

Expand Down Expand Up @@ -188,7 +189,7 @@ class DigitalSink(DigitalBase):
bridge_type = DigitalSinkBridge

@staticmethod
def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink], *,
def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink], *,
voltage_limit_abs: Optional[RangeLike] = None,
voltage_limit_tolerance: Optional[RangeLike] = None,
current_draw: RangeLike = RangeExpr.ZERO,
Expand Down Expand Up @@ -276,17 +277,17 @@ class DigitalSource(DigitalBase):
bridge_type = DigitalSourceBridge

@staticmethod
def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink],
def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink],
current_limits: RangeLike = RangeExpr.ALL, *,
output_threshold_offset: Optional[Tuple[FloatLike, FloatLike]] = None) -> DigitalSource:
supply_range = VoltageLink._supply_voltage_range(neg, pos)
if output_threshold_offset is not None:
output_offset_low = FloatExpr._to_expr_type(output_threshold_offset[0])
output_offset_high = FloatExpr._to_expr_type(output_threshold_offset[1])
output_threshold = (VoltageLink._voltage_range(neg).upper() + output_offset_low,
output_threshold = (GroundLink._voltage_range(neg).lower() + output_offset_low,
VoltageLink._voltage_range(pos).lower() + output_offset_high)
else:
output_threshold = (VoltageLink._voltage_range(neg).upper(), VoltageLink._voltage_range(pos).lower())
output_threshold = (GroundLink._voltage_range(neg).upper(), VoltageLink._voltage_range(pos).lower())

return DigitalSource(
voltage_out=supply_range,
Expand Down Expand Up @@ -365,7 +366,7 @@ class DigitalBidir(DigitalBase):
not_connected_type = DigitalBidirNotConnected

@staticmethod
def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink],
def from_supply(neg: Port[GroundLink], pos: Port[VoltageLink],
voltage_limit_abs: Optional[RangeLike] = None,
voltage_limit_tolerance: Optional[RangeLike] = None,
current_draw: RangeLike = RangeExpr.ZERO,
Expand All @@ -384,11 +385,13 @@ def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink],
else: # generic default
voltage_limit = supply_range + RangeExpr._to_expr_type((-0.3, 0.3))

neg_base = GroundLink._voltage_range(neg).upper()
input_threshold: RangeLike
if input_threshold_factor is not None:
assert input_threshold_abs is None, "can only specify one input threshold type"
input_threshold_factor = RangeExpr._to_expr_type(input_threshold_factor) # TODO avoid internal functions?
input_threshold = VoltageLink._voltage_range(pos) * input_threshold_factor
input_range = VoltageLink._voltage_range(pos).lower() - neg_base
input_threshold = RangeExpr._to_expr_type(neg_base) + RangeExpr._to_expr_type(input_range) * input_threshold_factor
elif input_threshold_abs is not None:
assert input_threshold_factor is None, "can only specify one input threshold type"
input_threshold = RangeExpr._to_expr_type(input_threshold_abs) # TODO avoid internal functions?
Expand All @@ -400,14 +403,13 @@ def from_supply(neg: Port[VoltageLink], pos: Port[VoltageLink],
assert output_threshold_abs is None, "can only specify one output threshold type"
output_threshold_factor = RangeExpr._to_expr_type(output_threshold_factor)
# use a pessimistic range
output_range = VoltageLink._voltage_range(pos).lower() - VoltageLink._voltage_range(neg).upper()
output_threshold = (VoltageLink._voltage_range(neg).upper() + output_threshold_factor.lower() * output_range,
VoltageLink._voltage_range(neg).upper() + output_threshold_factor.upper() * output_range)
output_range = VoltageLink._voltage_range(pos).lower() - neg_base
output_threshold = RangeExpr._to_expr_type(neg_base) + output_threshold_factor * RangeExpr._to_expr_type(output_range)
elif output_threshold_abs is not None:
assert output_threshold_factor is None, "can only specify one output threshold type"
output_threshold = RangeExpr._to_expr_type(output_threshold_abs) # TODO avoid internal functions?
else: # assumed ideal
output_threshold = (VoltageLink._voltage_range(neg).upper(), VoltageLink._voltage_range(pos).lower())
output_threshold = (neg_base, VoltageLink._voltage_range(pos).lower())

return DigitalBidir( # TODO get rid of to_expr_type w/ dedicated Range conversion
voltage_limits=voltage_limit,
Expand Down
Loading

0 comments on commit becfb8f

Please sign in to comment.