Search code examples
carmkeil

ARMCC 5 optimization of strtol and strtod


I have a board based on STM32L4 MCU (Ultra Low Power Cortex-M4) for GNSS tracking purposes. I don't use RTOS, so I use a custom scheduler. Compiler and environment is KEIL uVision 5 (compiler 5.05 and 5.06, behavior doesn't change)

The MCU speaks with GNSS module via plain UART and the protocol is a mix of NMEA and AT. GNSS position is given as plain text that must be converted to a pair of float/double coordinates. To get the double/float value from text, I use strtod (or strtof). Note that string operations are made in a separate buffer, different from the UART RX one.

The typical string for a latitude on the UART is

4256.45783

which means 42° 56.45783'

to get absolute position in degrees, I use the following formula

42 + 56.45783 / 60

When there is no optimization the code works fine and the position is converted right. When I turn on level 1 optimization (or higher), if I use standard C library I can convert the integer part (42 in the example) and when it comes to convert 56.45783, I get only 56 (so the integer part of minutes until the dot).

If I get rid of standard library and I use a custom strtod function downloaded from ANSI C source library I simply get 0 with ERANGE error.

In other parts of the code I use strtol, which has a strange behavior when L1 optimization is turned ON: when the first digit is 9 and conversion base is 10 it simply skips that 9 going on with the other digits.

So if in the buffer I have 92, I will get just 2 parsed. To get rid of this I simply prepended a sign + to the number and the result is always OK (as far as I can tell). This WA doesn't work with strtod.

Note that I tried to use static, volatile and on-stack variables, behavior doesn't change.

EDIT: I simplified the code in order to get where it goes wrong, as per comments hereafter

C code is like this:

void GnssStringToLatLonDegMin(const char* str, LatLong_t* struc)
{
    double dbl = 0.0;
    dbl = strtod("56.45783",NULL);
    if(struc != NULL)
    {
        struc->Axis = (float)((dbl / 60.0) + 42.0);
    }
}

Level 0 optimization:

559: void GnssStringToLatLonDegMin(const char* str, LatLong_t* struc) 
0x08011FEE BDF8      POP           {r3-r7,pc}
560: { 
0x08011FF0 B570      PUSH          {r4-r6,lr}
0x08011FF2 4605      MOV           r5,r0
0x08011FF4 ED2D8B06  VPUSH.64      {d8-d10}
0x08011FF8 460C      MOV           r4,r1
561:         double dbl = 0.0; 
0x08011FFA ED9F0BF8  VLDR          d0,[pc,#0x3E0]
0x08011FFE EEB08A40  VMOV.F32      s16,s0
0x08012002 EEF08A60  VMOV.F32      s17,s1
562:         dbl = strtod("56.45783",NULL); 
0x08012006 2100      MOVS          r1,#0x00 
0x08012008 A0F6      ADR           r0,{pc}+4  ; @0x080123E4
0x0801200A F7FDFED1  BL.W          __hardfp_strtod (0x0800FDB0)
0x0801200E EEB08A40  VMOV.F32      s16,s0 
0x08012012 EEF08A60  VMOV.F32      s17,s1
563:         if(struc != NULL) 
564:         { 
0x08012016 B1A4      CBZ           r4,0x08012042
565:                 struc->Axis = (float)((dbl / 60.0) + 42.0); 
566:         } 
0x08012018 ED9F0BF5  VLDR          d0,[pc,#0x3D4]
0x0801201C EC510B18  VMOV          r0,r1,d8
0x08012020 EC532B10  VMOV          r2,r3,d0
0x08012024 F7FEF880  BL.W          __aeabi_ddiv (0x08010128)
0x08012028 EC410B1A  VMOV          d10,r0,r1
0x0801202C ED9F0BF2  VLDR          d0,[pc,#0x3C8]
0x08012030 EC532B10  VMOV          r2,r3,d0
0x08012034 F7FDFFBC  BL.W          __aeabi_dadd (0x0800FFB0)
0x08012038 EC410B19  VMOV          d9,r0,r1
0x0801203C F7FDFF86  BL.W          __aeabi_d2f (0x0800FF4C)
0x08012040 6020      STR           r0,[r4,#0x00]
567: } 

LEVEL 1 optimization

557: void GnssStringToLatLonDegMin(const char* str, LatLong_t* struc) 
0x08011FEE BDF8      POP           {r3-r7,pc}
558: { 
559:         double dbl = 0.0; 
0x08011FF0 B510      PUSH          {r4,lr}
0x08011FF2 460C      MOV           r4,r1
560:         dbl = strtod("56.45783",NULL); 
0x08011FF4 2100      MOVS          r1,#0x00
0x08011FF6 A0F7      ADR           r0,{pc}+2  ; @0x080123D4
0x08011FF8 F7FDFEDA  BL.W          __hardfp_strtod (0x0800FDB0)
561:         if(struc != NULL) 
562:         { 
0x08011FFC 2C00      CMP           r4,#0x00
0x08011FFE D010      BEQ           0x08012022
563:                 struc->Axis = (float)((dbl / 60.0) + 42.0); 
564:         } 
0x08012000 ED9F1BF7  VLDR          d1,[pc,#0x3DC]
0x08012004 EC510B10  VMOV          r0,r1,d0
0x08012008 EC532B11  VMOV          r2,r3,d1
0x0801200C F7FEF88C  BL.W          __aeabi_ddiv (0x08010128)
0x08012010 ED9F1BF5  VLDR          d1,[pc,#0x3D4]
0x08012014 EC532B11  VMOV          r2,r3,d1
0x08012018 F7FDFFCA  BL.W          __aeabi_dadd (0x0800FFB0)
0x0801201C F7FDFF96  BL.W          __aeabi_d2f (0x0800FF4C)
0x08012020 6020      STR           r0,[r4,#0x00]
565: }

I looked at the disassembly of __hardfp_strtod and __strtod_int called by these functions and, as they are incorporated as binaries, they don't change with respect of optimization level.


Solution

  • Due to optimization, strtod didn't work. Thanks to @old_timer, I had to make my own strtod function, which works even with optimization level set at level 2.

    double simple_strtod(const char* str)
    {
     int8 inc;
     double result = 0.0;
     char * c_tmp;
     c_tmp = strchr(str, '.');
     if(c_tmp != NULL)
     {
        c_tmp++;
        inc = -1;
        while(*c_tmp != 0 && inc > -9)
        {
            result += (*c_tmp - '0') * pow(10.0, inc);
            c_tmp++; inc--;
        }
        inc = 0;
        c_tmp = strchr(str, '.');
        c_tmp--;
        do
        {
            result += (*c_tmp - '0') * pow(10.0,inc);
            c_tmp--; inc++;
        }while(c_tmp >= str);
     }
     return result; 
    }
    

    It can be further optimized by not calling 'pow' and use something more clever, but just like this it works perfectly.