Do you know Arduino? – sprintf and floating point

sprint result on arduino IDE

I like Arduino devices, but I don't quite like Arduino IDE. Among all the reasons, one is its printf() and sprintf() implementation on floating point support.

In Arduino programming, you often see code like this:

Serial.print("Temperuature = ");
Serial.print(temp);
Serial.print("c, Humidity = ");
Serial.print(humidity);
Serial.println("%");

The code is ugly, repeatitive, and feel like written by someone who is learning programming, but you see this kind of code even in the examples that come with popular and well-written libraries. All those 5 lines of code do is to print one line of text like this to Serial Monitor:

Temperature = 32.6c, Humidity = 80.4%

If you are coming from the background of Python programming, you probably know the Zen of Python, it emphasis on 'Beautiful is better than ugly' and 'Readability counts'. I personally think it should apply to all programming languages because after all ‘code is read more often than it is written’.

Beautiful is better than ugly.
Readability counts.
Code is read more often than it is written.

printf() function

If you have experience in C or C++ programming, you probably know the C library function printf(), so why not using printf()?

printf("Temperature = %.1fc, Humidity = %.1f%%\n", temp, humidity);

The first argument of the function is a formatter string that contains the text to be written to stdout. It can optionally contain embedded format tags that are replaced by the values specified in subsequent additional arguments and formatted as requested. The function return an integer that represented the total characters written to the the stdout if successful, or a -1 is returned if for some reason it is failed to write to stdout.

The function is available by default in Arduino (stdio.h is included in <Arduino.h> automatically), so if you write the code as shown above, it will compiled and run without any problem, but you won't see any output on the Serial Monitor, this is because Arduino does not use stdout, all the print (and keyboard input) are done via Serial interface.

sprintf() function

What about sprintf() which is available in both C and C++? sprintf() allows you send formatted output to an array of character elements where the resulting string is stored. You can then send the formatted string via Serial.print() function.

char buffer[50];
sprintf(buffer, "temperature = %.1fc, Humidity = %.1f%%\n", temp, humidity);
Serial.print(buffer);

This seems to be a good solution. But if you are rushing to use sprintf() in your Arduino sketch, not so fast until you see the result from this sketch:

void setup() {
  Serial.begin(115200);
  int x = 10;
  float y = 3.14159;
  char name[] = "Henry";

  char buff[50];
  sprintf(buff, "this is an integer %d, and a float %f\n", x, y);
  Serial.print(buff);
  sprintf(buff, "Hello %s\n", name);
  Serial.print(buff);
}

If you run the sketch, you will see this result:

this is an integer 10, and a float ?
Hello Henry

Both integer and string output works like a charm, but what is that ? for the float point value y? If you run Serial.print(y) on Arduino, it print out the correct floating point value. If you run the code in standard C++ environment (after replace Serial.print with std::cout), the code will also print out the floating point 3.14159 without any problem.

This could cost hours of frustration in finding out why the code is not working as it is supposed to be on Arduino. It turns out that Arduino's sprintf() does not support floating point formatting.

Why Arduino sprintf() does not support floating point?

So what's going on? Why the sprintf() on Arduino behave differently from standard C++ library. To find out why, you have to know where the sprintf() is coming from. Arduino compiler is based on gcc, or more precisely, avr-gcc for Atmel's AVR microcontrollers. Together with avr-binutils, and ave-libc form the heart of toolchain for the Atmel AVR microcontrollers. All the printf-like functions in avr-gcc come from vprintf() function in avr-libc. I found the answer to my question in AVR libc documentation on vprintf(), it said:

"Since the full implementation of all the mentioned features becomes fairly large, three different flavours of vfprintf() can be selected using linker options. The default vfprintf() implements all the mentioned functionality except floating point conversions... "

it further mentioned that:

"If the full functionality including the floating point conversions is required, the following options should be used:"

-Wl,-u,vfprintf -lprintf_flt -lm

As Arduino compiler options and linker process are hardcoded in its Java code and there is no way for user to select the optional build flags. The problem existed since day 1 of Arduino release and for years people seeking for a solution.

The reason that Serial.print(float) is able to print the floating point is because Arduino painfully and tediously implemented the Serial.print() function (the source code can be viewed at ArduinoCore-avr github page, it is almost like a hack) to support the floating point print.

One workaround is to use the dtostrf() function available in the avr-libc (dtostrf() is avr-libc specific function, not available in standard gcc:

dtostrf(floatvar, StringLengthIncDecimalPoint, numVarsAfterDecimal, charbuf);

The dtostrf() converts a float to a string before passing it into the buffer.

float f = 3.14159;
char floatString[10];
dtostrf(f,4,2,floatString);
sprintf(buffer, "myFloat in string is = %s\n", floatString);
Serial.print(buffer);

The dtostrf() works but it introduce extra line for setup the buffer before printing out the formatted string, it must well just use Serial.print(). Another workaround looks cleaner but only works for positive number.

sprintf(str, "String value: %d.%02d", (int)f, (int)(f*100)%100);

There is a discussion on Arduino Forum to manually swap out vprint_std.o (i.e. the standard default version of vprintf()) from libc.a and replace it with vprint_flt.o (the floating point version of vprintf()). This hack works, it is a little bit terious, but you only need to do it once until you update the Arduino IDE in future, the update process will overide your hack and you will have to re-do it again.

During my search of a solution, I also found PrintEx library on github. The PrintEx offers a clean API and much more features without compromising the performance with reasonable memory footprint, and is the best solution in my opinion.

#include <PrintEx.h>

PrintEx myPrint = Serial;

void setup(){
   Serial.begin(115200);
   float pi=3.14159;
   myPrint.printf("pi = %.2f\n", pi);
}

Although PrintEx library is almost perfect and solve the problem, but personally I think it is still a workaround. The original problem is trivia to fix, we need all those workarounds, hacking or external library just because the inflexibility of Arduino IDE. So why should we still using Arduino IDE if Arduino is not able to offer a solution for at least the past 8 years?

Fix the sprintf() with PlatformIO

This is when I realised that PlatformIO might be able to solve the problem. I have been using PlatformIO as my Arduino development environment for a couple of years now. PlatformIO is a better IDE than Arduino IDE in terms of project-based library dependency management, configuration and more importantly, allows me to customise my build flags for each project.

If you never use PlatformIO for Arduino programming, the following two videos from Robin Reiter on YouTube provides a quick installation guide for setting up PlatformIO.

PlatformIO put the configuration settings of each project in a file called platforio.ini, I can add customised build_flags into my Arduino Nano's platformio.ini as:

[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino
build_flags = -Wl,-u,vfprintf -lprintf_flt -lm

The first three parameters about platform, board and framework were created automatically when I create a project via PlatformIO. The last line about build_flags is what I need to add manually based the vprintf information provided by avr-libc. This allows the compiler to replace the default lprintf_std with lprintf_flt during the build process so that the compiled code will have floating point support on vprintf.

I compiled and upload the sketch to my Arduino Nano via PlatformIO, and run the sketch, the correct floating point result shown up on Serial Monitor!

this is an integer 10, and a float 3.14159
Hello Henry

The vprintf floating point support added about 1500 bytes to the memory usage which might be a big deal when avr libs was developed 20 years ago, but for Arduino adding 1500 bytes extra memory usage out of total 37000+ bytes is not a big deal for many applications, and now I have a fully function sprintf() instead of the previous half-baked version!

Memory usage without floating point support
Memory usage without floating point support
Memory usage with floating point avr libc
Memory usage with floating point avr libc

Create a print() function

sprintf() requires some setup and take 2 lines of code to print a formatted string, but it is still better and more readable than the 5 lines that you seen at the beginning of this article. To make it a one liner, I create a function and wrapped all the code with C++ Template Parameter Pack pattern which was introduced since C++11.

template <typename... T>
void print(const char *str, T... args) {
  int len = snprintf(NULL, 0, str, args...);
  if (len) {
    char buff[len+1];
    snprintf(buff, len+1, str, args...);
    Serial.print(buff);
  }
}

Now I have a one liner print() function that is elegant and easy to read:

print("temperature = %.1fc, Humidity = %.1f%%\n", temp, humidity);

What other Arduino-compatible platforms handle floating point

After done all those research and implement my own print function, I'm wondering how other Arduino-compatible platforms handle floating point in their Arduino core implementation? Here is my finding on ESP8266/ESP32 and STM32duino.

ESP8266/ESP32

Both ESP8266 and ESP32 Arduino core implementation added a printf method to its Serial class, so that you can do call Serial.printf() like the way c printf() do. What this means is that ESP32 and ESP8266 supports floating point on Arduino out-of-box.

The following code running on ESP32 and ESP8266 will provide the correct floating point results. How nice!

float number = 32.3;
char str[30];

void setup() {
  Serial.begin(115200); 
  
  // This is ESP32/ESP8266 specific Serial method
  Serial.printf("floating point = %.2f\n", number);

  // which is equivalent to this (BTW, this also works)
  sprintf(str, "floating point = %.3f\n", number);
  Serial.print(str);
}

void loop() {
 
}

STM32duino

STM32duino does not supports floating by default, it follows faithfully to the Arduino APIs. However, on the drop down menu of Arduino IDE, you can select the optional c runtime to pull in the printf floating point support during compilation. This is similar to the intention of original AVR implementation and similar to what PlatformIO did, but with an even nicer and user-friendly UI. Why Arduino.cc can't do that?

stm32duino offers compiler option for supporting printf floating point
stm32duino offers compiler option for supporting printf floating point

Final Word

The avr function dtostrf() remain a viable workaround because it is part of the Arduino core and implemented across all Arduion-compatible platforms.

The sprintf() floating point support on Arduino is a well known issue for years, and is actually trivia to fix. Arduino announced the release of alpha version of Arduino Pro IDE in Oct 2019. I have not try the Arduino Pro IDE yet, but I sincerely hope that Arduino.cc learnt something from other platform owners, and gives users more flexibility in project management and build options.

I hope now you will know Arduino a little bit better than before.

12 comments by readers

  1. Thank you for posting this excellent blog article. I learned a huge amount. Many new Arduino developers fall foul of the sprintf/float sinkhole and most developers just write many lines of unsightly code to work around. I agree with you : can do better (and you have!). I used PrintEx but found a bug in PrintEx sprintf (does not write ‘\0’ at end of string sometimes) and I used your elegant C++11 template parameter pack solution to wrap it and fix the problem. I am still using the Arduino and I will give the Pro IDE a go soon…

  2. I found that this works for an esp32 including negative numbers. The following prints out a negative number in two decimals:

    sprintf(tbs, "%6d.%02d, ", (int)x,(int)(abs(x)*100)%100);   
    Serial.print(tbs);
    

    Thanks for the tutorial!

  3. I switched over to PlatformIO – I’ve already used Atom for web pages, anyway. There’s a problem with Beginning Arduino project 10 – “Serial Controlled Mood Lamp”

    The Arduino IDE Serial Monitor accumulates your text in a text box until you hit RETURN or click the SEND button, but PlatformIO seems to send characters as soon as they are available. So if you type slowly, or pause, the Arduino board gets only a fraction of your intended command.

    Suggestions on how to make the Serial Monitor wait until a complete line has been entered?

  4. Nice effort on Tutorial, just pity you have missed-out the obvious, and a solution…
    Due to the tedious work necessary to print More complex data, it is natural need for users to add the printf support themselves. For example: https://playground.arduino.cc/Main/Printf/
    This should have been first stated and dealt with long before dealing with less common derivations.
    Openly, the ESP8266 libraries are far better done then those from Arduino foundation itself, especially when you go into less basic projects, but do have a steeper learning curve…

    1. Yeah… And did you bother to read the full article? At the end it says:

      I’ve tweaked the previous example to use regular integers rather than floating point values. My environment (Ubuntu Linux 10.04, Arduino IDE 1.0) doesn’t include support for floating points (%f) by default when compiling printf support. According to notes in stdio.h it’s possible to change this behaviour by passing gcc extra parameters.

  5. Thank you for saving my day! As HeatPumper mentioned above, sprintf misses the terminating zero. This is easily fixed in your “Create a print() function”:

    Just replace:

        char buff[len];
        sprintf(buff, str, args...);
    

    by:

        char buff[len+1];
        sprintf(buff, fmt, args...);
        buff[len] = 0;
    
    1. Thanks for pointing that out, I took a look at my little function again and made some small changes to use snprintf() instead of sprintf() as snprintf() will always terminate the string with \0 even when it is truncated, so it is safer, and there is no need to manually add \0.

  6. I just added this in \hardware\arduino\avr\platform.txt to make sprintf work with float:

    # These can be overridden in platform.local.txt
    compiler.c.extra_flags=
    compiler.c.elf.extra_flags=-Wl,-u,vfprintf -lprintf_flt -lm

Leave a Reply to Dag Rende Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.