Teensy Math Error Parsing NMEA data

Status
Not open for further replies.

Jack1688

Member
I have a marine autopilot project I am converting to Teensy 3.6. Part of it is a GPS NMEA parser. I have what appears to be Teensy math error that I do not understand. Code and result are shown below. The subcode here converts Lat or Lon from a NMEA string to a floating point number. The sample given is String Lat_Lon_in = "10617.5254".
If you do this manually you get:
Degrees 106
Minutes 17
Decimal Minutes .5254
The floating point degrees should be 106 + 17/60.0 + .5254/60 =106.2920900, the code gives 106.29209137
I can't understand this result.

The code gives the following Printed result :
Lat_Lon String 10617.5254
Decimal 0.525400
minutes 17.000
Degrees 106.00000000
Decimal_Minutes 0.29209000
Lat_Lon_Deg 106.29209137
Answer should be 106.2920900

The code parses the pieces correctly but when it adds Degrees (106.0000) + Decimal_Minutes(.2920900) it gets 106.29209137 instead of 106.2920900.
Code:
String Lat_Lon_in = "10617.5254";
String Lat_Lon;
String string1;
float float1;
char char_buf[16];
float Lat_Lon_Deg;
unsigned  long long1;
char *brkb, *pEnd;


void setup() {
 Serial.begin(57600);
 Serial.println("Setup  ");
}

void loop() {
   
To_Degrees(Lat_Lon_in);

delay(10000);
}

 void To_Degrees(String Lat_Lon)
{

  // NOTE ASSUMES NORTH LATITUDE AND WEST LOGITUDE ARE POSITIVE NEED TO ADD HANDLING FOR SOUTH LATITUDE AND EAST LOGITUDE (treat them as negative
float Degrees;
float Minutes;
float Decimal;
float Decimal_Minutes;
int N1;
// if LatLon_decimals is 0 in user input this will auto detect number of decimals or manually set the number of decimals in user input
//if(LatLon_decimals == 0 && Detect_LatLon_decimals == 0){ 
 N1 = Lat_Lon.length() - Lat_Lon.indexOf('.') -1;
// N1 = 4;
 //Detect_LatLon_decimals = 1; This is used to only detect number of decimals once to save time 
//}
//else N1 = LatLon_decimals; // 
 //Serial.print("number of decimals = "); Serial.println(N1);
 //int N1 = 4; // number of characters after decimal (precision of lat lon sentence
 int N2 = N1+1; // length - n2 is position after ending position for minutes counting the first position as 0 = N1 + 1
 int N3 = N1+3; // lenghth - n3 is starting position for minutes = N1 + 3
 int N4 = pow(10,N1); // divisor for decimal part of minutes to convert interger to decimal = 10^N1

// Lat/Lon have form dddmm.pppp(p, ddd is degrees (1 to 3 characters, mm is min, and ppp(p) is decimal minutes 3 o 4 characters

//for (int i=0;i<16;i++)  char_buf[i]=' ';
  Serial.print("Lat_Lon String "); Serial.println(Lat_Lon);
string1 = Lat_Lon.substring(Lat_Lon.length() - N1);// decimal part of Lat Lonchar_buf[] = "";
string1.toCharArray(char_buf,16);
long1 = strtol(char_buf,&pEnd,10);
Decimal = float(long1)/N4; // convert integer to decimal
 Serial.print("Decimal "); Serial.println(Decimal,6);
string1 = Lat_Lon.substring(Lat_Lon.length()-N3, Lat_Lon.length()-N2); // gets minutes Note start is inclusive, stop is exclusive (position it stops before) see arduino substring

string1.toCharArray(char_buf,16);
long1 = strtol(char_buf,&pEnd,10);
Minutes = float(long1);
  Serial.print("minutes "); Serial.println(Minutes,3);

string1 = Lat_Lon.substring(0, Lat_Lon.length()-N3); // gets degrees

string1.toCharArray(char_buf,16); 
long1 = strtol(char_buf,&pEnd,10);
Degrees = float(long1);
  Serial.print("Degrees "); Serial.println(Degrees,8);
  Decimal_Minutes = Minutes/60.0 + Decimal/60.0;
Serial.print("Decimal_Minutes ");Serial.println(Decimal_Minutes,8);
//Lat_Lon_Deg = Degrees + Minutes/60.0 + Decimal/60.0;
Lat_Lon_Deg = 0.0;
  Lat_Lon_Deg = Degrees + Decimal_Minutes;
  Serial.print("Lat_Lon_Deg "); Serial.println(Lat_Lon_Deg,8);
  Serial.println("Answer should be 106.2920900");
  Serial.println();
} // end To_Degrees
 
maybe float has insufficient precision. if i change your float's to double's i get
Code:
Lat_Lon String 10617.5254
Decimal 0.525400
minutes 17.000
Degrees 106.00000000
Decimal_Minutes 0.29209000
Lat_Lon_Deg 106.29209000
Answer should be 106.2920900
 
You should understand that floats in the Teensy are represented in the so called 32bit single precision IEC format. The mantissa part of a float is encoded with 24 (23 explicit) bits which allows for a numerical precision of 6 - 7 significant decimals. The "error" which you see appears only in the 8th decimal, thus, everything is technically perfect and it's absolutely not nice to blame the Teensy for that in the thread title.

This single precision float format has the advantage of increased calculating speed since the Teensy 3.5 and 3.6 can do everything in the hardware FPU. If you want to trade higher precision against speed, you might declare all floats as double which gives 64bit resolution. But then, be aware that calculation will be much slower since everything has to be done in software.
 
maybe float has insufficient precision. if i change your float's to double's i get
Code:
Lat_Lon String 10617.5254
Decimal 0.525400
minutes 17.000
Degrees 106.00000000
Decimal_Minutes 0.29209000
Lat_Lon_Deg 106.29209000
Answer should be 106.2920900

manitou and Theremingieur Thank you both for your replies. When I changed my floats to double I did indeed get the correct result, and equally important I understand why. So thanks to you both.
 
Thanks, glad the latency is not too much I will have to live with it I need the accuracy. A minute of Latitude or a minute of longitude at the equator is one nautical mile 1852 meters (6076.1 feet). So .0001 minute which is the accuracy of the incoming GPS equates to .6 feet. I need to retain that accuracy.
 
if your getting gps data from uart, i suggest you use serialevent, i was handling data both ways at 3megabaud rates (3000000) without the use of rts/cts, youll likely collect the gps data from that if your loop() is busy doing other things
 
if your getting gps data from uart, i suggest you use serialevent, i was handling data both ways at 3megabaud rates (3000000) without the use of rts/cts, youll likely collect the gps data from that if your loop() is busy doing other things

serialEvent() is not magic - it is not interrupt driven, it is just called more often - I know of these:: Exiting each loop() iteration, every call of delay() or yield() [which is what the first two call]. If those events don't happen - serialEvent() is not called.
 
Off topic - but perhaps useful: With GPS messages only 5-10 Hz and perhaps ~100 bytes - increasing the default receive size [64 to 128 bytes] allows a full incoming message to be buffered without intermediate reads. Message can take 1-4 ms to transfer {depending on length and baud} - and only needs to be read when complete faster than 100-200 ms.

In order to call serialEvent every active Serial. device gets if (Serial#.available()) call made {7 times on T_3.6}. That can be bypassed when not used by declaring a void yield(){} and when loop is cycling at 100K-500K+ times per second it results in improved overall timing.

So as noted serialEvent is not magic or efficient - though it can work and can be really nice when it does. On an UNO with fewer ports to check .available() and lower loop() counts it would be less overhead. On Teensy if no delay() or yield is called and loop() isn't cycling - the buffer will still overrun even with serialEvent() usage.
 
Well here is what I do which I think is pretty efficient. Every loop() if (serial.available()) charIn = serial.read(). Then I test the charIn to see if is 13 (carriage return, end of sentence). Also I test to see if the buffer position is greater than one less than maximum buffer length. This means I have missed the CR or gotten some bad data. In that case I erase the erase the buffer and start over. If the charIn is CR I fill the rest of the buffer with null characters to be sure there is not overflow junk in there. Then I process (parse) the complete sentence. this seems to work quite well, it processes data as it becomes available and does not waste time waiting for a sentence to complete.
Code:
 void GET_sentence() {
    // long loop_time=millis();
     //word_count = 1; // word count counts the commas add one for the star
if (bufpos > buffer_length -1) Reset_buffer(); //if buffer length exceeded w/o finding byteGPS = 13 toss buffer and start again hope eliminate buffer getting garbage from RF
if (Serial_GPS.available() > 0){
    //static int bufpos = 0;
    byteGPS = Serial_GPS.read();
    if(byteGPS !=13){
      gps_buffer[bufpos] = byteGPS;
      if(byteGPS == ',') word_count_temp = word_count_temp+1;      
      if (print_NEMA) Serial.write(byteGPS);                                                
      bufpos++;
    } // if byteGPS != 13
    else { Process_GPS_Data();
    }
  }
 }
void Process_GPS_Data(){
     checksum_status= false;   
     NEMA_sentence=false;
     gps_buffer[bufpos] = '\0';  // terminate buffer with null character       
     GPSheader = "";
     for (int i=4;i<7;i++){           
       GPSheader = GPSheader + gps_buffer[i];
     } // end for i= 1,7
     
     // Clear rest of buffer
       for (int i=bufpos+1;i<buffer_length;i++){      
         gps_buffer[i]=' '; 
          bufpos = 0;                 
         }
      word_count = word_count_temp;
      word_count_temp = 1; //reset for next sentence
         if(print_GPS_buffer) Serial.print(gps_buffer);
       //Serial.println(); Serial.print(GPSheader); 
                            
      if (GPSheader == "RMC"){ 
        Get_GPRMC();
        return;   
     } 
         
     if (GPSheader == "APB") {         
        Get_GPAPB();
        return;  
     } 
                    
     if (GPSheader == "RMB"){  
        Get_GPRMB();
        return; 
     }  

//   version G3_V2comment out BOD, WPL, RTE to speed other seentences for reliability 9/21/15 
// version H2 changed to using "Use_CTS" to skip these statements if using CTS from $GPAPB like my garmin gpsmap 740s       
       
     if(Use_CTS == 0)
   {  
      if (GPSheader == "BOD"){  
          Get_GPBOD();
          return;              
      }   
     
      if (GPSheader == "WPL"){
          Get_GPWPL();
          return;              
      }     
      
        if (GPSheader == "RTE"){
          Get_GPRTE();
          return;              
      }     
   }  // end if(Use_CTS ==0 )
       NewData=false;
       Reset_buffer();   
  }
 
That generally works and is similar to how the uBlox system in use works. Only problem is when loop() is busy too long, that is where inadvertent calls to delay()/yield() can help serialEvent(). That can happen if some loop passes do lots of work, so extending the buffer depending on the Serial# port used can make the difference since the messages are short and long between - modify installed files like in this post.

That thread is using uBlox and a different message - it has a nice looking parser in the github\library.
 
Status
Not open for further replies.
Back
Top