InfiniTimeOrg / InfiniTime

Firmware for Pinetime smartwatch written in C++ and based on FreeRTOS
GNU General Public License v3.0
2.7k stars 923 forks source link

Better hardware abstraction using C++20 concepts #1387

Open JF002 opened 1 year ago

JF002 commented 1 year ago

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 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 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 : https://github.com/InfiniTimeOrg/InfiniSim/pull/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?

JF002 commented 1 year ago

@NeroBurner

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

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...

JF002 commented 1 year ago

I think this PR is ready for review.

In this PR, I added a new concept and proxy object for the following low-level drivers:

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:

Riksu9000 commented 1 year ago

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?

JF002 commented 1 year ago

It seems to me like there's no framework to choose the drivers yet, unless I'm missing something.

The selection of the driver can be done by editing files in src/port (should I name this folder ports?). Currently, InfiniTime only supports the PineTime, so this is the only option available. There are a few variant that are supported (MOY-TFK5 MOY-TIN5 MOY-TON5 MOY-UNK) but they only need different clock settings, which was not in the scope of this PR.

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 src/port/TouchPanel.h could be edited this way:

#pragma once
#include "drivers/TouchPanel.h"

#ifdef TARGET_DEVICE_PINETIME
  #include <drivers/touchpanels/Cst816s.h>
#endif

namespace Pinetime {
  namespace Drivers {
#ifdef TARGET_DEVICE_PINETIME
    using TouchPanel = Interface::Touchpanel<Pinetime::Drivers::TouchPanels::Cst816S>;
#elif TARGET_DEVICE_COLMI_P8
    using TouchPanel = Interface::Touchpanel<Pinetime::Drivers::TouchPanels::TheOtherTouchPanelDriver>;
#else
  #error "No target device specified!"
#endif
  }
}

How would you make two drivers selectable with CMake?

This is done using the variable TARGET_DEVICE, which is set to PINETIME by default. We could probably avoid using #ifdefs here and include only the "port" file specific to the hardware target (by splitting port file by target).

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.

Riksu9000 commented 1 year ago

What about main.cpp, where the touchPanel is hard coded to be constructed with the Cst816S implementation?

JF002 commented 1 year ago

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. To solve this in InfiniTime, we could move the initialization of the driver objects into another file, specific to the target, or even provide 1 main.cpp file per target. This would give more freedom to make custom initialization on a target basis.

JF002 commented 1 year ago

@Riksu9000 May I suggest that we limit the scope of this PR to the re-design of the low-level hardware drivers while keeping exactly the same functionalities that the current project? I think we can work on actually choosing the drivers and implement support for other variants based on this design in a future PR (which I plan to work on, obviously) ?

Unless there's any doubt that this design is correct and that we'll be able to actually support multiple hardware with it ?

Riksu9000 commented 1 year ago

https://stackoverflow.com/questions/21152018/is-it-worth-it-to-avoid-polymorphism-in-order-to-gain-performance

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.

qookei commented 1 year ago

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.

JF002 commented 1 year ago

@Riksu9000

Reading the linked discussion it seems like avoiding runtime polymorphism might not make a big difference, but it makes the architecture more confusing

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. Also, the kind of smartwatch targeted by InfiniTime is based on a very memory and resource constraint hardware : low power cpu, low RAM, low FLASH.

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).

@qookei

This abstraction scheme as-is is unable to handle multiple differing implementations of the same concept being needed at runtime

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" ?

qookei commented 1 year ago

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.

Avamander commented 1 year ago

Is this really an issue? Up to [...]

I tend to agree that it's unlikely to be an issue for InfiniTime any time soon.

JF002 commented 1 year ago

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.