-
-
Notifications
You must be signed in to change notification settings - Fork 952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better hardware abstraction using C++20 concepts #1387
base: main
Are you sure you want to change the base?
Conversation
…epts features of C++20 for Spi, SpiMaster and SpiNorFlash. The 'interface' of the drivers are defined in `Pinetime::Drivers::Interface::Spi`, `Pinetime::Drivers::Interface::SpiMaster` and `Pinetime::Drivers::Interface::SpiNorFlash`. The actual implementation will be injected via the template parameter `T`. T is required to conform to the corresponding concepts `IsSpi`, `IsSpiMaster` and `IsFlashMemory`. This implementation should be practically free in terms of memory and cpu resource usage, but it provides way to implement multiple variations of the same driver. It also describes very clearly the interface of the drivers thanks to the concepts. This will allow us to support more easily other hardwares : different SoC, different sensors and memory chips. It'll also make the implemenation of InfiniSim much easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the usage of the impl
reference, as I was wary of the compile time overhead if so much code must be in the headers as templates require. But with the impl
reference this should be ok as the heavy lifting is in the impl.cpp file
uint8_t pinMOSI; | ||
uint8_t pinMISO; | ||
template <typename T> | ||
concept IsSpiMaster = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(outsider suggestion) (ditto for other places) Concepts should be named like adjectives rather than predicates, i.e. SpiMaster
rather than IsSpiMaster
. This convention is established in the standard (same_as
, derived_from
, destructible
, regular
, ...) and is in line with how other languages treat traits and trait-like features
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with this. However... this rule is not easy to apply here :
- in drivers/SpiMaster.h, there's
Pinetime::Drivers::IsSpiMaster
, the concept definition - in drivers/SpiMaster.h, there's also
Pinetime::Drivers::Interface::SpiMaster
, the actual driver, that owns animpl
of typeIsSpiMaster
. - in port/infinitime.h, we declare
PineTime::Drivers::SpiMaster
, the type that will be used in the rest of the code.
Renaming the concept from IsSpiMaster
to SpiMaster
will conflict with SpiMaster
in InfiniTime.h. I'm open to suggestions regarding the naming of the types and namespaces, though!
…ate types name with more explicit names
In theory (and according to my experiments with compiler explorer), those calls to impl.xxx() should be optimized by the compiler, since the type of impl is known at compile time. Looks like the new cmake Unity build feature does not like some of those changes... |
SpiNorFlash : use the generic SPI driver instead of the NRF52 one.
I think this PR is ready for review. In this PR, I added a new
The goals of these changes are:
The current naming of the classes / namespaces are: namespace Pinetime {
namespace Drivers {
template <typename DeviceImpl>
concept IsDevice = { /* ... */ };
namespace Interface {
template <class T>
requires IsDevice<T>
class Device {
public:
explicit Device(T& impl) : impl {impl} {}
/* ... */
private:
T& impl;
};
}
using Device = Interface::Device<ActualDevice>;
}
} Basically:
TODO:
|
It seems to me like there's no framework to choose the drivers yet, unless I'm missing something. How would you make two drivers selectable with CMake? |
The selection of the driver can be done by editing files in I implemented this mecanism in InfiniSim in this PR. See this file, for example I think, for example, that some Colmi P8 are using another touch panel. The file
This is done using the variable In the future, I think we could also move the initialization of the objects (that are currently created and initialized in the global scope in main.cpp) so that we can apply different parameter values to the constructors of the driver (ex : to apply a different display mirroring). But I figured that this could be done in a future PR, once this design is approved. |
What about main.cpp, where the touchPanel is hard coded to be constructed with the Cst816S implementation? |
Yes, right. I didn't encounter this issue in InfiniSim since it has its own main.cpp file, that is distinct from the one in InfiniTime. |
@Riksu9000 Unless there's any doubt that this design is correct and that we'll be able to actually support multiple hardware with it ? |
Reading the linked discussion it seems like avoiding runtime polymorphism might not make a big difference, but it makes the architecture more confusing. I don't really understand why it wouldn't get optimized without the proxy, but I think it could be worth the tradeoff to make the code significantly simpler. |
This abstraction scheme as-is is unable to handle multiple differing implementations of the same concept being needed at runtime, for example if trying to port to a device that has two different SPI flashes on board, or (if this is extended to GPIOs), if some things are wired up to GPIOs from the SoC and some are going through an I2C GPIO expander chip. Handling these cases would require either explicitly templating the user on the peripheral type, or introducing virtual functions. |
This is an interesting discussion! Let me (try to) explain the goals of this new design. The first goal of this design is not optimizations. Instead, it's more about "semantics" : I want the design to be as close as the hardware as possible. InfiniTime is a firmware for smartwatches. To the InfiniTime point of view, a smartwatch is a device with a display, a touchpanel, a motion sensor, a heart rate sensor, a button, an external memory,... This list of peripherals is know and does not change : every PineTime has the exact same hardware. So the idea is to provide a design that explicitly builds one and only one instance of each driver : the one that will be needed at runtime for a specific hardware. Also, the concept brings a nice way to describe the interface of a driver, in my opinion. In the end, I wanted this design to be as close as what we currently have in InfiniTime : everything is statically resolved and allocated. BUT... you said that this new implementation is confusion, and that is certainly something I would like to avoid so I guess I still need to work a bit on that. Using dynamic polymorphism and trust the compiler to optimize everything nicely might actually work! This is something I need to think about and experiment (compare).
Is this really an issue? Up to now, I've I've never seen a smartwatch with multiple memory cheap (let alone 2 different ones), or multiple displays, multiple HR sensors,... Do you think there are reasonable reason to need to instantiate 2 different implementations of the same "category" ? |
To be fair, in most cases I can't think of any applicable to InfiniTime, apart from potentially the aforementioned GPIO expanders vs SoC GPIO banks. I thought it still might be worth mentioning. |
I tend to agree that it's unlikely to be an issue for InfiniTime any time soon. |
Thanks everyone for your feedback on this PR. I'll think a bit more about this new hardware abstraction since the additional complexity brought by the new design might not be needed after all. |
This PR improves the hardware abstraction by integrating the Concepts features of C++20 for Spi, SpiMaster and SpiNorFlash.
My goal is to provide a design that abstracts the hardware nicely and clearly. I want to avoid preprocessor (
#ifdef
), CMake black magic and code generation as much as possible. I also want the abstraction to have the lowest possible overhead : most if not all of the abstraction work should be done at compile time. And I think that new features from C++20 will make this possible.The 'interface' of the drivers are defined in
Pinetime::Drivers::Interface::Spi
,Pinetime::Drivers::Interface::SpiMaster
andPinetime::Drivers::Interface::SpiNorFlash
. The actual implementation will be injected via the template parameterT
. T is required to conform to the corresponding conceptsIsSpi
,IsSpiMaster
andIsFlashMemory
.This implementation should be practically free in terms of memory and cpu resource usage, but it provides way to implement multiple variations of the same driver. It also describes very clearly the interface of the drivers thanks to the concepts.
This will allow us to support more easily other hardware : different SoC, different sensors and memory chips. It'll also make the implementation of InfiniSim much easier.
Note that the C++ standard was upgraded from 14 to 20.
This is still a work in progress, other drivers must still be ported to this new design.
The corresponding PR for InfiniSim : InfiniTimeOrg/InfiniSim#73.
Here is a shorter version of the design in Compiler Explorer : https://godbolt.org/z/zTTPTPvEv
But, as this is the first time I'm using concepts, this PR is also a request for comments : what do you think of this "hardware abstraction" design?