Skip to content

Commit e895c5f

Browse files
authored
Standardize power path current draw conventions (#411)
Standardize conventions defined in the Buck/BoostConverterPowerPath: switch on buck is an VoltageSink that draws the average inductor current, and switch on the boost is a VoltageSource that provides the max average output current. Refactors the BuckBoostConverterPowerPath to use this convention. This isn't the cleanest, but this allows these parameters to propagate to the connected device (eg, buck converter controller IC's power-in pin) through the connection instead of needing to be plumbed explicitly. This makes the custom discrete converter a bit tougher, but probably a good trade-off. Additional fixes: - current limit on the boost power path, accounts for the boost ratio properly. - account for efficiency factor on current draw for buck and boost converters - updates examples to tolerate this - plumb rds_on through the custom converters - model switch current limits in custom converters (generally not the limiting factor, but still good practice) - add ForcedVoltageCurrentLimit as a library block - unit tests that run the buck and boost converter power path and check generated parameters Adds the actual inductor current peak for all converter power paths. Makes the discrete custom buck converter use this, instead of calculating it independently. Resolves #406 and #307
1 parent 73a7c6c commit e895c5f

File tree

12 files changed

+204
-58
lines changed

12 files changed

+204
-58
lines changed

edg/abstract_parts/AbstractPowerConverters.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def _ilim_expr(inductor_ilim: RangeExpr, sw_ilim: RangeExpr, inductor_iripple: R
325325
iout_limit_inductor = inductor_ilim - (inductor_iripple.upper() / 2)
326326
iout_limit_sw = (sw_ilim.upper() > 0).then_else(
327327
sw_ilim - (inductor_iripple.upper() / 2), Range.all())
328-
return iout_limit_inductor.intersect(iout_limit_sw)
328+
return iout_limit_inductor.intersect(iout_limit_sw).intersect(Range.from_lower(0))
329329

330330
@init_in_parent
331331
def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike,
@@ -337,9 +337,11 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
337337
ripple_ratio: RangeLike = Range.all()):
338338
super().__init__()
339339

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

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

361363
self.actual_dutycycle = self.Parameter(RangeExpr())
362364
self.actual_inductor_current_ripple = self.Parameter(RangeExpr())
365+
self.actual_inductor_current_peak = self.Parameter(RangeExpr())
363366

364367
def contents(self):
365368
super().contents()
@@ -390,14 +393,16 @@ def generate(self) -> None:
390393
self._buck_inductor_filter, values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple)
391394
))
392395
self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance)
396+
self.assign(self.actual_inductor_current_peak,
397+
values.inductor_avg_current + self.actual_inductor_current_ripple / 2)
393398

394399
self.connect(self.switch, self.inductor.a.adapt_to(VoltageSink(
395-
current_draw=self.pwr_out.link().current_drawn * values.dutycycle
400+
current_draw=self.output_current * values.effective_dutycycle
396401
)))
397402
self.connect(self.pwr_out, self.inductor.b.adapt_to(VoltageSource(
398403
voltage_out=self.output_voltage,
399404
current_limits=self._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
400-
self.actual_inductor_current_ripple)
405+
self.actual_inductor_current_ripple) * self.efficiency
401406
)))
402407

403408
self.in_cap = self.Block(DecouplingCapacitor(
@@ -521,16 +526,16 @@ def _calculate_parameters(cls, input_voltage: Range, output_voltage: Range, freq
521526
@init_in_parent
522527
def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequency: RangeLike,
523528
output_current: RangeLike, sw_current_limits: RangeLike, *,
524-
input_voltage_ripple: FloatLike = 75*mVolt,
525-
output_voltage_ripple: FloatLike = 25*mVolt,
529+
input_voltage_ripple: FloatLike,
530+
output_voltage_ripple: FloatLike,
526531
efficiency: RangeLike = (0.8, 1.0), # from TI reference
527532
dutycycle_limit: RangeLike = (0.1, 0.9), # arbitrary
528533
ripple_ratio: RangeLike = Range.all()):
529534
super().__init__()
530535

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

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

552557
self.actual_dutycycle = self.Parameter(RangeExpr())
553558
self.actual_inductor_current_ripple = self.Parameter(RangeExpr())
559+
self.actual_inductor_current_peak = self.Parameter(RangeExpr())
554560

555561
def contents(self):
556562
super().contents()
@@ -582,15 +588,17 @@ def generate(self) -> None:
582588
values.inductor_avg_current.upper, values.ripple_scale, values.min_ripple)
583589
))
584590
self.assign(self.actual_inductor_current_ripple, values.ripple_scale / self.inductor.actual_inductance)
591+
self.assign(self.actual_inductor_current_peak,
592+
values.inductor_avg_current + self.actual_inductor_current_ripple / 2)
585593

586594
self.connect(self.pwr_in, self.inductor.a.adapt_to(VoltageSink(
587-
current_draw=self.pwr_out.link().current_drawn / (1 - values.dutycycle)
595+
current_draw=values.inductor_avg_current
588596
)))
589597
self.connect(self.switch, self.inductor.b.adapt_to(VoltageSource(
590598
voltage_out=self.output_voltage,
591599
current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
592600
self.actual_inductor_current_ripple)
593-
* self.input_voltage / self.output_voltage
601+
* (1 - values.effective_dutycycle.upper)
594602
)))
595603

596604
self.in_cap = self.Block(DecouplingCapacitor(
@@ -647,10 +655,10 @@ def __init__(self, input_voltage: RangeLike, output_voltage: RangeLike, frequenc
647655
ripple_ratio: RangeLike = Range.all()):
648656
super().__init__()
649657

650-
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # connected to the input cap, models input current
651-
self.switch_in = self.Port(Passive.empty()) # models input high and low switch current draws
652-
self.switch_out = self.Port(Passive.empty()) # models output high and low switch current draws
653-
self.pwr_out = self.Port(VoltageSink.empty()) # only used for the output cap
658+
self.pwr_in = self.Port(VoltageSink.empty(), [Power]) # no modeling, input cap only
659+
self.switch_in = self.Port(VoltageSink.empty()) # models input / inductor avg. current draw
660+
self.switch_out = self.Port(VoltageSource.empty()) # models maximum output avg. current
661+
self.pwr_out = self.Port(VoltageSink.empty()) # no modeling, output cap only
654662
self.gnd = self.Port(Ground.empty(), [Common])
655663

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

677684
def contents(self):
678685
super().contents()
@@ -713,13 +720,18 @@ def generate(self) -> None:
713720
BuckConverterPowerPath._buck_inductor_filter,
714721
combined_inductor_avg_current.upper, combined_ripple_scale, combined_min_ripple)
715722
))
716-
self.connect(self.switch_in, self.inductor.a)
717-
self.connect(self.switch_out, self.inductor.b)
723+
self.connect(self.switch_in, self.inductor.a.adapt_to(VoltageSink(
724+
current_draw=combined_inductor_avg_current
725+
)))
726+
self.connect(self.switch_out, self.inductor.b.adapt_to(VoltageSource(
727+
voltage_out=self.output_voltage,
728+
current_limits=BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
729+
self.actual_inductor_current_ripple)
730+
* (1 - boost_values.effective_dutycycle.upper)
731+
)))
718732
self.assign(self.actual_inductor_current_ripple, combined_ripple_scale / self.inductor.actual_inductance)
719-
self.assign(self.actual_avg_current_rating,
720-
BuckConverterPowerPath._ilim_expr(self.inductor.actual_current_rating, self.sw_current_limits,
721-
self.actual_inductor_current_ripple))
722-
self.assign(self.actual_inductor_current, combined_inductor_avg_current + self.actual_inductor_current_ripple / 2)
733+
self.assign(self.actual_inductor_current_peak,
734+
combined_inductor_avg_current + self.actual_inductor_current_ripple / 2)
723735

724736
self.in_cap = self.Block(DecouplingCapacitor(
725737
capacitance=buck_values.input_capacitance.intersect(boost_values.input_capacitance) * Farad,

edg/abstract_parts/DummyDevices.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ def __init__(self, forced_current_draw: RangeLike) -> None:
106106
), [Output])
107107

108108

109+
class ForcedVoltageCurrentLimit(DummyDevice, NetBlock):
110+
"""Forces some output current limit, which should be tighter than the input's actual current draw."""
111+
@init_in_parent
112+
def __init__(self, forced_current_limit: RangeLike) -> None:
113+
super().__init__()
114+
115+
self.pwr_in = self.Port(VoltageSink(
116+
current_draw=RangeExpr(),
117+
voltage_limits=RangeExpr.ALL
118+
), [Input])
119+
120+
self.pwr_out = self.Port(VoltageSource(
121+
voltage_out=self.pwr_in.link().voltage,
122+
current_limits=forced_current_limit
123+
), [Output])
124+
125+
self.assign(self.pwr_in.current_draw, self.pwr_out.link().current_drawn)
126+
127+
109128
class ForcedVoltage(DummyDevice, NetBlock):
110129
"""Forces some voltage on the output regardless of the input's actual voltage.
111130
Current draw is passed through unchanged."""

edg/abstract_parts/PowerCircuits.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def __init__(self, frequency: RangeLike, fet_rds: RangeLike = (0, 1)*Ohm,
4646
self.fet_rds = self.ArgParameter(fet_rds)
4747
self.gate_res = self.ArgParameter(gate_res)
4848

49+
self.actual_current_limits = self.Parameter(RangeExpr())
50+
4951
def contents(self):
5052
super().contents()
5153
self.driver = self.Block(HalfBridgeDriver(has_boot_diode=True))
@@ -94,6 +96,9 @@ def contents(self):
9496
self.out)
9597
self.connect(self.out.as_ground((0, 0)*Amp), self.driver.high_gnd) # TODO model driver current
9698

99+
self.assign(self.actual_current_limits, self.low_fet.actual_drain_current_rating.intersect(
100+
self.high_fet.actual_drain_current_rating))
101+
97102

98103
class FetHalfBridgeIndependent(FetHalfBridge, HalfBridgeIndependent):
99104
def contents(self):

edg/abstract_parts/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@
107107

108108
from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, \
109109
DummyAnalogSource, DummyAnalogSink
110-
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltage, ForcedVoltageCurrent, ForcedAnalogVoltage,\
111-
ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
110+
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltageCurrentLimit, ForcedVoltage, ForcedVoltageCurrent, \
111+
ForcedAnalogVoltage, ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
112112
from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController
113113

114114
from .Nonstrict3v3Compatible import Nonstrict3v3Compatible

edg/abstract_parts/test_switching_converters.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import unittest
22

33
from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath
4-
from ..core import Range
4+
from ..electronics_model import *
5+
from .AbstractInductor import Inductor
6+
from .AbstractCapacitor import Capacitor
7+
from .DummyDevices import DummyVoltageSource, DummyVoltageSink, DummyGround
58

69

710
class SwitchingConverterCalculationTest(unittest.TestCase):
@@ -87,3 +90,88 @@ def test_boost_converter_example(self):
8790
self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.44, places=2)
8891
# the example calculation output is wrong, this is the correct result of the formula
8992
self.assertAlmostEqual(values.output_capacitance.lower, 9.95e-6, places=7)
93+
94+
95+
class TestCapacitor(Capacitor):
96+
def contents(self):
97+
super().contents()
98+
self.assign(self.actual_capacitance, self.capacitance)
99+
self.assign(self.actual_voltage_rating, Range.all())
100+
101+
102+
class TestInductor(Inductor):
103+
def contents(self):
104+
super().contents()
105+
self.assign(self.actual_inductance, self.inductance)
106+
self.assign(self.actual_current_rating, (0, 1.5)*Amp)
107+
self.assign(self.actual_frequency_rating, Range.all())
108+
109+
110+
class BuckPowerPathTestTop(DesignTop):
111+
def __init__(self):
112+
super().__init__()
113+
self.dut = self.Block(BuckConverterPowerPath(
114+
input_voltage=Range(4, 6), output_voltage=(2, 3),
115+
frequency= Range.exact(100e3), output_current=Range(0.2, 1),
116+
sw_current_limits=Range(0, 2),
117+
input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt,
118+
))
119+
(self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in)
120+
(self.switch, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.switch)
121+
(self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.pwr_out)
122+
(self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd)
123+
124+
self.require(self.dut.actual_dutycycle.contains(Range(0.334, 0.832)))
125+
self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.433, 0.478)))
126+
self.require(self.dut.pwr_out.current_limits.contains(Range(0.0, 1.260)))
127+
self.require(self.dut.switch.current_draw.contains(Range(0.067, 0.832)))
128+
129+
def refinements(self) -> Refinements:
130+
return Refinements(
131+
class_refinements=[
132+
(Capacitor, TestCapacitor),
133+
(Inductor, TestInductor),
134+
],
135+
instance_values=[
136+
(['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05))
137+
]
138+
)
139+
140+
141+
class BoostPowerPathTestTop(DesignTop):
142+
def __init__(self):
143+
super().__init__()
144+
self.dut = self.Block(BoostConverterPowerPath(
145+
input_voltage=Range(4, 6), output_voltage=(10, 14),
146+
frequency=Range.exact(200e3), output_current=Range(0.2, 0.5),
147+
sw_current_limits=Range(0, 2),
148+
input_voltage_ripple=75*mVolt, output_voltage_ripple=25*mVolt,
149+
))
150+
(self.pwr_in, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_in)
151+
(self.pwr_out, ), _ = self.chain(self.Block(DummyVoltageSource()), self.dut.pwr_out)
152+
(self.switch, ), _ = self.chain(self.Block(DummyVoltageSink()), self.dut.switch)
153+
(self.gnd, ), _ = self.chain(self.Block(DummyGround()), self.dut.gnd)
154+
155+
self.require(self.dut.actual_dutycycle.contains(Range(0.4, 0.771)))
156+
self.require(self.dut.actual_inductor_current_ripple.contains(Range(0.495, 0.546)))
157+
self.require(self.dut.pwr_in.current_draw.contains(Range(0.334, 2.185)))
158+
self.require(self.dut.switch.current_limits.contains(Range(0.0, 0.280)))
159+
160+
def refinements(self) -> Refinements:
161+
return Refinements(
162+
class_refinements=[
163+
(Capacitor, TestCapacitor),
164+
(Inductor, TestInductor),
165+
],
166+
instance_values=[
167+
(['dut', 'inductor', 'actual_inductance'], Range.from_tolerance(33e-6, 0.05))
168+
]
169+
)
170+
171+
172+
class PowerPathBlockTest(unittest.TestCase):
173+
def test_buck_power_path(self) -> None:
174+
ScalaCompiler.compile(BuckPowerPathTestTop)
175+
176+
def test_boost_power_path(self) -> None:
177+
ScalaCompiler.compile(BoostPowerPathTestTop)

0 commit comments

Comments
 (0)