I am not an embedded system expert, I'm starting to attempt to build some toys and things with Arduino and Arduino like. I also love C++ and try, as much as possible, to follow the ways of the new C++ incarnations.
Thanks for my Wife, Ana Sofia Mariz and Simon Brand for taking their time to read an early version of this article, giving suggestions and pointing out some mistakes.
Most of Arduino sketches have a basic life cycle, initialization and loop. The initialization is run exactly once at boot and loop is run an indefinitely as times go by.
Most components libraries follow a similar pattern, having an initialization in the form of a
begin() method and different controls. In normal C++ this would be achieved by a constructor, but that doesn't easily translate into this architecture.
The main issue here is that the initialization free function
setup() don't have a common context with the
loop() free function, apart from the global context.
In the "/usual development world/" globals are a kind of bad word. They are flagged in code reviews, or perhaps hidden inside those magical singleton patterns. But the fact that you're controlling a physical hardware that is unique is probably the best fit for the Singleton pattern or a global that I can think of.
But even accepting that this pattern is fine for this kind of application, I believe that the usual Singleton offers very little safety and control to a system. As the system gets complex over time, there could be multiple subsystems attempting to access the same set of hardware states and stepping on each other's foots is just a matter of time.
The way I figured out to solve those issues was by separating the different responsibilities of those device drivers classes. There are a few different things that the usual driver object are doing, definition, initialization and control.
By virtue of the design described here we can create a third one, access control, to those global variables. This can enable advanced requirements like multi-threading safety or transactional access.
The "definition" responsibility is to describe how a hardware piece is connected to the microprocessor. It holds the knowledge that is essential to the execution of the others responsibilities like initialing and controlling.
These definitions once used are useless since on most cases the values are remembered by the actual driver module instance. That makes it a good candidate for compile time only values, like template parameters.
In the standard C++ way this step is famously integrated with the "acquisition". Unfortunately in the Arduino's architecture this is a separated, at least on most device libraries, step. My hunch is that in some hardware environments there are special or "magical" initializations going on before
main is invoked and that means that the famous fiasco on the static initialization order, or lack of, on C++ can get in the way.
For that reason my architecture idea will delay initialization and acquisitions to be done on first use. That lazy initialization is similar to how many Singleton implementations solve the same problem on C++. This can have an impact on the first invocation of the
loop function, but can be easily avoided by simply adding an access on the initialization stage.
By making acquisition and initialization together we alleviate the burden of having separate locations for both. In fact this basically reverts our Arduino world to a modern C++ status quo where the controls are obtained, used and destroyed when finished.
Each chip usually have its own library, or libraries, to control it. The definition class will give access to a controller instance. This instance can either be a direct instance of the library original class.
In some cases an adapter might be a good idea to isolate bad usages from the final modules. See the part on access control. This adapter can be used to separate different aspects of the same hardware into different views.
This is not 100% necessary but the possibility is there. By having a central holder for the driver module instance the same class can have a customization point that allows easy access control. Like for instance concurrency control in processors that allow for multiple threads.
Other uses could be to manage different players interactions with displays, allowing each one to draw as if he is the only one accessing the hardware. All the reconciliation can be handled by the access control piece.
The idea goes like that, the device definition of the hardware is done by specializing a template class. This template class is simply a Singleton holder or a container that can have only a single instance. This container has to be trivially constructed. It will need to be "empty" by default, remember that we don't want any initialization what so ever before
Any access to the hardware will require getting the content of this definition. The first access to this object will create and execute any necessary initialization required. Subsequent access will always return the same instance.
Occasionally the returned object can be a wrapper around the original controller. That wrapper can be returned as value and it's lifetime can have some advanced access control baked in.
I am certain where I got that name, but as I'm terrible with naming anyway I will just keep it anyways.
This is my implementaion of this idea, the code is at git-hub. There's a sample blink project that uses the library. The whole think is using platformio and make files as a builder block.
The sample code is configured to work on the boards that I have available here:
Each board seems to have it's own set of limitations that limit what parts of the standard we can actually use. The ESP8266 toolkit is limited to an old GCC version that only supports C++11 and while the ATmega328P on the trinket does have an up-todate GCC it looks like there's limited stdlib support, for instance there were no
type_traits include file available.
The mocd lib is implemented under those limitations. For that reason there's a few naive implementations of some traits and no C++14 or C++17 language fature has been used.
Those are the nomenclature that I've tryed to use across this article and on the mocd lib. There might be some inconsistences laying around as this evolved as I was writing. If you find one that bothers you, please let me know, I don't mind corrections or suggestions, thank you.
© Victor Bogado da Silva Lins
Last edited by Victor Bogado, 2018-08-04 18:08:56