cmathrangemicrocontrollerinteger-arithmetic

How to map a value from one range to another accurately without using floating point?


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

Solution

  • 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: