Arduino Playground is read-only starting December 31st, 2018. For more info please look at this Forum Post

A multiMap function for Arduino.

Last Modified: August 31, 2017, at 04:50 AM
By:
Platforms: All

Forum Thread for remarks

multiMap function

One of the main applications for the Arduino board is measuring. Often the raw measurements e.g. from the analogRead() function must be converted to get a physical meaning. This convertion function is often linear but can also be more complex. In fact it can even be no function: meaning that a measured value can have two meanings. This is the case with the - SHARP 2Y0A02 F 9Y - distance sensor. Almost every output voltage can mean 2 different distances [graph]. Fortunely only the part from 20-150 cm is the "working range", so there is a function. In practice - using real measured points from the sensor - this function is not approximatable accurate enough with one mathematical function. I found the reMap() function on the playground http://interface.khm.de/index.php/lab/experiments/nonlinear-mapping/ but I wrote a new version to optimize it for speed and to make it a bit more robust.

The code:

// note: the _in array should have increasing values
int multiMap(int val, int* _in, int* _out, uint8_t size)
{
  // take care the value is within range
  // val = constrain(val, _in[0], _in[size-1]);
  if (val <= _in[0]) return _out[0];
  if (val >= _in[size-1]) return _out[size-1];

  // search right interval
  uint8_t pos = 1;  // _in[0] allready tested
  while(val > _in[pos]) pos++;

  // this will handle all exact "points" in the _in array
  if (val == _in[pos]) return _out[pos];

  // interpolate in the right segment for the rest
  return (val - _in[pos-1]) * (_out[pos] - _out[pos-1]) / (_in[pos] - _in[pos-1]) + _out[pos-1];
}

code for floats see at the end.

As I only needed integers the function returns an int. The parameters are the int val which comes from the analogRead() function, an array of input values and an array of corresponding output values and a parameter to indicate the size of the array's used. Of course the arrays need to be equal in length (for the part used). NOTE: the input array must be a monotone increasing array of values.

First the input value is constrained to the input range, so we can do a search for the right interpolation segment (while loop). If the input value equals the lower bound of the array (index 0), the mapping in the last line cannot be one. So its handled separately. In fact this check is expanded to all known exact values. Finally a call to the well known map() function is made to do a linear interpolation in the right segment.

Performance

In a small test with an input array of 14 elements, 10.000 worst case calls took 1003 millis, 10.000 best case calls took 358 millis so on average 1361/2 => ~68 micros per call. This performance is most affected by the size of the array it has to search, so the general advice is keep the array as small as possible. Note these numbers are only indicative.

The performance can be improved by using binary search, especially when the array is "larger". A discussion about bin search is done on the forum - http://forum.arduino.cc/index.php?topic=205281 -

Usage

Some snippets shows how multiMap() can be used:

//My calibrated distance sensor - SHARP 2Y0A02 F 9Y
  // out[] holds the values wanted in cm
  int out[] = {150,140,130,120,110,100, 90, 80, 70, 60, 50, 40, 30, 20};

  // in[] holds the measured analogRead() values for defined distances
  // note: the in array should have increasing values
  int in[]  = { 90, 97,105,113,124,134,147,164,185,218,255,317,408,506};
  val = analogRead(A0);
  cm = multiMap(val, in, out, 14);


  // Some "sinus" approximation
  int out[] = {0,316,601,827,972,1023,972,827,601,316,0,-316,-601,-827,-972,-1023,-972,-827,-601,-316, 0 };  // 21
  // note: the in array should have increasing values
  int in[]  = {0,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000};
  val = analogRead(A0);  // connect a potmeter?
  x = multiMap(val, in, out, 21);


  // A normal distribution
  int out[] = { 0, 5, 20, 50, 80, 95, 100, 95, 80, 50, 20, 5, 0 };  // 13
  // note: the in array should have increasing values
  int in[]  = {0,80,160,240,320,400,480,560,640,720,800,880,960};
  val = analogRead(A0);
  x = multiMap(val, in, out, 13);
  y = multiMap(val/2, in , out, 7);  // using only left halve of the array.


  // S curve
  int s[]  = { 0,  0, 10, 30, 70, 130, 180, 320, 350, 370, 380}; // 11
  // note: the in array should have increasing values
  int in[] = { 0, 10, 20, 30, 40,  50,  60,  70,  80,  90, 100};
  val = analogRead(A0)/10;
  x = multiMap(val, in, s, 11);


  // trapezium
  int trapx[] = { 0, 100, 100, 0}; // 4
  int trapy[] = { 0, 100, 200, 0}; // 4
  int trapz[] = { 0, 400, 200, 0}; // 4
  // note: the in array should have increasing values
  int in[]  = { 0, 100, 700, 1000};
  val = analogRead(A0);
  x = multiMap(val, in, trapx, 4);
  y = multiMap(val, in, trapy, 4);
  z = multiMap(x, in, trapx, 4);     // can you see what it does?

Notes

The multiMap function is not completely optimized, see TODO section. Note the parameter: - size - can be a smaller as the size of the array, in the normal distribution snippet it uses only half the array for the y value. I considered using a 2 dimensional array as that guarantees the arrays have equal length, but it would prevent reuse of arrays like in the trapezium snippet. Think an input array {0 - 1023} in ten steps would be very reusable.

Todo

  • Check if binary search is faster
  • Implement a simple cache that holds the last used value (some projects would benefit)

Done

  • Optimize, size and pos can be uint8_t (byte) iso int
  • Replace constrain with smarter code.
  • 2011-03-23 Added float variant
  • 2014-10-31 Added template version (in discussion thread) + small updates.
  • 2014-10-31 replaced the map call with inline code

Enjoy tinkering,

rob.tillaart@removethisgmail.com

Float Version

// note: the in array should have increasing values
float FmultiMap(float val, float * _in, float * _out, uint8_t size)
{
  // take care the value is within range
  // val = constrain(val, _in[0], _in[size-1]);
  if (val <= _in[0]) return _out[0];
  if (val >= _in[size-1]) return _out[size-1];

  // search right interval
  uint8_t pos = 1;  // _in[0] allready tested
  while(val > _in[pos]) pos++;

  // this will handle all exact "points" in the _in array
  if (val == _in[pos]) return _out[pos];

  // interpolate in the right segment for the rest
  return (val - _in[pos-1]) * (_out[pos] - _out[pos-1]) / (_in[pos] - _in[pos-1]) + _out[pos-1];
}

2D Version

See discussion on forum thread

Template version

(31 Oct 2014) added template version in discussion thread @top