As always, optimize when necessary, but don't overdo it.
Very true; my goal for showing that implementation was to show how to do it lightweight on an AVR. (I do realize it was a bit too deep to fit in the discussion; apologies to everyone!)
As discussed in the
Constructing short C strings ... thread and elsewhere, there are lots of ways to do the conversion, some of them simple, some of them efficient, and many both (but exactly which ones depends on the exact hardware architecture). Even readable pure-standard C implementations are fast enough if used only say few hundred times per second or less.
Let me retry my original attempt from a different tack. Hopefully, this will be easier to understand for everyone than my previous descriptions.
Any real value
V can be approximated using an integer
X in fixed point format using a constant factor
F,
X ≃
V ×
F V ≃
X /
FFor some use cases, you want a power of ten
F, for other use cases a power of two
F, and for others an arbitrary, possibly even rational or irrational non-integer
F.
Power of ten
F is nice for display purposes; conversion to string is fast, but arithmetic operations (except addition and subtraction) slower.
Power of two
F is nice for computation; conversion to string is slower, but arithmetic fast: integer speed except for a binary right-shift after each integer arithmetic multiplication or division.
Non-integer
F are rare, and mostly used in special cases. (Even
X≃V²,
V≃sqrt(
X), is more common, for example in computations involving 2D (lattice) Euclidean distances.)
Simon wants
X to be human-readable in mV, but with at least one digit of precision. The obvious solution is to have
X=1 represent
V=0.1mV, i.e.
F=10 [mV]⁻¹. Then, 5.0 V = 5 000 mV corresponds to
X=50 000. An
uint16_t type
X can then represent voltages between 0 and 6.5535 mV = 6 553.5 mV; useful on 8-bit architectures like AVRs.
However, the ADC in a microcontroller provides a ratiometric value, usually with some power of two having a specific voltage, and zero at zero volts (although the ADC might have to be biased using e.g. an opamp circuit if the interesting voltage range does not include zero). This we solve by linearly mapping the ADC range to the correct
X range, using exact integer math (because both are integers); this is the rational
F case,
F=
N/
D where
N is the range (number of integer values) of
X, and
D is the range (number of integer values) of ADC results. We only need to be careful to use integer types that are large enough to hold the intermediate integer values.
That mapping operation requires one multiplication and one division, and possibly an addition and/or subtraction (when one or both ranges do not start at zero). If the full range of a typical binary ADC is used, multiplication by
F is an integer multiplication (by
N) followed by a binary right shift (to implement the division by power-of-two
D), and the entire operation is rather fast. (Also note that for runtime calibration, calibrating the
V range – assigning correct voltage values to ADC zero reading and to the value the ADC compares to –, while keeping the ADC range
D a constant power of two, means this efficiency is retained! Unfortunately, in practical circuits, it means we need to extrapolate the voltage from an ADC reading, rather than just store the ADC reading for known voltages, which can reduce the accuracy of the calibration.)
Using
F=10 (for mV units) or
F=10000 (for V units) makes display fast an easy, and does not affect addition or subtraction, but it does add an integer multiply-by-
F when dividing by a voltage
V, and an integer divide-by-
F when multiplying by a voltage
V. If such arithmetic operations are rare compared to displaying the values as strings, then a power-of-ten-
F makes sense. If voltages are often used in multiplication and/or division, then using a power of two
F would be more efficient, although then
X would be in units of (some power of two) millivolts or volts. (For tenth of millivolt precision,
F=2³=8 would work, although
F=2⁴=16 would allow all fractional digits to appear in the value; these two bracketing the tenth-of-a-millivolt precision. Is octal human-readable? With
F=8,
X=01 (decimal 1) would refer to
V=0.125,
X=010 (decimal 8) to
V=1.0, and say
X=035 (decimal 29) to
V=3.0+5/8=3.625. However,
X=07231 (decimal 3737) would refer to
V=7×64+2×8+3+1/8=467.125, so I don't think larger octal values are that human-readable.)