Serial.Printf limit on decimal places

bobpellegrino

Active member
Is there a limit on the number of decimal places Serial.printf will handle?

Serial.printf("%.13f",305.6381031000000000000000);
gives 305.6381031000000

but Serial.printf("%.14f",305.6381031000000000000000);
gives 305.63810310000002
.....................................^ Spurious digit

and Serial.printf("%.22f",305.6381031000000000000000);
gives 305.6381031000000234598701
....................................^^^^^^^ Even more spurious digits

This is on a Teensy 4 micromod.
 
There is a well known limit to the precision of 64 bit floating point. It's not just printf(). It's simply how floating point works.
 
Last edited:
There is a well known limit to the precision of 64 bit floating point. It's not just printf(). It's simply how floating point works.

I think I used to know that, a very long time ago. But 14 digits is surprisingly short to run into a problem. It seems to do better in scientific notation. Either that or I go find a bignum library. Thanks Paul.

BTW, if a double is 8 bytes I should get 20 digits in an ideal world, right?
 
Last edited:
I think i will be fine if I keep everything in scientific notation, but now I face another problem: There doesn't seem to be a scanf for arduino. So do I really have to write my own parser to input these large numbers?
 
See this for how to enable floating-point `scanf`: https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1

Or, are you referring to wiring up scanf/stdin with, say `Serial` (or any `Stream`) input?

@shawn- thanks: I found that linked 'asm' code looking for sscanf() usage in a sketch from way back (3/16) - and I hadn't put the post link in code comment.

Was going to note sscanf() works for sure - but since it was written as scanf() assumed it was a desire for CIN to work - and recent note that COUT works with updated toolchain, noted the CIN still not usable.

So, was going to note parsing would just read chars to a c-string and watch for delimiters, add a NULL - then sscanf() the c-string for desired input.

Fun note: When doing 'Ctrl+L' to insert Hyper_Link like this from post #6:

https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1

Edit the second copy text part after the URL and remove the https:// to get a more readble text link like:
forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1

or delete even more 'https://forum.pjrc.com/threads/27827-' for less clutter:
Float-in-sscanf-on-Teensy-3-1
 
Here's some quickie off-the-cuff code that implements `stdin` reading in a non-blocking way. There's a bunch of ways to improve this code, for example, checking for `stderr` or `stdout` being an error, handling zero-length requests, using `errno`, etc. There's also a way to do this in a blocking manner. In any case, this should get you started:

Code:
#includes <exercise-for-the-reader>

// If this is in a C++ file, wrap in `extern "C"`

// https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1
// This link shows how to enable float scanning.
int _read(int file, void *buf, size_t len) {
  Stream *in;

  if (file == stdin->_file) {  // TODO: Check for output-only files and do an error
    in = &Serial;
  } else {
    in = (Stream *)file;
  }

  // Non-blocking input
  // Optional: blocking input, or some function call that lets you know which
  int avail = in->available();
  if (avail <= 0) {
    return 0;
  }
  size_t toRead = avail;
  if (toRead > len) {
    toRead = len;
  }
  return in->readBytes((char *)buf, toRead);
}

Hopefully that gets you started.
 
I think I used to know that, a very long time ago. But 14 digits is surprisingly short to run into a problem. It seems to do better in scientific notation. Either that or I go find a bignum library. Thanks Paul.

BTW, if a double is 8 bytes I should get 20 digits in an ideal world, right?

IEEE 64-bit floating point uses:

  • 1 bit for the sign
  • 11 bits for the exponent
  • 52 bits for the mantissa, plus the hidden bit that is implied to be 1 unless the value is 0, a NaN, or a de-normal number
  • https://en.wikipedia.org/wiki/Double-precision_floating-point_format
  • In general, you will get 15-17 decimal digits after converting the binary double to a decimal representation for printing.
 
So do I really have to write my own parser to input these large numbers?
No, you can use strtod().

Use example:
Code:
    // src points to the number, or to whitespace preceding the number.
    // It can also be a buffer.  If it is n'th character in buffer, just use (buffer + n) instead.
    char *src;

    // end will point to the first character after the parsed number.
    // We can use it to check the length and whether the parsing succeeded.
    char *end;

    // This is the parsed value.
    double  val;

    // Before the conversion, we set end to a known value; the start is a good one.
    end = src;
    val = strtod(src, (char **)&end);
    if (end == src) {
        // Error: Nothing was parsed.

    } else
    if (!isfinite(val)) {
        // Error: Infinity or not-a-number.

    } else {
        // Parsed (size_t)(end - src) chars.
        // end points to the first unparsed character,
        // which could be a letter or something.
        // Parsed finite numeric value is in val.
    }

All floating-point numbers are of type m×bx, storing the sign, exponent x, and mantissa m.
The floating-point types in Teensies use IEEE 754 Binary32 ('float') and Binary64 ('double'), where b is always 2, and the highest bit of mantissa is 1 unless the value is zero (all zeros).
'float' m is 24-bit, and 'double' m is 53-bit, thus they have 7.22 and 15.95 decimal digits worth of precision.

Because 1/10 is in binary .00011, i.e. 0.000110011001100110011..., they cannot represent most decimal fractions (like 0.1, 0.09, 0.007) exactly.
Fractions like 1/2 = 0.5, 1/4 = 0.25, and those that can be expressed as a sum of 1/2k (where minimum and maximum k differ by not more than 23 or 52, for 'float' and 'double' respectively), are the only ones that can be represented exactly. For example, 0.75 is equal to 0.112 in binary. (The 2 is the traditional 'math' way to indicate the number is in binary, AKA base-2.)

If you need even more precision, fixed-point formats are much easier to implement than floating-point.

Then, the question is whether you do more computation or input/output with them. In particular, on Teensies, the limbs can be 32-bit, 28-bit (for more efficient conversion to/from decimal formats for 14% larger storage), nine-digit decimal (000000000..999999999) in 30-bit unsigned integer, or one of the other binary-coded decimal formats. The 32-bit one is most efficient for computation, generally speaking. The 28-bit halves the number of multiplications during conversions, but since Teensies tend to have fast (single-cycle or close) multiplication operations, it is probably not worthwhile. The nine-digit decimal is precise (all decimal fractions can be expressed exactly) and extremely fast for decimal input and output, as well as addition and subtraction, but multiplication and division is slowed down due to an extra division-modulus per limb. All trigonometric functions need to be expressed in series form, and are thus quite slow, although there are some tricks to speed up the calculation (notably using double-precision approximate and then iterative optimization using e.g. Newton-Raphson).

Knowing what kind of math operations you need to do with these would help.

When using floating-point types, precision-preserving approaches to seemingly trivial things, like summing a set of values, helps a lot: see Kahan summation algorithm. There are many other approaches for various operations, including alternatives for summing (especially if the values are already sorted).
 
See this for how to enable floating-point `scanf`: https://forum.pjrc.com/threads/27827-Float-in-sscanf-on-Teensy-3-1

Or, are you referring to wiring up scanf/stdin with, say `Serial` (or any `Stream`) input?

Ok, this was the magic I needed. I did notice something odd though. Since I was inputting the numbers in scientific notation, I figured "%e" was the right format string for sscanf.
But:

Serial.println(inputBuffer);
sscanf(inputBuffer, "%e", &test); // "%e"
Serial.printf("%.15e", test);
Serial.printf("%.24lf", test);

Yields:
-5.020861434E-13 (what I typed)
1.417858317339368e-314
0.000000000000000000000000

Well, that ain't right! However:
Serial.println(inputBuffer);
sscanf(inputBuffer, "%lf", &test); //"%lf"
Serial.printf("%.15e", test);
Serial.printf("%.24lf", test);

Yields:
-5.020861434E-13 (what I typed)
-5.020861434000000e-13
-0.000000000000502086143400

Which is exactly right. So what's up with %e?

BTW, test=strtod(inputBuffer, NULL);
and
sscanf(inputBuffer, "%lf", &test);

both yield the correct result, for the record.

Thanks again for your help.
 
"%e" expects the argument to be a pointer to a float. Assuming test is a double (please post complete source code!) you want to use "%le".
 
Arduino’s just a framework (and a style). It usually exists on top of whatever C++ system. In the case of Teensy and GCC 11.3.1, there’s also the whole C/C++ standard library including most of the stuff defined in the C++14 standard (soon to be C++17, likely in Teensyduino 1.59). The stuff that doesn’t work usually just requires some internal “wiring and glue”. For the C standard library part, the GCC implementation includes newlib.

To wire up newlib, some functions need to be defined. They’re mostly all there in Teensyduino, just not complete, probably for space reasons. For stdio, look for `_write(…)` and `_read(…)` definitions in the Teensyduino source. `stdout` is connected inside the default `_write()` function implementation in a rudimentary way, but `_read()` doesn’t do much. However, since they’re both defined as “weak”, you can override them. If you include the `_read()` implementation above in your program, you’ll have stdin connected, meaning `scanf()` and its variants will work.

Note: scanf(stuff) is essentially the same as fscanf(stdin, stuff).
scanf(), fscanf(), and sscanf() are all considered to be part of C-style I/O and all use the same underlying machinery. The only difference is where the input comes from: a FILE or a string.
 
Back
Top