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