From a7d64e4a523e0f8b3382c59889e5251921c2914a Mon Sep 17 00:00:00 2001 From: Dennis Marttinen Date: Sat, 15 May 2021 21:23:49 +0300 Subject: [PATCH 1/3] Add tool for computing component values for BD9E302EFJ --- tools/psu/bd9e302efj.py | 155 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tools/psu/bd9e302efj.py diff --git a/tools/psu/bd9e302efj.py b/tools/psu/bd9e302efj.py new file mode 100644 index 0000000..41abd51 --- /dev/null +++ b/tools/psu/bd9e302efj.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# Copyright 2021 The Racklet Authors +# SPDX-License-Identifier: Apache-2.0 + +# Tool for computing component constants for the ROHM Semiconductor BD9E302EFJ. +# Component naming and computed values apply to the Application Example 2 (Fast load response) +# in https://fscdn.rohm.com/en/products/databook/datasheet/ic/power/switching_regulator/bd9e302efj-e.pdf. + +import itertools +import iec60063 +import math +from tabulate import tabulate + + +def avg(*args): + return sum(args) / len(args) + + +# BD9E constants +F_osc = 550000 # Hertz (this can't be changed) + +# Variables +# TODO: Take these as command line arguments? +V_in = 12 # Volts, input voltage +A_max = 3 # Amperes, maximum output current +dI_L = 1.0 # Amperes, inductor ripple current +V_fb = 0.8 # Volts, feedback reference voltage +G_mp = 20 # A/V, current sense gain +G_ma = 140E-6 # A/V, error amplifier transconductance + +# Output capacitor C_out specifications +C_C_out = 47E-6 # Farads, capacitance of C_out +R_C_out = 2E-3 # Ohms, equivalent series resistance of C_out + +V_out_min = 5.1 # Volts, minimum voltage for V_out + +R_div_max = 700E3 # 700 KOhm maximum from the spec +R_div_min = 500E3 # 500 KOhm minimum + + +def compute_V_out(dR1, dR2): + return (dR1 + dR2) / dR2 * V_fb + + +def generate_resistors(multipliers): + result = [] + for m in multipliers: + result += [m * float(v) for v in iec60063.E96] + return result + + +def resolve_V_out(): + (b_voltage, b_R1, b_R2) = (math.inf, 0, 0) + for d_R1, d_R2 in itertools.permutations(generate_resistors([10000, 100000]), 2): + d_sum = d_R1 + d_R2 + if d_sum > R_div_max or d_sum < R_div_min: + continue + value = compute_V_out(d_R1, d_R2) + if value < V_out_min: + continue + if value < b_voltage: + (b_voltage, b_R1, b_R2) = (value, d_R1, d_R2) + + assert (b_voltage < math.inf) + assert (b_R1 > 0) + assert (b_R2 > 0) + return b_voltage, b_R1, b_R2 + + +V_out, R1, R2 = resolve_V_out() + +# Enable fast transient response +fast_transient = True + +# These frequencies are resolved from the application examples. +# The spec states that F_crs should typically be around 20 KHz, +# but we'll go with the values that are actually used. +F_crs = 48000 if fast_transient else 32000 # Hertz + +# The values from the application examples land somewhere between +# 0.0325 and 0.04875 times times F_crs for the fast transient and +# between 0.04875 and 0.073125 for the slow transient. All of the +# examples use a 6.8 nF capacitor for C2 though. +F_z = (avg(0.0325, 0.04875) if fast_transient else avg(0.04875, 0.073125)) * F_crs + + +# Inductance of the external inductor L +def L_inductance(): + if V_out * 2 > V_in: + return V_in / (4 * F_osc * dI_L) # Alternate calculation from the datasheet + return V_out * (V_in - V_out) * 1 / (V_in * F_osc * dI_L) + + +def L_saturation_A_min(): + return A_max + 0.5 * dI_L + + +def voltage_ripple(): + return dI_L * (R_C_out + 1 / (8 * C_C_out * F_osc)) + + +def R3_resistance(): + return 2 * math.pi * V_out * F_crs * C_C_out / (V_fb * G_mp * G_ma) + + +def C1_capacitance(): + return 1 / (2 * math.pi * R1 * 20000) + + +def C2_capacitance(): + return min(1 / (2 * math.pi * R3_resistance() * F_z), 15E-9) # Upper limit from spec sheet + + +def sub(s): + return f"{s}" + + +def table_format(d): + return tabulate(d, headers=["Parameter", "Value"], tablefmt="github") + + +configuration = [ + ["Frequency", F_osc], + ["Input voltage", V_in], + ["Maximum current", A_max], + ["Ripple current", dI_L], + ["Feedback reference V", V_fb], + ["Current sense gain", G_mp], + ["Error amp transconductance", G_ma], + [f"C{sub('out')} capacitance", C_C_out], + [f"C{sub('out')} ESR", R_C_out], + [f"Minimum V{sub('out')}", V_out_min], + ["Fast transient response", fast_transient], + [f"F{sub('CRS')} frequency", F_crs], + [f"F{sub('Z')} frequency", F_z], +] + +computed = [ + ["Output voltage", V_out], + ["L inductance", L_inductance()], + ["L saturation A min", L_saturation_A_min()], + ["Voltage ripple", voltage_ripple()], + [f"C{sub(1)} capacitance", C1_capacitance()], + [f"C{sub(2)} capacitance", C2_capacitance()], + [f"R{sub(1)} resistance", R1], + [f"R{sub(2)} resistance", R2], + [f"R{sub(1)} + R{sub(2)}", R1 + R2], + [f"R{sub(3)} resistance", R3_resistance()], +] + +print("Configured input values:") +print(table_format(configuration)) +print() +print("Computed results:") +print(table_format(computed)) From cb8436cb2f822774972147b27c30d722272876ed Mon Sep 17 00:00:00 2001 From: Dennis Marttinen Date: Sun, 16 May 2021 18:28:54 +0300 Subject: [PATCH 2/3] Fix the frequencies inferred from the datasheet --- tools/psu/bd9e302efj.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) mode change 100644 => 100755 tools/psu/bd9e302efj.py diff --git a/tools/psu/bd9e302efj.py b/tools/psu/bd9e302efj.py old mode 100644 new mode 100755 index 41abd51..a16f8c3 --- a/tools/psu/bd9e302efj.py +++ b/tools/psu/bd9e302efj.py @@ -21,7 +21,7 @@ def avg(*args): # Variables # TODO: Take these as command line arguments? -V_in = 12 # Volts, input voltage +V_in = 24 # Volts, input voltage A_max = 3 # Amperes, maximum output current dI_L = 1.0 # Amperes, inductor ripple current V_fb = 0.8 # Volts, feedback reference voltage @@ -34,7 +34,7 @@ def avg(*args): V_out_min = 5.1 # Volts, minimum voltage for V_out -R_div_max = 700E3 # 700 KOhm maximum from the spec +R_div_max = 700E3 # 700 KOhm maximum from the specification R_div_min = 500E3 # 500 KOhm minimum @@ -50,6 +50,7 @@ def generate_resistors(multipliers): def resolve_V_out(): + resolved = False (b_voltage, b_R1, b_R2) = (math.inf, 0, 0) for d_R1, d_R2 in itertools.permutations(generate_resistors([10000, 100000]), 2): d_sum = d_R1 + d_R2 @@ -59,29 +60,28 @@ def resolve_V_out(): if value < V_out_min: continue if value < b_voltage: + resolved = True (b_voltage, b_R1, b_R2) = (value, d_R1, d_R2) - - assert (b_voltage < math.inf) - assert (b_R1 > 0) - assert (b_R2 > 0) + assert resolved return b_voltage, b_R1, b_R2 V_out, R1, R2 = resolve_V_out() -# Enable fast transient response -fast_transient = True +# Enable fast load response +fast_response = True -# These frequencies are resolved from the application examples. -# The spec states that F_crs should typically be around 20 KHz, -# but we'll go with the values that are actually used. -F_crs = 48000 if fast_transient else 32000 # Hertz +# According to the datasheet F_crs should typically be around 20 KHz. For +# the two fast load response application examples it has been set to around +# 24 KHz, and for the two others it is around 16 Khz (reverse-engineered +# from the equation for the phase compensation resistor R3). +F_crs = 24000 if fast_response else 16000 # Hertz -# The values from the application examples land somewhere between -# 0.0325 and 0.04875 times times F_crs for the fast transient and -# between 0.04875 and 0.073125 for the slow transient. All of the -# examples use a 6.8 nF capacitor for C2 though. -F_z = (avg(0.0325, 0.04875) if fast_transient else avg(0.04875, 0.073125)) * F_crs +# The datasheet recommends inserting a zero point under 1/6 of F_crs. +# All application examples use a 6.8 nF capacitance for C2, based on +# which the actual multipliers are roughly 1/12 with fast load response +# and 1/6 without. +F_z = (1/12 if fast_response else 1 / 6) * F_crs # Inductance of the external inductor L @@ -108,7 +108,7 @@ def C1_capacitance(): def C2_capacitance(): - return min(1 / (2 * math.pi * R3_resistance() * F_z), 15E-9) # Upper limit from spec sheet + return min(1 / (2 * math.pi * R3_resistance() * F_z), 15E-9) # Upper limit from the datasheet def sub(s): @@ -130,7 +130,7 @@ def table_format(d): [f"C{sub('out')} capacitance", C_C_out], [f"C{sub('out')} ESR", R_C_out], [f"Minimum V{sub('out')}", V_out_min], - ["Fast transient response", fast_transient], + ["Fast load response", fast_response], [f"F{sub('CRS')} frequency", F_crs], [f"F{sub('Z')} frequency", F_z], ] From 147a3b20ff1e58c06ffc00d69eb28437179722c0 Mon Sep 17 00:00:00 2001 From: Dennis Marttinen Date: Mon, 17 May 2021 19:55:15 +0300 Subject: [PATCH 3/3] Implement ICCC, a better tool for computing IC component values --- .gitignore | 3 + tools/iccc/bd9e302efj.py | 80 +++++++++++++++++++ .../bd9e302efj.py => iccc/bd9e302efj_old.py} | 0 tools/iccc/iccc.py | 32 ++++++++ tools/iccc/util.py | 75 +++++++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 tools/iccc/bd9e302efj.py rename tools/{psu/bd9e302efj.py => iccc/bd9e302efj_old.py} (100%) create mode 100644 tools/iccc/iccc.py create mode 100644 tools/iccc/util.py diff --git a/.gitignore b/.gitignore index 7607071..e08d02c 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,9 @@ node_modules/ .env .env.test +# Python +__pycache__ + # Misc tmp .DS_Store diff --git a/tools/iccc/bd9e302efj.py b/tools/iccc/bd9e302efj.py new file mode 100644 index 0000000..de3329e --- /dev/null +++ b/tools/iccc/bd9e302efj.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# Copyright 2021 The Racklet Authors +# SPDX-License-Identifier: Apache-2.0 + +# Tool for computing component constants for the ROHM Semiconductor BD9E302EFJ. +# Component naming and computed values apply to the Application Example 2 (Fast load response) +# in https://fscdn.rohm.com/en/products/databook/datasheet/ic/power/switching_regulator/bd9e302efj-e.pdf. + +import math + +from iccc import IC, Component +from quantities import Quantity +from util import VoltageDivider + +# Common IEC60063 series for components +series = "E96" + + +class BD9E302EFJ(IC): + target_voltage_div = VoltageDivider( + lambda v, r1, r2: (r1 + r2) / r2 * v['V_fb'], + "V_tgt", "R_dmin", "R_dmax", "R_div series", minimum_magnitude=4 + ) + + valuations = { + "L": lambda v: Component( + # Conditional alternate calculation from the datasheet + v["V_in"] / (4 * v["F_osc"] * v["dI_l"]) if v["V_out"] * 2 > v["V_in"] else + v["V_out"] * (v["V_in"] - v["V_out"]) * 1 / (v["V_in"] * v["F_osc"] * v["dI_l"]), + "H", series, True + ), + "L saturation min": lambda v: v["A_max"] + 0.5 * v["dI_l"], + "Voltage ripple": lambda v: (v["dI_l"] * (v["C_out ESR"] + 1 / + (8 * v["C_out CAP"] * v["F_osc"]))).rescale("mV"), + "V_out": target_voltage_div.voltage(), + "R1": target_voltage_div.r1(), + "R2": target_voltage_div.r2(), + "R3": lambda v: Component(2 * math.pi * v["V_out"] * v["F_crs"] * v["C_out CAP"] / + (v["V_fb"] * v["G_mp"] * v["G_ma"]), "ohm", series, True), + "C1": lambda v: Component(1 / (2 * math.pi * v["R1"] * 20000), "F", series, True), + "C2": lambda v: Component(min(1 / (2 * math.pi * v["R3"] * v["F_z"]), 15E-9), "F", series, True), + } + + +if __name__ == "__main__": + # TODO: Replace Quantity with q. + config = { + "F_osc": Quantity(550E3, "Hz"), # This is a constant in the BD9E302EFJ + "V_in": Quantity(24, "V"), # Input voltage + "A_max": Quantity(3, "A"), # Maximum output current + "dI_l": Quantity(1, "A"), # Inductor ripple current + "V_fb": Quantity(0.8, "V"), # Feedback reference voltage + "G_mp": Quantity(20, "A/V"), # Current sense gain + "G_ma": Quantity(140E-6, "A/V"), # Error amplifier transconductance + "C_out CAP": Quantity(47E-6, "F"), # Capacitance of C_out + "C_out ESR": Quantity(2E-3, "ohm"), # Equivalent series resistance of C_out + "V_tgt": Quantity(5.1, "V"), # Lower bound of the target voltage + "R_dmin": Quantity(500E3, "ohm"), # Minimum resistance of the voltage divider + "R_dmax": Quantity(700E3, "ohm"), # Maximum resistance of the voltage divider (specified in datasheet) + "R_div series": "E96", # The resistor series to use in the voltage divider + } + + # Optimize for fast load response + fast_response = True + + # According to the datasheet F_crs should typically be around 20 KHz. For + # the two fast load response application examples it has been set to around + # 24 KHz, and for the two others it is around 16 Khz (reverse-engineered + # from the equation for the phase compensation resistor R3). + config["F_crs"] = Quantity(24000 if fast_response else 16000, "Hz") + + # The datasheet recommends inserting a zero point under 1/6 of F_crs. + # All application examples use a 6.8 nF capacitance for C2, based on + # which the actual multipliers are roughly 1/12 with fast load response + # and 1/6 without. + config["F_z"] = (1 / 12 if fast_response else 1 / 6) * config["F_crs"] + + ic = BD9E302EFJ(config) + for k in ic.valuations.keys(): + print(f"{k}: {ic.evaluated[k]}") diff --git a/tools/psu/bd9e302efj.py b/tools/iccc/bd9e302efj_old.py similarity index 100% rename from tools/psu/bd9e302efj.py rename to tools/iccc/bd9e302efj_old.py diff --git a/tools/iccc/iccc.py b/tools/iccc/iccc.py new file mode 100644 index 0000000..0774cb5 --- /dev/null +++ b/tools/iccc/iccc.py @@ -0,0 +1,32 @@ +# Copyright 2021 The Racklet Authors +# SPDX-License-Identifier: Apache-2.0 + +import lazydict +from quantities import Quantity + + +# Integrated Circuit Component Configurator (iccc) is a tool to help with computing +# correct values for external components of various integrated circuits. An integrated +# circuit should be represented as a class that inherits IC and fills in the valuations. +# See the implementation for ROHM's BD9E302EFJ (bd9e302efj.py) as an example. + +class IC: + # Named valuations (components properties, voltages, currents) for the IC + valuations = {} + + def __init__(self, external_variables): + self.evaluated = lazydict.LazyDictionary(self.valuations) + self.evaluated.update(external_variables) + + +class Component(Quantity): + def __new__(cls, data, unit, series, round_up, dtype=None, copy=True): + if isinstance(data, Quantity): + # We need to drop units here since datasheet formulas might not + # necessarily be "mathematically valid" wrt. the quantities. + data = data.simplified.magnitude + cls.series = series + cls.round_up = round_up + return super().__new__(cls, data, unit, dtype, copy) + + # TODO: __str__ that automatically resolves micro, nano, pico... diff --git a/tools/iccc/util.py b/tools/iccc/util.py new file mode 100644 index 0000000..49580c0 --- /dev/null +++ b/tools/iccc/util.py @@ -0,0 +1,75 @@ +# Copyright 2021 The Racklet Authors +# SPDX-License-Identifier: Apache-2.0 + +import iec60063 +import itertools +import math + +import quantities as q + + +class VoltageDivider: + """ + Generate a resistor voltage divider. + + :param compute_v: function to use for computing the target voltage + :param target_voltage: target voltage for the divider as determined by compute_v + :param sum_min: minimum total resistance of the voltage divider + :param sum_max: maximum total resistance of the voltage divider + :param series: resistor series to use, one of iec60063.all_series + :param minimum: should the given target voltage be treated as a minimum instead of maximum + :param minimum_magnitude: the minimum magnitude (power of 10) to consider for candidate resistors + :return: triple containing the final resistor values and resulting voltage + """ + + def __init__(self, compute_v, target_voltage, sum_min, sum_max, series, minimum=True, minimum_magnitude=0): + self.compute_v = compute_v + self.target_voltage = target_voltage + self.sum_min = sum_min + self.sum_max = sum_max + self.series = series + self.minimum = minimum + self.minimum_magnitude = minimum_magnitude + + def compute(self, v): + series = v[self.series] + sum_max = v[self.sum_max].simplified.magnitude + sum_min = v[self.sum_min].simplified.magnitude + target_voltage = v[self.target_voltage].simplified.magnitude + assert sum_max > sum_min + + resistances = [] + maximum_magnitude = math.floor(math.log10(sum_max)) + 1 + assert maximum_magnitude > self.minimum_magnitude + for i in range(self.minimum_magnitude, maximum_magnitude): + ohms = [float(n) * (10 ** i) * q.ohm for n in iec60063.get_series(series)] + resistances += [n for n in ohms if n < sum_max] + + solution_found = False + (best_voltage, best_r1, best_r2) = ((math.inf if self.minimum else -math.inf) * q.V, 0 * q.ohm, 0 * q.ohm) + for r1, r2 in itertools.permutations(resistances, 2): + r_sum = r1 + r2 + if r_sum > sum_max or r_sum < sum_min: + continue + voltage = self.compute_v(v, r1, r2) + if (self.minimum and voltage < target_voltage) or (not self.minimum and voltage > target_voltage): + continue + if (self.minimum and voltage < best_voltage) or (not self.minimum and voltage > best_voltage): + solution_found = True + (best_voltage, best_r1, best_r2) = (voltage, r1, r2) + assert solution_found + v[id(self)] = (best_voltage, best_r1, best_r2) + + def retrieve(self, v, i): + if not id(self) in v: + self.compute(v) + return v[id(self)][i] + + def voltage(self): + return lambda v: self.retrieve(v, 0) + + def r1(self): + return lambda v: self.retrieve(v, 1) + + def r2(self): + return lambda v: self.retrieve(v, 2)