0

I got a Resistive sender fuel gauge sketch off the internet that I have adapted. The Resistive sender is a floating magnet that activates read switches to vary the resistance. Using a stable 5v power source and 100-ohm resistor I have a voltage bridge that is connected to the sender and pin A1. The original sketch uses this to get the voltage into liters:

TankValue0 = map(sensorTankValue0, 352, 824, 0, 100);
if (TankValue0 < 0) {
  TankValue0 = 0;
}
if (TankValue0 > 100) {
  TankValue0 = 100;

The lower range is not too far off but as you get to the top it is out to lunch.

I have mapped the segments of MY float sensor's intervals, and have found there to be 17 distinct points-- I measured the voltage at each point and made a table (I mean like on paper) to relate the measured voltage at a given point to a distance in mm, and then converted that to the relevant metric of liters in a tank when the float switch is reading a given voltage.

V L
4.04 94
3.981 88
3.902 84
3.81 81
3.703 77
3.637 72
3.564 69
3.483 65
3.393 61
3.293 57
3.18 53
3.053 50
2.908 46
2.546 42
2.192 38
1.726 35

How do I get that information into the sketch to give me a read-out of L in my tank?

I have no programming knowledge really. I work with Arduino IDE a lot, but only using sketches other people have written. So, I have a huge basic knowledge gap. So, please bare that in mind.

This is the whole sketch:

#include <LiquidCrystal_I2C.h>


int TankValue0;
int TankValue1;

LiquidCrystal_I2C lcd(0x27, 16, 2);   // set the LCD address to 0x27 for a 16 chars and 2 line display


void setup() {
  lcd.init();                          // initialize the lcd
  lcd.init();
  lcd.backlight();
  lcd.setCursor(1, 0);
  lcd.clear();
  lcd.print("WORD");
  lcd.setCursor(0, 1);
  lcd.print("TO");
  delay(1500);
  lcd.clear();
  lcd.print("YO ");
  lcd.setCursor(0, 1);
  lcd.print("MOMMA");        
  delay(2000);
  lcd.clear();
  pinMode(A0, INPUT);
  Serial.begin(9600);               // starting the Serial Monitor
 
}
void loop() {
  int sensorTankValue0 = analogRead(A0);
  int sensorTankValue1 = analogRead(A1);
  TankValue0 = map(sensorTankValue0, 352, 824, 34, 94);
  TankValue1 = map(sensorTankValue1, 785, 310, 0, 100);
  if (TankValue0 < 0) {
    TankValue0 = 0;
  }
  if (TankValue1 < 0) {
    TankValue1 = 0;
  }
  if (TankValue0 > 100) {
    TankValue0 = 100;
  }
  if (TankValue1 > 100) {
    TankValue1 = 100;
  }
  lcd.print("S.V. SAVANNAH");
  lcd.setCursor(0, 1);
  delay(100);
  lcd.print("DAY TANK   ");
  lcd.print(TankValue0);
  lcd.print("%");
  lcd.setCursor(0, 0);

  delay(1000);
  lcd.clear();
}       
5
  • BTW this tank is square, and hold 100L, the float is not long enough to reach the bottom. Later i want to uses this same Arduino for 2 other tanks that are oddly shaped and hold different amounts. Mar 11 '21 at 4:00
  • start by drawing a graph
    – jsotola
    Mar 11 '21 at 4:30
  • please post the data table as text, not as a picture
    – jsotola
    Mar 11 '21 at 7:11
  • V L 4.04 94 3.981 88 3.902 84 3.81 81 3.703 77 3.637 72 3.564 69 3.483 65 3.393 61 3.293 57 3.18 53 3.053 50 2.908 46 2.546 42 2.192 38 1.726 35 Mar 11 '21 at 9:46
  • I'd use a lookup table for such tasks. It's probably not worth the effort of modelling.
    – Sim Son
    Mar 11 '21 at 17:08
1

Curve fitting

Well, a lazy way to do it is just to fit a curve to it, using Excel, or Libreoffice, Google Docs or whatever. Here's gnuplot being used that way to fit a cubic polynomial to it:

"points.txt" containing your data, tab-delimited:

4.04    94
3.981   88
3.902   84
3.81    81
3.703   77
3.637   72
3.564   69
3.483   65
3.393   61
3.293   57
3.18    53
3.053   50
2.908   46
2.546   42
2.192   38
1.726   35

Gnuplot session:

gnuplot> f(x) = A*(x**3) + B*(x**2) + C*x + D
gnuplot> fit f(x) "points.txt" via A, B, C, D
iter      chisq       delta/lim  lambda   A             B             C             D            
   0 1.8323200047e+01   0.00e+00  9.28e+01    2.818423e+00  -1.100151e+01   1.447650e+01   2.873579e+01
   1 1.8323200047e+01  -4.65e-10  9.28e+00    2.818423e+00  -1.100151e+01   1.447650e+01   2.873579e+01
iter      chisq       delta/lim  lambda   A             B             C             D            

After 1 iterations the fit converged.
final sum of squares of residuals : 18.3232
rel. change during last iteration : -4.6534e-15

degrees of freedom    (FIT_NDF)                        : 12
rms of residuals      (FIT_STDFIT) = sqrt(WSSR/ndf)    : 1.23569
variance of residuals (reduced chisquare) = WSSR/ndf   : 1.52693

Final set of parameters            Asymptotic Standard Error
=======================            ==========================
A               = 2.81842          +/- 1.156        (41.02%)
B               = -11.0015         +/- 10.22        (92.86%)
C               = 14.4765          +/- 29.11        (201.1%)
D               = 28.7358          +/- 26.52        (92.27%)

correlation matrix of the fit parameters:
                A      B      C      D      
A               1.000 
B              -0.998  1.000 
C               0.990 -0.997  1.000 
D              -0.974  0.987 -0.996  1.000 

And here's what looks like against the actual data with plot "points.txt" with linespoints, f(x): Curve fitting with data.

So you have a function:

float f(float x) {
  static const float A =   2.81842;
  static const float B = -11.0015 ;    
  static const float C =  14.4765 ;
  static const float D =  28.7358 ;
  const float x_squared = x * x;
  const float x_cubed   = x * x_squared;

  return A * x_cubed + B * x_squared + C * x + D;
}

If you don't care about the code size and the slowness of float arithmetic, then that might be okay.

Otherwise you may want to convert that function to something like fixed point arithmetic.

A cubic polynomial may be overkill, but then whole technique may be also.

Peicewise Linear

You can make a table of you points and just do what graphing programs typically do, linear interpolation between the points.

float f(float x) {
  static const struct pt {
    float v;
    int l;
  } points[] = {
    {0.0  ,  0},  // I put this here for testing, but put a better value here.
    {1.726, 35},
    {2.192, 38},
    {2.546, 42},
    {2.908, 46},
    {3.053, 50},
    {3.18 , 53},
    {3.293, 57},
    {3.393, 61},
    {3.483, 65},
    {3.564, 69},
    {3.637, 72},
    {3.703, 77},
    {3.81 , 81},
    {3.902, 84},
    {3.981, 88},
    {4.04 , 94}
    // you should probably have another entry here so that readings up to 5V work.
  };
  static const size_t table_len = sizeof points / sizeof points[0];

  for (size_t i = 0; i < table_len - 1; ++i) {
    if (x >= points[i].v && x < points[i + 1].v) {
      return 
            (x - points[i].v) 
          / (points[i + 1].v - points[i].v) 
          * (points[i + 1].l - points[i].l)
          + points[i].l;
    }
  }

  return -1; 
}

Sitting in the center of this in the return statement is basically a crude implementation of Arduino's map(), but for floating point, which again, you don't really need floating point, even if you want to use this piecewise approach.

In short, you find which two entries of the table you're sitting between, and apply what amounts to map() on the line segment formed by the two table entries.

PROGMEM

Either of the curve fitting or piecewise approach could be used to generate a much larger lookup table out of which you could just chose the nearest point.

If you're doing this on an AVR-based Arduino, the table for the piecewise linear approach or a pure lookup table approach could use PROGMEM for the array.

Regarding float and fixed point

Any of these techniques could be adapted to fixed-point math.
Which ever you use, if you're on AVR based Arduino, keep in mind that there's no hardware floating point support at all, and although double exists as a type it is implemented in avr-gcc as a single precision float.

If you're using one of the non-AVR based Arduinos, it may have support for float (and maybe for double), in which case the generated code may not be so bad, though may still be slower than using integers.

If you can do everything with integers, resulting code should be a lot smaller and faster. I'm guessing for what you're doing, if either of them are more important, it's probably being smaller.

For using integers, you may be able to keep your input "voltage" as an ADC count, not having an intermediate voltage calculation. And even if you want single digit liters when you're done, the intermediate interpolation, curve fitting, or table lookup may require that you work in something like centiliters, milliliters, so that values like the A constant from the curve-fitting example can be represented as an integer, e.g. 2818 mL rather than a cruder 3 L. If you're not using a plain lookup table and finding the nearest entry, that is if you're using the curve or piecewise linear approach, doing everything in integer arithmetic from ADC count to mL. Although you'll need to run another curve fitting, since the A, B, etc will representing mL-per-ADC-count not L-per-Volt. Or scale them from the value given.

And there are other options, piecewise cubic splines, etc.

2
  • Thank you so much @timemage. This is a great resource. I must admit I don't fully understand all of it;-) I will do my research and educate myself. in the mean time I tried inserting the "Float" code into my sketch but I get "'lcd' does not name a type" error. Mar 15 '21 at 3:47
  • I don't know how literal you are about the uppercase 'F' in "Float", but it's lowercase in actual use. If you need something more interactive than stackexchange, there's a few hundred Arduino users, including me, on freenode. There's also the official forums. If you have specific questions about what I wrote, I can try to address them.
    – timemage
    Mar 15 '21 at 13:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.