Skip to content

Designing a HAL in C++

Published On:
Dec 2, 2024
Last Updated:
Dec 16, 2024

Using C++ for an embedded system? Great! At some point you might find yourself needing (or wanting) to implement a HAL (Hardware Abstraction Layer) atop of your hardware peripherals (e.g. for the uninitiated, a HAL typically includes things like GPIO drivers, UART drivers, e.t.c.). Why? Perhaps there is not one provided by your MCU vendor (although this is rare these days), or perhaps the existing one doesn’t work well (unfortunately not as rare), or perhaps it doesn’t easily provide the ability to mock it for testing (very common). You might have specific requirements on what you want the HAL to be able to do. Vendor provided or framework provided HAL layers can be hard to test.

Objectives:

  • Provides an appropriate level of abstraction.
  • Must be easy to mock and run on Linux.
  • Must be easy to support different hardware platforms.
  • Must provide a nice developer experience.
  • Must be easy to understand.

We will use a simple GPIO HAL driver as our example when we look at different ways to implement a HAL in C++. We’ll be mostly focusing on ways that we can write the HAL so that we can run our firmware on both real hardware and on a Linux machine with the hardware mocked.

Swapping Out .cpp Files

One of the simplest ways to setup a HAL that you can mock is to define the interface in a header file, and then swap out the implementation in the .cpp files depending on whether you are building for the real hardware or for testing. For example, if you were using CMake, the CMakeLists.txt for the real hardware would include a different .cpp file than the CMakeLists.txt for the testing.

This method can also be done just as easily in C.

This method doesn’t work if you have function definitions in the header file, which are unavoidable if you are using templates.

Stub Generation

One way to mock hardware is to use a library like CppUMock (suitable for C++) or CMock (suitable for C) to generate mock functions.

CppUMock can work in tandem with some of the other methods described here, such as dynamic and static polymorphism. In these scenarios, CppUMock can be used to make sure specific functions were called, and with what parameters.

Inheritance and Virtual Methods

Inheritance and virtual methods allow us to implement runtime polymorphism as a way to create real and mock implementations of the HAL. The process goes like this:

  1. Define a base class which will act as your interface. Name it something like GpioBase. Define virtual methods for the interface, such as void set(uint8_t value) and uint8_t get().
  2. The generic parts of your app will get passed a pointer to a GpioBase object.
  3. Create derived classes which implement the interface for specific platforms, e.g. GpioReal and GpioFake.
  4. Create the appropriate derived classes for the platforms you are targeting in main().
  5. Pass these into the generic part of your app, as a GpioBase*.

Let’s start by defining a base class:

GpioBase.hpp
class GpioBase {
public:
virtual void set(uint8_t value) = 0;
};

Note the use of virtual and = 0. virtual means that we can override the method in derived classes, and = 0 means that method must be implemented in derived classes, i.e. no default implementation exists. Together they make what is known as an abstract class, a class that cannot be instantiated by itself, only derived classes can be instantiated.

Let’s now create a derived class for a real GPIO pin:

GpioReal.hpp
#include <SomeHardwareDependency.h>
#include "GpioBase.hpp"
class GpioReal : public GpioBase {
public:
void set(uint8_t value) override {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
// realHardwareGpioSet(value);
}
};

Note that in GpioReal we can include headers which are only valid when building for real hardware, and make calls to functions which actually set the GPIO pin (or you could modify the memory-mapped registers directly here, up to you!).

And a similarly derived class for a fake GPIO:

GpioFake.hpp
#include "GpioBase.hpp"
class GpioFake : public GpioBase {
public:
void set(uint8_t value) override {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};

In GpioFake, we don’t include/depend on any hardware specific headers, i.e. we want to be able to run this code on Linux. We could make set() just print like in the example here, or you could increment a counter to check that set() was indeed called (you could add counter as a member variable to GpioFake).

Now we can write an app class which doesn’t care if it’s using a real or fake GPIO. Do this we make it accept a reference to a GpioBase object rather than any specific implementation. Note that this means it can only call methods which are defined in GpioBase, but this is what we want, as this defines the interface.

App.hpp
#include "GpioBase.hpp"
class App {
public:
App(GpioBase &gpio) : gpio(gpio) {}
void run() {
gpio.set(1);
}
private:
GpioBase& gpio;
};

Our rudimentary App class just sets the GPIO pin high, but this enough to demonstrate the concept.

We can tie this all together with the following main() function below:

main.cpp
#include "App.hpp"
#include "GpioFake.hpp"
#include "GpioReal.hpp"
int main() {
// If we were in the real main()
{
GpioReal realGpio;
App app(realGpio);
app.run();
}
// If we were in the test main()
{
GpioFake fakeGpio;
App app(fakeGpio);
app.run();
}
return 0;
}

This gives us the following output:

virtual void GpioReal::set(uint8_t)() called with value: 1
virtual void GpioFake::set(uint8_t)() called with value: 1

For any real world largish project, passing many GPIO objects (and all the other HAL objects, such as ADCs, DACs, Timers, etc.) into the App as individual parameters is going to be cumbersome. Instead, we could create a Hal object that wraps all these, and pass a single Hal object into the App.

Hal.hpp
class Hal {
public:
GpioBase* gpio1;
GpioBase* gpio2;
GpioBase* gpio3;
GpioBase* gpio4;
GpioBase* gpio5;
Hal() : gpio1(nullptr), gpio2(nullptr), gpio3(nullptr), gpio4(nullptr), gpio5(nullptr) {}
};

Then in main(), we can create a Hal object, and pass it into the App:

virtual-methods/main.cpp

Static Polymorphism via Templates

One of the downsides to runtime polymorphism is that it can add additional CPU overhead. This is because of two reasons:

  1. The compiler needs to look up the correct virtual method to call at runtime, which it does using a vtable. The cost of this is essentially one level of indirection.
  2. Because of this indirection, the compiler cannot optimize the call to the method. This may be a more serious problem than the single level of indirection, as the compiler cannot inline the call or do other optimizations.

Static polymorphism aims to implement the lookup at compile time, rather than at runtime. It is sometimes called early binding.1 One way to do this is with templates and the CRTP (Curiously Recurring Template Pattern).

Let’s start by defining a base class:

templates/GpioBase.hpp
template <typename T>
class GpioBase {
public:
void set(uint8_t value) {
// This is where the magic happens
// NOTE Intellisense will not be able to give you any help on what properties the class T has,
// this is one of the disadvantages of template polymorphism.
static_cast<T*>(this)->set(value);
}
};

Let’s now create a derived class for a real GPIO:

templates/GpioReal.hpp
#pragma once
#include <cstdio>
#include "GpioBase.hpp"
namespace templates {
// We inherit from ourselves (sort of!). This is called CRTP (Curiously Recurring Template Pattern).
class GpioReal : public GpioBase<GpioReal> {
public:
void set(uint8_t value) {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};
} // namespace templates

And a similarly derived class for a fake GPIO:

templates/GpioFake.hpp
#include "GpioBase.hpp"
class GpioFake : public GpioBase<GpioFake> {
public:
void set(uint8_t value) {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};

We can demonstrate this with the following main() function below. C++17 or later is required for template parameter deduction.

templates/main.cpp
#include <cstdio>
#include "templates/App.hpp"
#include "templates/GpioFake.hpp"
#include "templates/GpioReal.hpp"
namespace tpl = templates;
namespace vm = virtual_methods;
int main() {
// If we were in the real main()
{
tpl::GpioReal realGpio;
tpl::App app(realGpio); // Template parameter deduction, no <GpioReal> needed!
app.run();
}
// If we were in the test main()
{
tpl::GpioFake fakeGpio;
tpl::App app(fakeGpio);
app.run();
}
return 0;
}

This gives us the following output:

void templates::GpioReal::set(uint8_t)() called with value: 1
void templates::GpioFake::set(uint8_t)() called with value: 1

Notice how we get the same polymorphism capabilities as we did with virtual methods, but these operations are performed at compile time and there should be no additional runtime overhead.

Because the casting of this to a pointer to T is going to be very common in a large base class, we can add a helper method called self() to make this easier (I saw this idea on StackOverflow2):

template <typename T>
class GpioBase {
public:
T& self() { return static_cast<T&>(*this); }
void set(uint8_t value) {
// This is where the magic happens
// NOTE Intellisense will not be able to give you any help on what properties the class T has,
// this is one of the disadvantages of template polymorphism.
self().set(value);
}
};

C++ Concepts

C++ concepts allow us to expand on the idea of using templates to implement static polymorphism. They allow us to enforce the derived classes implement the required methods, just as virtual my_func() = 0 enforces this for dynamic polymorphism.

Without concepts, in the above template example, the only thing the compiler will enforce is that the derived class has a set() method and tha it can be passed a uint8_t. If you happened to define the derived function as void set(int32_t value), it would still work fine. Likewise, if you added a return value and defined it as bool set(int32_t value), it would still be happy. You can start to see how the enforcement of the API of the derived class are loose. Concepts can help tighten this up!

Roger Booth shows a way of using concepts along with inheritance to provide better compiler errors. However, you do have to define the functions in the base class, even though you have already defined them in the concept, and will also have to define them in the derived classes.1 Thomas Sedlmair shows a method which doesn’t involve inheritance, and also gets around the hassle of having to define the functions in the base class.3 Will will compare both of the methods below.

Concepts with Inheritance

Let’s start by making our GpioBase class.

concepts/GpioBase.hpp
template <typename T>
concept Gpio = requires(T t, uint8_t value) {
{ t.set_impl(value) } -> std::same_as<void>;
};
// Empty base class, the concept will force the derived class to implement the set method
template <typename T>
class GpioBase {
public:
GpioBase() {
// This static assertion will fail if the derived class does not both:
// 1. Implement the set_impl() method
// 2. Inherit from GpioBase
static_assert(Gpio<T> && std::derived_from<T, GpioBase>);
}
void set(uint8_t value) {
static_cast<T&>(*this).set_impl(value);
}
};

The concept part of the above code is the important part in this example. It is used to enforce that the derived class implements the set_impl() method that takes a uint8_t as a parameter and returns nothing (void). This concept is then checked in the static_assert in the constructor of GpioBase.

Now let’s define the derived classes, GpioReal and GpioFake. Firstly, GpioReal:

GpioReal.hpp
#include "GpioBase.hpp"
class GpioReal : public GpioBase<GpioReal> {
public:
void set_impl(uint8_t value) {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};

And now GpioFake:

GpioFake.hpp
#include "GpioBase.hpp"
class GpioFake : public GpioBase<GpioFake> {
public:
void set_impl(uint8_t value) {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};

And an App class which doesn’t care if it’s using a real or fake GPIO:

App.hpp
#include "GpioBase.hpp"
template <typename T>
class App {
public:
App(GpioBase<T>& gpio) : gpio(gpio) {}
void run() {
gpio.set(1);
}
private:
GpioBase<T>& gpio;
};

And finally a main.cpp which demonstrates the use of the App class with both GpioReal and GpioFake:

main.cpp
int main() {
// Real main()
{
GpioReal realGpio;
App app(realGpio);
app.run();
}
// Test main()
{
GpioFake fakeGpio;
App app(fakeGpio);
app.run();
}
return 0;
}

Running main produces the following output:

void GpioReal::set_impl(uint8_t)() called with value: 1
void GpioFake::set_impl(uint8_t)() called with value: 1

See the working example here.

Concepts with No Inheritance

Now let’s use concepts again, but this time without inheritance.

concepts/GpioBase.hpp
template <typename T>
concept Gpio = requires(T t, uint8_t value) {
t.set(value);
};
// Empty base class, the concept will force the derived class to implement the set method
template <Gpio T>
class GpioBase : public T {};

Then define a real derived class:

concepts/GpioReal.hpp
#include "GpioBase.hpp"
class GpioRealImpl {
public:
void set(uint8_t value) {
printf("%s() called with value: %d\n", __PRETTY_FUNCTION__, value);
}
};
// This alias is important so we don't have to use GpioBase<GpioRealImpl> everywhere, and it means this
// concept will be checked even if it's not used anywhere.
using GpioReal = GpioBase<GpioRealImpl>;

Note that Impl is added to the end of the class name, because we don’t end up using that class in other code, we will use the alias GpioReal instead.

Our App.hpp can be identical to the one we used for the non-concept template example above:

concepts/App.hpp
#include "GpioBase.hpp"
template <typename T>
class App {
public:
App(GpioBase<T>& gpio) : gpio(gpio) {}
void run() {
gpio.set(1);
}
private:
GpioBase<T>& gpio;
};

And our main.cpp looks very similar to the others:

main.cpp
#include "GpioBase.hpp"
#include "GpioReal.hpp"
#include "GpioFake.hpp"
#include "App.hpp"
int main() {
// If we were in the real main()
{
GpioReal realGpio;
App app(realGpio);
app.run();
}
// If we were in the test main()
{
GpioFake fakeGpio;
App app(fakeGpio);
app.run();
}
return 0;
}

Summary

So we have looked at a number of different ways to implement a HAL in C++ for an embedded system. Dynamic polymorphism via inheritance and virtual methods feels the most natural and easiest to understand, however has the runtime burden of a vtable lookup. Whether or not that is a concern for your HAL implementation is something you must decide.

We can achieve static polymorphism via templates and the CRTP, which eliminates the runtime overhead of virtual methods. Given for a HAL on an embedded system all the types would be known at compile time, losing runtime polymorphism is not a big deal. The biggest downside is that the code feels a little more clunky to write, and you don’t get the automatic “you have forgot to implement this method” compile time error that virtual my_func() = 0 provides.

C++ concepts go some way to improving the developer experience of static polymorphism.

Some other languages like Rust default to compile time polymorphism (this is provided by Rust’s Traits and Generics). In Rust, the compile time polymorphism feels much more natural and terse than in C++ (Rust also provided dynamic polymorphism via Trait objects and the dyn keyword).4

Implementing a HAL you can easily mock on top of an existing HAL (e.g. any of the runtime or template polymorphism examples above, assuming the existing HAL does not support this) induces a maintenance burden. It’s worth keeping this in mind when you consider whether it is worth it. If you feel that this is not worth it, the swapping out of .cpp files method or using a method to override existing functions might be the best choice for you.

Further Reading

GuillaumeDua/cpp_legacy_inheritance_vs_std_variant.md is a good overview of standard dynamic polymorphism vs. static polymorphism via CRTP. It covers best practises for base classes such as making the constructor private and make the base class a friend of it’s derived class. It also covers the use of std::variant and std::visit to create containers which can hold any of the derived types.

Footnotes

  1. Roger Booth (2024, Feb 2). Using the CRTP and C++20 Concepts to Enforce Contracts for Static Polymorphism. Medium. Retrieved 2024-12-03, from https://medium.com/@rogerbooth/using-the-crtp-and-c-20-concepts-to-enforce-contracts-for-static-polymorphism-a27d93111a75 2 3

  2. StackOverflow (2021, Jan 11). Confusion about CRTP static polymorphism [forum post]. Retrieved 2024-12-04, from https://stackoverflow.com/questions/43821541/confusion-about-crtp-static-polymorphism.

  3. Thomas Sedlmair (2024, Sep 24). [C++] Static, Dynamic Polymorphism, CRTP and C++20’s Concepts. Coding with Thomas. Retrieved 2024-12-03, from https://www.codingwiththomas.com/blog/c-static-dynamic-polymorphism-crtp-and-c20s-concepts.

  4. Matt Oswalt (2021, Jun 22). Polymorphism in Rust [blog post]. Retrieved 2024-12-04, from https://oswalt.dev/2021/06/polymorphism-in-rust/.