I needed to generate resistor pairings for a resistor voltage divider to generate voltages from 0 to full scale in 256 steps for use with configuration on a PCB. The following script parses a CSV file dumped out from component supplier DigiKey and from the over 500 resistor values selects 58 that can be used to generate the 254 pairings needed (255 and 0 are special cases that aren't included).
use strict;
use warnings;
use Text::CSV qw(csv);
use List::BinarySearch;
my $maxInValue = 255; # Generate 8 bit
+ADC values
my $minResistance = 300;
my $maxResistance = 100e3;
my %unitMul = (kOhms => 1.0e3, Ohms => 1.0,);
my $rFileName = 'resistorSeries.csv';
my @rows = @{csv(in => $rFileName, headers => "auto")};
my @resistorValues = @rows;
my %rowByValue;
# Pull out resistor values
for my $rowIdx (0 .. $#rows) {
my $row = $rows[$rowIdx];
my ($resistance, $units) = split ' ', $row->{Resistance};
die "Can't scale by $units\n" if !exists $unitMul{$units};
$resistorValues[$rowIdx] = $resistance * $unitMul{$units};
$rowByValue{$resistorValues[$rowIdx]} //= $row;
}
# Make sure we only have one instance of each resistor value
my %unique = map {$_ => 1} @resistorValues;
@resistorValues = keys %unique;
@resistorValues =
grep {$_ >= $minResistance && $_ < $maxResistance} @resistorValues
+;
@resistorValues = sort {$a <=> $b} @resistorValues;
# Target values 0 and 255 are special cases so exclude those.
my @targets = map {{target => $_, pairs => []}} 1 .. $maxInValue
+ - 1;
my %targetByValue = map {$targets[$_]->{target} => $_} 0 .. $#targets;
my %usedCounts;
my $worstBestErr = 0.0;
# Find all the ways we can get close to each target value
for my $wanted (@targets) {
my $values = $wanted->{pairs} //= [];
my $usedValues = $wanted->{usedValues} //= {};
# $ratio = Vin / Vout = 255 / target
my $ratio = $maxInValue / $wanted->{target};
for my $r2 (@resistorValues) {
my $tryR1 = ($ratio - 1) * $r2;
next if $tryR1 < $resistorValues[0];
last if $tryR1 > $resistorValues[-1];
my $r1 = nearestR($tryR1);
my $actValue = 255 * $r2 / ($r2 + $r1);
my $err = abs($wanted->{target} - $actValue);
push @$values, {r1 => $r1, r2 => $r2, err => $err, value => $a
+ctValue};
$usedValues->{$r1} = $values->[-1];
$usedValues->{$r2} = $values->[-1];
}
# Sort values by error
@$values = sort {$a->{err} <=> $b->{err}} @$values;
$worstBestErr = $values->[0]{err} if $worstBestErr < $values->[0]{
+err};
}
# Count number of uses of each resistor value used
for my $target (@targets) {
my $values = $target->{pairs};
# Eliminate any pairings with a worse error than $worstBestErr
@$values = grep {$_->{err} <= $worstBestErr} @$values;
for my $value (@$values) {
++$usedCounts{$value->{r1}};
++$usedCounts{$value->{r2}};
}
}
# Select resistor pairings
my $valuesCount = keys %usedCounts;
my %missingTargetIdx = map {$_ => 1} 0 .. $maxInValue - 2;
my @rByCount = sort {$usedCounts{$b} <=> $usedCounts{$a}} keys %usedCo
+unts;
my %usedR;
my %targetPairs;
while (keys %missingTargetIdx) {
if (!@rByCount) {
die "Can't generate pairings for: @{[keys %missingTargetIdx]}\
+n";
}
my $targetR = shift @rByCount;
my @matches;
# Find all target values that use the current target resistor
for my $targetIdx (keys %missingTargetIdx) {
my $target = $targets[$targetIdx];
my $value = $target->{target};
for my $pair (@{$target->{pairs}}) {
next if $pair->{r1} != $targetR && $pair->{r2} != $targetR
+;
push @matches,
{targetIdx => $targetIdx, r1 => $pair->{r1}, r2 => $pa
+ir->{r2}};
$targetPairs{$target->{target}} =
[$pair->{r1}, $pair->{r2}, $pair->{value}, $pair->{err
+}];
last;
}
}
next if !@matches;
# Remove targets we've just found from %missingTargets and add bot
+h
# resistors in each target pair to %usedR
for my $match (@matches) {
++$usedR{$match->{r1}};
++$usedR{$match->{r2}};
delete $missingTargetIdx{$match->{targetIdx}};
}
# See if there are any other targets that are satisfied by %usedR
+values
for my $targetIdx (keys %missingTargetIdx) {
my $target = $targets[$targetIdx];
my $used = $target->{usedValues};
for my $pairKey (keys %$used) {
next
if !exists $usedR{$used->{$pairKey}{r1}}
|| !exists $usedR{$used->{$pairKey}{r2}};
$targetPairs{$target->{target}} = [
$used->{$pairKey}{r1}, $used->{$pairKey}{r2},
$used->{$pairKey}{value}, $used->{$pairKey}{err}
];
++$usedR{$used->{$pairKey}{r1}};
++$usedR{$used->{$pairKey}{r2}};
delete $missingTargetIdx{$targetIdx};
last;
}
}
}
# BOM report
my $bomCount = keys %usedR;
my @bomValues = sort {$a <=> $b} keys %usedR;
print "$bomCount values used excluding 0 Ohm and 'DNF'\n";
for my $value (@bomValues) {
my $row = $rowByValue{$value};
printf "%5d: %15s\n", $value, $row->{"Manufacturer Part Number"};
}
# Resistor pairings report
my $maxZ = 0;
my $minR = 100000;
for my $pair (sort {$a <=> $b} keys %targetPairs) {
my $r1 = $targetPairs{$pair}[0];
my $r2 = $targetPairs{$pair}[1];
my $r1t = 1.001 * $targetPairs{$pair}[0];
my $r1b = 0.999 * $targetPairs{$pair}[0];
my $r2t = 1.001 * $targetPairs{$pair}[1];
my $r2b = 0.999 * $targetPairs{$pair}[1];
my $topValue1 = 255 * $r2t / ($r2t + $r1b);
my $botValue1 = 255 * $r2b / ($r2b + $r1t);
my $topValue2 = 255 * $r2b / ($r2b + $r1b);
my $botValue2 = 255 * $r2t / ($r2t + $r1t);
my $topValue = $topValue1 > $topValue2 ? $topValue1 : $topValue2;
my $botValue = $botValue1 < $botValue2 ? $botValue1 : $botValue2;
my $span = $topValue - $botValue;
my $z = 1 / (1 / $r1 + 1 / $r2);
my $r = $r1 + $r2;
$maxZ = $z if $maxZ < $z;
$minR = $r if $minR > $r;
printf
"%3d: R1 %5d, R2 %5d = %6.2f (err %5.3f: %6.2f - %6.2f = %4.2f
+), z = %.0f\n",
$pair, @{$targetPairs{$pair}}, $botValue, $topValue, $span, $z
+;
}
printf "Max Z: %.0f, min R: %.0f\n", $maxZ, $minR;
sub nearestR {
my ($target) = @_;
my $insertPoint = List::BinarySearch::binsearch_pos {$a <=> $b}
$target, @resistorValues;
if ($insertPoint > 0) {
my ($prev, $next) = @resistorValues[$insertPoint - 1, $insertP
+oint];
if ($target - $prev < $next - $target) {
--$insertPoint;
}
}
return $resistorValues[$insertPoint];
}
Prints:
58 values used excluding 0 Ohm and 'DNF'
301: ERJ-PB3B3010V
340: ERJ-PB3B3400V
...
90900: ERJ-PB3B9092V
95300: ERJ-PB3B9532V
1: R1 90900, R2 357 = 1.00 (err 0.002: 1.00 - 1.00 = 0.00),
+z = 356
2: R1 95300, R2 750 = 1.99 (err 0.009: 1.99 - 2.00 = 0.01),
+z = 744
3: R1 49900, R2 590 = 2.98 (err 0.020: 2.97 - 2.99 = 0.01),
+z = 583
4: R1 80600, R2 1270 = 3.96 (err 0.044: 3.95 - 3.96 = 0.02),
+z = 1250
5: R1 80600, R2 1620 = 5.02 (err 0.024: 5.01 - 5.03 = 0.02),
+z = 1588
...
253: R1 340, R2 43200 = 253.01 (err 0.009: 253.00 - 253.01 = 0.01),
+z = 337
254: R1 357, R2 90900 = 254.00 (err 0.002: 254.00 - 254.00 = 0.00),
+z = 356
Max Z: 36437, min R: 602
The report shows the actual values generated with ideal resistors and the range of values taking resistor tolerances into account. The z value is the effective impedance the ADC input sees. min R is the minimum series resistance of any resistor pair which relates to the maximum current through the resistors.
Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond
-
Are you posting in the right place? Check out Where do I post X? to know for sure.
-
Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
<code> <a> <b> <big>
<blockquote> <br /> <dd>
<dl> <dt> <em> <font>
<h1> <h2> <h3> <h4>
<h5> <h6> <hr /> <i>
<li> <nbsp> <ol> <p>
<small> <strike> <strong>
<sub> <sup> <table>
<td> <th> <tr> <tt>
<u> <ul>
-
Snippets of code should be wrapped in
<code> tags not
<pre> tags. In fact, <pre>
tags should generally be avoided. If they must
be used, extreme care should be
taken to ensure that their contents do not
have long lines (<70 chars), in order to prevent
horizontal scrolling (and possible janitor
intervention).
-
Want more info? How to link
or How to display code and escape characters
are good places to start.