Esp32 Float Array is involuntary truncated to only a couple decimals

General newbie disclaimer, bad formatting, inefficient, and anything done poorly.
Project, I'm trying to average out GPS location for precise 8 decimal precision.
Hardware, ESP32-cam, GY-NEO6MV2 NEO-6M GPS, using an SD card to store the data.
GPS is using the rx/tx pins for serial communication, and the built in SD reader is using IO4,2,12,13,14,15 (just in case someone wants to replicate)

My code does run error free, just having issues with the Float Arrays I'm using to collect a large averaging pool, truncating the decimal to two places.
Trying to keep array precision to 8 decimals.

any feedback is welcome as well
(i would post the actual created table to show the loss of precision, but does contain raw location data)



#include "SD_MMC.h"
#include "TinyGPS++.h"
#include <HardwareSerial.h>

TinyGPSPlus gps;  // the TinyGPS++ object

/////////////////////////////////////
// variables

bool flag = false;
double time1 = 0;
double row = 0;
byte row1 = 0;
int M_yr = 0;
int M_mon = 0;
int M_day = 0;
int M_hr = 0;
int M_min = 0;
int M_sec = 0;

float check = 0; // using for troubleshooting

byte sat = 0;
unsigned long myTime;
unsigned long myTimeUpdate;
unsigned long myTimeUpdate2;
double count = 0;

float arrayLng[2][100]; //
float arrayLat[2][100]; //arrays for location data
float arrayAlt[2][100]; //

float rawLocLng = 0;
float offsetLng = 0;
float adjLng = 0;
float locationLng = 0;

float rawLocLat = 0;
float offsetLat = 0;
float adjLat = 0;
float locationLat = 0;

float rawLocAlt = 0;
float offsetAlt = 0;
float adjAlt = 0;
float locationAlt = 0;

/////////////////////////////////////
//starting serial and SD reader

void setup() {
  
  Serial.begin(9600);
  SD_MMC.begin();

}

/////////////////////////////////////
//collecting information from gps

void GpsRead(){

if (Serial.available()>0) {
  gps.encode(Serial.read());
} else {
 return;
}

  // updates time and date
  if (gps.date.isValid() && gps.time.isValid()) {

    M_yr = gps.date.year();
    M_mon = gps.date.month();
    M_day = gps.date.day();
    M_hr = gps.time.hour();
    M_min = gps.time.minute();
    M_sec = gps.time.second();

  } else {
    M_yr = 0;
    M_mon = 0;
    M_day = 0;
    M_hr = 0;
    M_min = 0;
    M_sec = 0;

  }


  // updates location
  if(gps.location.isValid()){
    if (gps.location.isUpdated()){
      rawLocAlt = gps.altitude.feet();
      rawLocLat = gps.location.lat();
      rawLocLng = gps.location.lng();
      flag = true;
    }    
  }

  //satellite lock
  sat = gps.satellites.value();

}

////////////////////////////////////////////
// math to avrage out gps location

void calcLocation(){
  float t1Lat = 0;
  float t1Lng = 0;
  float t1Alt = 0;
  float t2Lat = 0;
  float t2Lng = 0;
  float t2Alt = 0;
  byte t1 = 0;

  arrayLng[0][row1] = rawLocLng;
  arrayLat[0][row1] = rawLocLat;
  arrayAlt[0][row1] = rawLocAlt;

  while(t1 < 101){

    t1Lng += arrayLng[0][t1];
    t1Lat += arrayLat[0][t1];
    t1Alt += arrayAlt[0][t1];

    t1++;
  }

  check = arrayLng[0][row1]; // 

  t1Lat /= 100;
  t1Lng /= 100;
  t1Alt /= 100;  

  arrayLng[1][row1] = rawLocLng + (t1Lng - rawLocLng);
  arrayLat[1][row1] = rawLocLat + (t1Lat - rawLocLat);
  arrayAlt[1][row1] = rawLocAlt + (t1Alt - rawLocAlt);

  t1 = 0;

  while(t1 < 101){

    t2Lng += arrayLng[1][t1];
    t2Lat += arrayLat[1][t1];
    t2Alt += arrayAlt[1][t1];

    t1++;
  }
  
  t2Lat /= 100;
  t2Lng /= 100;
  t2Alt /= 100;

  offsetLat = t2Lat;
  offsetLng = t2Lng;
  offsetAlt = t2Alt;

  locationLat = t1Lat + t2Lat;
  locationLng = t1Lng + t2Lng;
  locationAlt = t1Alt + t2Alt;



  if(row1 == 100){
    row1=0;
  }

}

/////////////////////////////////////////
// setting up the .csv file for excel, writing and formatting the data

void SDsaveline(int tm){
    
  char FileName[] = "/GPS.csv";
  File GPSfile;
  char Dash[] = "----------,";

  //creates GPS.csv file if one doesnt exsist
  if(!SD_MMC.exists(FileName)) {
    fs::FS &fs = SD_MMC;
    File GPSfile = fs.open(FileName, FILE_WRITE, true);

    // collom headers

    GPSfile.print("Row,");
    GPSfile.print("Time +5hr,");
    GPSfile.print("Date,");
    GPSfile.print("Sat Lock,");
    GPSfile.print("Raw Longitude,");
    GPSfile.print("Longitude Offset,");
    GPSfile.print("True Longitude,");
    GPSfile.print("Raw Latitude,");
    GPSfile.print("Latitude Offset,");
    GPSfile.print("True Latitude,");
    GPSfile.print("Raw Altitude,");
    GPSfile.print("Altitude Offset,");
    GPSfile.print("True Altatude,");
    GPSfile.print("check,");
    GPSfile.print("GPS not valid checks");
    GPSfile.println("Update time");
    GPSfile.close();
  }

  //appends GPS file with seperation apon boot
  else if(row == 0){
    fs::FS &fs = SD_MMC;
    File GPSfile = fs.open(FileName, "a", false);

    
    GPSfile.println((String)Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash + Dash);
    GPSfile.close();
    row ++;
    }

  //writes data to GPS file
  else {
    fs::FS &fs = SD_MMC;
    File GPSfile = fs.open(FileName, "a", false);
    GPSfile.print((String)row + ",");
    GPSfile.print((String)M_hr + ":" + M_min + ":" + M_sec + ",");
    GPSfile.print((String)M_mon + "-" + M_day + "-" + M_yr + "," + sat + ",");

    GPSfile.print(rawLocLng,8);
    GPSfile.print(",");
    GPSfile.print(offsetLng,8);
    GPSfile.print(",");
    GPSfile.print(locationLng,8);
    GPSfile.print(",");

    GPSfile.print(rawLocLat,8);
    GPSfile.print(",");
    GPSfile.print(offsetLat,8);
    GPSfile.print(",");
    GPSfile.print(locationLat,8);
    GPSfile.print(",");

    GPSfile.print(rawLocAlt,8);
    GPSfile.print(",");
    GPSfile.print(offsetAlt,8);
    GPSfile.print(",");
    GPSfile.print(locationAlt,8);
    GPSfile.print(",");

    GPSfile.println((String)check + "," + count + "," + tm);
    GPSfile.close();
    row ++;
  }

  count = 0;
}


void loop() {

myTime = millis();
check = 0;

 GpsRead();


  if(flag){

    if((myTime-myTimeUpdate) >= 100){
      
      //avrages and normalizes all location data
      calcLocation();

      //sends data to file void
      SDsaveline((myTime - myTimeUpdate));
      myTimeUpdate = millis();
      flag = false;
      count = 0;
    }
    
  }
  else {
    count++;
  }
  
}

On Arduino, Serial.print() and its offspring default to two decimal places for a floating point value. For more, use for example
Serial.print(value,5); //for 5 digits past the decimal point.

Avoid using Strings on MCUs, especially on AVR based Arduinos like the Uno, Mega, etc. with very limited memory. Due to poor memory management, String operations lead to program crashes.

1 Like

replaced

    GPSfile.println((String)check + "," + count + "," + tm);
    GPSfile.close();

with

    GPSfile.print(check,8);
    GPSfile.print(",");
    GPSfile.println((String)count + "," + tm);
    GPSfile.close();

fixed visual display issue truncating to two decimals.
stupid error on my part, thank you for the input.

OK. Now, from what I have read, floats on an Arduino are only good to 7 digits, I think, and everything after 7 or in some rare cases 8 digits will be garbage. Now, this counts digits before and after the decimal point, so if you have 2 good digits before the decimal point, then expect only 5 good digits after the decimal point, and then everything after it will be garbage. For a demo of what I mean, try this out: IEEE-754 Floating Point Converter

Now, I have read some information about TinyGPS++, as well as taken a look at the source code, and apparently there is a way to access latitude and longitude information down to billionths of a degree, although of course the low digits of that will be garbage (a billionth of a degree translates to about one-ninth of a millimeter). More info here: TinyGPS++ | Arduiniana

I don't know how many digits you really need. Why do you think you need 8 digits after the decimal point? Do you really think your GPS readings are good to the millimeter, or even sub-centimeter? Is your GPS device really that accurate?

Supposing I am wrong and your GPS really is good to centimeter- or millimeter-level precision, you will need some sort of arithmetic capable of handling the numbers. You might want to look into using long longs, which can handle bigger numbers than ordinary longs. (Note: I don't think that print works with long longs, so the long longs are just for arithmetic, and then you will store the results in some smaller datatype in order to print them).

If it is only the average you are interested in, then there is no reason to store 100 sets of data in RAM and then compute the average. (That uses up a lot of RAM.) Instead of storing each piece of data as it comes in, you can just use += to add it to a running total, and also keep count of how many pieces of data you have received. Then, divide the total by the number of pieces of data to get the average of the data.

2 Likes

As i understand it Arduino 'based' chips can physically only handle 7(put a damper on the project when i found that) although surprisingly the esp32 chip has a larger float cap.

The over arching goal of this project is to test a theory i heard a while ago, where even a GPS lacking in reasonable precision can be compensated for threw the use of a base station that dosn't move. Capable of sending out correction data to nearby units improving accuracy. Use case, a DIY auto mower maintaining accurate location and geofence locations. It is my first real programing project so the ambitions might be high XD.

Thanks for the input on the new variable type i will look into it to see if its some thing i (personally) would be able to utilize.

from my understanding 7 decimals gets roughly 9m radius of accuracy where as 8 gives about 2cm (could be wrong), seems like a good target to shoot for.

having a running average could have the affect of floating the absolute location of the unit. As the information it brings in might be skewed in one direction for a length of time decreasing the accuracy of the non moving location.

greatly appreciate the input

On the ESP32, you can use double instead of float, that will give you up to 15 digits of accuracy. Many of the Arduino boards are based on 8-bit processors, and do not implement double.

The actual data from the GPS gives the latitude/longitude as degrees and minutes, with the minutes having three decimal places. That would limit the accuracy to about 1/60000 of a degree.

As @odometer pointed out, you can obtain the data from TinyGPS++ in the form of a struct that contains a 16-bit unsigned integer for degrees, 32-bit unsigned integer for billionths of a degree, and a bool to indicate a negative number. You could also just parse the NMEA sentence yourself and work directly with the degrees / minutes.

1 Like

Which is about +/- 2 m at the Earth's equator. Only RTK GPS can do better than that, with a reference signal and a lot more work.

Does that mean that if (for double precision float) there are 13 digits to the left of the decimal point, then there can be only two meaningful digits to the right of the decimal point?

But when it is looked at the template of IEEE-754 standard, it is seen that there are 52 binary bits (Fig-2.2) to the right of the decimal point -- what does that mean?

binary64
Figure-2.2:

That can't be right. Each additional decimal place should improve the accuracy by a factor of 10. You wouldn't have a single additional decimal place improving the accuracy by well over a factor of 100.

Let's work this out. The earth has a circumference of roughly 40,000,000 meters. Divide that by 360 degrees, and you get about 111,000 meters per degree. For thousandths of a degree (3 decimal places), that would be 111 meters for each 1/1000 of a degree. Then, for millionths of a degree (6 decimal places), that would be 111 millimeters for each 1/1,000,000 of a degree, or in inches, about 4.4 inches for each 1/1,000,000 of a degree, and that's for 6 decimal places. So, 7 decimal places would give you to within a centimeter or two (that's less than an inch), which I would expect is as good as you are going to need, and probably better than what you are actually going to get.

A double is accurate to 15 digits, the position of the decimal point itself is not in any fixed position.

The double is stored in a fixed format, as a binary number multiplied by 2 raised to the power of the exponent. The number is always of the form 1.fraction, with only the fraction stored, since the 1 can be assumed. Where the actual decimal point is in the resulting number depends on the exponent, positive exponents shift the decimal point to the right, negative exponents shift it to the left, and the resulting decimal point position can be in excess of 15 digits, in which case the number is generally displayed in scientific notation. As an example 1.23456789012345e-40 would have about 40 decimal places if printed out.

2 Likes

yeah, i did an extremely basic search quite a while ago so not surprising i wasn't correct XD. the goal is to see if i can indeed get a stable 8 out of a non moving object. but any result is a result XD and i am curious how stable $20 of electronics can get XD

I was completely not expecting a 'deep dive in variable hardware storage capacity and functions, pertaining to the accuracy of a GPS reading', when i asked my question XD.

But i am totally here for it XD
@david_2018 I did switch all the variables using the GPS data to a double to help prevent any unwanted overflows or loss of decimal precision

To help understand "binary bits", the IEEE-754 Floating Point Converter at h-schmidt.net posted earlier is nice, and easy to find with Google. But it only does 32-bit floats.

One at the easier-to-remember URL float.exposed does 64-bit doubles as well, and has some nifty features, like having the number in URL so you can share it; e.g. this number which is not quite one-and-a-half.

This longer article on floats might also help.

1 Like

Do you have the answer to this question?

When we execute this code: Serial.print(12.3456, 2);, are there transmissions of the ASCII codes for the sysmbols: 1, 2, ., 3, and 4 or the corresponding binary32 formatted 4-byte data (0x414570A4) is transmitted (as ASCII/Binary?) from which the receiver reconstructs the float value (12.34) and dsiplays?

1 Like

The first one.
The Serial monitor is basically a dumb terminal.

1 Like

Unfortunately, the Serial Monitor show: 12.35 and NOT 12.34 when Serial.print(12.3456, 2); is executed!

Does that mean that the ASCII codes for the symbols 1, 2, ., 3, and 4 are not being transmitted?

Output:

12.35

The compiler does the relatively tedious work of converting the ASCII text of 12.3456 into a double. If you had an f suffix -- 12.3456f -- that's a float. If you're on an Arduino with no double, you'd also get a float, I guess. It would not pre-round, since the same machine code would be used for a variable -- unless the compiler was doing some heavy optimization: "These values are constant, the result will always be the same for this function, so I can save space and time by instead generating the machine code for Serial.print("12.35") instead".

It's probably not doing that, so you have the "raw" value 0x4028b0f27bb2fec5 for 12.3455999999999992411, which is the closest double value for what you asked for, in the resulting binary and uploaded to the MCU's firmware. It also has the integer 2 for the number of decimal places.

Then the MCU takes that double/float value and does operations on it to

  • probably round it first
  • push out the ASCII values for
    • an optional - for negative
    • the integer part, digit by digit
    • optional . if there are going to be digits to the right
    • as many digits as you asked for

I glanced at the code that does this on the board I have handy, and it does this without actually "reconstructing" the float from the binary, or really considering its value. For example, to round, it does NOT do

  1. Reconstruct the decimal value
  2. Look at the 3rd decimal place
  3. If it's >= 5, add 0.01

Instead it

  1. Starts with 0.5, a value that can be represented exactly in binary floating point
  2. Divides by 10 for each desired decimal place (zero or more), an operation that cannot be represented exactly in binary
  3. Adds the result to the argument, exactly what floating point hardware is designed to do. In this case, like this
      0x4028b0f27bb2fec5
    + 0x3f747ae147ae147b
    --------------------
    
    but harder.

And when it is pushing out the decimal places, one at a time in ASCII, it just stops after it has done 2 (in this case). It doesn't matter how the addition affected the "binary approximation" of the original value.

So the raw 32- or 64-bit binary is in the code written to the MCU, but when Serial.print actually runs, it is sending those ASCII bytes, one bit at a time, most likely using 8-N-1: 1 2 . 3 5

1 Like

Like I said, it's a dumb terminal.

The Arduino does the conversion from the floating-point number to its text representation. The text representation "12.35" is what gets sent to the Serial monitor.
So, to answer your question: indeed, I missed the minor detail that the last character to be output will be a "5" and not a "4", but except for that minor detail, yes, what gets transmitted to the Serial monitor is the ASCII codes of the characters (digits and decimal point) which are to be shown on the Serial monitor.

Are you even being serious with this question, or are you just trolling?

1 Like

See here: ArduinoCore-avr/cores/arduino/Print.cpp at master · arduino/ArduinoCore-avr · GitHub

Please, see post #17 @kenb4; it has touched my inquisitiveness!