Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 15 of 15

Thread: Serial.Printf limit on decimal places

  1. #1

    Serial.Printf limit on decimal places

    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.

  2. #2
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    28,465
    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 by PaulStoffregen; 06-09-2023 at 04:56 PM. Reason: typo

  3. #3
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    794
    It also has to do with how floating-point numbers represent real numbers.

    Here's a random link I found while doing a Google search for "0.1 floating point":
    https://jvns.ca/blog/2023/02/08/why-...0000000000004/

  4. #4
    Quote Originally Posted by PaulStoffregen View Post
    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 by bobpellegrino; 06-09-2023 at 08:22 PM.

  5. #5
    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?

  6. #6
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    794
    See this for how to enable floating-point `scanf`: https://forum.pjrc.com/threads/27827...-on-Teensy-3-1

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

  7. #7
    Senior Member+ defragster's Avatar
    Join Date
    Feb 2015
    Posts
    17,433
    Quote Originally Posted by shawn View Post
    See this for how to enable floating-point `scanf`: https://forum.pjrc.com/threads/27827...-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...-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

  8. #8
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    794
    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.

  9. #9
    Senior Member+ MichaelMeissner's Avatar
    Join Date
    Nov 2012
    Location
    Ayer Massachussetts
    Posts
    4,458

    Cool

    Quote Originally Posted by bobpellegrino View Post
    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...g-point_format
    • In general, you will get 15-17 decimal digits after converting the binary double to a decimal representation for printing.

  10. #10
    Senior Member
    Join Date
    Feb 2015
    Location
    Finland
    Posts
    309
    Quote Originally Posted by bobpellegrino View Post
    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).

  11. #11
    Quote Originally Posted by shawn View Post
    See this for how to enable floating-point `scanf`: https://forum.pjrc.com/threads/27827...-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.

  12. #12
    Senior Member
    Join Date
    Mar 2023
    Posts
    213
    "%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".

  13. #13
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    794
    For posterity, here’s a link to the `scanf` conversion specifiers:
    https://en.cppreference.com/w/cpp/io/c/fscanf

  14. #14
    Quote Originally Posted by shawn View Post
    For posterity, here’s a link to the `scanf` conversion specifiers:
    https://en.cppreference.com/w/cpp/io/c/fscanf
    BTW, this was an initial point of confusion for me. Where I come from, scanf was for reading input and sscanf was for parsing strings. So I was surprised that Arduino had no scanf. It also has no JCL functions for processing my old punched card decks. Time moves on...

  15. #15
    Senior Member
    Join Date
    Mar 2017
    Location
    Oakland, CA, USA
    Posts
    794
    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •