Is there a better way of converting an uint8_t array into a single float value?
I am extremely new at using this.
I am currently using a ESP32-S3-WROOM-1 to take UART data from a NEO-6M GPS.
IDE: Visual Studio Code
I think my code (which I attached below the c and h file) should take the UART data and place it into an array. For time I can just set it two by two, but for longer values like Latitude and Longitude, I will have to have the code add and multiply each other until it reaches the decimal point, then add the rest of the values later.
Basically, going byte by byte.
char* Post_Time_String = strstr(String_Input, ",");
F_Extract_Until_Comma(Post_Time_String, Lat_Val);
for(int itr=0; itr<sizeof(Lat_Val),itr++)
{
if(Lat_Val[itr] != ".")
{
gps_info->GPS_POSITION->latitude = (10 * gps_info->GPS_POSITION->latitude) + Lat_Val[itr];
}
}
I have been trying to find a better way of doing this, because I get the feeling that this is very inefficient and slow. If I can get the above like a string, then I can use strof() I think?
For reference the header and main files I am currently working on. WIP and definitely has issues that I can’t find yet.
NMEA_PARSE_CODE.c
#include <stdio.h>
#include <NMEA_PARSE_CODE.h>
void F_FIND_COMMA(char *String_Input, char* String_Output)
{
char *String_Output = strstr(String_Input, ",");
return String_Output;
}
void F_Extract_Until_Comma(char *String_Input, uint8_t* Extracted_Output[])
{
uint8_t String_Length = strlen(String_Input);
uint8_t* F_Buffer[20] = {NULL};
char* comma_parse_string = F_FIND_COMMA(String_Input); //Get string after comma
//Find comma and extract data until next comma
uint8_t comma_parse_string_length = strlen(comma_parse_string); //Get length of string after comma
for (uint8_t itr = 0; itr < comma_parse_string_length, itr++)
{
if(comma_parse_string[itr] == ",")
{
break;
}
F_Buffer[itr] = comma_parse_string[itr];
Extracted_Output[itr] = comma_parse_string[itr];
}
}
void F_U8_to_F(uint8_t Input_val, uint8_t pos, float* Output)
{
float OutputHold;
OutputHold = 10*Input_val[pos];
OutputHold = OutputHold + Input_val[pos+1];
Output = OutputHold;
}
void F_NMEA_TIME_CONVERT(uint8_t Input_Val, NMEA_GPS gps_info)
{
F_U8_to_F(timeval, 0, gps_info->GPS_TIME->nmea_hours);
F_U8_to_F(timeval, 2, gps_info->GPS_TIME->nmea_minutes);
F_U8_to_F(timeval, 4, gps_info->GPS_TIME->nmea_seconds);
}
void F_GGA_Parse(uint8_t* String_Input, NMEA_GPS gps_info)
{
//TIME, Lat, N/S, Long, E/W, Fix, Sat#, HDOP, Alt, unit, Geoid, unit, .........
strof();
uint8_t time_value[12], Lat_Val[12]; //obtain from String_Input and to convert into float value
F_Extract_Until_Comma(String_Input, time_value);
F_NMEA_TIME_CONVERT(time_value, gps_info)
//time end
char* Post_Time_String = strstr(String_Input, ",");
F_Extract_Until_Comma(Post_Time_String, Lat_Val);
for(int itr=0; itr<sizeof(Lat_Val),itr++)
{
if(Lat_Val[itr] != ".")
{
gps_info->GPS_POSITION->latitude = (10 * gps_info->GPS_POSITION->latitude) + Lat_Val[itr];
}
}
F_U8_to_F(Lat_Val,0,gps_info);
}
char F_Extract_GPS_Data(char * raw_string_input, NMEA_GPS* gps_info)
{
char* GPS_ID_FOUND = strstr(raw_string_input, "$GP");
uint8_t Comma_Rounds = 0;
if(NULL == GPS_ID_FOUND)
{
return NULL;
}
else
{
if(strstr(GPS_ID_FOUND, "GGA"))
{
gps_info->GPS_TYPE = GGA;
gps_info->GGA_Flag = YES;
Comma_Rounds = 14;
}
else if(strstr(GPS_ID_FOUND,"RMC"))
{
gps_info->GPS_TYPE = RMC;
gps_info->RMC_Flag = YES;
Comma_Rounds = 11;
}
else if(strstr(GPS_ID_FOUND,"VTG"))
{
gps_info->GPS_TYPE = VTG;
gps_info->VTG_Flag = YES;
Comma_Rounds = 9;
}
if(gps_info->GPS_TYPE == 1)
{
F_GGA_Parse(GPS_ID_FOUND, gps_info);
}
// if(GPS_ID_FOUND[0])
}
}
NMEA_PARSE_CODE.h
#include <stdio.h>
typedef enum
{
NO=0,
YES=1
} NMEA_ID_GGA;
typedef enum
{
NO=0,
YES=1
} NMEA_ID_RMC;
typedef enum
{
NO=0,
YES=1
} NMEA_ID_VTG;
typedef enum
{
GGA=1,
RMC,
VTG
} NMEA_ID;
typedef struct
{
float nmea_hours;
float nmea_minutes;
float nmea_seconds;
float nmea_thousands;
} NMEA_UTC_TIME;
typedef struct
{
uint8_t nmea_year;
uint8_t nmea_month;
uint8_t nmea_day;
} NMEA_DATE;
typedef struct
{
float longitude;
float latitude;
} NMEA_POSITION;
typedef struct
{
char NORTHSOUTH;
char EASTWEST;
float course; //In degrees
float knot_speed; //In knots
float meter_speed; //In meters per second
} NMEA_VELOCITY;
typedef struct
{
NMEA_ID GPS_TYPE;
NMEA_ID_GGA GGA_Flag;
NMEA_ID_RMC RMC_Flag;
NMEA_ID_VTG VTG_Flag;
NMEA_UTC_TIME GPS_TIME;
NMEA_DATE GPS_DATE;
NMEA_POSITION GPS_POSITION;
NMEA_VELOCITY GPS_VELOCITY;
} NMEA_GPS;
typedef struct
{
NMEA_ID ID;
NMEA_UTC_TIME TIME;
} NMEA_GGA;
Going byte by byte, multiply the existing float value by 10 before adding the new byte. Repeat until end of length is reached. The code compiles somehow in Visual Studio Code, but that isn’t saying much. VSC isn’t showing me any errors either.
2
Answers
In answer to the question you asked, no, there is not a way of converting a string like
42.23234
to afloat
that does not involve a loop, somewhere. Moreover, simply "multiply the existing float value by 10 before adding the new byte" is completely insufficient for floating-point values, because it doesn’t handle the decimal point or the fractional digits after it.The simple way to convert a string to a floating-point value (which, incidentally, does not involve any loops in your code) is to call a prewritten library function such as
atof
,strtod
, orsscanf
. And using a prewritten library function is highly, highly recommended here, because properly converting floating-point strings is quite a hard problem in general, and it’s much better to let someone else do that work.Here’s a simple example:
This prints
The way this works is that
strtod
converts the first part of the string you give it to adouble
, then returns you (via a "reference" pointer parameter) a pointer to the rest of the string, that it didn’t convert, because it ran into a non-numeric character, in this case the first comma.[Beware, though, that in NMEA, 42.23234 does not mean 42.23234 degrees. It’s actually 42 degrees, 23.234 minutes, which works out to 42.3872333 degrees. So it turns out you’re not going to be able to use a straightforward call to
strtod
to get your latitudes and longitudes, after all.]Although not the question you asked, the rest of your code has many, many problems beyond that of converting floating-point strings. I recommend starting with a smaller, simpler problem. Or if you need to parse NMEA strings today, see if you can find some prewritten code that fits your needs — there’s gobs and gobs of it out there.
If you do want to write your own NMEA parser, I recommend treating the problem as two completely separate parts:
But trying to move along the string one character at a time, simultaneously converting field contents and looking for commas, tends to lead to an unholy mess.
One decent way to break a string up into comma-separated fields is to use the library function
strtok
.Miscellaneous other notes:
float
does not have enough precision for global latitudes and longitudes. Usedouble
. (Actually, a fine general rule is that typefloat
doesn’t really have enough precision for anything, so you should always usedouble
.)void F_U8_to_F(uint8_t Input_val, uint8_t pos, float* Output)
that’s supposed to return a value via the reference parameterOutput
, you need to assign that using*Output = OutputHold;
. (Your compiler should have warned you about this.)F_U8_to_F
that’s supposed to return a value via a reference parameter, you need to pass a pointer to the value you want to fill in:F_U8_to_F(timeval, 2, &gps_info->GPS_TIME->nmea_minutes);
int
(notfloat
) fornmea_hours
andnmea_minutes
. (And if you’re going to carrynmea_thousands
separately, you don’t need afloat
for it or fornmea_seconds
, either.) But depending on the needs of the rest of the program, you might find it more useful to convert to a more computation-friendly time format, not separate hours, minutes, and seconds fields.Extending on @Steve Summit‘s answer, profiting from the information about
NMEA
format given there:I personally recommend dropping floating point entirely and instead fall back to fixed-comma-arithmetic – meaning that you do not represent full degrees, but some appropriate sub-precision. This makes all internal handling much easier and avoids any potential rounding issues (where you might not get back exactly the same value as you have been reading before).
In given case it appears meaningful to me to use the same sub-precision as NMEA:
If this precision is fix (always thousands of minutes, so ‘milli-minutes’) and trailing zeros are included (the most simple case), then you can simply parse two integers and apply an appropriate factor:
Now
*end
should contain a the period. If not, then the fractional part is missing and you can just normalise the degrees to the corresponding unit, in case of ‘milli-minutes’ with 60000.With fraction available we now can go on:
If trailing zeros are guaranteed to be included then
end - nmea
should now be equal to 5. If it is not – well, you need to extend:This is fine even if the case appears that you have too many digits (you’d have chosen a too small precision then). To cover that case as well, you instead need to divide – and we might remember the last digit dropped to determine rounding:
Actually no need for surrounding
if
s, the condition within the loop covers the respective inverse cases. Now finally you can combine, but you need to consider that signs of the two numbers might differ:or, if you prefer branchless:
Now you have a single integral value and none of the issues with floating point – perfect for internal handling.
The sole disadvantage: When outputting to the user by whatever means you then need to convert the value again to NMEA format; that’s not much of an issue, though:
Of course you can adjust this to other precisions as well 😉
Side note: All code entirely untested, if you find a bug, please fix yourself…