What does it take to write a good Arduino library?

Arduino library should be easy to use, well documented, and covered with unit tests. How to do it?

3 years ago   •   17 min read

By Vladimír Záhradník
Image by Daan Lenaerts from Pixabay

Writing a decent library is not easy. Opinions on how such a library should look like, vary among developers. I think that a robust library should have detailed documentation, and developers should cover it with unit tests. Also, a decent library should hide away complexity and offer an interface, which is intuitive and easy to use.

I spent most of my years in software development, working on large projects. We built them on robust frameworks, and in most cases, we also wrote proper unit and integration tests. I believe that well-written tests can save you time, which you’d eventually spend on debugging and other activities.

I got my Arduino kit about a year and a half ago, and I understood the framework very quickly. The creators made Arduino with hobbyists and self-taught programmers in mind. On one side, it attracts a large community of people to electronics; on the other hand, the quality of the published code dramatically varies. When I needed some library for my Arduino projects, I often checked the code on GitHub, which was often unmaintained. Libraries, which were maintained, often didn’t have proper documentation. Most of them didn’t have unit tests.

Why should you test your code? Watch the following video.

In 1996, the maiden flight of the Ariane 5 launcher ended with a massive disaster caused by a programming error. Read the full story here.

Arduino is not that critical and expensive, but I still think that writing of tests is worth it. The downside is that it takes additional development time. I would say that each project is different, and you’ll need to decide whether the time to write the tests is worth it. In my opinion, you’ll greatly benefit from the tests if you implement some kind of a state machine. It is ubiquitous in Arduino and tests allow you to change the code with confidence. Also, Arduino libraries are usually tiny, only hundreds of lines of code, compared to thousands of lines in Android apps or elsewhere. To thoroughly test your library, you often need to write less than 10 test cases. For example, I was able to write unit tests for the relay library in less than three hours, and I was still learning to use the testing framework.

At this time, I work on a long-term Arduino project which implements a semi-automated gearbox. Imagine a steering wheel with two paddles mounted next to it.

Racing Wheel, courtesy of JSC electronics

When a driver presses a paddle on the right, Arduino will shift the gear up, and when he pushes a paddle on the left, Arduino will shift the lever down. It will also control the clutch. In racing, quick response matters in better times and potentially overall results.

To detect paddle presses, I initially used the OneButton Arduino library. However, it didn’t integrate well with my existing codebase, and also it didn’t broadcast events such that some button was released. These kinds of events are crucial to me because I need to know that some action has happened instantly. A delay of 50 to 100 milliseconds impacts latency vastly. Ultimately, I decided to write a custom library.


ObjectButton is an Arduino library for detecting common button actions from changes on GPIO input pins. This library should be easy to integrate with C++ objects, hence the name.

Callback function limitations

Most Arduino libraries allow you to hook a callback function, which gets called as soon as some action happens. The callback is usually implemented as a pointer to a function without input parameters.

extern "C" {
    typedef void (*callbackFunction)(void);

In practice, you’ll register a callback like this:

// Create a callback function for click event
void onClick() {
    Serial.println("A button was clicked");

// Register this function in a library
OneButton button = OneButton(INPUT_PIN, /* activeLow */ true);

The following approach works excellent for registering C-like functions, but not for C++ object member functions. It’s because all member functions have an implicit (first) parameter — this.

To circumvent the limitation, you could:

  • Pass the address of the static member function, which will call the member function running on the specific object
  • Create some C function outside of any object, which will call the member function on the object
LedController controller = LedController();

void onClick() {

Note: Lambda / anonymous functions are as far as I know unavailable in Arduino. Arduino doesn’t support the C++ standard completely.


Instead of passing a reference to the callback function, ObjectButton introduces Listeners. Such an approach is used heavily in Android. We define a listener as an abstract class. The listener defines a contract between our library and the code written by a user.

For example, the listener to receive onClick events is defined like this:

class IOnClickListener {
    virtual ~IOnClickListener() = default;

    virtual void onClick(ObjectButton &button) = 0;

A user writes a C++ class, which inherits from this abstract class. You need to implement all virtual functions. When you register such a class in the library, internal code in the library can safely call the onClick(...) function.

#include <ObjectButton.h>
#include <interfaces/IOnClickListener.h>

class OnClickListener : private virtual IOnClickListener {
    OnClickListener() = default;

    void onClick(ObjectButton &button) override;

void OnClickListener::onClick(ObjectButton &button) {
  // React to button click

You will register a listener with the library like this:

// Create an ObjectButton instance
ObjectButton button = ObjectButton(...);

OnClickListener onClickListener = OnClickListener();

How to structure your code

When you write your own Arduino library, it’s a whole new experience to writing a normal Arduino project. This time you write the code that somebody else uses in his project. In a library, there’s no setup() and loop() function to implement. Arduino gives you the guidelines on how to structure the code. Some of them are mandatory, others are optional, but recommended. Your goal should be to make your library easy-to-use for everyone, even beginners. For instance, make sure you have some kind of begin() function, which initializes your code. You often see this pattern in Arduino.

Let’s assume following about your users:

  • They may have no prior programming experience
  • They do not understand pointers
  • They didn’t grasp what a constructor and destructor is
  • They are familiar with begin() and end() functions from other libraries
  • They expects function names to be camelCase, i.e. getPressure()

One of your main goals should be to hide all complexity away and to provide many example uses of your library. Arduino provides several reference guides; you should at least check these:

After you know your users and recommended guidelines of the Arduino community, it’s time to define some targets to meet.

Goals to achieve

As you can see, unit tests coverage and documentation are just some of the goals to meet.

High-level abstraction

When you try to come up with a decent library design, start with a pen and paper. As an inspiration, look at the official Arduino libraries. They are all well-written. I’ll briefly mention the Servo library. Imagine you want to control a servo. Which functions you’d like to have? You will probably need some function to specify an output pin, on which you connect your servo. You will also need a function, which tells the servo how much it should turn itself and a function that reports the current position of your servo. And that’s pretty much all you need as a user. You don’t bother users with implementation details; they are all hidden.

In my case, I wanted to provide a way to register callbacks, a way to customize the debounce interval, and a way to adjust timeout values used to detect click, double-click, and long-press events. Most of the time, users will just register callback and won’t care about all the default values, but it’s a good practice to give users a way to customize the behavior. However, don’t expose all values. Instead, think of usage scenarios and decide what makes sense to expose. After I made my design decisions, typically, my library is initialized with 1 or 2 lines of code. Perfect! Feel free to check the examples of how to use the library.

Another thing you need to think of is how to structure the code with regards to testing. When you write the code, try to reason how to test it. Typically, you should avoid one large class. Instead, break your code into several smaller classes. Later, you can test them in isolation from the rest of your system. Their functions should be doing only one thing, and their name should give you a hint, what they supposed to do without reading the documentation. Under normal circumstances, these functions are short — less than ten lines of code. The code which implements state machine is usually huge. However, you can move a lot of the functionality into helper functions. When you do that, your state machine will provide a high-level picture of what it does. Here’s an example from my code to notify listeners about the occurrence of the click event.

Instead of having this code:

if (timeDelta > m_clickTicks) {
    m_state = State::BUTTON_NOT_PRESSED;
    if (m_onClickListener != nullptr)

You move the logic into another function, like this:

if (timeDelta > m_clickTicks) {
    m_state = State::BUTTON_NOT_PRESSED;

// factored-out internal logic
void ObjectButton::notifyOnClick() {
    if (m_onClickListener != nullptr)

If you look at the refactored code, you’ll see at first glance what it does. State machine code can have a lot of if ... else clauses; everything that makes it more readable is a good thing. Also, now you can write tests to ObjectButton::notifyOnClick(), which is a lot easier than writing tests that verify the same code in the core state machine code.

If you want to see an example of a more complicated library, which handles access rights, message parsing, and lots of other things, check out our recently published Adeon automation library. We tried hard to hide all the complexity, and we wrote detailed documentation.


So, you have a library which does what you intended. As a next step, document your code.

There are two approaches:

  • Write documentation to your code separately. Take a look at the Sphinx library to see the examples. Sphinx uses reStructuredText as a markup language.
  • Write documentation inside your code. Such an approach is used heavily in the Java world and its Javadoc documentation generator.

My suggestion is to combine both approaches. Start with documenting what your library does. Include flowcharts, diagrams, anything that is helpful. Markdown serves excellent for this purpose. Moreover, you can put your documents into the same repository as your main code. For example, you could create a docs folder to store these documents. If you use GitHub or GitLab, you can save the documents inside Wiki. Technically, it’s a separate git repository, but it’s bound to your project, and it’s easily accessible.

GitHub Wiki for ObjectButton

After you wrote your high-level overview of the library, it’s time to write comments inside your code. I decided to use Doxygen: it’s a de facto standard, and it’s easy to setup. Also, it supports several styles of comments, including the Javadoc style.


If you want to generate Doxygen documentation, you’ll need to create a Doxyfile. It’s a template for Doxygen with various settings, e.g., your project name, location of the source code, etc. I was lucky to find a Doxyfile, which somebody adapted for Arduino. One of the vital settings was the inclusion of the *.ino files. Doxygen usually doesn’t know that these files contain valid C/C++ code. If you ever need this template, just grab mine and change project name and path.

If you want to know the details about how to add comments in the code, it’s best to check the Doxygen webpage.

Here is an example of comment inside the code:

  * Pointer to object listening to click events. If event listener is not set,
  * such event won't be broadcast.
  * @see setOnClickListener(IOnClickListener *listener)
IOnClickListener *m_onClickListener = nullptr;

 * Pointer to object listening to double-click events. If event listener is not set,
 * such event won't be broadcast.
 * @see setOnDoubleClickListener(IOnDoubleClickListener *listener)
IOnDoubleClickListener *m_onDoubleClickListener = nullptr;

Javadoc comment starts with /**, which is still a valid C++ comment. The alternative syntax looks like this: ///. Also, notice flags like @see. These will help you to generate hyperlinks to other classes and functions.

After you’ve documented your code, generating the documentation is simple. Your Doxyfile has all the information.

doxygen <path to doxyfile>

You can put generated documentation anywhere you like. It’s a static HTML page with no external dependencies. I host the documentation on GitHub Pages for free. For inspiration, generated documentation for one of my projects is here.


Great, you have decent documentation now. Your library is now in better shape than maybe 80-percent of the libraries. As a next step, create the keywords.txt file. Arduino IDE has minimal syntax highlighting capabilities. Therefore, we need to write up all functions, classes, and variables in this file and assign a proper type. Only when Arduino IDE matches a keyword with your definition, it will highlight the code. Creating the keywords.txt is mandatory if you want your library included in the official Arduino library registry.

# Datatypes (KEYWORD1)

IOnClickListener    KEYWORD1
IOnDoubleClickListener  KEYWORD1
IOnPressListener    KEYWORD1

# Methods and Functions (KEYWORD2)

getId   KEYWORD2
setOnClickListener  KEYWORD2
setOnDoubleClickListener    KEYWORD2

As you can see, you list all your public data types, functions, even classes, and then you assign one of the defined keywords, like KEYWORD1. Each keyword corresponds to a different color inside the Arduino IDE. When you need to create this file, check the documentation or other libraries out there.


Lastly, you need to write up some examples of how to use your library. Examples are optional, but they are very beneficial for the users. It’s often the first thing people look for when they search for a library. You can access them directly from Arduino IDE. Think of some basic tasks on how people will use the library and also try to write at least one advanced example. The more examples you write, the better for your users.

All examples are Arduino project files (*.ino), which you can compile from Arduino IDE. If you worked with Arduino, I’m sure you’re more than familiar.

For inspiration, take a look at the examples from my library.

Automated testing

Now you have a library, which is well documented and supposedly easy to use. As a next step, write some unit tests to verify that your code does what it supposes to do. Automated testing is still not typical for Arduino libraries, and I’m not surprised. If we look into other programming environments, they often come with an official testing framework or at least a well-known third-party framework. In Java and Android, it’s JUnit; Python has a built-in module, unittest, sometimes people also use pytest; the C++ world uses Googletest. Arduino provides no official testing framework, and you won’t find many articles covering the subject.

Manual testing everywhere

Most of the Arduino developers test their code manually. When the code somewhat works, it’s considered complete. If they break the code in future updates, they may not notice this for a long time. Not a good thing!

Another use case is verification that your code works on multiple hardware boards. If you test it manually on Arduino Uno, how do you know that your library also works on Leonardo? If you verify your code manually on several platforms, you’ll lose time. If you don’t check all supported platforms, you have no guarantee the code will work on all boards.

Compiling library for different boards

Hopefully, you’ll agree that automated compilation for several boards makes sense. It is trivial to set up, and therefore I think every Arduino library should do it!

You have several options, how to accomplish this:

PlatformIO approach

PlatformIO projects always have a platformio.ini file. There you define whole project configuration, including platforms for which you build the binaries. Each platform represents a different Arduino board or even a different CPU architecture.

; Arduino Uno: First target board
platform = atmelavr
board = uno
framework = arduino
; Arduino Mega: Second target board
platform = atmelavr
board = megaatmega2560
framework = arduino

When you want to build a code for multiple target platforms, just enter the following commands:

# pio run -e <target environment>
pio run -e uno
pio run -e megaatmega2560

You can add these commands into a shell script, which you can run repeatedly. In a matter of seconds, you’ll know that your code compiles or that there’s an error.

Examples build approach

The other two mentioned options require you to create example sketches to demonstrate how to use your library. You should put these examples inside the examples folder, as defined in the Arduino library specification. When you run the script, it will go through all your examples and compile them for all listed platforms. When Arduino IDE builds your examples, it also makes your library. It’s their dependency.

Albeit writing examples is optional, every well-supported library should have at least one. And when you do, preparing your code to work with the build scripts is a matter of minutes. I see no reason not to use it!

Writing unit tests

Unit tests examine your library code in isolation. They are sometimes more challenging to write, but pros prevail over cons.

I found several third-party testing frameworks to use with Arduino:

Each of these frameworks supports running tests on native machines, which means that you can run the tests on a regular PC without Arduino board connected to it. It is beneficial, especially when you want to set up continuous integration on a remote server.

For ObjectButton, I chose ArduinoCI as a testing framework. But you can’t make the wrong choice here. Look at all the options and decide what’s best for you.

Why ArduinoCI?

ArduinoCI has a good overview of the testing frameworks. What got my attention is that it supports mocking of Arduino hardware. Imagine that you want to simulate a button click action. How do you do that without clicking a physical button? If you have control over input pins and internal timer, you can simulate the gesture programmatically. And that’s what ArduinoCI gives at our disposal.

Also, this framework compiles all tests into native code and supports continuous integration. The only thing I miss is the code coverage reports.

ArduinoCI unit tests run by Travis CI

Test Features

All your test files go into the test folder. You name them usually as the tested feature. ObjectButton should be able to recognize three types of distinct actions.

I created three test files to represent those and one test file to verify fundamental functionality:

  • action_click.cpp to run all unit tests related to button clicks
  • action_double_click.cpp to run unit tests related to double-clicks
  • action_press_release.cpp to run unit tests related to a button press, long press, etc.
  • sanity_checks.cpp to verify functions like getId to return proper button id

Each test file contains one or more unit tests related to the feature. You could write all the tests in one big file, and your tests would run just fine. However, if you split them into several test files by feature, you have fine-grained control over running a subset of tests. Also, if one of your tests fails, you should be able to locate it quicker.

Writing a unit test

Let’s go through a test, which verifies that our library correctly detects a click action.

To write a test for a click action, first, you need to understand what a click is. Did you ever think of that? Usually, when we use things, we don’t reason how precisely they work, but when we want to test a click action, we must define what a click is and how it will be detected. Then we can write a test to capture the behavior.

Definition: A click is an action, which occurs when a user presses a button and then releases the button. Press and release should happen before the click timeout elapses, and also button should not be touched again during that time (it would be possibly a double-click action).

Now when we’ve defined what a click is, we can simulate a click in our test:

  • A user presses the button
  • A user releases the button
  • Button press and release happened within defined click timeout

Below is a real example from my test suite:

unittest(receive_on_click_event_after_default_click_ticks) {
    ListenerMock testMock = ListenerMock(INPUT_PIN, true);
    GodmodeState* state = GODMODE();
    // press button
    state->digitalPin[INPUT_PIN] = LOW;
    // fire press event after debounce period elapses
    state->micros = (DEFAULT_DEBOUNCE_TICKS_MS + 1) * 1000;
    // release button
    state->digitalPin[INPUT_PIN] = HIGH;
    state->micros = (DEFAULT_CLICK_TICKS_MS + 1) * 1000;
    // refresh state machine to get click event
    // validate click event
    assertEqual(1, testMock.getPressEventsReceivedCount());
    assertEqual(1, testMock.getReleaseEventsReceivedCount());
    assertEqual(1, testMock.getClickEventsReceivedCount());
    assertEqual(0, testMock.getDoubleClickEventsReceivedCount());
    assertEqual(0, testMock.getLongPressStartEventsReceivedCount());
    assertEqual(0, testMock.getLongPressEndEventsReceivedCount());

State: A user pressed the button In this case, we consider a button as pressed when the voltage level on the input pin is 0 volts. Thanks to ArduinoCI, we can simulate that very easily:

state->digitalPin[INPUT_PIN] = LOW;

state is a pointer to ArduinoCI structure, which controls “virtual” Arduino running our test.

Now, we’ve pressed a button, and we need to release it after a particular time. If we just changed the input pin voltage, the internal timer would still return the same value — 0. Virtually no time has passed. To simulate time, we need to set micros, i.e.:

state->micros = (DEFAULT_DEBOUNCE_TICKS_MS + 1) * 1000;

Note that you control the timer by specifying time in microseconds, not milliseconds (as used by the millis() function).

State: A user released the button A user holds the button for some time; it’s time to release it.

state->digitalPin[INPUT_PIN] = HIGH;
state->micros = (DEFAULT_CLICK_TICKS_MS + 1) * 1000;

We’ve just set an input pin to 3.3 volts, which simulates releasing the button. If needed, you can also shift internal timer to simulate a real scenario.

Did the library detect the click action? Our unit test simulates precisely a click action. If our library works correctly, it will notify our OnClickListener about this action. We usually verify the results at the end of the test:

assertEqual(1, testMock.getClickEventsReceivedCount());

Above code checks, if ObjectButton sent precisely one notification about click event. To capture all events reported from my library, I created a helper listener registered for all events supported by the library. This listener increments an internal counter each time an event is received. It shows that you are not limited purely to a framework, but you can extend the framework to suit your specific needs.


Why it’s useful to have unit tests? It’s a confidence that my code does what it should do. Similarly to the previous test case, you write other tests. They should capture expected behavior — what you specified that your library should do — and verify that your library does that.

Writing tests is time-consuming, but running them is not. You can repeatedly run a test suite within seconds. It’s particularly useful when you work on a new feature, and you want to make sure that you didn’t affect existing functionality by introducing a bug. In the case of ObjectButton, all the internal logic happens in a state machine. Those of you, who implemented a state machine, know how tedious it can be. Sometimes it’s not clear at first glance what the code does. If you change the implementation of the state machine, there’s a high probability of introducing a bug. But if you have a working test suite, you can verify that everything still works, and you may always be confident about your code.

New feature == new tests

If you implement a new feature, which changes existing library behavior or adds new functionality, you should also write new unit tests or adapt existing ones. Maintaining your test suite should be a regular part of your development practice. Otherwise, over time your confidence level will sink just because you didn’t test new functionality.


In this post, I wrote about the necessary actions to produce quality Arduino libraries. These principles are also valid in your projects. You should still document what your code does, and you should at least consider writing some tests. You can run the test suite from the command line. Web developers are familiar with the workflow.

Even if you documented and tested your code, you still have some work to do. Next time I will explain how to automate your workflow and set up continuous integration for your repository to automatically run the tests and build the documentation after each commit.

If you have some questions or you have a feeling that I forgot about something, please let me know in the comments. Thanks!

Spread the word

Keep reading