3
\$\begingroup\$

I wanted to select resistor values for a voltage divider that sets the output voltage of a buck converter. I picked the values recommended in the datasheet, because 3.3V is a standard value there were recommended resistor values. But what if there weren't?

How would I go about optimally determining resistor sizes, that are available in standard packages (100k, 470k etc.), I don't mean ideal values that you could calculate.

Is there a software or a method for me to determine the resistor values optimally, as close to the ideal case, but with readily available standard resistor values?

Edit after Winnys comment:

The particular buck converter that made me ask this was BD9E302EFJ. Its voltage output is determined as given in datasheet. As I said there is already a recommended selection in the datasheet as well.

I usually go about selecting the smaller resistor as 100k then calculate the other. But most often this results in an error that is not to my liking due to rounding. For instance a .2V deviation in the output voltage etc.

enter image description here

Link to datasheet: https://fscdn.rohm.com/en/products/databook/datasheet/ic/power/switching_regulator/bd9e302efj-e.pdf

\$\endgroup\$
6
  • 3
    \$\begingroup\$ Depends on your impedance or leakage current needs. Once you know that and how much accuracy you need, the top resistor maximum residence is set. From there you select the best ratio you can in the highest E-value you are allowed. Do you have a particular example? \$\endgroup\$
    – winny
    Apr 28, 2021 at 11:02
  • \$\begingroup\$ @winny yes I do \$\endgroup\$
    – Emre Mutlu
    Apr 28, 2021 at 11:08
  • \$\begingroup\$ Do what @winny recommended, using Excel \$\endgroup\$
    – TonyM
    Apr 28, 2021 at 11:16
  • \$\begingroup\$ Use the E96 table, CHoose your smaller value, compute the larger and pick nearest value using 1% values , compute tolerance stackup error, redo if not met, or use a spredsheet \$\endgroup\$ Apr 28, 2021 at 11:17
  • \$\begingroup\$ The tolerance stackup for X% pairs ranges from X% to ~2X% depending on how far the from the ratio =1 is. \$\endgroup\$ Apr 28, 2021 at 12:25

2 Answers 2

6
\$\begingroup\$

I while back I wrote a script that accomplishes the same thing as the Douxchamps site above, but not limited to dividers - it does an exhaustive search on any system you feed it.

I haven't published it on GitHub have by request published it on GitHub.

For posterity, here it is in its entirety, with your 3.0/0.8 voltages in an example at the bottom:

"""
Do a quick, sequential, numerical (not symbolic) exploration of some electronic
component values to propose solutions that use standard, inexpensive parts.
"""


from bisect import bisect_left
from itertools import islice
from math import log10, floor
from typing import (
    Callable,
    Iterable,
    List,
    Optional,
    Sequence,
    Set,
    Tuple,
)


# See https://en.wikipedia.org/wiki/E_series_of_preferred_numbers
E3 = (1.0, 2.2, 4.7)

E6 = (1.0, 1.5, 2.2, 3.3, 4.7, 6.8)

E12 = (1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2)

E24 = (
    1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0,
    3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1,
)

E48 = (
    1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69,
    1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01,
    3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36,
    5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53,
)

E96 = (
    1.00, 1.02, 1.05, 1.07, 1.10, 1.13, 1.15, 1.18, 1.21, 1.24, 1.27, 1.30,
    1.33, 1.37, 1.40, 1.43, 1.47, 1.50, 1.54, 1.58, 1.62, 1.65, 1.69, 1.74,
    1.78, 1.82, 1.87, 1.91, 1.96, 2.00, 2.05, 2.10, 2.15, 2.21, 2.26, 2.32,
    2.37, 2.43, 2.49, 2.55, 2.61, 2.67, 2.74, 2.80, 2.87, 2.94, 3.01, 3.09,
    3.16, 3.24, 3.32, 3.40, 3.48, 3.57, 3.65, 3.74, 3.83, 3.92, 4.02, 4.12,
    4.22, 4.32, 4.42, 4.53, 4.64, 4.75, 4.87, 4.99, 5.11, 5.23, 5.36, 5.49,
    5.62, 5.76, 5.90, 6.04, 6.19, 6.34, 6.49, 6.65, 6.81, 6.98, 7.15, 7.32,
    7.50, 7.68, 7.87, 8.06, 8.25, 8.45, 8.66, 8.87, 9.09, 9.31, 9.53, 9.76,
)


def bisect_lower(a: Sequence[float], x: float) -> int:
    """
    Run bisect, but use one index before the return value of `bisect_left`
    :param a: The sorted haystack
    :param x: The needle
    :return: The index of the array element that equals or is lesser than `x`
    """
    i = bisect_left(a, x)
    if (
        (i < len(a) and a[i] > x) or
        (i >= len(a) and a[i % len(a)]*10 > x)
    ):
        i -= 1
    return i


def approximate(x: float, series: Sequence[float]) -> (int, float):
    """
    Approximate a value by using the given series.
    :param x: Any positive value
    :param series: Any of E3 through E96
    :return: An integer index into the series for the element lesser than or
             equal to the value's mantissa, and the value's decade - a power of
             ten
    """
    if x == float('inf'):
        return None, float('inf')

    decade = 10**floor(log10(x))
    mantissa = x / decade
    index = bisect_lower(series, mantissa)
    if index >= len(series):
        return 0, decade * 10
    return index, decade


def fmt_eng(x: float, unit: str, sig: int = 2) -> str:
    """
    Format a number in engineering (SI) notation
    :param x: Any number
    :param unit: The quantity unit (Hz, A, etc.)
    :param sig: Number of significant digits to show
    :return: The formatted string
    """
    if x == 0:
        p = 0
    elif x == float('inf'):
        return '∞'
    else:
        p = floor(log10(abs(x)))
    e = int(floor(p / 3))
    digs = max(0, sig - p%3 - 1)
    mantissa = x / 10**(3*e)

    if e == 0:
        prefix = ''
    elif 0 < e < 9:
        # See https://en.wikipedia.org/wiki/Metric_prefix
        prefix = ' kMGTPEZY'[e]
    elif 0 > e > -8:
        prefix = 'mμnpfazy'[-e-1]
    else:
        raise IndexError(f'Number out of SI range: {x:.1e}')

    fmt = '{:.%df} {:}{:}' % digs
    return fmt.format(mantissa, prefix, unit)


# a callable with any number of floating-point
# arguments, returning a float
CalculateCall = Callable #[
#    [float, ...],
#    float
#]


class ComponentValue:
    """
    A value associated with a component - to track approximated values
    """

    def __init__(
        self,
        component: 'Component',
        decade: Optional[float] = None,
        index: Optional[int] = None,
        exact: Optional[float] = None,
    ):
        """
        Valid combinations:
            exact - approximated value will be calculated
            exact, index, decade - approximated value = series[index]*decade
            index, decade - approximated value = series[index]*decade;
                            exact=approximate
        :param decade: The quantity's power-of-ten
        :param index: The integer index into the series for the quantity's
                      mantissa
        :param exact: The exact quantity, if known
        """

        self.component = component

        if index is None:
            assert decade is None
            assert exact is not None
            self.exact = exact
            self.index, self.decade = approximate(exact, component.series)
        else:
            assert decade is not None
            self.index, self.decade = index, decade

        if self.decade == float('inf'):
            self.approx = float('inf')
        else:
            self.approx = component.series[self.index] * self.decade

        if index is not None:
            if exact is None:
                self.exact = self.approx
            else:
                self.exact = exact

    @property
    def error(self) -> float:
        return self.approx / self.exact - 1

    def get_other(self) -> Optional['ComponentValue']:
        """
        :return: If this approximated value is below its exact value, then the
                 next-highest E24 value; otherwise None
        """
        if self.approx >= self.exact:
            return None

        index, decade = self.index + 1, self.decade
        if index >= len(self.component.series):
            index = 0
            decade *= 10
        return ComponentValue(component=self.component, exact=self.exact,
                              index=index, decade=decade)

    def get_best(self) -> 'ComponentValue':
        other = self.get_other()
        if other is None:
            return self

        if self.error**2 < other.error**2:
            return self
        return other

    def __str__(self):
        return fmt_eng(self.approx, self.component.unit, self.component.digits)

    def fmt_exact(self) -> str:
        return fmt_eng(self.exact, self.component.unit, 4)


class Component:
    """
    A component, without knowledge of its value - only bounds and defining
    formula
    """

    def __init__(
        self,
        prefix: str,
        suffix: str,
        unit: str,
        series: Sequence[float] = E24,
        calculate: Optional[CalculateCall] = None,
        minimum: float = 0,
        maximum: Optional[float] = None,
        use_for_err: bool = True,
    ):
        """
        :param prefix: i.e. R, C or L
        :param suffix: Typically a number, i.e. the "2" in R2
        :param unit: i.e. Hz, A, F, ...
        :param series: One of E3 through E96
        :param calculate: A callable that will be given all values of previous
                          components in the calculation sequence. These values
                          are floats, and the return must be a float.
                          If this callable is None, the component will be
                          interpreted as a degree of freedom.
        :param minimum: Min allowable value; the return of calculate will be
                        checked against this and failures will be silently
                        dropped.
                        Must be at least zero, or greater than zero if
                        calculate is not None.
        :param maximum: Max allowable value; the return of calculate will be
                        checked against this and failures will be silently
                        dropped.
        :param use_for_err: If True, error from this component's ideal to
                            approximated value will influence the solution rank.
        """
        (
            self.prefix, self.suffix, self.unit, self.series,
            self.calculate, self.min, self.max, self.use_for_err,
        ) = (
            prefix, suffix, unit, series, calculate, minimum, maximum,
            use_for_err,
        )

        assert minimum >= 0
        assert maximum is None or maximum >= minimum

        if calculate:
            self.values = self._calculate_values
        elif minimum > 0:
            self.start_index, self.start_decade = approximate(minimum, series)
            self.values = self._iter_values

        self.digits: int = 3 if len(series) > 24 else 2

        self.fmt_field: Callable[[str], str] = (
            ('{:>%d}' % (4 + self.digits)).format
        )

    def __str__(self):
        return self.name

    @property
    def name(self) -> str:
        return f'{self.prefix}{self.suffix}'

    def _calculate_values(
            self, prev: Sequence[ComponentValue]
    ) -> Iterable[ComponentValue]:

        def values():
            # Get the value based on exact values first
            from_exact_val = self.calculate(*(p.exact for p in prev))
            if from_exact_val <= 0:
                return

            from_exact = ComponentValue(self, exact=from_exact_val)
            yield from_exact
            other = from_exact.get_other()
            if other:
                yield other

            # See if there's a difference when calculating against approximated
            # values
            from_approx_val = self.calculate(*(p.approx for p in prev))
            if from_approx_val > 0:
                from_approx = ComponentValue(self, exact=from_approx_val)
                if from_approx.exact != from_exact.exact:
                    yield from_approx
                    other = from_approx.get_other()
                    if other:
                        yield other

        for v in values():
            if (
                self.min <= v.exact and
                (self.max is None or self.max >= v.exact)
            ):
                yield v

    def _all_values(self) -> Iterable[Tuple[int, float]]:
        decade = self.start_decade
        for index in range(self.start_index, len(self.series)):
            yield index, decade
        while True:
            decade *= 10
            for index in range(len(self.series)):
                yield index, decade

    def _iter_values(
        self, prev: Sequence[ComponentValue]
    ) -> Iterable[ComponentValue]:
        for index, decade in self._all_values():
            value = ComponentValue(self, index=index, decade=decade)
            if value.approx > self.max:
                return
            yield value


class Resistor(Component):
    def __init__(
        self,
        suffix: str,
        series: Sequence[float] = E24,
        calculate: Optional[CalculateCall] = None,
        minimum: float = 0,
        maximum: Optional[float] = None,
        use_for_err: bool = True,
    ):
        super().__init__('R', suffix, 'Ω', series, calculate, minimum, maximum,
                         use_for_err)


class Capacitor(Component):
    def __init__(
        self,
        suffix: str,
        series: Sequence[float] = E24,
        calculate: Optional[CalculateCall] = None,
        minimum: float = 0,
        maximum: Optional[float] = None,
        use_for_err: bool = True,
    ):
        super().__init__('C', suffix, 'F', series, calculate, minimum, maximum,
                         use_for_err)


class Output:
    """
    A calculated parameter - potentially but not necessarily a circuit output -
    to be calculated and checked for error in the solution ranking process.
    """

    def __init__(
            self, name: str, unit: str, expected: float,
            calculate: CalculateCall):
        """
        :param name: i.e. Vout
        :param unit: i.e. V, A, Hz...
        :param expected: The value that this parameter would assume under ideal
                         circumstances
        :param calculate: A callable accepting a sequence of floats - one per
                          component, in the same order as they were passed to
                          the Solver constructor; returning a float.
        """
        self.name, self.unit, self.expected, self.calculate = (
            name, unit, expected, calculate,
        )

    def error(self, value: float) -> float:
        """
        :return: Absolute error, since the expected value might be 0
        """
        return value - self.expected

    def __str__(self):
        return self.name


class Solver:
    """
    Basic recursive solver class that does a brute-force search through some
    component values.
    """

    def __init__(
        self,
        components: Sequence[Component],
        outputs: Sequence[Output],
        threshold: Optional[float] = 1e-3,
    ):
        """
        :param components: A sequence of Component instances. The order of this
                           sequence determines the order of parameters passed to
                           Output.calculate and Component.calculate.
        :param outputs: A sequence of Output instances - can be empty.
        :param threshold: Maximum error above which solutions will be discarded
        """
        self.components, self.outputs = components, outputs
        self.candidates: List[Tuple[
            float,                     # error
            Sequence[float],           # output values
            Sequence[ComponentValue],  # component values to get the above
        ]] = []
        self.approx_seen: Set[Tuple[float, ...]] = set()
        self.threshold = threshold

    def _recurse(self, values: List[Optional[ComponentValue]], index: int = 0):
        if index >= len(self.components):
            self._evaluate(values)
        else:
            comp = self.components[index]
            for v in comp.values(values[:index]):
                values[index] = v
                self._recurse(values, index+1)

    def solve(self):
        """
        Recurse through all of the components, doing a brute-force search.
        Results are stored in self.candidates and sorted in order of increasing
        error.
        """
        values = [None]*len(self.components)
        self._recurse(values)
        self.candidates.sort(key=lambda v: v[0])

    def _evaluate(self, values: Sequence[ComponentValue]):
        approx = tuple(v.approx for v in values)
        if approx in self.approx_seen:
            return

        outputs = tuple(
            o.calculate(*approx)
            for o in self.outputs
        )
        err = sum(
            o.error(v)**2
            for o, v in zip(self.outputs, outputs)
        ) + sum(
            v.error**2
            for c, v in zip(self.components, values)
            if c.use_for_err
        )
        if self.threshold is None or err < self.threshold:
            self.candidates.append((err, outputs, tuple(values)))
            self.approx_seen.add(approx)

    def print(self, top: int = 10):
        """
        Print a table of all component values, output values and output error.
        :param top: Row limit.
        """

        print(' '.join(
            comp.fmt_field(comp.name)
            for comp in self.components
        ), end=' ')
        print(' '.join(
            f'{output.name:>10} {"Err":>8}'
            for output in self.outputs
        ))

        for err, outputs, values in islice(self.candidates, top):
            print(' '.join(
                value.component.fmt_field(str(value))
                for value in values
            ), end=' ')
            print(' '.join(
                f'{fmt_eng(value, output.unit, 4):>10} '
                f'{output.error(value):>8.1e}'
                for value, output in zip(outputs, self.outputs)
            ))


def buck():
    # https://fscdn.rohm.com/en/products/databook/datasheet/ic/power/switching_regulator/bd9e302efj-e.pdf
    # page 30

    Vout = 3.0
    Vref = 0.8
    R12max = 700e3
    # 700e3 / R2 * Vref = Vout at limit
    R2max = Vref / Vout * R12max

    svout = Solver(
        (
            Resistor('2', E96, None, R2max/10, R2max, False),
            Resistor(
                '1', E96,
                lambda R2: R2*(Vout/Vref - 1),
                0, None, False,
            ),
        ),
        (
            Output('Vout', 'V', Vout, lambda R2, R1: Vref*(1 + R1/R2)),
        ),
        threshold=1e-3,
    )
    svout.solve()
    svout.print()

buck()

with output

     R2      R1       Vout      Err
 115 kΩ  316 kΩ    2.998 V -1.7e-03
 107 kΩ  294 kΩ    2.998 V -1.9e-03
 150 kΩ  412 kΩ    2.997 V -2.7e-03
 130 kΩ  357 kΩ    2.997 V -3.1e-03
68.1 kΩ  187 kΩ    2.997 V -3.2e-03
59.0 kΩ  162 kΩ    2.997 V -3.4e-03
 118 kΩ  324 kΩ    2.997 V -3.4e-03
84.5 kΩ  232 kΩ    2.996 V -3.6e-03
 169 kΩ  464 kΩ    2.996 V -3.6e-03
49.9 kΩ  137 kΩ    2.996 V -3.6e-03
\$\endgroup\$
6
  • 1
    \$\begingroup\$ So cool! Why don't you document its usage and put it somewhere for everyone to use? \$\endgroup\$
    – Sohail
    Apr 28, 2021 at 18:36
  • \$\begingroup\$ @Sohail Thanks :) I should get around to doing that, yeah. \$\endgroup\$
    – Reinderien
    Apr 28, 2021 at 19:43
  • 1
    \$\begingroup\$ This is amazing! I was thinking of doing sth like this would be very cool. Beat me to it :) \$\endgroup\$
    – Emre Mutlu
    Apr 28, 2021 at 20:25
  • 1
    \$\begingroup\$ Wow... this is the first time I see the use of value tables for the job. You can get rid of about 70% of the code by using simple formulas. See for example Excel formulas for E24, E48 and E96 in the comment by ivica at the end of this post. Or simply use Excell to do the same without coding. \$\endgroup\$
    – Maple
    Apr 29, 2021 at 7:55
  • 1
    \$\begingroup\$ @Maple Re. the formulas - they are value tables "in real life". There are entries that do not follow the logarithm, and so attempting to be clever and calculate as many as possible offers no benefit. As for Excel... For the level of complexity of systems I solve with this tool for real application, it would be a poor fit, and I'd only fall back to it if I wasn't familiar-enough with Python. \$\endgroup\$
    – Reinderien
    Apr 29, 2021 at 13:27
9
\$\begingroup\$

Yes, there are online calculators that will allow you to find resistor pairs that will closely approximate the desired ratio. For example, this one.

If you wanted to have (say) 3.0V out and your reference is 0.8V and we're ignoring bias current, and we're using E96 series (1%) values we get the following results:

enter image description here

Obviously you can scale the values by decades up or down, for example you could use 1.18K and 3.24K.

This particular calculator (and some others) works well in this case because we're not overly concerned with the total resistance of the divider.

Note that a 0.108% (for example) error in the nominal values is dwarfed by the 1.47% worst-case error due to resistor tolerances.

You can also do it manually, takes a bit longer, but just try values from the E96 table and find the closest pair.

And if you really want to hit the nominal values virtually bang-on you can add a resistor series or parallel to one of the pair. That may not be as bad it seems because you can probably pick two of the three resistors as values that you are already stocking, have a reel of, etc. and only one "weird" value is required.

As with anything else online, best pull your your calculator or fire up a spreadsheet and double-check the numbers for yourself.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ great answer, thank you :) \$\endgroup\$
    – Emre Mutlu
    Apr 28, 2021 at 11:20
  • \$\begingroup\$ I said last comment backwards , it shud be, the closer Rl/Rh is to 1, the stackup error = the sum of individual tolerance or 2x. The further away , it approaches the 1X tolerance of the smaller value , thus 1% R’s result in 1 to 2% tolerance stackup error. Rather than rethink the design or use a trimmer as the web author indicates, for the 5% example, the better solution for very low or inversely high ratios is use tighter tolerance resistor for the lower R value only. e.g. 1000:1 0.25% max , using 0.2% for R1 ($) and 1% for 1000R1 will achieve this. Or laser trimmed R network ratios ($) \$\endgroup\$ Apr 28, 2021 at 13:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.