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() convert 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];
    sprintf(buff, 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);

Final Word

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 it will give users more flexibility in project management and build options.

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

Leave a 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.