Writing Hardware Abstraction Layers (HALs) in C
Jacob Beningo | May 19, 2023
Hardware abstraction layers (HALs) are an important layer to every embedded software application. A HAL allows a developer to abstract or decouple the hardware details from the application code. Decoupling the hardware removes the application's dependency on the hardware, which means it is in a perfect position to be written and tested off target, or in other words, on the host. Developers can then simulate, emulate, and test the application much faster, removing bugs, getting to market faster, and decreasing overall development costs. Let's explore how embedded developers can design and use HALs written in C.
It's relatively common to find embedded application modules that directly access hardware. While this makes writing the application simpler, it's also a poor programming practice because the application becomes tightly coupled to the hardware. You might think this isn't a big deal—after all, who really needs to run an application on more than one set of hardware or port the code? In that case, I’d direct you to everyone who suffered chip shortages recently and had to go back and not just redesign their hardware, but also rewrite all their software. There is a principle that many in object-oriented programming (OOP) know as the dependency inversion principle that can help solve this problem.
The dependency inversion principle states that "high-level modules should not depend on low-level modules, but both should depend on abstractions." The dependency inversion principle is often implemented in programming languages using interfaces or abstract classes. For example, if I were to write a digital input/output (dio) interface in C++ that supports a read and write function, it might look something like the following:
class dio_base {
public:
virtual ~dio_base() = default;
// Class methods
virtual void write(dioPort_t port, dioPin_t pin, dioState_t state) = 0;
virtual dioState_t read(dioPort_t port, dioPin_t pin) = 0;
}
For those of you familiar with C++, you can see that we are using virtual functions to define the interface, which requires us to provide a derived class that implements the details. With this type of abstract class, we can use dynamic polymorphism in our application.
From the code, it's difficult to see how the dependency has been inverted. Instead, let's look at a quick UML diagram. In the diagram below, an led_io module is dependent on a dio interface through dependency injection. When the led_io object is created, it is provided a pointer to the implementation for digital input/outputs. The implementation for any microcontrollers dio must also meet the dio interface that is defined by dio_base.
Looking at the UML class diagram above, you might be thinking that while this is great for designing an application in an OOP language like C++, this doesn't apply to C. However, you can in fact get this type of behavior in C that inverts the dependencies. There is a simple trick that can be used in C using structures.
First, design the interface. You can do this by just writing the function signatures that you believe the interface should support. For example, if you’ve decided that the interface should support initializing, writing, and reading the digital input/output, you could just list the functions to be something like the following:
void write(dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t read(dioPort_t const port, dioPin_t const pin);
Notice that this looks a lot like the functions that I defined earlier in my C++ abstract class, just without the virtual keyword and the pure abstract class definition (= 0).
Next, I can package these functions up into a typedef struct. The struct will act like a custom type that contains the entire dio interface. The initial code will look something like the following:
typedef struct {
void init (DioConfig_t const * const Config);
void write (dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t read (dioPort_t const port, dioPin_t const pin);
} dio_base;
The problem with the above code is that it will not compile. You can't include a function in a structure in C. However, you can include a function pointer! The last step is to convert the dio HAL functions in the struct to function pointers. The function can be converted by placing a * in front of the function name and then putting () around it. For example, the struct now becomes the following:
typedef struct {
void (*init) (DioConfig_t const * const Config);
void (*write) (dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t (*read) (dioPort_t const port, dioPin_t const pin);
} dio_base;
Let's now say that you want to use the Dio HAL in a led_io module. You could write a led init function that takes a pointer to the dio_base type. By doing so, you would be injecting the dependency and removing the dependency on the low-level hardware. The C code for the led init module would look something like the following:
void led_init(dio_base * const dioPtr, dioPort_t const portInit, dioPin_t const pinInit){
dio = dioPtr;
port = portInit;
pin = pinInit;
}
Internal to the led module, a developer can use the HAL interface without knowing anything about the hardware! For example, you could write to the dio peripheral in a led_toggle function as follows:
void led_toggle(void){
bool state = (dio->read(port, pin) == dio->HIGH) ? dio->LOW : dio->HIGH);
dio->write(port, pin, state};
}
The led code would be completely portable, reusable, and abstracted from the hardware. No real dependencies on hardware—just on the interface. At this point, you still need an implementation for the hardware that also implements the interface for the led code to be usable. To make this happen, you would implement a dio module with functions that match the interface signature. You would then assign those functions to the interface using C code similar to the following:
dio_base dio_hal = {
Dio_Init,
Dio_Write,
Dio_Read
}
The led module would then be initialized using something like the following:
led_init(dio_hal, PORTA, PIN15);
That's it! If you follow this process you can decouple your application code from the hardware through a series of hardware abstraction layers!
Hardware abstraction layers are a critical component that every embedded software developer needs to leverage to minimize coupling to hardware. We’ve explored a simple technique for defining an interface and implementing it in C. As it turns out, you don't need an OOP language like C++ to get the benefits of interfaces and abstraction layers. C has enough capabilities in it to make it happen. One point to keep in mind is that there is a little bit of cost in this technique from a performance and memory standpoint. You’ll most likely lose a function call worth of performance and enough memory to store the function pointers from your interfaces. At the end of the day, this small cost is well worth it!
More information about text formats