Skip to content

Commit da1fa13

Browse files
authored
Ramp limiter generator (#420)
This adds a PMOS-based ramp limiter generator with a enable control input. This can be used for inrush current limiting and has better characteristics (faster reset time, lower power dissipation) compared to a NTC. This circuit is not robust to steps in input voltage post-turn-on, but may be externally commanded off by a digital signal prior to a step. A future change may make the control line optional, this needs schematic import to be able to skip parts. Adds an analysis file for RampLimiter. Other changes: - Make ResistiveDivider schematic-importable and defines a symbol pinning - Defines gate drive voltage for FETs - Allows specification of threshold voltage for FETs - Add default zero frequency and infinite drive current for SwitchFet (zero switching loss). API change: these parameters are now kwarg only. - Move CustomDiode, CustomFet, GenericCapacitor, GenericResistor to abstract_parts, so tests can use these. - Add DummyDigitalSource
1 parent 404c0c2 commit da1fa13

15 files changed

+1791
-24
lines changed

edg/abstract_parts/AbstractFets.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ class Fet(KiCadImportableBlock, DiscreteSemiconductor, HasStandardFootprint):
5353
"""Base class for untyped MOSFETs
5454
Drain voltage, drain current, and gate voltages are positive (absolute).
5555
56+
The gate voltage is only checked against maximum ratings.
57+
Optionally, the gate threshold voltage can also be specified.
58+
59+
The actual gate drive voltage is specified as (threshold voltage, gate drive voltage), where the top end of that
60+
is either the voltage at Rds,on or the specified driving voltage level.
61+
5662
MOSFET equations
5763
- https://inst.eecs.berkeley.edu/~ee105/fa05/handouts/discussions/Discussion5.pdf (cutoff/linear/saturation regions)
5864
@@ -81,7 +87,8 @@ def PFet(*args, **kwargs) -> 'Fet':
8187

8288
@init_in_parent
8389
def __init__(self, drain_voltage: RangeLike, drain_current: RangeLike, *,
84-
gate_voltage: RangeLike = (0, 0), rds_on: RangeLike = Range.all(),
90+
gate_voltage: RangeLike = (0, 0), gate_threshold_voltage: RangeLike = Range.all(),
91+
rds_on: RangeLike = Range.all(),
8592
gate_charge: RangeLike = Range.all(), power: RangeLike = Range.exact(0),
8693
channel: StringLike = StringExpr()) -> None:
8794
super().__init__()
@@ -93,6 +100,7 @@ def __init__(self, drain_voltage: RangeLike, drain_current: RangeLike, *,
93100
self.drain_voltage = self.ArgParameter(drain_voltage)
94101
self.drain_current = self.ArgParameter(drain_current)
95102
self.gate_voltage = self.ArgParameter(gate_voltage)
103+
self.gate_threshold_voltage = self.ArgParameter(gate_threshold_voltage)
96104
self.rds_on = self.ArgParameter(rds_on)
97105
self.gate_charge = self.ArgParameter(gate_charge)
98106
self.power = self.ArgParameter(power)
@@ -142,15 +150,16 @@ class TableFet(PartsTableSelector, BaseTableFet):
142150
@init_in_parent
143151
def __init__(self, *args, **kwargs):
144152
super().__init__(*args, **kwargs)
145-
self.generator_param(self.drain_voltage, self.drain_current, self.gate_voltage, self.rds_on, self.gate_charge,
146-
self.power, self.channel)
153+
self.generator_param(self.drain_voltage, self.drain_current, self.gate_voltage, self.gate_threshold_voltage,
154+
self.rds_on, self.gate_charge, self.power, self.channel)
147155

148156
def _row_filter(self, row: PartsTableRow) -> bool:
149157
return super()._row_filter(row) and \
150158
row[self.CHANNEL] == self.get(self.channel) and \
151159
self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \
152160
self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \
153161
self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \
162+
(row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \
154163
row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \
155164
row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \
156165
self.get(self.power).fuzzy_in(row[self.POWER_RATING])
@@ -184,7 +193,7 @@ def PFet(*args, **kwargs):
184193

185194

186195
@init_in_parent
187-
def __init__(self, frequency: RangeLike, drive_current: RangeLike, **kwargs) -> None:
196+
def __init__(self, *, frequency: RangeLike = 0*Hertz(tol=0), drive_current: RangeLike = Range.all(), **kwargs) -> None:
188197
super().__init__(**kwargs)
189198

190199
self.frequency = self.ArgParameter(frequency)
@@ -200,8 +209,9 @@ class TableSwitchFet(PartsTableSelector, SwitchFet, BaseTableFet):
200209
@init_in_parent
201210
def __init__(self, *args, **kwargs):
202211
super().__init__(*args, **kwargs)
203-
self.generator_param(self.frequency, self.drain_voltage, self.drain_current, self.gate_voltage, self.rds_on,
204-
self.gate_charge, self.power, self.channel, self.drive_current)
212+
self.generator_param(self.frequency, self.drain_voltage, self.drain_current,
213+
self.gate_voltage, self.gate_threshold_voltage,
214+
self.rds_on, self.gate_charge, self.power, self.channel, self.drive_current)
205215

206216
self.actual_static_power = self.Parameter(RangeExpr())
207217
self.actual_switching_power = self.Parameter(RangeExpr())
@@ -213,6 +223,7 @@ def _row_filter(self, row: PartsTableRow) -> bool: # here this is just a pre-fi
213223
self.get(self.drain_voltage).fuzzy_in(row[self.VDS_RATING]) and \
214224
self.get(self.drain_current).fuzzy_in(row[self.IDS_RATING]) and \
215225
self.get(self.gate_voltage).fuzzy_in(row[self.VGS_RATING]) and \
226+
(row[self.VGS_DRIVE].lower in self.get(self.gate_threshold_voltage)) and \
216227
row[self.RDS_ON].fuzzy_in(self.get(self.rds_on)) and \
217228
row[self.GATE_CHARGE].fuzzy_in(self.get(self.gate_charge)) and \
218229
self.get(self.power).fuzzy_in(row[self.POWER_RATING])
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from ..abstract_parts import *
1+
from ..electronics_model import *
2+
from .AbstractDiodes import Diode
23

34

45
class CustomDiode(Diode, FootprintBlock, GeneratorBlock):
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from ..abstract_parts import *
1+
from ..electronics_model import *
2+
from .AbstractFets import SwitchFet
23

34

4-
class CustomFet(Fet, FootprintBlock, GeneratorBlock):
5+
class CustomFet(SwitchFet, FootprintBlock, GeneratorBlock):
56
@init_in_parent
67
def __init__(self, *args, footprint_spec: StringLike = "",
78
manufacturer_spec: StringLike = "", part_spec: StringLike = "", **kwargs):

edg/abstract_parts/DummyDevices.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
4545
self.current_limits = self.Parameter(RangeExpr(self.pwr.link().current_limits))
4646

4747

48+
class DummyDigitalSource(DummyDevice):
49+
@init_in_parent
50+
def __init__(self, voltage_out: RangeLike = RangeExpr.ZERO,
51+
current_limits: RangeLike = RangeExpr.ALL) -> None:
52+
super().__init__()
53+
54+
self.io = self.Port(DigitalSource(
55+
voltage_out=voltage_out,
56+
current_limits=current_limits
57+
), [InOut])
58+
59+
4860
class DummyDigitalSink(DummyDevice):
4961
@init_in_parent
5062
def __init__(self, voltage_limit: RangeLike = RangeExpr.ALL,
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from typing import NamedTuple, Dict, Optional
22
import math
33

4-
from ..abstract_parts import *
4+
from ..electronics_model import *
5+
from .AbstractCapacitor import Capacitor, DummyCapacitorFootprint
6+
from .SelectorArea import SelectorArea
7+
from .ESeriesUtil import ESeriesUtil
58

69

710
class GenericMlcc(Capacitor, SelectorArea, FootprintBlock, GeneratorBlock):
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from typing import List, Tuple
22

3-
from ..abstract_parts import *
3+
from ..electronics_model import *
4+
from .AbstractResistor import Resistor
5+
from .ESeriesUtil import ESeriesUtil
6+
from .SelectorArea import SelectorArea
47

58

69
@non_library

edg/abstract_parts/PowerCircuits.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from ..electronics_model import *
22
from .Resettable import Resettable
33
from .AbstractResistor import Resistor
4-
from .AbstractFets import SwitchFet
4+
from .AbstractFets import SwitchFet, Fet
5+
from .AbstractCapacitor import Capacitor
56
from .GateDrivers import HalfBridgeDriver, HalfBridgeDriverIndependent, HalfBridgeDriverPwm
7+
from .ResistiveDivider import VoltageDivider, ResistiveDivider
68
from .Categories import PowerConditioner
79

810

@@ -118,3 +120,109 @@ def generate(self):
118120
self.connect(self.pwm_ctl, self.driver.with_mixin(HalfBridgeDriverPwm()).pwm_in)
119121
if self.get(self.reset.is_connected()):
120122
self.connect(self.reset, self.driver.with_mixin(Resettable()).reset)
123+
124+
125+
class RampLimiter(KiCadSchematicBlock):
126+
"""PMOS-based ramp limiter that roughly targets a constant-dV/dt ramp.
127+
The cgd should be specified to swamp (10x+) the parasitic Cgd of the FET to get more controlled parameters.
128+
The target ramp rate is in volts/second, and for a capacitive load this can be calculated from a target current with
129+
I = C * dV/dt => dV/dt = I / C
130+
The actual ramp rate will vary substantially, the values calculated are based on many assertions.
131+
132+
A target Vgs can also be specified, this is the final Vgs of the FET after the ramp completes.
133+
The FET will be constrained to have a Vgs,th below the minimum of this range and a Vgs,max above the maximum.
134+
135+
A capacitive divider with Cgs will be generated so the target initial Vgs at less than half the FET Vgs,th
136+
(targeting half Vgs,th at Vin,max).
137+
138+
TODO: allow control to be optional, eliminating the NMOS with a short
139+
140+
HOW THIS WORKS:
141+
When the input voltage rises, the capacitive divider of Cgs, Cgd brings the gate to a subthreshold voltage.
142+
The gate voltage charges via the divider until it gets to the threshold voltage.
143+
At around the threshold voltage, the FET begins to turn on, with current flowing into (and charging) the output.
144+
As the output rises, Cgd causes the gate to be pulled up with the output, keeping Vgs roughly constant.
145+
(this also keeps the current roughly constant, mostly regardless of transconductance)
146+
During this stage, if we assume Vgs is constant, then Cgs is constant and can be disregarded.
147+
For the output to rise, Vgd must rise, which means Cgd must charge, and the current must go through the divider.
148+
Assuming a constant Vgs (and absolute gate voltage), the current into the divider is constant,
149+
and this is how the voltage ramp rate is controlled.
150+
Once the output gets close to the input voltage, Cgd stops charging and Vgs rises, turning the FET fully on.
151+
152+
Note that Vgs,th is an approximate parameter and the ramp current is likely larger than the Vgs,th current.
153+
Vgs also may rise during the ramp, meaning some current goes into charging Cgs.
154+
155+
References: https://www.ti.com/lit/an/slva156/slva156.pdf, https://www.ti.com/lit/an/slyt096/slyt096.pdf,
156+
https://youtu.be/bOka13RtOXM
157+
158+
Additional more complex circuits
159+
https://electronics.stackexchange.com/questions/294061/p-channel-mosfet-inrush-current-limiting
160+
"""
161+
@init_in_parent
162+
def __init__(self, *, cgd: RangeLike = 10*nFarad(tol=0.5), target_ramp: RangeLike = 1000*Volt(tol=0.25),
163+
target_vgs: RangeLike = (4, 10)*Volt, max_rds: FloatLike = 1*Ohm,
164+
_cdiv_vgs_factor: RangeLike = (0.05, 0.75)):
165+
super().__init__()
166+
167+
self.gnd = self.Port(Ground.empty(), [Common])
168+
self.pwr_in = self.Port(VoltageSink.empty(), [Input])
169+
self.pwr_out = self.Port(VoltageSource.empty(), [Output])
170+
self.control = self.Port(DigitalSink.empty())
171+
172+
self.cgd = self.ArgParameter(cgd)
173+
self.target_ramp = self.ArgParameter(target_ramp)
174+
self.target_vgs = self.ArgParameter(target_vgs)
175+
self.max_rds = self.ArgParameter(max_rds)
176+
self._cdiv_vgs_factor = self.ArgParameter(_cdiv_vgs_factor)
177+
178+
def contents(self):
179+
super().contents()
180+
181+
pwr_voltage = self.pwr_in.link().voltage
182+
self.drv = self.Block(SwitchFet.PFet(
183+
drain_voltage=pwr_voltage,
184+
drain_current=self.pwr_out.link().current_drawn,
185+
gate_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.upper()),
186+
gate_threshold_voltage=(0 * Volt(tol=0)).hull(self.target_vgs.lower()),
187+
rds_on=(0, self.max_rds)
188+
))
189+
190+
self.cap_gd = self.Block(Capacitor(
191+
capacitance=self.cgd,
192+
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
193+
))
194+
# treat Cgs and Cgd as a capacitive divider with Cgs on the bottom
195+
self.cap_gs = self.Block(Capacitor(
196+
capacitance=(
197+
(1/(self.drv.actual_gate_drive.lower()*self._cdiv_vgs_factor)).shrink_multiply(self.pwr_in.link().voltage) - 1
198+
).shrink_multiply(
199+
self.cap_gd.actual_capacitance
200+
),
201+
voltage=(0 * Volt(tol=0)).hull(self.pwr_in.link().voltage)
202+
))
203+
# dV/dt over a capacitor is I / C => I = Cgd * dV/dt
204+
# then calculate to get the target I: Vgs,th = I * Reff => Reff = Vgs,th / I = Vgs,th / (Cgd * dV/dt)
205+
# we assume Vgs,th is exact, and only contributing sources come from elsewhere
206+
self.div = self.Block(ResistiveDivider(ratio=self.target_vgs.shrink_multiply(1/self.pwr_in.link().voltage),
207+
impedance=(1 / self.target_ramp).shrink_multiply(self.drv.actual_gate_drive.lower() / (self.cap_gd.actual_capacitance))
208+
))
209+
div_current_draw = (self.pwr_in.link().voltage/self.div.actual_impedance).hull(0)
210+
self.ctl_fet = self.Block(SwitchFet.NFet(
211+
drain_voltage=pwr_voltage,
212+
drain_current=div_current_draw,
213+
gate_voltage=(self.control.link().output_thresholds.upper(), self.control.link().voltage.upper())
214+
))
215+
216+
self.import_kicad(
217+
self.file_path("resources", f"{self.__class__.__name__}.kicad_sch"),
218+
conversions={
219+
'pwr_in': VoltageSink(
220+
current_draw=self.pwr_out.link().current_drawn + div_current_draw
221+
),
222+
'pwr_out': VoltageSource(
223+
voltage_out=self.pwr_in.link().voltage
224+
),
225+
'control': DigitalSink(),
226+
'gnd': Ground(),
227+
})
228+

edg/abstract_parts/ResistiveDivider.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from math import log10, ceil
4-
from typing import List, Tuple
4+
from typing import List, Tuple, Mapping
55

66
from ..electronics_model import *
77
from . import Analog, Resistor
@@ -55,8 +55,12 @@ def intersects(self, spec: 'DividerValues') -> bool:
5555
self.parallel_impedance.intersects(spec.parallel_impedance)
5656

5757

58-
class ResistiveDivider(InternalSubcircuit, GeneratorBlock):
58+
class ResistiveDivider(InternalSubcircuit, KiCadImportableBlock, GeneratorBlock):
5959
"""Abstract, untyped (Passive) resistive divider, that takes in a ratio and parallel impedance spec."""
60+
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
61+
assert symbol_name == 'Device:VoltageDivider'
62+
return {'1': self.top, '2': self.center, '3': self.bottom}
63+
6064
@classmethod
6165
def divider_ratio(cls, rtop: RangeExpr, rbot: RangeExpr) -> RangeExpr:
6266
"""Calculates the output voltage of a resistive divider given the input voltages and resistances."""
@@ -131,12 +135,16 @@ def generate(self) -> None:
131135

132136

133137
@non_library
134-
class BaseVoltageDivider(Block):
138+
class BaseVoltageDivider(KiCadImportableBlock):
135139
"""Base class that defines a resistive divider that takes in a voltage source and ground, and outputs
136140
an analog constant-voltage signal.
137141
The actual output voltage is defined as a ratio of the input voltage, and the divider is specified by
138142
ratio and impedance.
139143
Subclasses should define the ratio and impedance spec."""
144+
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
145+
assert symbol_name == 'Device:VoltageDivider'
146+
return {'1': self.input, '2': self.output, '3': self.gnd}
147+
140148
@init_in_parent
141149
def __init__(self, impedance: RangeLike) -> None:
142150
super().__init__()
@@ -218,6 +226,10 @@ def contents(self) -> None:
218226

219227
class SignalDivider(Analog, Block):
220228
"""Specialization of ResistiveDivider for Analog signals"""
229+
def symbol_pinning(self, symbol_name: str) -> Mapping[str, BasePort]:
230+
assert symbol_name == 'Device:VoltageDivider'
231+
return {'1': self.input, '2': self.output, '3': self.gnd}
232+
221233
@init_in_parent
222234
def __init__(self, ratio: RangeLike, impedance: RangeLike) -> None:
223235
super().__init__()

edg/abstract_parts/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
from .AbstractPowerConverters import BuckConverter, DiscreteBuckConverter, BoostConverter, DiscreteBoostConverter
7373
from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath, BuckBoostConverterPowerPath
7474
from .PowerCircuits import HalfBridge, FetHalfBridge, HalfBridgeIndependent, HalfBridgePwm, FetHalfBridgeIndependent,\
75-
FetHalfBridgePwmReset
75+
FetHalfBridgePwmReset, RampLimiter
7676
from .AbstractLedDriver import LedDriver, LedDriverPwm, LedDriverSwitchingConverter
7777
from .AbstractFuse import Fuse, SeriesPowerFuse, PptcFuse, FuseStandardFootprint, TableFuse, SeriesPowerPptcFuse
7878
from .AbstractCrystal import Crystal, TableCrystal, OscillatorReference, CeramicResonator
@@ -106,8 +106,13 @@
106106
from .PinMappable import PinResource, PeripheralFixedPin, PeripheralAnyResource, PeripheralFixedResource
107107
from .VariantPinRemapper import VariantPinRemapper
108108

109-
from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSink, \
110-
DummyAnalogSource, DummyAnalogSink
109+
from .CustomDiode import CustomDiode
110+
from .CustomFet import CustomFet
111+
from .GenericResistor import ESeriesResistor, GenericChipResistor, GenericAxialResistor, GenericAxialVerticalResistor
112+
from .GenericCapacitor import GenericMlcc
113+
114+
from .DummyDevices import DummyPassive, DummyGround, DummyVoltageSource, DummyVoltageSink, DummyDigitalSource, \
115+
DummyDigitalSink, DummyAnalogSource, DummyAnalogSink
111116
from .DummyDevices import ForcedVoltageCurrentDraw, ForcedVoltageCurrentLimit, ForcedVoltage, ForcedVoltageCurrent, \
112117
ForcedAnalogVoltage, ForcedAnalogSignal, ForcedDigitalSinkCurrentDraw
113118
from .MergedBlocks import MergedVoltageSource, MergedDigitalSource, MergedAnalogSource, MergedSpiController

0 commit comments

Comments
 (0)