skip to Main Content

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


  1. In answer to the question you asked, no, there is not a way of converting a string like 42.23234 to a float 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, or sscanf. 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:

    char *p = "42.23234,N,071.34681,E";    /* might be part of a NMEA string */
    char *p2;
    double lat = strtod(p, &p2);
    printf("lat = %f, rest = %sn", lat, p2);
    

    This prints

    lat = 42.232340, rest = ,N,071.34681,E
    

    The way this works is that strtod converts the first part of the string you give it to a double, 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:

    1. Break a sentence up into comma-separated fields
    2. Handle each field separately, depending on the particular string you’re parsing ($GPGGA, $GPVTG, $GPZDA, etc.)

    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:

    1. Type float does not have enough precision for global latitudes and longitudes. Use double. (Actually, a fine general rule is that type float doesn’t really have enough precision for anything, so you should always use double.)
    2. If you have a function void F_U8_to_F(uint8_t Input_val, uint8_t pos, float* Output) that’s supposed to return a value via the reference parameter Output, you need to assign that using *Output = OutputHold;. (Your compiler should have warned you about this.)
    3. When you’re calling that function 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);
    4. I recommend using int (not float) for nmea_hours and nmea_minutes. (And if you’re going to carry nmea_thousands separately, you don’t need a float for it or for nmea_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.
    Login or Signup to reply.
  2. 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:

    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.

    [Steve Summit]

    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:

    char* nmea = ...;
    char* end;
    signed long degrees = strtol(nmea, &end, 10);
    

    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:

    nmea = end + 1; // assuming we're in a function thus have a copy
                    // of the pointer anyway
    signed long milliMinutes = strtol(nmea, &end, 10);
    

    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:

    ptrdiff_t length = end - nmea;
    for(; length < 5; ++length) // or whatever else desired precision
    {
        milliMinutes *= 10;
    }
    

    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:

    int digit = 0;
    for(; length > 5; --length)
    {
        digit = milliMinutes % 10;
        milliMinutes /= 10;
    }
    milliMinutes += digit >= 5;
    

    Actually no need for surrounding ifs, 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:

    if(degrees < 0)
    {
        milliMinutes = -milliMinutes;
    }
    milliMinutes += degrees + 60000;
    

    or, if you prefer branchless:

    milliMinutes
        = degrees * 60000
        + milliMinutes * (degrees >= 0)
        - milliMinutes * (degrees < 0);
    

    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:

    printf("%l.%.5l", milliMinutes / 10000, milliMinutes % 10000);
    

    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…

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search