Teensy3 alternative for dtostrf() ?

Status
Not open for further replies.
just edit fcvt() to fcvtf()

ok, i understand the compatibility-reason

so, i'm right that the existing dtostrf really uses float and not double as Michael said ?
Or did i understand him wrong ?
No, it was somebody else that said it used float. I said it used double, but I was responding that in the AVR environment, even if it was declared using float, it would still 'work'.

On my Arduino 1.6.5 installation, dtostrf is declared in hardware/tools/avr/avr/include/stdlib.h, and the first argument is a double. Even in my Arduino 1.0.6 installation, it is declared double. Perhaps some ancient Arduino library declared it float.
 
Just above my changed code in nonstd.c is this handy function declaration - all I see are FLOATS in use?

char * dtostrf(float val, int width, unsigned int precision, char *buf) {...}

and in the avr_functions.h file it is this: char * dtostrf(float val, int width, unsigned int precision, char *buf);

In the sketch provided and I modified it this is the usage:
float y;
dtostrf(y, 10, 5, cbuf);
 
However this code as LOOP COMPILES without any warning and RUNS just fine. Looking HERE as printf is defined there is no DOUBLE type - just FLOAT, that can be printed in exponential form. So practically they are the same . . . <edit> interchangeable thanks to the compiler for this purpose. LINK: float-vs-double-precision

<EDIT>: Added new cases to actually show that the dtostrf() truly gets a FLOAT worth of data - though it will "process" a DOUBLE worth just like printf()

Code:
int stopp = 0;
void loop() {
  if ( !stopp ) {
    stopp++;
    double dd = -0.0000123456789012345678901234L;
    float ff = -0.0000123456789012345678901234L;
    dtostrf(dd, 10, 5, cbuf);
    Serial.println(cbuf); //   -0.00001
    dtostrf(dd, 10, 29, cbuf);
    Serial.println(cbuf); // [U]-0.00001234567[/U]935171071439981460
    dtostrf(ff, 10, 29, cbuf);
    Serial.println(cbuf); // [U]-0.00001234567[/U]935171071439981460
    Serial.printf("%f\n", dd);  // -0.000012
    Serial.printf("%4.29f\n", dd);  // [U]-0.0000123456789012345678[/U]0746176
    Serial.printf("%f\n", ff);  // -0.000012
    Serial.printf("%4.29f\n", ff);  // [U]-0.00001234567[/U]935171071439981461
  }
}

Output of the prints:
-0.00001
-0.00001234567935171071439981460
-0.00001234567935171071439981460
-0.000012
-0.00001234567890123456780746176
-0.000012
-0.00001234567935171071439981461

Note - of course - being abusive with %d INTEGER in printf does not work - but it gives no warning - it just does the wrong thing when the exchanges values are not DOUBLE .vs. FLOAT:

code:
Serial.printf("%d\n", d);
Serial.printf("%ld\n", d);

output:
536870912
536870912
 
Last edited:
The 'else' fix when zeros follow the decimal on a number <0 was good to fix the failed functionality.

However in trying to automate a test to see if dtostrf could work on double as well as float [generally yes as far as I got] I discovered in the existing float code there is a last digit rounding error typically when there is a single zero after the decimal (compared to sprintf), but not for all numbers - with non-integers I suppose this is acceptable? It seems to be a factor of 'precision' as returned by fcvt() as used. Shown here from post #11.
dtostrf(y, 10, 5, cbuf); == -0.01234
sprintf(cbuf, "%10.5f", y); == -0.01235

If WIDTH and PRECISION are mismatched for small numbers resulting in ZERO shown- then WIDTH is violated and extra '0' are added. This is from the ZERO fill I did with a while that needs to read like this: "while ( decpt < 0 && !(p >= e) ) {" as it was running p past e. sprintf() does not place these extra zeroes.

I also noticed in doing the strcmp that in many of the fixed cases there is an extra padding blank making the return string one char longer than 'width', again that is a space padding for the numbers - but only when under 0.1 it seems. So instead of e++ in the else it should be "if ( decpt < 0 ) reqd++;" before the pad is calculated. In which case Frank was right we can make the ELSE clause the primary IF clause and the added while will drop out. Width is MIN not MAX string length to return - but extra pad doesn't match sprintf(), pull request to fix this?

Code:
	if (sign) reqd++;
	p = buf;
[B]	if ( decpt < 0 ) reqd++;[/B]
	e = p + reqd;
	pad = width - reqd;
	if (pad > 0) {
		e += pad;
		while (pad-- > 0) *p++ = ' ';
	}
	if (sign) *p++ = '-';
[B]	if (decpt <= 0 && precision > 0) {[/B]
		*p++ = '0';
		*p++ = '.';
[B]		while ( decpt < 0 && !(p >= e) ) {[/B]
			decpt++;
			*p++ = '0';
		}
	}

Other than the last digit comparing when small numbers - it looks like these are improvements.
 
dtostrf(dd, 10,5, cbuf);
hmm.. displays 6 chars afters the decimal-point. I'm not sure if that was the case before the update ? Or is it correct? (i don't know, really)
 
I just checked it :)
It showed -0.00001 (5 chars after ".") before your change in post #30
After that : -0.000012

edit: sry, double-post..
 
I don't see that now - maybe I fixed it? My Auto tester does string compares against: sprintf(spbuf, "%10.*f", kk, ff); to catch extra padding and missed or altered last digits, or errors.

<edit> That must have shifted with my recent fix (per above) as that is one of the cases where I get an extra byte of padding [ON other than NEW code].

Attached is nonstd.c and .h file.

You can call the old code side by side as: B1dtostrf(ff, 10, kk, tobuf); for old and for my current of course: dtostrf(ff, 10, kk, tobuf);

I also put in : B3dtostrf(dd, 20, kk2, tobuf); that takes a DOUBLE instead of FLOAT.

View attachment nonstd.c
View attachment avr_functions.h
 
Last edited:
Here is a snippet of my test output - showing GOOD for the current code, and some error on the other old or DOUBLE code.

ll=0 ------dtostrf( << my current updated 'fixed' code
ll=1 ------B1dtostrf( << Beta1 code
ll=2 ------B3dtostrf(
ll=3 ------B3dtostrf( DOUBLE >> WIDTH up to 20 from 10
------
ll ------which function above
ii ------interation multiplying number * 10
jj ------Which number +/-
kk ------precision
------
====== NEGATIVE ===========------
[ll=0 ii=0 jj=2 kk=5] :: GOOD___-0.0000123457, buffers==< -0.00001>
[ll=1 ii=0 jj=2 kk=5] :: BAD_PAD___-0.0000123457:: | @2 | spbuf=< -0.00001>, tobuf=< -12346> << (bad?) PAD???
[ll=2 ii=0 jj=2 kk=5] :: BAD_PAD___-0.0000123457:: | @2 | spbuf=< -0.00001>, tobuf=< -0.00001> << (bad?) PAD???
[ll=3 ii=0 jj=2 kk=5] :: BAD_PAD___-0.0000123457:: | @7 | spbuf=< -0.0000123457>, tobuf=< -0.000012345> << (bad?) PAD???
[ll=0 ii=1 jj=2 kk=5] :: GOOD___-0.0001234568, buffers==< -0.00012>
[ll=1 ii=1 jj=2 kk=5] :: BAD_PAD___-0.0001234568:: | @2 | spbuf=< -0.00012>, tobuf=< -12346> << (bad?) PAD???
[ll=2 ii=1 jj=2 kk=5] :: BAD_PAD___-0.0001234568:: | @2 | spbuf=< -0.00012>, tobuf=< -0.00012> << (bad?) PAD???
[ll=3 ii=1 jj=2 kk=5] :: BAD_PAD___-0.0001234568:: | @7 | spbuf=< -0.0001234568>, tobuf=< -0.000123456> << (bad?) PAD???
[ll=0 ii=2 jj=2 kk=5] :: GOOD___-0.0012345679, buffers==< -0.00123>
[ll=1 ii=2 jj=2 kk=5] :: BAD_PAD___-0.0012345679:: | @2 | spbuf=< -0.00123>, tobuf=< -12346> << (bad?) PAD???
[ll=2 ii=2 jj=2 kk=5] :: BAD_PAD___-0.0012345679:: | @2 | spbuf=< -0.00123>, tobuf=< -0.00123> << (bad?) PAD???
[ll=3 ii=2 jj=2 kk=5] :: BAD_PAD___-0.0012345679:: | @7 | spbuf=< -0.0012345679>, tobuf=< -0.001234567> << (bad?) PAD???
[ll=0 ii=3 jj=2 kk=5] :: LastDigit___-0.0123456789:: [#9 | 5>< 4 ] [ -0.01234]
[ll=1 ii=3 jj=2 kk=5] :: BAD_PAD___-0.0123456789:: | @2 | spbuf=< -0.01235>, tobuf=< -12346> << (bad?) PAD???
[ll=2 ii=3 jj=2 kk=5] :: BAD_PAD___-0.0123456789:: | @2 | spbuf=< -0.01235>, tobuf=< -0.01234> << (bad?) PAD???
[ll=3 ii=3 jj=2 kk=5] :: BAD_PAD___-0.0123456789:: | @7 | spbuf=< -0.0123456789>, tobuf=< -0.012345678> << (bad?) PAD???
[ll=0 ii=4 jj=2 kk=5] :: GOOD___-0.1234567890, buffers==< -0.12346>
[ll=0 ii=5 jj=2 kk=5] :: GOOD___-1.2345678901, buffers==< -1.23457>
[ll=0 ii=6 jj=2 kk=5] :: GOOD___-12.3456789012, buffers==< -12.34568>
[ll=0 ii=7 jj=2 kk=5] :: GOOD___-123.4567890123, buffers==<-123.45679>
[ll=0 ii=8 jj=2 kk=5] :: GOOD___-1234.5678901235, buffers==<-1234.56787>
[ll=0 ii=9 jj=2 kk=5] :: GOOD___-12345.6789012346, buffers==<-12345.67871>
[ll=0 ii=10 jj=2 kk=5] :: GOOD___-123456.7890123457, buffers==<-123456.78906>
[ll=0 ii=11 jj=2 kk=5] :: GOOD___-1234567.8901234567, buffers==<-1234567.87500>
[ll=0 ii=12 jj=2 kk=5] :: GOOD___-12345678.9012345672, buffers==<-12345679.00000>
[ll=0 ii=13 jj=2 kk=5] :: GOOD___-123456789.0123456717, buffers==<-123456792.00000>
 
Last edited:
Done with that - not perfect but further improved. Pull request submitted. There are some few low precision cases that were not corrected that show an off by one on the last displayable digit. These are only in places where worse errors existed per my test case code. Many of these showing the wrong digit are GIGO from the fcvt() function as it compares to sprint() output. It may be rounding specified to wrong digit as this is the number in the output string that is copied.

updated nonstd.c file, sample test output showing detected errors, where ll=0 shows remaining, ll=1 shows prior fix with more numerous padding blank errors etc, also my test sketch
View attachment dtostrf Errors list.txt
View attachment dtostrf_TEST.ino
View attachment nonstd.c
 
Last edited:
Thanks to Frank (/sarcasm :) ) :: Heads up - the FUNCTION spec says that this dtostrf() should work on a neg width. I don't see my change having any impact - neither it nor the TD_1.25 code seen to give uniform results in my test app - if I used it right - lots of blank strings with rare showing of numbers.
 
I don't know if this is relevant to this thread but I've found that for small numbers dtostrf will return a string that is too long:
Code:
// the setup function runs once when you press reset or power the board
void setup() 
{
	char str[100];
	Serial.begin(9600);
	Serial.println("hello");
	for (float f = -0.12; f < 0.12; f+= 0.005)
	{
		dtostrf(f, 4, 1, str);
		Serial.print(f); Serial.print("|"); Serial.print(str); Serial.print("|"); Serial.println();
	}
	Serial.println("done");
}

// the loop function runs over and over again until power down or reset
void loop(){}
This gives a output of:
hello
-0.12|-0.1|
-0.11|-0.1|
-0.11|-0.1|
-0.10|-0.1|
-0.10|-0.1|
-0.09| -0.0|
-0.09| -0.0|
-0.08| -0.0|
-0.08| -0.0|
-0.07| -0.0|
-0.07| -0.0|
-0.06| -0.0|
-0.06| -0.0|
-0.05| -0.0|
-0.05| -0.0|
-0.04| -0.0|
-0.04| -0.0|
-0.03| -0.0|
-0.03| -0.0|
-0.02| -0.0|
-0.02| -0.0|
-0.01| -0.0|
-0.01| -0.0|
-0.00| -0.00|
0.00| 0.0000000|
0.01| 0.00|
0.01| 0.0|
0.02| 0.0|
0.02| 0.0|
0.03| 0.0|
0.03| 0.0|
0.04| 0.0|
0.04| 0.0|
0.05| 0.0|
0.05| 0.0|
0.06| 0.0|
0.06| 0.0|
0.07| 0.0|
0.07| 0.0|
0.08| 0.0|
0.08| 0.0|
0.09| 0.0|
0.09| 0.0|
0.10| 0.1|
0.10| 0.1|
0.11| 0.1|
0.11| 0.1|
0.12| 0.1|
done

Teensyduino 1.28, Arduino 1.6
Chris
 
Last edited:
I ended up doing my own conversion function, it works the way I want and is also much smaller. It's not so universal but copes with the numbers I throw at it.

I found that dtostrf uses about 13924 bytes of program memory and 116 bytes of dynamic while my f2str function adds 144 bytes of program memory and no dynamic. It makes the difference between my program fitting in the LC or not.

My test program is:
Code:
#define F2STR

void setup()
{
 char str[100];
 Serial.begin(9600);
 // put your setup code here, to run once:
  //dtostrf(3.14192, 5, 1, str);
#ifdef F2STR
  f2str(3.14192, 5, 1, str);
#endif
  Serial.println(str);
}

void loop()
{
}

#ifdef F2STR

// send a float to a string that's a specified length and with a specified number of decimal places.
// the string is left justified and padded with spaces.
// if dp is -ve a leading space is added for values >= 0
// values that parse to longer than the length are truncated
// does not cope with large numbers
char* f2str(float v, uint8_t len, int8_t dp, char * str)
{
  char * c = str;
  int8_t l = len;

  // handle the sign or possible leading space
  if (v < 0)
  {
    *c++ = '-';
    l--;
  }
  else if (dp < 0)
  {
    *c++ = ' ';
    l--;
  }
  v = abs(v);
  dp = abs(dp);

  // move the number into the integer range and
  // set up integer equivalents
  uint32_t dpn = 1;
  for (int8_t i = 0; i < dp; i++)
  {
    v *= 10.0;
    dpn *= 10;
  }

  uint32_t n = dpn;
  // convert to an integer, with rounding. The number of digits must fit into a uint32
  uint32_t iv = (int)(v + 0.5);
  // this ensures that n is not greater than iv so less chance of an overflow
  while (iv / 10 >= n)
    n *= 10;

  bool fc = false;  // set when the first character has been added so that trailing zeroes are added but not leading zeroes
  while (n >= 1 && l > 0) // this stops the output overflowing but the value returned may be truncated
  {
    uint32_t i = iv / n;
    // print digits
    if (i > 0 || fc)
    {
      *c++ = '0' + i;
      l--;
      iv -= i * n;  // remove this digit from the number
      fc = true;
    }
    // add the decimal point when we get to it
    if (n == dpn)
    {
      if (!fc)
      {
        // add a leading 0 if required
        *c++ = '0';
        l--;
        fc = true;
      }
      if (dp)
      {
        *c++ = '.';
        l--;
      }
    }
    n /= 10;
  }
  // fill with spaces
  while (l > 0)
  {
    *c++ = ' ';
    l--;
  }
  // add terminator
  *c++ = 0;
  return str;
};

#endif
I commented out bits of code and did a verify build to get the space used. I have to print the result, if I don't f2str is optimised out.

I guess that dtostrf is pulling in a lot of additional libraries,

Chris
 
if pressed, you could omit all floating point in calculations and I/O, and use a fixed point library instead, for real numbers. Much faster, too.
 
Status
Not open for further replies.
Back
Top