I always follow a decoupling approach when coding my software layer for any hardware boards. Because I believe, spending time on decoupling your firmware from the underlying hardware enables us portability and re-usability.
I thought I would take the example of one of the ESP32 board for two reasons,
It's cheap, readily available and opensource ( I don't accept that it is completely open-source, but we can argue on this in a separate post!).Setting up an ESP dev-board is pretty easy for any beginner who wants to get started with radio drivers.
In this example, I planned to keep the hardware interactions on top of single interface abstraction (UART transfer), this enables me to quickly migrate the driver from one platform to another.
Why UART you ask?
Well, I need to accept ESP-IDF is still in development and I am not fully convinced with their SPI interfaces, there are few issues popping up here and there. I don't want my communication to be at risk, but definitely SPI would have been my call if not UART.
Framing the problem:
I wanted to focus on ESP-IDF sdk as it is more standard operating mode, and I am ignoring the AT-firmware and arduino ide.
The typical implementation approach (taken on most projects we've encountered) would be to define a set of radio interfaces which talk directly to ESP32 device using existing APIs. The approach described above would work just fine until one of these scenarios forces a change:
Migrate to a new SDK (due to a processor change or underlying framework change).
Support multiple hardware revisions which have different driver configurations.
Support multiple driver instances on a single board
So at this point the driver will be ripped apart in order to support the new SDK or when new functional parameters are enabled. And believe me, with open-source platform this will be happening with every new SDK release, so it is always better to have an abstraction layer from the hardware.
Some times we get more ambitious and start defining generic APIs for each driver types. So, modules which need to interact with a given driver type will only know about and use the generic APIs, which is great until our driver performs more roles than what it is actually responsible for.
Let me take the following example for ESP32's UART,
Here, all we know about are the function pointers, not the details of the functions being pointed to.
Now we should take a step back and think about responsibility. What are the core responsibilities of an ESP32 radio driver?
Communication with the radio
Initialization of the radio
Adjusting radio settings
Sending and receiving data over the radio
So our driver doesn't need to be responsible for initializing the UART in ESP, changing UART's flow controls, setting bit ordering, or starting/stopping the UART driver.
All our radio driver needs to know about ESP's UART is, how to transfer data over the bus it's connected to. It would be reasonable for the driver documentation to say: "We need the UART to be initialized and operating in 115200 baud-rate and parity bit", leaving it to the application programmer to ensure these requirements are met.
What are the problems, if my driver takes control of a bit more configuration parameters?
For one, we've given the radio driver more control over the UART driver than it actually needs. So it's easy to misinterpret the availability of an API as it being appropriate to use in the current context.
If our drivers are reaching beyond their responsibilities and configuring the ESP's UART (or stopping/starting the UART peripheral), we can easily introduce problems into our system - especially if drivers start changing configurations without the application developer knowing his settings are being overridden.
Another major problem is, we are not decoupling anything. Our radio driver, for better or worse, is dependent upon the UART driver abstract interface definition.
I think I will stop here, I don't want to dump everything in a single article.
In my next post, I will talk about how I took the approach of designing the minimalist APIs for radio driver to send and receive data over the UART it's connected to. Handling the configurations, because somewhere in the program we still need to initialize the UART.
To put it all together,
I want you to take a step back and think about the APIs your drivers actually require from other modules.
Are you creating too many dependencies, or exposing too much information? Can you limit that exposure by requiring a minimal HAL?