Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 37 additions & 25 deletions edg/abstract_parts/AbstractPowerConverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def _ilim_expr(inductor_ilim: RangeExpr, sw_ilim: RangeExpr, inductor_iripple: R
iout_limit_inductor = inductor_ilim - (inductor_iripple.upper() / 2)
iout_limit_sw = (sw_ilim.upper() > 0).then_else(
sw_ilim - (inductor_iripple.upper() / 2), Range.all())
return iout_limit_inductor.intersect(iout_limit_sw)
return iout_limit_inductor.intersect(iout_limit_sw).intersect(Range.from_lower(0))

@init_in_parent
def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike,
Expand All @@ -337,9 +337,11 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
ripple_ratio: RangeLike = Range.all()):
super().__init__()

self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models the input cap only
self.pwr_out = self.Port(VoltageSource.empty()) # models the output cap and inductor power source
self.switch = self.Port(VoltageSink.empty()) # current draw defined as average
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only
self.pwr_out = self.Port(VoltageSource.empty()) # models max output avg. current
# technically VoltageSink is the wrong model, but this is used to pass the current draw to the chip
# (and its input pin) without need the top-level to explicitly pass a parameter to the chip
self.switch = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw
self.gnd = self.Port(Ground.empty(), [Common])

self.input_voltage = self.ArgParameter(input_voltage)
Expand All @@ -360,6 +362,7 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc

self.actual_dutycycle = self.Parameter(RangeExpr())
self.actual_inductor_current_ripple = self.Parameter(RangeExpr())
self.actual_inductor_current_peak = self.Parameter(RangeExpr())

def contents(self):
super().contents()
Expand Down Expand Up @@ -390,14 +393,16 @@ def generate(self) -> None:
self._buck_inductor_filter, values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple)
))
self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance)
self.assign(self.actual_inductor_current_peak,
values.inductor_avg_current + self.actual_inductor_current_ripple / 2)

self.connect(self.switch, self.inductor.a.adapt_to(VoltageSink(
current_draw=self.pwr_out.link().current_drawn * values.dutycycle
current_draw=self.output_current * values.effective_dutycycle
)))
self.connect(self.pwr_out, self.inductor.b.adapt_to(VoltageSource(
voltage_out=self.output_voltage,
current_limits=self._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
self.actual_inductor_current_ripple)
self.actual_inductor_current_ripple) * self.efficiency
)))

self.in_cap = self.Block(DecouplingCapacitor(
Expand Down Expand Up @@ -521,16 +526,16 @@ def _calculate_parameters(cls, input_voltage: Range, output_voltage: Range, freq
@init_in_parent
def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike,
output_current: RangeLike, sw_current_limits: RangeLike, *,
input_voltage_ripple: FloatLike = 75*mVolt,
output_voltage_ripple: FloatLike = 25*mVolt,
input_voltage_ripple: FloatLike,
output_voltage_ripple: FloatLike,
efficiency: RangeLike = (0.8, 1.0), # from TI reference
dutycycle_limit: RangeLike = (0.1, 0.9), # arbitrary
ripple_ratio: RangeLike = Range.all()):
super().__init__()

self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models input cap and inductor power draw
self.pwr_out = self.Port(VoltageSink.empty()) # only used for the output cap
self.switch = self.Port(VoltageSource.empty()) # current draw defined as average
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # models input / inductor avg. current draw
self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only
self.switch = self.Port(VoltageSource.empty()) # models maximum output avg. current
self.gnd = self.Port(Ground.empty(), [Common])

self.input_voltage = self.ArgParameter(input_voltage)
Expand All @@ -551,6 +556,7 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc

self.actual_dutycycle = self.Parameter(RangeExpr())
self.actual_inductor_current_ripple = self.Parameter(RangeExpr())
self.actual_inductor_current_peak = self.Parameter(RangeExpr())

def contents(self):
super().contents()
Expand Down Expand Up @@ -582,15 +588,17 @@ def generate(self) -> None:
values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple)
))
self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance)
self.assign(self.actual_inductor_current_peak,
values.inductor_avg_current + self.actual_inductor_current_ripple / 2)

self.connect(self.pwr_in, self.inductor.a.adapt_to(VoltageSink(
current_draw=self.pwr_out.link().current_drawn / (1 - values.dutycycle)
current_draw=values.inductor_avg_current
)))
self.connect(self.switch, self.inductor.b.adapt_to(VoltageSource(
voltage_out=self.output_voltage,
current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
self.actual_inductor_current_ripple)
* self.input_voltage / self.output_voltage
* (1 - values.effective_dutycycle.upper)
)))

self.in_cap = self.Block(DecouplingCapacitor(
Expand Down Expand Up @@ -647,10 +655,10 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
ripple_ratio: RangeLike = Range.all()):
super().__init__()

self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # connected to the input cap, models input current
self.switch_in = self.Port(Passive.empty()) # models input high and low switch current draws
self.switch_out = self.Port(Passive.empty()) # models output high and low switch current draws
self.pwr_out = self.Port(VoltageSink.empty()) # only used for the output cap
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only
self.switch_in = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw
self.switch_out = self.Port(VoltageSource.empty()) # models maximum output avg. current
self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only
self.gnd = self.Port(Ground.empty(), [Common])

self.input_voltage = self.ArgParameter(input_voltage)
Expand All @@ -671,8 +679,7 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
self.actual_buck_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in buck mode
self.actual_boost_dutycycle = self.Parameter(RangeExpr()) # possible actual duty cycle in boost mode
self.actual_inductor_current_ripple = self.Parameter(RangeExpr())
self.actual_inductor_current = self.Parameter(RangeExpr()) # inductor current accounting for ripple (upper is peak)
self.actual_avg_current_rating = self.Parameter(RangeExpr()) # determined by inductor rating excl. ripple
self.actual_inductor_current_peak = self.Parameter(RangeExpr()) # inductor current accounting for ripple (upper is peak)

def contents(self):
super().contents()
Expand Down Expand Up @@ -713,13 +720,18 @@ def generate(self) -> None:
BuckConverterPowerPath._buck_inductor_filter,
combined_inductor_avg_current.upper, combined_ripple_scale, combined_min_ripple)
))
self.connect(self.switch_in, self.inductor.a)
self.connect(self.switch_out, self.inductor.b)
self.connect(self.switch_in, self.inductor.a.adapt_to(VoltageSink(
current_draw=combined_inductor_avg_current
)))
self.connect(self.switch_out, self.inductor.b.adapt_to(VoltageSource(
voltage_out=self.output_voltage,
current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
self.actual_inductor_current_ripple)
* (1 - boost_values.effective_dutycycle.upper)
)))
self.assign(self.actual_inductor_current_ripple, combined_ripple_scale / self.inductor.actual_inductance)
self.assign(self.actual_avg_current_rating,
BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
self.actual_inductor_current_ripple))
self.assign(self.actual_inductor_current, combined_inductor_avg_current + self.actual_inductor_current_ripple / 2)
self.assign(self.actual_inductor_current_peak,
combined_inductor_avg_current + self.actual_inductor_current_ripple / 2)

self.in_cap = self.Block(DecouplingCapacitor(
capacitance=buck_values.input_capacitance.intersect(boost_values.input_capacitance) * Farad,
Expand Down
19 changes: 19 additions & 0 deletions edg/abstract_parts/DummyDevices.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ def __init__(self, forced_current_draw: RangeLike) -> None:
), [Output])


class ForcedVoltageCurrentLimit(DummyDevice, NetBlock):
"""Forces some output current limit, which should be tighter than the input's actual current draw."""
@init_in_parent
def __init__(self, forced_current_limit: RangeLike) -> None:
super().__init__()

self.pwr_in = self.Port(VoltageSink(
current_draw=RangeExpr(),
voltage_limits=RangeExpr.ALL
), [Input])

self.pwr_out = self.Port(VoltageSource(
voltage_out=self.pwr_in.link().voltage,
current_limits=forced_current_limit
), [Output])

self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn)


class ForcedVoltage(DummyDevice, NetBlock):
"""Forces some voltage on the output regardless of the input's actual voltage.
Current draw is passed through unchanged."""
Expand Down
5 changes: 5 additions & 0 deletions edg/abstract_parts/PowerCircuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def __init__(self, frequency: RangeLike, fet_rds: RangeLike = (0, 1)*Ohm,
self.fet_rds = self.ArgParameter(fet_rds)
self.gate_res = self.ArgParameter(gate_res)

self.actual_current_limits = self.Parameter(RangeExpr())

def contents(self):
super().contents()
self.driver = self.Block(HalfBridgeDriver(has_boot_diode=True))
Expand Down Expand Up @@ -94,6 +96,9 @@ def contents(self):
self.out)
self.connect(self.out.as_ground((0, 0)*Amp), self.driver.high_gnd) # TODO model driver current

self.assign(self.actual_current_limits, self.low_fet.actual_drain_current_rating.intersect(
self.high_fet.actual_drain_current_rating))


class FetHalfBridgeIndependent(FetHalfBridge, HalfBridgeIndependent):
def contents(self):
Expand Down
4 changes: 2 additions & 2 deletions edg/abstract_parts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@

from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, \
DummyAnalogSource, DummyAnalogSink
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltage, ForcedVoltageCurrent, ForcedAnalogVoltage,\
ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltageCurrentLimit, ForcedVoltage, ForcedVoltageCurrent, \
ForcedAnalogVoltage, ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController

from .Nonstrict3v3Compatible import Nonstrict3v3Compatible
90 changes: 89 additions & 1 deletion edg/abstract_parts/test_switching_converters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import unittest

from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath
from ..core import Range
from ..electronics_model import *
from .AbstractInductor import Inductor
from .AbstractCapacitor import Capacitor
from .DummyDevices import DummyVoltageSource, DummyVoltageSink, DummyGround


class SwitchingConverterCalculationTest(unittest.TestCase):
Expand Down Expand Up @@ -87,3 +90,88 @@ def test_boost_converter_example(self):
self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.44, places=2)
# the example calculation output is wrong, this is the correct result of the formula
self.assertAlmostEqual(values.output_capacitance.lower, 9.95e-6, places=7)


class TestCapacitor(Capacitor):
def contents(self):
super().contents()
self.assign(self.actual_capacitance, self.capacitance)
self.assign(self.actual_voltage_rating, Range.all())


class TestInductor(Inductor):
def contents(self):
super().contents()
self.assign(self.actual_inductance, self.inductance)
self.assign(self.actual_current_rating, (0, 1.5)*Amp)
self.assign(self.actual_frequency_rating, Range.all())


class BuckPowerPathTestTop(DesignTop):
def __init__(self):
super().__init__()
self.dut = self.Block(BuckConverterPowerPath(
input_voltage=Range(4, 6), output_voltage=(2, 3),
frequency= Range.exact(100e3), output_current=Range(0.2, 1),
sw_current_limits=Range(0, 2),
input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt,
))
(self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in)
(self.switch, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.switch)
(self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.pwr_out)
(self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd)

self.require(self.dut.actual_dutycycle.contains(Range(0.334, 0.832)))
self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.433, 0.478)))
self.require(self.dut.pwr_out.current_limits.contains(Range(0.0, 1.260)))
self.require(self.dut.switch.current_draw.contains(Range(0.067, 0.832)))

def refinements(self) -> Refinements:
return Refinements(
class_refinements=[
(Capacitor, TestCapacitor),
(Inductor, TestInductor),
],
instance_values=[
(['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05))
]
)


class BoostPowerPathTestTop(DesignTop):
def __init__(self):
super().__init__()
self.dut = self.Block(BoostConverterPowerPath(
input_voltage=Range(4, 6), output_voltage=(10, 14),
frequency=Range.exact(200e3), output_current=Range(0.2, 0.5),
sw_current_limits=Range(0, 2),
input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt,
))
(self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in)
(self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_out)
(self.switch, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.switch)
(self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd)

self.require(self.dut.actual_dutycycle.contains(Range(0.4, 0.771)))
self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.495, 0.546)))
self.require(self.dut.pwr_in.current_draw.contains(Range(0.334, 2.185)))
self.require(self.dut.switch.current_limits.contains(Range(0.0, 0.280)))

def refinements(self) -> Refinements:
return Refinements(
class_refinements=[
(Capacitor, TestCapacitor),
(Inductor, TestInductor),
],
instance_values=[
(['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05))
]
)


class PowerPathBlockTest(unittest.TestCase):
def test_buck_power_path(self) -> None:
ScalaCompiler.compile(BuckPowerPathTestTop)

def test_boost_power_path(self) -> None:
ScalaCompiler.compile(BoostPowerPathTestTop)
Loading