NOTE: I'm leaving the original answer, even though I figured out how to make it provably correct. I'll insert that at the bottom.
Here is an approach that will find a good answer very quickly. I don't have a proof, but I think it will always be optimal. If not always, then often.
If we rearrange:
fout = fin / (i + n/d) / p
we get
fin / fout = (i + n/d) * p = (d*i*p + n*p) / d
So we're looking for a small fraction that comes close to a known number, that you can create from your parameters.
Now let's talk about the Stern-Brocot Tree. (Incidentally Brocot was a clock maker trying to come up with good gearing systems, which isn't that different from your problem.)
Suppose that x
is a positive number. We start the tree with:
0 / 1 < x < 1 / 0 (aka infinity)
(It is usual to start with 1
instead of infinity on the right, but this works and lets you handle numbers bigger than 1
.)
At each step we have:
a / b < x < c / d
Our next guess is (a + c) / (b + d)
. We compare that, replace one side, and continue.
The numbers we generate turn out to include all of the best possible rational approximations. Meaning that if we have a / b
in this sequence, then there is no fraction with denominator smaller than b
that is as good an approximation. And any fraction with that property WILL occur in this sequence.
Let's demonstrate it on finding approximations to e = 2.718281828459045
with a denominator at most 512
. This is doable by hand.
0 / 1 < e < 1 / 0, guess 1 / 1 = 1 < e
1 / 1 < e < 1 / 0, guess 2 / 1 = 2 < e
2 / 1 < e < 1 / 0, guess 3 / 1 = 3 > e
2 / 1 < e < 3 / 1, guess 5 / 2 = 2.5 < e
5 / 2 < e < 3 / 1, guess 8 / 3 = 2.666666666666666 < e
8 / 3 < e < 3 / 1, guess 11 / 4 = 2.75 > e
8 / 3 < e < 11 / 4, guess 19 / 7 = 2.714285714285714 < e
19 / 7 < e < 11 / 4, guess 30 / 11 = 2.727272727272727 > e
19 / 7 < e < 30 / 11, guess 49 / 18 = 2.722222222222222 > e
19 / 7 < e < 49 / 18, guess 68 / 25 = 2.72 > e
19 / 7 < e < 68 / 25, guess 87 / 32 = 2.7185 > e
19 / 7 < e < 87 / 32, guess 106 / 39 = 2.717948717948718 < e
106 / 39 < e < 87 / 32, guess 193 / 71 = 2.718309859154929 > e
106 / 39 < e < 193 / 71, guess 299 / 110 = 2.718181818181818 < e
299 / 110 < e < 193 / 71, guess 492 / 181 = 2.718232044198895 < e
492 / 181 < e < 193 / 71, guess 685 / 252 = 2.718253968253968 < e
685 / 252 < e < 193 / 71, guess 878 / 323 = 2.718266253869969 < e
878 / 323 < e < 193 / 71, guess 1071 / 394 = 2.718274111675127 < e
1071 / 394 < e < 193 / 71, guess 1264 / 465 = 2.718279569892473 < e
1264 / 465 < e < 193 / 71, guess 1457 / 536 ...and we're out of range
Optimization note, the first few steps are literally just finding what integers you're between. You can skip them by starting with int(fin/fout) / 1 <= x < (1 + int(fin/fout)) / 1
. From there, at most 511
guesses are needed, and usually you need considerably less than that.
And now our candidates for a good fraction below the number we want are:
1264/465, 1071/394, 878/323, ...
And our candidates for a good fraction above the number we want are:
193/71, 87/32, 68/25, ...
Now how do we try to meet the parameters? Well, we have the following rules. If the fraction we want is a/b
then:
d = b
p
is at most min(64, a//d//2)
where //
is integer division. (If p < 1
then no solution.)
p
divides a
and is as large as possible.
i = min(a//p//d, 256)
.
n = a//p - i*d
and we don't find a solution if 511 < n
.
Now let's attempt to get the lower bound 1264/465
.
d = b = 465
p
is at most min(64, a//d//2) = 1
. Note that a//d
is the integer component of a/b
which will usually be the integer part of fout/fin
. So the upper bound on p
in practice is simply min(int(fin/fout/2), 64)
.
p = 1
.
i = min(a//p//d, 256) = min(1264//1//465, 256) = min(2, 256) = 2
n = a//p - i*d = 1264//1 - 2*465 = 334
We don't have to look at the next lower bound because this one worked.
Now let's try to get the upper bound 193/71
.
d = b = 71
p
is at most min(64, a//d//2) = 1
.
p = 1
.
i = min(a//p//d, 256) = min(193//1//71, 256) = 2
.
n = a//p - i*d = 193//1//71 - 2*71 = 51
We don't have to look at the next upper bound because this one worked.
Side note: The cases where you have to check multiple bounds to get an answer will be when 1 < p
is required. So 256 < fin/fout
.
I know this was a lot of writing. But a calculation that can be written out by hand like this should be able to be executed on your microcontroller well within the desired millisecond.
OK, the above did not always work at the top end of the range. Here is a fix.
First of all, p
only has a maximum of 64
values. So only a finite and generally small list of values for p
will leave us with
2 <= fin / fout / p < 257
For each of them we have i = int(fin / fout / p)
. If we have an integer then n = 0
and d = 1
. Else we just need to find n/d
by walking the Stern-Brocot tree as above.
So for each of at most 64 values of p
get a candidate upper and lower candidate. And then pick the best one as your final answer.
If that's too slow, there are possible speedups with either continued fractions, or a binary search past the long stretches of < x
over and over again. But I suspect that the complexity won't pay for itself with such small numbers.