Designing a HAL in C++
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:
- Define a base class which will act as your interface. Name it something like
GpioBase
. Define virtual methods for the interface, such asvoid set(uint8_t value)
anduint8_t get()
. - The generic parts of your app will get passed a pointer to a
GpioBase
object. - Create derived classes which implement the interface for specific platforms, e.g.
GpioReal
andGpioFake
. - Create the appropriate derived classes for the platforms you are targeting in
main()
. - Pass these into the generic part of your app, as a
GpioBase*
.
Let’s start by defining a base class:
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:
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:
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.
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:
This gives us the following output:
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.
Then in main()
, we can create a Hal
object, and pass it into the App:
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:
- 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. - 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:
Let’s now create a derived class for a real GPIO:
And a similarly derived class for a fake GPIO:
We can demonstrate this with the following main()
function below. C++17 or later is required for template parameter deduction.
This gives us the following output:
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):
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.
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
:
And now GpioFake
:
And an App
class which doesn’t care if it’s using a real or fake GPIO:
And finally a main.cpp
which demonstrates the use of the App
class with both GpioReal
and GpioFake
:
Running main produces the following output:
See the working example here.
Concepts with No Inheritance
Now let’s use concepts again, but this time without inheritance.
Then define a real derived class:
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:
And our main.cpp
looks very similar to the others:
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
-
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
-
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. ↩
-
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. ↩
-
Matt Oswalt (2021, Jun 22). Polymorphism in Rust [blog post]. Retrieved 2024-12-04, from https://oswalt.dev/2021/06/polymorphism-in-rust/. ↩