3

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:

  1. Written in C (not C++!)
  2. Can handle uint8_t and uint16_t values (either using templates, or by having two different versions of the function).
  3. Able to map any non-negative integer value from any range of the same type to any other range of the same type, with perfect accuracy.
  4. Does not use float or double data types.
  5. Microcontrollers often have very small memories. The code size and the RAM usage of the function must be kept small. (How small? I'm not sure. But for reference, the chip I'm using today has space for 2048 assembly code instructions and 512 bytes of RAM, so a good solution should probably not use more than about 10% of that; ideally less.)
  6. The computation should not be so time-inefficient that it impacts performance. (Assume the chip can perform 1 million assembly instructions per second, and the computation shouldn't take more than a 20th of that.)

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
4
  • 2
    The only inaccuracy I see is that it always rounds down. Did you want it to round to closest? Round to even? Or did you mean you want to return a non-"integer" result? Commented Jul 18 at 22:11
  • Yes, round to closest. To my mind, that is the only "accurate" way to do the conversion. But you can't round an integer, and we can't use floating point types... Commented Jul 18 at 22:14
  • My naive solution of multiplying the numbers by 100 allows me to "round" the first two decimal places that conceptually exist. But I was wondering if there was a better way. Commented Jul 18 at 22:17
  • 4
    To round to closest you can add (in_max - in_min)/2 after multiplication. Commented Jul 18 at 22:24

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you. The tests you provided allowed me to check its behaviour for my specific use-case too. Such a simple solution really, but one I was totally ignorant of.
I mapped [0..9] to [9..0] with your code and got correct results. What safeguards would be needed for reversed ranges?
I assume if out_min == out_max I should just return out_min? And if in_min == in_max I just return out_min/2 + out_max/2? (Division before addition to avoid overflow)
To round correctly the sign of (x - in_min) * (out_max - out_min) deserves to be applied to (in_max - in_min) / 2.
Its OPs homework :)
I suggest to introduce a temporary variable long in_span = in_max - in_min; to improve readability.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.