Playground.arduino.cc will be read-only starting December 31st, 2018. For more info please look at this Forum Post

Last Modified: | August 31, 2017, at 04:50 AM |

By: | |

Platforms: | All |

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];

}

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.

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 -

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?

// 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?

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.

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

- 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

// 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];

}

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];

}

See discussion on forum thread

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