Arduino Pro Tips: Improve the Serial Print Function Using the Variadic Print Function vsnprintf()

Congratulations on purchasing an Arduino Pro line product! Perhaps you purchased a Portenta H7 as pictured in Figure 1.

How are you going to leverage all this new power? Let’s start with the Arduino Serial library, specifically the Serial.println() function.

What is wrong with Arduino’s Serial.println() function?

Perhaps it’s time to let go of the Serial.println() function and exchange it for a sleek formatter that can print the entire message in a single line. While the venerable function has served us well for nearly 20 years, we can do better with the pro equipment.

Specifically, we can reduce program clutter, as we no longer need to stack multiple Serial.print() functions on top of a Serial.println(). This improves program clarity, allowing us to focus on advanced functionality. For example, how many Serial.print() and Serial.println() functions calls are necessary to print this line

Variadic functions are cool! 
 	Runtime is 762 seconds, and this message has been printed 1520 times.

Depending on the program, we are somewhere around five function calls. We can do better.

Figure 1: Image of a Arduino Portenta H7 installed on a carrier board.

How can we improve the Arduino Serial library’s Serial.println() function?

The heart of our improved solution is a clever function called vsnprintf(). You may be familiar with its old unsafe cousin sprintf() or the improved snprintf(). All printf functions in the C standard library (stdio.h) accept a formatted string and a list of arguments to be insert to that string. Here is an example:

sprintf(buffer, "Vin: %.2fV, Vout: %.2fV, Gain: %.2f", v_in, v_out, gain);
Serial.println(buffer);

Which yields something like this:

Vin: 2.00V, Vout: 5.00V, Gain: 2.50

In this example, the formatted string is “Vin: %.2fV, Vout: %.2fV, Gain: %.2f” while the arguments are v_in, v_out, gain. The buffer is a character array into which the formatted string is held. This buffer is then sent to the Arduino Serial.println() function.

Tech Tip: The sprintf() function is undesirable as there are no safeties. If we are not careful, it will continue writing past the allocated buffer thereby corrupting other variables in your program. For example is we allocate a buffer size of 50 but send sprint() a string of length 100, it will overwrite the next 50 consecutive bytes in the microcontroller memory thereby corrupting important parts of you program.

From experience, I can tell you that this type of error is difficult to troubleshoot. The added upfront effort with snprintf() is more than justified when we consider the time to troubleshoot an intermittent buffer overwrite bugs.

Improving the Arduino Serial library by adding a message formatter

Before we get ahead of ourselves, we recognize that the Serial library is deeply embedded into Arduino. As a core library, Serial takes care of the microcontroller specific Universal Asynchronous Receiver-Transmitter (UART) configurations across all Arduino products. Consequently, we abandon the Serial library at our peril.

Instead, we add a formatter on top of the Serial library. Specifically, we will use sprintf() like formatting commands but with a twist. We will use the vsnprintf() function along with Serial.

Before we explore a full solution, let’s see how our solution shines in this representative Arduino .ino file. Specifically, study the logger.print() statement which replaces at least four Serial.print() statements stacked on top of a Serial.println() function.

#include "Logger.h"
Logger logger(200);

void setup() {
  logger.begin(19200);
}

void loop()  {
  static uint16_t loopCnt = 0;
  uint32_t now = millis();

  logger.print("Variadic functions are cool! \r\n \tRuntime is %lu seconds, and this message has been printed %u times.\r\n\n", now / 1000, loopCnt);

  loopCnt++;
  delay(500);
}

Tech Tip: The sprintf() and snprintf()functions are core routines in stdio.h for nearly every modern C and C++ compiler. They differ from printf() in that they store the formatted string in a character buffer as opposed to direct printing to a stdout.

Be sure to consult the hundreds of books and tens of thousands of web resources dedicated to the sprintf() family formatters. Recommend you make a notecard containing the common formatters such as %d, %i, %f, \n, and \t.

The supporting .h file is:

#ifndef LOGGER_H
#define LOGGER_H

#include <Arduino.h>

class Logger {
public:
    Logger(uint8_t bufferSize = 200); // Custom buffer size
    void begin(uint32_t baudRate = 9600);
    void print(const char* format, ...);

private:
    bool initialized;
    char* tempBuffer;    // Dynamic buffer (allocated in constructor)
    uint8_t bufferSize;  // Size of the buffer
};

#endif // LOGGER_H

Tech Tip: Other .cpp files may access the logger.print() method provided they know it exists. Be sure to include an extern reference to the logger object.

The supporting logger.cpp code is described here:

#include "Logger.h"
#include <stdarg.h>

/**********************************************************************
 *
 * Constructor for the message logger
 *
 * Assume that the Logger object is permanent—no destructor required.
 *
 *********************************************************************/
Logger::Logger(uint8_t bufferSize)
  : initialized(false), bufferSize(bufferSize) {
  tempBuffer = new char[bufferSize]; 
  memset(tempBuffer, 0, bufferSize); 
}

/*******************************************************************
 *
 * Initialize the object with desired baud rate
 *
 ******************************************************************/
void Logger::begin(uint32_t baudRate) {
  Serial.begin(baudRate);
  while (!Serial) {}
  initialized = true;
}

/******************************************************************************************************************************************************************
 *
 * Print formatted message
 * 
 * Accept a string as if formatted using the old school sprintf. The vsnprintf() function provides length protection based on bufferSize.
 *
 * This is a variadic solution where the ellipsis indicate a variable number of arguments are passed to the method.
 *
 * Example usage: 
 * 
 *   logger.print("Variadic functions are cool! \r\n \tRuntime is %d seconds, and this message has been printed %d times.\r\n\n",  now / 1000, loopCnt);
 *
 *****************************************************************************************************************************************************************/
void Logger::print(const char* format, ...) { // A variadic function
  if (initialized) {
    va_list args;                                     // Declare a va_list buffer to hold the arguments.
    va_start(args, format);                           // Parse out the arguments.
    vsnprintf(tempBuffer, bufferSize, format, args);  // Prepare the string and place it in tempBuffer.
    va_end(args);                                     // clean up.
    Serial.print(tempBuffer);                         // Print the formatted message.
  }
}

Parting thoughts

The formatter object described in this engineering brief offers a reasonable way to reduce Arduino code clutter. Instead of stacks of Serial.print() and Serial.println() function we have a single line of code. It is a professional tip for use on professional products—actually, it will work on most Arduino microcontrollers.

This is the beginning of our journey, as the logger utility may be expanded to other serial like ports. For example, you could log a message to an Ethernet port or even a local SD card.

Please give a like if you found this information useful. Also, let us know you modified the idea to best incorporate it into your project.

Best wishes,

APDahlen

Related information

Please follow these links to related and useful information:

About this author

Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET-accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this.

1 Like