diff options
Diffstat (limited to 'tools')
| -rwxr-xr-x | tools/resistor_selector.py | 327 | 
1 files changed, 327 insertions, 0 deletions
diff --git a/tools/resistor_selector.py b/tools/resistor_selector.py new file mode 100755 index 0000000..144a1c0 --- /dev/null +++ b/tools/resistor_selector.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 + + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + + +import argparse +import dataclasses +from decimal import getcontext, Decimal +import pathlib + + +DESCRIPTION = """Help resistor value calculation for hardware version detection""" +# fmt: off +IMPLEMENTED_SERIES = { +    24: (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), +} +"""Series are hardcoded from Wikipedia since they do not follow the formula""" +# fmt: on +MULTIPLIERS = ( +    100.0, +    1000.0, +    10000.0, +) + + +def main() -> None: +    getcontext().prec = 3 + +    args = Arguments.from_cli() + +    resistors: list[Resistor] = [] +    for multiplier in MULTIPLIERS: +        for value in IMPLEMENTED_SERIES[args.series]: +            resistors.append( +                Resistor( +                    resistance=Decimal(multiplier) * Decimal(value), +                    tolerance=args.tolerance, +                ) +            ) + +    voltage_dividers = [] +    for r1 in resistors: +        for r2 in resistors: +            combination = VoltageDivider(voltage=args.voltage, r1=r1, r2=r2) +            voltage_dividers.append(combination) + +    sorted_voltage_dividers = sorted(voltage_dividers, key=lambda c: c.v_adc) + +    filtered_voltage_dividers = filter( +        source=sorted_voltage_dividers, +        margin=args.margin, +        n_bits_adc=args.n_bits_adc, +        power=args.power, +    ) + +    results = VoltageDividers( +        combinations=tuple[VoltageDivider, ...](filtered_voltage_dividers) +    ) + +    output: str = results.to_tsv( +        voltage=args.voltage, +        n_bits_adc=args.n_bits_adc, +    ) + +    if args.output is None: +        print(output) +    else: +        args.output.write_text(output) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Arguments: +    series: int +    voltage: Decimal  # volts +    n_bits_adc: int  # number of bits +    output: pathlib.Path | None +    margin: int +    power: Decimal  # watt +    tolerance: Decimal  # percent + +    def __post_init__(self) -> None: +        assert isinstance(self.series, int) +        assert self.series in IMPLEMENTED_SERIES + +        assert isinstance(self.voltage, Decimal) +        assert 0.0 < self.voltage + +        assert isinstance(self.n_bits_adc, int) +        assert self.n_bits_adc > 0 + +        if self.output is not None: +            assert isinstance(self.output, pathlib.Path) + +        assert isinstance(self.margin, int) +        assert self.margin >= 0 + +        assert isinstance(self.power, Decimal) +        assert self.power >= Decimal(0.0) + +        assert isinstance(self.tolerance, Decimal) +        assert self.tolerance >= Decimal(0.0) + +    @staticmethod +    def from_cli() -> "Arguments": +        parser = argparse.ArgumentParser( +            description=DESCRIPTION, +        ) + +        default_series = 24 +        parser.add_argument( +            "-s", +            "--series", +            default=default_series, +            help=f"resistor E series (supported: {[k for k in IMPLEMENTED_SERIES]}, default: {default_series})", +        ) + +        default_voltage = 3.3 +        parser.add_argument( +            "-v", +            "--voltage", +            default=default_voltage, +            help=f"voltage [V] powering the voltage divider (default: {default_voltage})", +        ) + +        default_bits = 12 +        parser.add_argument( +            "-b", +            "--bits", +            default=default_bits, +            help=f"number of ADC bits (default: {default_bits})", +        ) + +        parser.add_argument( +            "-o", +            "--output", +            default=None, +            type=pathlib.Path, +            help="output file to write to (default: stdout)", +        ) + +        default_margin: int = 10 +        parser.add_argument( +            "-m", +            "--margin", +            default=default_margin, +            help=f"min. ADC value difference between adjacent voltage dividers (default: {default_margin})", +        ) + +        default_power = 0.001 +        parser.add_argument( +            "-p", +            "--power", +            default=default_power, +            help=f"max. power [W] consumed by voltage divider (default: {default_power})", +        ) + +        default_tolerance = 1.0 +        parser.add_argument( +            "-t", +            "--tolerance", +            default=default_tolerance, +            help=f"resistor tolerance [percent] (default: {default_tolerance})", +        ) + +        args = parser.parse_args() + +        return Arguments( +            series=int(args.series), +            voltage=Decimal(args.voltage), +            n_bits_adc=int(args.bits), +            output=args.output, +            margin=int(args.margin), +            power=Decimal(args.power), +            tolerance=Decimal(args.tolerance), +        ) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Resistor: +    resistance: Decimal  # ohm +    tolerance: Decimal  # percent + +    def __post_init__(self) -> None: +        assert isinstance(self.resistance, Decimal) +        assert self.resistance >= Decimal(0.0) + +        assert isinstance(self.tolerance, Decimal) +        assert self.tolerance >= Decimal(0.0) + +    def __str__(self) -> str: +        return f"{float(self.resistance)} Ohm ({float(self.tolerance)} %)" + +    @property +    def resistance_min(self) -> Decimal: +        return self.resistance * (Decimal(1.0) - (self.tolerance / Decimal(100.0))) + +    @property +    def resistance_max(self) -> Decimal: +        return self.resistance * (Decimal(1.0) + (self.tolerance / Decimal(100.0))) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class VoltageDivider: +    voltage: Decimal  # voltage over both resistors in volts +    r1: Resistor  # resistor closer to + +    r2: Resistor  # resistor closer to - + +    @property +    def power(self) -> Decimal: +        return self.voltage**2 / (self.r1.resistance + self.r2.resistance) + +    @property +    def v_adc_min(self) -> Decimal: +        return self.voltage / ( +            Decimal(1.0) + self.r1.resistance_max / self.r2.resistance_min +        ) + +    @property +    def v_adc(self) -> Decimal: +        return self.voltage / (Decimal(1.0) + self.r1.resistance / self.r2.resistance) + +    @property +    def v_adc_max(self) -> Decimal: +        return self.voltage / ( +            Decimal(1.0) + self.r1.resistance_min / self.r2.resistance_max +        ) + +    @staticmethod +    def volts_to_n_adc( +        max_voltage: Decimal, voltage_adc: Decimal, n_bits_adc: int +    ) -> int: +        n_max = 2**n_bits_adc - 1 +        continuous = voltage_adc / max_voltage * Decimal(n_max) +        discrete = int(continuous + Decimal(0.5)) +        return discrete + +    def n_adc_min(self, n_bits_adc: int) -> int: +        return self.volts_to_n_adc( +            max_voltage=self.voltage, +            voltage_adc=self.v_adc_min, +            n_bits_adc=n_bits_adc, +        ) + +    def n_adc(self, n_bits_adc: int) -> int: +        return self.volts_to_n_adc( +            max_voltage=self.voltage, +            voltage_adc=self.v_adc, +            n_bits_adc=n_bits_adc, +        ) + +    def n_adc_max(self, n_bits_adc: int) -> int: +        return self.volts_to_n_adc( +            max_voltage=self.voltage, +            voltage_adc=self.v_adc_max, +            n_bits_adc=n_bits_adc, +        ) + +    def to_tsv(self, voltage: Decimal, n_bits_adc: int) -> str: +        return ( +            f"{self.r1}" +            f"\t{self.r2}" +            f"\t{self.v_adc_min}" +            f"\t{self.v_adc}" +            f"\t{self.v_adc_max}" +            f"\t0x{self.n_adc_min(n_bits_adc=n_bits_adc):03X}" +            f"\t0x{self.n_adc(n_bits_adc=n_bits_adc):03X}" +            f"\t0x{self.n_adc_max(n_bits_adc=n_bits_adc):03X}" +            f"\t{self.power}" +        ) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class VoltageDividers: +    combinations: tuple[VoltageDivider, ...] + +    def to_tsv(self, voltage: Decimal, n_bits_adc: int) -> str: +        output = ( +            "R1" +            "\tR2" +            "\tV_ADC_min" +            "\tV_ADC" +            "\tV_ADC_max" +            "\tn_ADC_min" +            "\tn_ADC" +            "\tn_ADC_max" +            "\tpower" +        ) +        for combination in self.combinations: +            output += "\n" + combination.to_tsv( +                voltage=voltage, +                n_bits_adc=n_bits_adc, +            ) +        return output + + +def filter( +    source: list[VoltageDivider], +    margin: int, +    n_bits_adc: int, +    power: Decimal, +) -> list[VoltageDivider]: +    sink: list[VoltageDivider] = [] +    v_adc_max = Decimal(0.0) +    n_adc_max = 0 + +    for voltage_divider in source: +        if voltage_divider.v_adc_min <= v_adc_max: +            continue  # overlapping voltage ranges + +        if voltage_divider.n_adc_min(n_bits_adc) < n_adc_max + margin: +            continue  # not enough ADC value margin + +        if voltage_divider.power > power: +            continue  # draws too much power + +        sink.append(voltage_divider) +        v_adc_max = voltage_divider.v_adc_max +        n_adc_max = voltage_divider.n_adc_max(n_bits_adc) + +    return sink + + +if __name__ == "__main__": +    main()  | 
