|
1 | 1 | from ..electronics_model import * |
2 | 2 | from .Resettable import Resettable |
3 | 3 | from .AbstractResistor import Resistor |
4 | | -from .AbstractFets import SwitchFet |
| 4 | +from .AbstractFets import SwitchFet, Fet |
| 5 | +from .AbstractCapacitor import Capacitor |
5 | 6 | from .GateDrivers import HalfBridgeDriver, HalfBridgeDriverIndependent, HalfBridgeDriverPwm |
| 7 | +from .ResistiveDivider import VoltageDivider, ResistiveDivider |
6 | 8 | from .Categories import PowerConditioner |
7 | 9 |
|
8 | 10 |
|
@@ -118,3 +120,109 @@ def generate(self): |
118 | 120 | self.connect(self.pwm_ctl, self.driver.with_mixin(HalfBridgeDriverPwm()).pwm_in) |
119 | 121 | if self.get(self.reset.is_connected()): |
120 | 122 | 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 | + |
0 commit comments