Building a Printf Library for Arduino

I was asked by a client write a logging library for a project using the Arduino SDK. Since the library will be open-source, I’m going to run an experiment and document the design, development, and feedback cycles on this project. My hope is to expose readers to a real development process, so you can see how a project evolves due to real-world customer feedback.

To start the series, we’re going to implement a simple printf library for the Arduino SDK.

I’ve never really worked in-depth with Arduino code, but I quickly learned that the printf() family of functions wasn’t supported. The Arduino Print class functions provide sufficient printing capability, but the interface requires a lot of manual formatting and splitting of strings.

I don’t want to implement a logging library on top of the Print interfaces, mostly because I don’t enjoy using it. I’m going to get standard format strings and the printffamily working on Arduino before tackling the logging functionality.

Table of Contents:

  1. Requirements
  2. Brainstorming the Library
  3. Building the Library
  4. Creating Examples
  5. Documentation
  6. Library Properties
  7. Releasing
  8. Putting it All Together
  9. Further Reading

Requirements

I like to have a list of requirements to work with before starting a project. The requirements guide me during the brainstorming and implementation processes, and also serve as a checklist when writing tests and planning development.

Our requirements for this library are simple:

  • I want to be able to use printf() on an Arduino!
  • Standard format strings must be supported
  • For ease of use, the library should output to the Serial class by default
  • We should be able to use any other Print class with our library

Brainstorming the Library

Luckily, I already know of a great starting point for this effort: the mpaland/printf library, which we’ve featured in a previous article. I love this library, and it’s the cornerstone of the printf implementation in the Embedded Artistry libc.

The mpaland/printf library is small (600 lines of code), well-tested (400+ test cases), reentrant, thread-safe, and warning free. All of the important flags are supported, including floating-point. You can disable commonly-avoided format options with compile-time flags. Best of all: there are no external dependencies.

The library is extremely easy to port to a new system. Our library only needs to implement a single function for the printf family to work properly:

void _putchar(char character);

We can supply a default implementation for _putchar inside our library. This implementation will print characters using the Serial device. But we want to allow users to customize the output point, so we’ll need a printf_init function to allow users to set their desired Print class for the output.

We can also go one step further and make our implementation of the _putchar function weakly-linked, allowing users to re-define the function in their sketch. That way we don’t have to worry about hindering users with our design decision.

Since the original library is well-tested, I don’t plan on adding any tests to our code. The only part that is left to me is creating the _putchar function implementations, so we will test those manually using examples.

Building the Library

First steps are easy: create a GitHub repository, set up a license, and clone to the system. Our library will be named arduino-printf.

For this project, we will use the MIT license. This is because the mpaland/printf code is licensed under the MIT license. We’re simply providing a wrapper around that library, so I don’t want to impose different licensing terms.

Arduino library conventions has us place the source files in the top-level directory or within a src/ folder. I like the src/ approach. I’ll start with two files:

cd src
touch LibPrintf.cpp LibPrintf.h

I added the mpaland/printf to the arduino-printf repository as a submodule within the extras/ folder. I want to be able to conveniently track updates within the repository. The extras/ folder is ignored by the Arduino library manager, so I don’t need to worry about the files being compiled.

Normally, I would keep the printf files in the submodule and reference them directly. However, we don’t get much control over Arduino libraries, and the IDE will automatically compile all files in the src/ folder. The tests are compiled and have unsupported C++ features, causing a build failure.

So, I’m going to copy the printf.c and printf.h files contained in the submodule and put them in the src/ folder:

cp extras/printf/printf.* src/

There’s a good (and tricky) reason I’m putting them in the src/ folder instead of hiding them in a sub-folder. I’m looking ahead a bit. I know I want to use this library within the logging library. I want to add tests for the logging library, since I will be creating that portion. I don’t want to depend on any Arduino-specific headers or classes, so I need to be able to include printf.h directly from the logging library. This only works if I keep printf.h in the top-level of the src/ folder.

I tried another approach, which involved creating a shim header at the top-level which includes the proper printf.h in the submodule folder. However, the Arduino IDE did not identify printf.c for linking purposes. So we’re left with the ugly approach of copying the files to the top-level.

I don’t have to include the submodule if I have a second copy of the headers, but I keep it embedded because copying from the submodule directory makes it easy to update the files.

Onto the Implementation

Now that we have a boilerplate structure for the project in place, it’s time to knock out our requirements.

I like to start with header files and APIs. Normally, I would try my best to follow a test-driven approach, but again, we’re skipping tests with this library since the primary code is already tested.

I always start with header guards. The first addition to LibPrintf.h:

#ifndef ARDUINO_PRINTF_H_
#define ARDUINO_PRINTF_H_

// Code goes here

#endif //ARDUINO_PRINTF_H_

The one interface we need to define for this library is a printf_init function. As we described in the requirements, users should be able to use any class derived from the Print base class with printf.

Knowing that, my first prototype for the printf_init function is:

void printf_init(Print& StreamClass);

The Print class is defined in the Arduino Print.h header, so we need to include that. We also want to include printf.h for our users so they only need to worry about including LibPrintf.h:

#include "Print.h"
#include "printf.h"

That’s enough to get started in the header, so let’s move to LibPrintf.cpp.

I’ll start the file with two include directives that I know I need: our library header (LibPrintf.h) and the Arduino.h header, which gives us the Serial class.

#include "LibPrintf.h"
#include "Arduino.h"

We have a prototype for printf_init from the header, so let’s create the skeleton:

void printf_init(Print& PrintClass)
{
}

I also know that the library requires a definition of _putchar. We’re in C++ land, and the mpaland/printf library is written in C, so we’ll mark the language accordingly:

extern "C" void _putchar(char character)
{
}

First, we need to fulfill two requirements in the implementation:

  1. Print to Serial by default
  2. Allow a user to specify a Print class that is used instead of Serial

From these requirements, I can determine that I need a variable which indicates where I need to send output data. That variable should be initialized to Serial. Here’s how I’ll declare it:

static Print& print_instance = Serial;

Then, in _putchar, I can have our print_instance do the work:

extern "C" void _putchar(char character)
{
    print_instance.print(character);
}

Now printf_init must override the value of print_instance. Since we’re accepting a reference, I don’t need any nullptr checks.

void printf_init(Print& PrintClass)
{
    print_instance = PrintClass;
}

Notice that we aren’t configuring the Serial/Print class or calling Serial.begin(baudrate). The caller is responsible for configuring the output class as required for the system. We’ll need to document that usage note in our library header (as well as the README).

Here’s our final header implementation with the documentation:

#ifndef ARDUINO_PRINTF_H_
#define ARDUINO_PRINTF_H_

#include "Print.h"
#include "printf.h"

// In Setup(), you must initialize printf with a Print class if you don't want
// to use the default Serial object. If you want the default behavior, calling this
// function is not necessary.
//
// The caller is responsible for configure the Serial interface in setup() and calling
// Serial.begin().
void printf_init(Print& StreamClass);

#endif //ARDUINO_PRINTF_H_

Before I start creating examples, we should handle an additional goal identified during the brainstorm session: allow users to override _putchar with their own implementation. (Perhaps a user wants printf() to both send over Serial and write to a log file on an SD card.)

All we need to do is make the function weakly-linked using __attribute__((weak)):

extern "C" __attribute__((weak)) void _putchar(char character)
{
    print_instance->print(character);
}

Since this function is weakly linked, a user’s implementation will be picked over our library default.

Creating Examples

We’ve put together our simple printf library, now it’s time to test it and create examples

First, we’ll test the basic requirement: printf() should work with the Serial object. Our first example initializes the Serial object in setup() and prints output in loop():

#include <LibPrintf.h>

void setup() {
  Serial.begin(115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  printf("I'm alive!\n");
  delay(1000);
}

That works well, so we need to check that we can specify another Print class instead of Serial:

#include <LibPrintf.h>

void setup() {
  // Specify the print class to use with printf().
  // Any class derived from Print will work.
  printf_init(Serial1);

  // But the important detail: you are responsible for initializing the interface!
  Serial1.begin(115200);
}

void loop() {
  printf("I'm alive!\n");
  delay(1000);
}

I don’t have Serial1 hooked up to anything on my board, so no output comes through by default. Wiring up a debugger to the Serial1 lines on my board, I can see output. All good!

Finally, we need a test that let’s us override _putchar with a custom implementation. We’ll add an obvious difference, like printing an extra space after each character:

#include <LibPrintf.h>

void setup() {
  // But the important detail: you are responsible for initializing the interface!
  Serial.begin(115200);
}

void _putchar(char character)
{
  Serial.print(character);
  Serial.print(' ');
}

void loop() {
  printf("I'm alive!\n");
  delay(1000);
}

These three simple examples serve well to showcase the use of the class, and everything is behaving as expected.

Documentation

Everything works, and I’ve done some source-level commenting along the way. But we don’t have a useful library if developers don’t know how to use it, so we need to create a README.

We’ve previously written about writing a great README and shared a README template.

For this library and the intended audience, we’re going to simplify things a bit. Here’s what we want to cover:

  • Does this library have any dependencies?
  • How should this library be incorporated into the project?
  • How do you interact with the library?
  • What customization options are there?
  • What other requirements should the user know about?

From this list of questions, I know what I need to highlight:

  • What header to include (LibPrintf.h)
  • Using the class with the default Serial object
  • Specifying a custom Print class to use
  • Overriding _putchar with a custom implementation
  • The fact that users must configure the output object on their own in setup()

I also want to add some notes about the different examples that are available and what each example is expected to demonstrate.

You can see the full README contents in the repository.

Library Properties

We want to define metadata for our library within the Arduino ecosystem. To do that, we need to create a file called library.properties:

name=LibPrintf
version=1.0.0
author=Embedded Artistry
maintainer=Embedded Artistry <contact@embeddedartistry.com>
sentence=Library adding support for the printf family of functions to the Arduino SDK.
paragraph=This library provides support for printf() and other printf-like functions with full format-string support. Default output is to Serial, but can be customized.
category=Communication
url=https://github.com/embeddedartistry/arduino-printf
architectures=*
includes=LibPrintf.h

Now we’ll show up correctly in the Arduino IDE.

Releasing

To release, we want to tag a version 1.0.0:

git tag -m "Tagging release 1.0.0" 1.0.0

And then we can follow these instructions to release the library:

Open an issue on Arduino’s GitHub, specifying the URL of the repository from where to download your library. If you have multiple libraries to submit you are welcome to do them all in a single issue.

Putting it All Together

As a starting point, creating this library was refreshingly easy! There were no major gotchas, roadblocks, or points where we needed to change the design. For the most part, development never works this way. I was simply lucky that I was leveraging an excellent existing library for most of the work.

You can find the full implementation of the project in the embeddedartistry/arduino-printf repository.

Next, we’ll leverage the arduino-printf library to build a our first version of the Arduino logging library.

Further Reading

Share Your Thoughts

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