Tuesday, June 16, 2015

Arduino IDE - Nasty bug in dtostrf() ESP8266 STDLIB function - FIXED!



    Working last days on the driver for MAX7219 8 digit display module I found that dtostrf() ESP8266 STDLIB function is not working properly for values containing "0"'s after dot, like: 1.0256,1.00256, etc.

With the quick written test code from below and  the collaboration of the great guys from Arduino.cc Forum was able to confirm that it is a nasty BUG and is affecting ESP8266 libraries only, on Arduino (AVR) boards results confirmed from different sources as been OK.


TEST CODE:


void setup() {
 // initialize serial communication at 9600 bits per second:
 Serial.begin(9600);
}

void print_float(int no, float fVal, String sVal )
{
char charVal[12];          //temporarily holds data from vals 
String stringVal = "";     //data on buff is copied to this string

dtostrf(fVal, 8, 4, charVal);  
//4 is mininum width, 3 is precision; float value is copied onto buff

stringVal = charVal;
int strl = stringVal.length()-1;

Serial.print(no);  //just a order number
//the number represented as string for reference 
Serial.print(". Expected value :"); Serial.print(sVal); 
//display lenght of the obtained string after conversion  
Serial.print(" - String Length: ");Serial.print(strl);
//display the value as stored in stringVal 
Serial.print(" - Conversion value in stringVal :");
Serial.println(stringVal); 
}

void loop() {
   print_float(1,1.256,"1.2560");   //No 1
   print_float(2,1.0256,"1.0256");  //No 2
   print_float(3,1.00256,"1.0026"); //No 3
   Serial.println();  
   delay(5000);        // delay in between reads for stability
}



WRONG RESULT - ESP8266 build

1. Expected value :1.2560 - String Length: 5 - Conversion value in stringVal: 1.2560
2. Expected value :1.0256 - String Length: 4 - Conversion value in stringVal: 1.256
3. Expected value :1.0026 - String Length: 3 - Conversion value in stringVal: 1.26
 
 
 
TEST PASSED RESULT - Arduino (AVR) build:

1. Expected value :1.2560 - String Length: 7 - Conversion value in stringVal: 1.2560
2. Expected value :1.0256 - String Length: 7 - Conversion value in stringVal: 1.0256
3. Expected value :1.0026 - String Length: 7 - Conversion value in stringVal: 1.0026
 


For ESP8266 Arduino IDE package the "dtostrf()" function is hidding in "core_esp8266_noniso.c" and is called by "#include<stdlib_noniso.h>"


Original code below:

char * dtostrf(double number, signed char width, unsigned char prec, char *s) 
{

    if(isnan(number)) {
        strcpy(s, "nan");
        return s;
    }
    if(isinf(number)) {
        strcpy(s, "inf");
        return s;
    }

    if(number > 4294967040.0 || number < -4294967040.0) {
        strcpy(s, "ovf");
        return s;
    }
    char* out = s;
    // Handle negative numbers
    if(number < 0.0) {
        *out = '-';
        ++out;
        number = -number;
    }

    // Round correctly so that print(1.999, 2) prints as "2.00"
    double rounding = 0.5;
    for(uint8_t i = 0; i < prec; ++i)
        rounding /= 10.0;

    number += rounding;

    // Extract the integer part of the number and print it
    unsigned long int_part = (unsigned long) number;
    double remainder = number - (double) int_part;
    out += sprintf(out, "%d", int_part);

    // Print the decimal point, but only if there are digits beyond
    if(prec > 0) {
        *out = '.';
        ++out;
    }

    while(prec-- > 0) {
        remainder *= 10.0;
    }
    sprintf(out, "%d", (int) remainder);

    return s;
}


   Going step-by-step thru the code is quite easy to find out that the remainder part is moved into integer, but on the iteration process the leading zeroes are not added into the buffer.


  The fix is simple, changing the remainder add section in a way to properly add the "0"'s to the buffer  until the remainder part become non-zero:

while(prec-- > 0) {
        remainder *= 10.0;
        if((int)remainder == 0){
                *out = '0';
                 ++out;
        }
    }


The fixed dtostrf() code below:


char * dtostrf(double number, signed char width, unsigned char prec, char *s)
 {

    if(isnan(number)) {
        strcpy(s, "nan");
        return s;
    }
    if(isinf(number)) {
        strcpy(s, "inf");
        return s;
    }

    if(number > 4294967040.0 || number < -4294967040.0) {
        strcpy(s, "ovf");
        return s;
    }
    char* out = s;
    // Handle negative numbers
    if(number < 0.0) {
        *out = '-';
        ++out;
        number = -number;
    }

    // Round correctly so that print(1.999, 2) prints as "2.00"
    double rounding = 0.5;
    for(uint8_t i = 0; i < prec; ++i)
        rounding /= 10.0;

    number += rounding;

    // Extract the integer part of the number and print it
    unsigned long int_part = (unsigned long) number;
    double remainder = number - (double) int_part;
    out += sprintf(out, "%d", int_part);

    // Print the decimal point, but only if there are digits beyond
    if(prec > 0) {
        *out = '.';
        ++out;
    }

    while(prec-- > 0) {
        remainder *= 10.0;
        if((int)remainder == 0){
                *out = '0';
                 ++out;
        }
    }
    sprintf(out, "%d", (int) remainder);

    return s;
}


To fix your own ESP8266 Arduino IDE lib, just replace the old "dtostrf()" function  in "core_esp8266_noniso.c" with the code above.

I have posted it also on esp8266.com, maybe somebody will put the fix also in mainstream code.

All the credits for the bugfix will go this time to "Delta_G" from Arduino.cc forum, who was the quickest in posting a solution. They are also other solutions, including mine, but this one it's the most elegant until now.

 Please feel free to add yours if you want :)





No comments:

Post a Comment