# Non linear list and sensor values (that are not Integers)

I am starting a new project that will use a saturation table for a pressure to temperature conversion. The input will be a pressure reading in decimal format. The codes "output" for further use in the program will be from a list (non linear).

Example;

pressure sensor reads 14.3psi, I have a list of some type that would be something like this:

12psi = 30c
13psi = 32
c
14psi = 33c
15psi = 35
c
etc...

Notice the pressures are linear, but the related temperature is not, What is the most effective way to build the list, and for the sketch to round the sensor pressure up or down, then produce a corresponding temperature?

I have been going in circles and feel that the amount of code I am building is WAY over complicated.

Is there a mathematical relationship between pressure and temperature that you can exploit?

If not, how accurate does this conversion have to be? You might be able to approximate it with a lookup table of a number of line segments and use linear extrapolation between points to calculate values in the middle of line segments.

What code have you made already? Can you provide more details about this pressure -> temperature conversion? What does the curve look like over the full range of values?

The code I have made is just garbage of me trying to figure out C++ (I am a Basic kind of guy -drum roll snare- /jk) I am use to VB code and lookup tables in Excel, so my code is just starting to be honest.

The data I need to reference is not formula friendly (at least not at my math skills). I have no problem hammering in the array data if that what is needed.

And yes, eventually I will need to have many lists with different charts. Once for each of nearly a dozen refrigerants that will be user selected.

You’re going to need a look up table if you can’t use mathematical functions. I would use a two dimensional array, with X values on one dimension and Y values on another. You can use a loop to search through the X values until you find the right one, then use linear interpolation (helpfully encompassed by the Arduino’s map() function) to find values in between.

Here’s a simple sketch that uses a lookup table (with a simple x^2 relationship) to demonstrate the principle.

``````#define SEGMENT_POINTS 5

float pressure_temp_lookup[SEGMENT_POINTS] PROGMEM =
{
{0,  1,  2,  3,  4},
{0,  1,  4,  9,  16}
};

float lookup_temp_from_pressure(float pressure)
{
// Search table for appropriate value.
uint8_t index = 0;
while( pgm_read_float(&pressure_temp_lookup[index])<=pressure && index < SEGMENT_POINTS )
index++;

// If index is zero, pressure is smaller than our table range
if( index==0 )
{
return map_f( pressure,
}
// If index is maxed out, pressure is larger than our range.
else if( index==SEGMENT_POINTS )
{
return map_f( pressure,
}
// index is between 0 and max, just right
else
{
return map_f( pressure,
}
}

void setup()
{
Serial.begin( 19200 );
Serial.print( lookup_temp_from_pressure(3.5) );
}

void loop()
{

}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
``````

I spot-checked the algorithm at a couple of points and it seems to work nicely. I’ve used floats for this example, so I had to redo the map function a bit. The map function that comes with Arduino only works with long data type.

Incidentally, of you can use ints or longs, do so. Floating point operations take up a lot of space on the Arduino. This simple sketch is 4.5 kB!

You’ll need to decide for yourself how accurate you need to be, which will determine how many points you need to take to approximate the graph.

Nice! I really appreciate you taking the time to point me in the right direction. I will go through it and try to stretch my brain around it.

Once again thanks, at least I have an idea of what path to go down.
Bryan

If your table goes by linear temperature, you only need to store pressure for each and use temperature to get the index.
You may have room for 1/10th degree steps for integer values in Pa instead of kPa.

Another route from flash is to use data store in an SD card. It's slower but still fast and the limits are sky high.

looking at the code, its WAY over my head at the moment, but I am willing to put the time in to learn. I would prefer PSI as but that is a non issue at the moment, its just to learn how to store and call the data needed.

I am thinking using a float is making it over complicating. The pressure can be rounded before lookup.

Storing on a SD is an option but ultimately I will need a display that updates at least every 1/2 second. Not sure if that would cause an issue with that rate of change.

Oh and one of of the axis of the lookup will be linear (pressure) and the temperature will be non-linear. Your table isn't linear over either axis. However, this is not a problem. One of the advantages of using a 2-dimensional look up table like I posted is that you don't need a linear x or y axis. You can make the points closer together where you need to, a farther apart where you don't care as much about accuracy. You just put in a set of points in order and the algorithm will handle it.

And whether you use floats or ints, the look up + interpolation algorithm will be the same. There's no added complexity by using floats.

Sorting some of the code out, the first question is the ability to add a row for a new data. (top row is the lookup row, following rows contain the data. I made a few notes in the code below for myself to understand the logic. Obviously I can use a var to define what row to search, but what will I need to change to add more rows of data?

``````#define SEGMENT_POINTS 6

//pressure_temp_lookup is the "table", [_] is the number of rows, SEGMENT_POINTS ?????
//PROGMEM puts data into flash memory, pgm calls it back
//Top row is what is used to return the lower rows (use the top row for pressure)
float pressure_temp_lookup[SEGMENT_POINTS] PROGMEM =
{
{0,  1,  2,  3,  4, 5},
{0,  1,  4,  9,  16, 22},
{0,  1,  6,  12,  20,  26}
};

float lookup_temp_from_pressure(float pressure)
{
// Search table for appropriate value.
uint8_t index = 0;
while( pgm_read_float(&pressure_temp_lookup[index])<=pressure && index < SEGMENT_POINTS )
index++;

// If index is zero, pressure is smaller than our table range
if( index==0 )
{
return map_f( pressure,
}
// If index is maxed out, pressure is larger than our range.
else if( index==SEGMENT_POINTS )
{
return map_f( pressure,
}
// index is between 0 and max, just right
else
{
return map_f( pressure,
}
}

void setup()
{
Serial.begin( 19200 );
Serial.print( lookup_temp_from_pressure(4.5) );
}

void loop()
{

}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
``````

You can make a linear based table easily enough. Question is would you have room to store it? And does it need to cover how many kinds of refrigerant?

You can get more flash and RAM with a bigger chip. You can build or buy that. Just keep in mind that you’re not stuck with one set of limits when you’re looking at approaches to solutions. A \$7 (if you’re just buying one) ATmega1284P chip has 128K flash, 16K RAM and 2 serial ports. A less than \$3 ATmega328P (the UNO chip) has 32K flash, 2K RAM and 1 serial port (software serial can be used for more). Both are covered in build your own tutorials, both are covered in kits and sold on assembled and tested boards. And those are just 2 choices out of a very wide range.

What you explore for techniques now can likely be taken farther through easy choices down the road.
So definitely explore before you choose!

You can use an external flash chip or EEPROM as easily as an SD adapter and get away with a smaller AVR.

Half a second is a long time to Arduino, 8 million CPU cycles. But with the wrong code that spends most of the time waiting for things to finish it can be hardly any time at all, 500 milliseconds to fritter and waste. The right code can run a small circus of devices smoothly simultaneously.

Lookup tables are a great way to save computation. If the pressure you look up in the table is going to be used to calculate PWM values to run a servo or motor and nothing else, store those instead of pressure! PWM is only 8 bits!

Here’s an example of using a lookup table of integer sine x 10000 (Y = radius x value / 10000) vs using sin() to generate floats. It does the process many times for each (adding every value and printing it to keep the compiler from optimizing the calculations away and providing result checks in the process) to take long enough to notice then shows that calculating just the sines with floats takes 100x as long. Tables work.
It runs on a bare UNO just fine and it is not the fastest or most efficient code for what it does.

``````// Getting 16 bit unsigned vars in and out of PROGMEM (flash)
// Written for public domain by GoForSmoke 2014

// this program also compares the time to do table lookup vs using floats and sin()
// sorry about having to add and print the totals but otherwise the compiler optimizes the test to nothing

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <math.h>

const word PROGMEM PROGTBL[ 91 ] = { // sine * 10000 by degree 0 to 90
0U,  175U,  349U,  523U,  698U,  872U, 1045U, 1219U, 1392U, 1564U,
1736U, 1908U, 2079U, 2250U, 2419U, 2588U, 2756U, 2924U, 3090U, 3256U,
3420U, 3584U, 3746U, 3907U, 4067U, 4226U, 4384U, 4540U, 4695U, 4848U,
5000U, 5150U, 5299U, 5446U, 5592U, 5736U, 5878U, 6018U, 6157U, 6293U,
6428U, 6561U, 6691U, 6820U, 6947U, 7071U, 7193U, 7314U, 7431U, 7547U,
7660U, 7771U, 7880U, 7986U, 8090U, 8192U, 8290U, 8387U, 8480U, 8572U,
8660U, 8746U, 8829U, 8910U, 8988U, 9063U, 9135U, 9205U, 9272U, 9336U,
9397U, 9455U, 9511U, 9563U, 9613U, 9659U, 9703U, 9744U, 9781U, 9816U,
9848U, 9877U, 9903U, 9925U, 9945U, 9962U, 9976U, 9986U, 9994U, 9998U,
10000U, };

void setup( void )
{
Serial.begin( 115200 );

char  formatted[ 8 ];

word  W;

for ( byte i = 0; i < 91; i++ )
{
sprintf( formatted, "%6u", W );
Serial.print( formatted );
if ( i < 90 ) Serial.print( ", " );
if (( i % 5 ) == 4 ) Serial.println( );
}
Serial.println( "\n\n Timed test" );

Serial.flush(); // getting serial interrupts out before time test
delay( 1000 );  // making sure of it

unsigned long accum = 0UL; // to make the compiler use all the lines below

unsigned long mics = micros();
for ( byte i = 0; i < 91; i++ )
{
accum += (unsigned long) W;
}
unsigned long tookMics = micros() - mics;

Serial.print( "\n accum = " );
Serial.print( accum );
Serial.print( " took " );
Serial.print( tookMics );
Serial.print( " micros to add up" );
Serial.println( );

Serial.flush(); // getting serial interrupts out before time test
delay( 1000 );  // making sure of it

float fAccum = 0.0;

mics = micros();
float T = sin( M_PI * 30.0 / 180.0 );
for ( byte i = 0; i < 91; i++ )
{
T = sin( M_PI * (float) i / 180.0 );
fAccum += T;
}
tookMics = micros() - mics;

Serial.print( "\n float accum = " );
Serial.print( fAccum );
Serial.print( " took " );
Serial.print( tookMics );
Serial.print( " micros to add up" );
Serial.println( );

}

void loop( void )
{
}
``````

I agree with GoForSmoke; use an int data type instead of a float and code it with a fixed-point notation. That is, if you need to store the temperature of 98.6, store it as 986 and divide by 10 when you need it. That will save you two bytes for every temperature.

Yeah but still, if the temperature is used to directly determine something else (formula of temperature and constants) then table the something else and save needing those steps and constants as well.

IE, don't table factors if results or partial results are more useful.

PS I could go farther.
Suppose you have 2 factors and a number of constants used in a complex formula, what do you do?
You use a 2 dimensional table as Jiggy-Ninja did. The formula reduces to lookups and interpolation if any is needed.

Zerodegreec:
Sorting some of the code out, the first question is the ability to add a row for a new data. (top row is the lookup row, following rows contain the data. I made a few notes in the code below for myself to understand the logic. Obviously I can use a var to define what row to search, but what will I need to change to add more rows of data?

``````#define SEGMENT_POINTS 6
``````

//pressure_temp_lookup is the “table”, [_] is the number of rows, SEGMENT_POINTS ???
//PROGMEM puts data into flash memory, pgm calls it back
//Top row is what is used to return the lower rows (use the top row for pressure)
float pressure_temp_lookup[SEGMENT_POINTS] PROGMEM =
{
{0,  1,  2,  3,  4, 5},
{0,  1,  4,  9,  16, 22},
{0,  1,  6,  12,  20,  26}
};

float lookup_temp_from_pressure(float pressure)
{
// Search table for appropriate value.
uint8_t index = 0;
while( pgm_read_float(&pressure_temp_lookup[index])<=pressure && index < SEGMENT_POINTS )
index++;

// If index is zero, pressure is smaller than our table range
if( index==0 )
{
return map_f( pressure,
}
// If index is maxed out, pressure is larger than our range.
else if( index==SEGMENT_POINTS )
{
return map_f( pressure,
}
// index is between 0 and max, just right
else
{
return map_f( pressure,
}
}

void setup()
{
Serial.begin( 19200 );
Serial.print( lookup_temp_from_pressure(4.5) );
}

void loop()
{

}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

You made some changes right, but missed out on others.

``````#define SEGMENT_POINTS 6

//pressure_temp_lookup is the "table", [_] is the number of rows, SEGMENT_POINTS ????? <-- SEGMENT_POINTS is the number of points you have to approximate the curve.
//PROGMEM puts data into flash memory, pgm calls it back <-- Correct
//Top row is what is used to return the lower rows (use the top row for pressure) <-- Correct

// You can add some defines here that specify what each row of the table
// represents.

#define PRESSURE 0
#define REFRIDGE_1_TEMP 1
#define REFRIDGE_2_TEMP 2
float pressure_temp_lookup[SEGMENT_POINTS] PROGMEM =
{
{0,  1,  2,  3,  4, 5},
{0,  1,  4,  9,  16, 22},
{0,  1,  6,  12,  20,  26}
};

// Add a new function parameter that tells which refrigerant to look up.
// I called it refridge_index
float lookup_temp_from_pressure(uint8_t refridge_index, float pressure)
{
// Search table for appropriate value.
uint8_t index = 0;
while( pgm_read_float(&pressure_temp_lookup[PRESSURE][index])<=pressure && index < SEGMENT_POINTS )
index++;

// If index is zero, pressure is smaller than our table range
if( index==0 )
{
return map_f( pressure,
}
// If index is maxed out, pressure is larger than our range.
else if( index==SEGMENT_POINTS )
{
return map_f( pressure,
}
// index is between 0 and max, just right
else
{
return map_f( pressure,
}
}

void setup()
{
Serial.begin( 19200 );
Serial.print( lookup_temp_from_pressure(REFRIDGE_1_TEMP, 4.5) );
}

void loop()
{

}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
``````

thanks, I will load it and tinker later tonight.

You guys are amazing with your mad code skills. Thanks for the continued help everyone.

Found some time to try wrap my brain around the code. I made some headway but one thing has be a bit confused. The return value from the pressure temperature conversion is not correct. I am not sure if it is because of a number(s) being rounded.

I setup a couple Pots to emulate the pressure.

``````#define SEGMENT_POINTS 78

const int temperaturePin = 1; //pin used for temperature input
const int pressurePin = 0; //pin used for pressure transducer input
int pres, high = 0, low = 1023; //defines the high and low levels of the sensor

//pressure_temp_lookup is the "table", [_] is the number of rows, SEGMENT_POINTS ????? <-- SEGMENT_POINTS is the number of points you have to approximate the curve.
//PROGMEM puts data into flash memory, pgm calls it back <-- Correct
//Top row is what is used to return the lower rows (use the top row for pressure) <-- Correct

// You can add some defines here that specify what each row of the table
// represents.
#define PRESSURE 0
#define REF_1_TEMP 1 //507
#define REF_2_TEMP 2

float pressure_temp_lookup[SEGMENT_POINTS] PROGMEM =
{
{-5,  -4,  -3,  -2,  -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140},
{-60, -58, -57, -55, -54, -53, -50, -48, -46, -44, -41, -39, -38, -36, -34, -32, -31, -29, -27, -26, -24, -23, -21, -20, -19, -17, -16, -15, -14, -12, -11, -9, -8, -7, -6, -4, -3, -2, -1, 0, 1 , 2, 3, 4 , 4, 6, 8, 10, 11, 13, 15, 16, 18, 19, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 37, 40, 43, 45, 48, 51, 53, 55, 58, 60, 62, 64},
{0,  1,  6,  12,  20,  26}
};

// Add a new function parameter that tells which refrigerant to look up called ref_index
float lookup_temp_from_pressure(uint8_t ref_index, float pressure)
{
// Search table for appropriate value.
uint8_t index = 0;
while( pgm_read_float(&pressure_temp_lookup[PRESSURE][index])<=pressure && index < SEGMENT_POINTS )
index++;

// If index is zero, pressure is smaller than our table range
if( index==0 )
{
return map_f( pressure,
}
// If index is maxed out, pressure is larger than our range.
else if( index==SEGMENT_POINTS )
{
return map_f( pressure,
}
// index is between 0 and max, just right
else
{
return map_f( pressure,
}

}

void setup()
{
Serial.begin( 19200 );

}

void loop()
{
float voltage, degreesC, degreesF; //these are float values that are used to calculate the actual temp from the sensors 0-5v output
voltage = getVoltage(temperaturePin); //temperature sensor voltage. (pin number defined at the top of sketch)
degreesC = (voltage - 0.5) * 100.0; //Math to get *c
degreesF = degreesC * (9.0/5.0) + 32.0; //Math to get *f
pres = analogRead(pressurePin); //returns the pressure transducer input (pin number defined at the top of sketch)

int sst;
int sh;
sst = lookup_temp_from_pressure(REF_1_TEMP, pres);
sh = degreesF - sst;

//Serial.print("  deg C: ");
//Serial.print(degreesC);
Serial.print(degreesF);
Serial.println("*F");
Serial.print(pres);
Serial.print("psi");
Serial.print("  SST: ");
Serial.println(sst);
Serial.print("SH: ");
Serial.println(sh);
Serial.println();
//Serial.println( lookup_temp_from_pressure(REF_1_TEMP, 70) );
delay(5000); // repeat once per second
}

//This section manually sets the pressure to voltage from the transducer
void manualTune()
{
pres = map(pres, 0, 1023, 0, 255); //the input is 0-1023, it is mapped to 0-255)
pres = constrain(pres, 0, 255); //prevents the pressure from being outside of these values)
}

float getVoltage (int temperaturePin)
{
// This function has one input parameter, the analog pin number to read. It returns a floating-point value, which is the true voltage on that pin (0 to 5V).

// To take in parameters, put their type and name in the
// parenthesis after the function name (see above). You can
// have multiple parameters, separated with commas.

// To return a value, put the type BEFORE the function name
// (see "float", above), and use a return() statement in your code
// to actually return the value (see below).

// all the math we need to do within this statement:
// This equation converts the 0 to 1023 value that analogRead()
// returns, into a 0.0 to 5.0 value that is the true voltage
// being read at that pin.
}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
``````

I think you're missing a value in the second line. I spot checked the lookup table at a few points by deleting everything in your loop and replacing it with:

``````void loop()
{
Serial.println( lookup_temp_from_pressure(REF_1_TEMP, 140) );
delay(5000); // repeat once per second
}
``````

When a look up 135, I get 64 returned. When I look up 140, I get 0. When I check values above 140 I get crazy negative values. Something's missing in that second line.

Other than that, the look up is working fine at the points I checked, and is interpolating correctly. I noticed you're taking the pot reading directly from the analogRead() and using it in the lookup table. Most of the readings are going to be higher than 140, so you'll run into big problems there. Since you don't specify any further what you mean by "The return value from the pressure temperature conversion is not correct." I can only guess at what your problem is.

What do you expect the result to be? What actually is the result? Post some logs from your Serial Monitor.

EDIT: I counted, and you only have 77 values on the second line. You'll need to find which one you're missing.

yep I found the missing value. I have it working as you intended now and its spot on. Thanks.
Sorry for the late replay, I was laying on a beach all last week Step one - is well on its way thanks to your code for lookup tables
Step two - will be figuring out XBee radios and reading remote data.
Step three - calibrating remote temp sensors and pressure transducers
Step four - real time clock
Step five - LCD for sensors and conversion from step 1
Step six - figure out battery(s) for remote modules and main readout
Step seven - data logging with SD card
Step eight - Max/Min, alarms and inicators
Step nine - beta testing.

A fair bit of work ahead but during the week I managed to plow through Jeremy Blumb's latest book. Most of the list looks like I can muddle through.

There is a much simpler approach that does not involve a large table and interpolation. Those are very smooth curves that can be accurately fit with a simple polynomial, giving you a function P(T) or T(P). For example, the pressure data for Refrigerant 12 can be fit over the range of 0-150 degrees with a simple cubic polynomial, accurate to about 0.2 PSI over that entire range.

I used MATLAB to calculate the fit, with the result that P(T) = 1.8586e-5T3 + 0.003803T2 + 0.51359*T + 9.1194 (see attached plot). So, you need to store only four numbers to calculate P for any T in that range (and outside of it as well). You could use a quartic polynomial for larger ranges.

Curve fits can be done with any spreadsheet, and there are also online services to do curve fits for you.

Edit: to fit T(P) for the range -10 to 150 takes a fourth order polynomial, and the result is accurate to +/- 1 degrees. As you can see from the plot, the fit diverges outside of the temperature range used for the fit, but using more data points in the fit might fix that.  jremington:
There is a much simpler approach that does not involve a large table and interpolation. Those are very smooth curves that can be accurately fit with a simple polynomial, giving you a function P(T) or T(P). For example, the pressure data for Refrigerant 12 can be fit over the range of 0-150 degrees with a simple cubic polynomial, accurate to about 0.2 PSI over that entire range.

I used MATLAB to calculate the fit, with the result that P(T) = 1.8586e-5T3 + 0.003803T2 + 0.51359*T + 9.1194 (see attached plot). So, you need to store only four numbers to calculate P for any T in that range (and outside of it as well). You could use a quartic polynomial for larger ranges.

Curve fits can be done with any spreadsheet, and there are also online services to do curve fits for you.

Edit: to fit T(P) for the range -10 to 150 takes a fourth order polynomial, and the result is accurate to about 2 degrees. However, as you can see from the plot, the fit digresses outside of the temperature range used for the fit.

The reason to use tables is when you need speed and simplicity in your code. Speed factor goes up 100x just by not generating as simple as sine values.

This is especially true when it is running on a 16 MHz 8-bit processor with no FPU and completely lacks support for 64 bit or larger floats as opposed to a 2+ GHz 32 or 64-bit pipelined multi-core processor with FPU’s and more L1 cache than any AVR has SRAM.

When you don’t need speed and don’t care past 3 or 4 places on answers, then get that 2 degrees accuracy that a table would not have to be off by at all.

Oh yeah. Didn’t I mention that tables can be calibrated?