Developing in C for the ATmega328P: Mapping Values

6 minute read

Where I describe how to map values in one domain to another domain.


Mapping is the concept of translating a range of numbers to a second range of numbers, where the second range might be smaller (typically) or larger than the first range. For example, if you had a 10-bit analog-to-digital converter (ADC) and wanted to use its values for the duty cycle of an 8-bit pulse-width modulator (PWM), you could “divide by 4” to achieve the desired results. That is, if, the range of the ADC values were 0-1024 and the desired range of the PWM values are 0-255. If not, we’ll need to map the values of the first range into the desired values of the second range. We’ll talk about both approaches in this entry.

Simple Mapping

Simple mapping makes the assumption that the values from both domains will range from 0 to maximum. As in the example above, the 10-bit values range from 0 - 1023 and the 8-bit values range from 0 - 255, and there is a linear relationship between the two domains. In this case, a simple “divide by 4” will solve our problem.

However, when faced with a n-bit to m-bit simple transformation, do not use the divide operator, use shift. The shift operation is a specifically designed microcontroller operation to be fast and efficient. When the processor shifts a value in binary, it automatically divides by 2 to the n (shift n bits right) or multiplies by 2 to the n (shift n bits left). The operation looks like this:

int x = 14;
int by_4 = 2;
int fourth_x = x >> by_4;
int four_x = x << by_4;
printf("x= %d fourth_x= %d four_x= %d \n", x, fourth_x, four_x);
# x= 14 fourth_x= 3 four_x= 56

If n = 2, than the division (or multiplication) is 2 to the second power or 4. Note that as this is an integer operation, a fourth of 14 is 3 and not 3.5. I strongly recommend you copy and paste the above code into a main.c template and try different values. Using the shift operator, needs to be second-nature when you are coding. Here is a good tutorial on the concept as well.

More Complex Mapping

Many times, we won’t have values which range as they do above. The range from one domain, might be 238 - 799, while the second domain might range from 10 - 250. Clearly, the former domain is more than 8-bits, while the latter is within 8-bits. And neither domains have a simple 0 - max range, as well.

In a situation like this, you need to mathematically map the values from the current domain into the desired domain. To do this, you need a specific formula:

N = N1 + ((S - S1) * (N2 - N1)) / (S2 - S1)

  • where N = desired value, S = corresponding value in the current domain
  • N1, N2 are the min and max of the desired domain
  • S1, S2 are the min and max of the current domain

It helps to be more specific, so let’s use the formula above to translate values from a 10-bit ADC to a 8-bit PWM, where we know the ADC will provide values 238 - 799 and we want to ensure we don’t maximize the range of the PWM, so we will map to 50 - 220. We will use the following definitions:

  • ADC_value = S = current value in the ADC domain
  • ADC_min = S1 = minimum value in the ADC domain
  • ADC_max = S2 = maximum value in the ADC domain
  • PWM_value = N = desired value for the PWM domain
  • PWM_min = N1 = minimum value for the PWM domain
  • PWM_max = N2 = maximum value for the PWM domain

PWM_value = PWM_min + ((ADC_value - ADC_min) * (PWM_max - PWM_min)) / (ADC_max - ADC_min)

Even with more appropriately named variables, this equation can appear confusing. To resolve this, I’ve added the equation as a function called appropriately map. This function operates in an identical fashion as the Arduino map function. To the point, the function uses the same code called out on the page in the Appendix.

Testing Map

I created an code to demonstrate how map works. It tests map and shows how long it takes to execute in 62.5nsec ticks. For example, I typically find the map function executes in 892 ticks or 55usec.

puts("\nMapping: Complex Example (map() function)");
uint16_t now;
uint16_t elapsed;

int32_t ADC_value = 1020;
int32_t ADC_max = 1023;
int32_t ADC_min = 0;

int32_t PWM_value;
int32_t PWM_max = 255;
int32_t PWM_min = 0;

now = ticks();
PWM_value = map(ADC_value, ADC_min, ADC_max, PWM_min, PWM_max);
elapsed = ticks();
printf("%u ticks - given %ld in range of %ld - %ld,  mapped value is %ld in range of %ld - %ld \n",\
 elapsed - now, ADC_value, ADC_min, ADC_max, PWM_value, PWM_min, PWM_max);
# Mapping: Complex Example (map() function)
# 892 ticks - given 512 in range of 0 - 1023,  mapped value is 127 in range of 0 - 255 

Note: See this entry to understand more about timing program execution.

Again, I recommend using the code above and evaluating the values returned based on different ranges. In the example above, I used a simple range so that it was immediately obvious the answer was correct. What are the values when the ranges don’t begin at 0?

Additional Points

There are three functions, Arduino constrain(), Arduino min(), and Arduino max() which also need to be mentioned. While they are in the Arduino framework, they are best handled on a local basis, meaning the programmer implements them in their code based on their needs. The reason is this, all three of these functions will be dependent of the type of variable provided. Whether the variable is signed or unsigned, will make a significant difference in the results. In a less impactful manner, it is also important as to whether the type is 8 bits, 16 bits or 32 bits, the results won’t differ, however, having the proper type alignment will reduce the possibility for future errors.

One solution would be to create 6 versions of each function, one to match each type (int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t), however, this seems burdensome for such a simple function.

Note: The Arduino software is able to handle this properly by over-loading the function with multiple types. This is due to the Arduino using C++, as we are using C, we don’t have access to this capability.

Implementation Guidance*

In the examples folder there is an example of using min() and max() as well as an example of using constrain(). All of them use uint16_t as the function type. I recommend changing the type to match what your needs are. I also recommend using min()/max() or constrain(), however not both at the same time.

The functions min()/max() offer a finer control over the range of values than constrain(). And the application constrain() is more of a “let’s keep the outliers out of the data”-type approach. Use min()/max() to ensure you have a good understanding of your data, prior to using map() and use constrain() when you understand your data and want to ensure your numbers don’t have some wild swings.

Comments powered by Talkyard.