I am writing firmware for a microcontroller. (In this specific case it is a Microchip PIC, but I am looking for an answer that is equally applicable to any brand of microcontroller.)
Arduino has a map function to convert a value from one range to another...
long map(long x, long in_min, long in_max, long out_min, long out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
...but the internet is full of people claiming it is very imprecise due to it not using floating-point arithmetic. My math skills are poor-to-moderate, so I will just take their word for it.
I believe there ought to be a better solution, without resorting to "expensive" floating-point math. For example, where the inputs are all 8-bit integers (uint8_t
), I have had reasonable success simply casting the values to uint16_t
and multiplying them by some factor (such as 10 or 100) before performing a mapping similar to that shown above. In this way, we can simulate one or two fixed-point fractional places without using floating-point arithmetic, significantly improving precision. (Unless I'm just grossly misunderstanding the situation, which is certainly possible.)
I suspect that my solution is also fairly naive and inefficient, so I want to see if smarter people can come up with a better way.
My requirements are:
uint8_t
and uint16_t
values (either using templates, or by having two different versions of the function).float
or double
data types.Example:
uint8_t input = 162;
uint8_t in_min = 12, in_max = 233, out_min = 5, out_max = 199;
uint8_t output = map_uint8(input, in_min, in_max, out_min, out_max);
// output == 137
The logic adds half of the denominator ((in_max - in_min) / 2) before dividing to achieve rounding to the nearest integer instead of truncating down. This mimics how you'd manually round a fraction.
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return ((x - in_min) * (out_max - out_min) + (in_max - in_min) / 2) / (in_max - in_min) + out_min;
}
void test_map(long x, long in_min, long in_max, long out_min, long out_max, long expected)
{
long result = map(x, in_min, in_max, out_min, out_max);
printf("map(%ld, %ld, %ld, %ld, %ld) = %ld [expected: %ld] %s\n",
x, in_min, in_max, out_min, out_max, result, expected,
result == expected ? "Y" : "N");
}
int main()
{
// Range: 0 to 10 → 0 to 100
test_map(5, 0, 10, 0, 100, 50); // exact middle
test_map(6, 0, 10, 0, 100, 60); // just over halfway
test_map(4, 0, 10, 0, 100, 40); // just under halfway
// Test rounding behaviour
test_map(3, 0, 7, 0, 10, 4); // 3/7 * 10 ≈ 4.285 → rounded to 4
test_map(4, 0, 7, 0, 10, 6); // 4/7 * 10 ≈ 5.714 → rounded to 6
// Test with negative ranges
test_map(5, 0, 10, 100, 200, 150); // midpoint
test_map(7, 0, 10, 100, 200, 170); // > midpoint
return 0;
}
https://godbolt.org/z/7aaGdd1oq
Remember about dangers too:
Division by zero – if in_max == in_min, the denominator becomes zero, causing undefined behaviour.
Signed integer overflow – intermediate multiplication or addition may exceed the range of long, leading to undefined behaviour.
Range inversion errors – if input or output ranges are reversed, it may lead to incorrect or unintended results without safeguards.