HOWTO: Avoid floating-points in (low level) embedded systems en

By RoadRunner84 on Monday 1 April 2013 15:25 - Comments (3)
Category: -, Views: 4.530

On this forum I attend, there was a guy asking how to scale a read ADC value to be fit for human reading. I'll start out writing the exact situation and then continue with a copy of my story about avoiding floating points.

The situation
The user wanted to link a LiPo battery to a microcontroller to measure discharged capacity. The LiPo pack sources up to 4.2V to the circuit.
The used microcontroller allows for 2.5V on it's 10-bit ADC input. To scale the 4.2V to 2.5V or the ADC, a voltage divider is used. The values of the resistors would be 1k and 1.5k, resulting in Vo = Vi * 1.5k / (1.5k + 1k) with Vi being 4.16V and Vo being 2.5V.

Another guy suggests translating the value as follows:
You'll have to correct your voltage to be:
voltage = value/1023*2.5*2.5/1.5;
The maximum you can measure is 4.17V.

Ow! This hurts! Value (0-1023) divided by 1023 will be 0 unless value is 1023. At least, that's the case for integers! And in a microcontroller you don't want to use floating point!

So, I feel obligated to describe how this is done without making a monster of your code.
We have a value that is in the range 0 to 1023 which actually maps onto a voltage of 4.16V
I stated we don't want to use floating point, so 4.16V is actually 4167mV or 417cV (centivolts)
Would we divide 4166.6 over 1023 the value would be about 4.073. In an integer multiplication this would become 4.
The error this generates is noticably large, since 1023 * 4 = 4092 or 4.092V
If you can settle for a displayed value of 4.09V for an actual voltage of 4.17V then you're done: just multiply by 4 to get the value in millivolts.

If you want higher accuracy, seek for the larges multiplier within the 0 to 216-1 (65535) range.
So, we already determined that the multiplier value is close to 4.073. Let's divide 65536/1024 by that value to see how many times we can fit that in. (65536/1024)/4.073... is about 15.7.
Too bad it fits just under 16 times, since we don't have a hardware accelerated divider we settle for 8 (highest power of 2 that fits under 15.7).
So, we have a value of (4166.6/1023)*8, which turns out to be about 32.55. Too bad this is close to 32.5. But just for the example we'll continue.
A value of 1023 would be 4.16V, so we'll multiply the value 1023 by the rounded value of 32.55, which is 33. The result is 33759. This result is now divided by 8 (23) (since that is the multiple of the constant we just used). 33759/8=4219.875. which rounds down to 4.21V.

So an actual voltage of 4.16V is displayed as
4.09V for the formula voltage = value * 4; with an error of 0,06V
4.21V for the formula voltage = (value * 33u) >> 3; with an error of 0.043V
Both are about the same. But what I wanted to explain is, using this little bit of math results in way smaller and way faster code, at the expense of a mere 67 millivolts worst case.

Let me add a third calculation. In the second example I showed that the multiplicand is close to 32.5. This means that multiplying it by 2 would get it close to 65.0. This is good, because a smaller discarded fraction results in a more accurate result. The expense here will be that we cannot use 16-bit (native) integer resolution anymore. We could actually expand the resolution to the full 32 bits now, but that's for example 4.
We repeat the calculation of dividing (2something/1024)/4.073... but with something being 17 intead of 16. The result is (217/1024)/4.073... which is about 31.4. This again is just under 32, so we'll go for 16 now (highest power of 2 under 31.4).
Again we calculate (4166.6/1023)*16, which now turns out to be about 65.1678. This is quite good, since this value is close to 65.
A value of 1023 would be 4.16V, so we'll multiply the value 1023 by 65. The result is 66,495. Notice this value is larger than a native integer can contain on the MSP430; the code will be a little less efficient. This result is now divided by 16 (24) (since that is the multiple of the constant we just used). 66,495/16=4155.9375. which rounds down to 4.15V. Wow, that's close to 4.16V!

Now, for a last example, since we crossed the 16 bit boundary, let's do this again for the highest value within the 32 bit realm.
Again we calculate (2something/1024)/4.073..., but now for something we use 32. The result is (232/1024)/4.073... which is about 1,029,785.5, this is just under 1,048,576 (220), so we'll settle for 524,288 (219). These numbers are larger, but the calculations are identical to the previous examples.
Now we calculate again (4166.6/1023)*524,288, which is about 2,135,418.7. The fracion is not close to a whole number, but it gets divided away so heavily that we won't notice it later on.
A value of 1023 would be 4.16V, so we'll multiply the value 1023 by 2,135,418. The result is 2,184,532,614. Now the result is divided by 524,288 (219): 2,184,532,614/524,288=4166.66529... which rounds down to 4.166665V which is even closer to 4.16V

For a last summary:
4.09V for the formula voltage = value * 4; with an error of 0.06V (about 67mV)
4.21V for the formula voltage = (value * 33u) >> 3; with an error of 0.043V (about 43mV)
4.15V for the formula voltage = (value * 65L) >> 4; with an error of 0.01072916V (about 11mV)
4.16V for the formula voltage = (value * 524288uL) >> 19; with an error of 0.0000013720194498697916V (about 1.4uV)

And a plot twist:
An accuracy of about 1.4 microvolt seems awesome, but remember that you're using a 10-bit ADC, so you can be off by 4.16/1024 is about 0.004V or 4mV by the quantisation noise introduced by your ADC! Then we assume the resistors are perfect and the ADC is perfectly callibrated and perfectly linear. Neither of these things is the case.
So maybe just saying that the voltage in millivolts is equal to the value multiplied by 4 is just accurate enough.