@@ -172,6 +172,87 @@ def generate(self):
172172 })
173173
174174
175+ class FcmlPowerPath (InternalSubcircuit , GeneratorBlock ):
176+ """FCML power path that accounts for inductor scaling behavior
177+ TODO: this basically completely duplicates BuckConverterPowerPath, but adds a scaling factor that doesn't exist there
178+ """
179+ @init_in_parent
180+ def __init__ (self , input_voltage : RangeLike , output_voltage : RangeLike , frequency : RangeLike ,
181+ output_current : RangeLike , current_limits : RangeLike , inductor_current_ripple : RangeLike , * ,
182+ input_voltage_ripple : FloatLike ,
183+ output_voltage_ripple : FloatLike ,
184+ efficiency : RangeLike = (0.9 , 1.0 ), # from TI reference
185+ dutycycle_limit : RangeLike = (0.1 , 0.9 ),
186+ inductor_scale : FloatLike = 1.0 ): # arbitrary
187+ super ().__init__ ()
188+
189+ self .pwr_in = self .Port (VoltageSink .empty (), [Power ]) # models the input cap only
190+ self .pwr_out = self .Port (VoltageSource .empty ()) # models the output cap and inductor power source
191+ self .switch = self .Port (VoltageSink .empty ()) # current draw defined as average
192+ self .gnd = self .Port (Ground .empty (), [Common ])
193+
194+ self .input_voltage = self .ArgParameter (input_voltage )
195+ self .output_voltage = self .ArgParameter (output_voltage )
196+ self .frequency = self .ArgParameter (frequency )
197+ self .output_current = self .ArgParameter (output_current )
198+ self .inductor_current_ripple = self .ArgParameter (inductor_current_ripple )
199+ self .efficiency = self .ArgParameter (efficiency )
200+ self .input_voltage_ripple = self .ArgParameter (input_voltage_ripple )
201+ self .output_voltage_ripple = self .ArgParameter (output_voltage_ripple )
202+ self .dutycycle_limit = self .ArgParameter (dutycycle_limit )
203+ self .generator_param (self .input_voltage , self .output_voltage , self .frequency , self .output_current ,
204+ self .inductor_current_ripple , self .efficiency ,
205+ self .input_voltage_ripple , self .output_voltage_ripple , self .dutycycle_limit )
206+
207+ self .current_limits = self .ArgParameter (current_limits )
208+ self .inductor_scale = self .ArgParameter (inductor_scale )
209+
210+ self .actual_dutycycle = self .Parameter (RangeExpr ())
211+ self .actual_inductor_current_ripple = self .Parameter (RangeExpr ())
212+
213+ def generate (self ) -> None :
214+ super ().generate ()
215+ values = BuckConverterPowerPath .calculate_parameters (
216+ self .get (self .input_voltage ), self .get (self .output_voltage ),
217+ self .get (self .frequency ), self .get (self .output_current ), self .get (self .inductor_current_ripple ),
218+ self .get (self .input_voltage_ripple ), self .get (self .output_voltage_ripple ),
219+ efficiency = self .get (self .efficiency ), dutycycle_limit = self .get (self .dutycycle_limit ))
220+ self .assign (self .actual_dutycycle , values .dutycycle )
221+ self .require (values .dutycycle == values .effective_dutycycle , "dutycycle outside limit" )
222+
223+ # TODO maximum current depends on the inductance, but this just uses a worst-case value for simplicity
224+ # TODO ideally the inductor selector would take a function that can account for this coupled equation
225+ self .inductor = self .Block (Inductor (
226+ inductance = values .inductance * Henry / self .inductor_scale ,
227+ current = values .inductor_peak_currents ,
228+ frequency = self .frequency
229+ ))
230+
231+ # expand out the equation to avoid double-counting tolerance
232+ actual_peak_ripple = (self .output_voltage .lower () * (self .input_voltage .upper () - self .output_voltage .lower ()) /
233+ (self .inductor .actual_inductance * self .frequency .lower () * self .input_voltage .upper ()))
234+ self .assign (self .actual_inductor_current_ripple , actual_peak_ripple / self .inductor_scale )
235+
236+ self .connect (self .switch , self .inductor .a .adapt_to (VoltageSink (
237+ voltage_limits = RangeExpr .ALL ,
238+ current_draw = self .pwr_out .link ().current_drawn * values .dutycycle
239+ )))
240+ self .connect (self .pwr_out , self .inductor .b .adapt_to (VoltageSource (
241+ voltage_out = self .output_voltage ,
242+ current_limits = (0 , self .current_limits .intersect (self .inductor .actual_current_rating ).upper () -
243+ (self .actual_inductor_current_ripple .upper () / 2 ))
244+ )))
245+
246+ self .in_cap = self .Block (DecouplingCapacitor (
247+ capacitance = values .input_capacitance * Farad ,
248+ exact_capacitance = True
249+ )).connected (self .gnd , self .pwr_in )
250+ self .out_cap = self .Block (DecouplingCapacitor (
251+ capacitance = values .output_capacitance * Farad ,
252+ exact_capacitance = True
253+ )).connected (self .gnd , self .pwr_out )
254+
255+
175256class DiscreteMutlilevelBuckConverter (PowerConditioner , GeneratorBlock ):
176257 """Flying capacitor multilevel buck converter. Trades more switches for smaller inductor size:
177258 for number of levels N, inductor value is reduced by a factor of (N-1)^2.
@@ -214,7 +295,7 @@ def generate(self):
214295 super ().generate ()
215296 levels = self .get (self .levels )
216297 assert levels >= 2 , "levels must be 2 or more"
217- self .power_path = self .Block (BuckConverterPowerPath (
298+ self .power_path = self .Block (FcmlPowerPath (
218299 self .pwr_in .link ().voltage , self .pwr_in .link ().voltage * self .get (self .ratios ), self .frequency ,
219300 self .pwr_out .link ().current_drawn , Range .all (), # TODO add current limits from FETs
220301 inductor_current_ripple = self .inductor_current_ripple ,
0 commit comments