0

When working on embedded devices (read: microcontrollers), you often write a driver for a peripheral such as an UART (aka. serial port). This driver may just be a thin HAL, locking the peripheral (to make it thread safe) and wrapping the calls to a C library (provided by the manufacturer of the microcontroller).

Further, a microcontroller often has multiple peripherals (physical interfaces) of the same type, i.e., UART0, UART1, UART2, etc.

That's how I'd represent them in C++:

#include <stdint.h>


class DevUart {

public:
    enum class interfaces_t {
        UART0 = 0,
        UART1 = 1,
        UART2 = 2,
        UART3 = 3,
        UART4 = 4
    };

    DevUart(interfaces_t interface) : Interface{interface}  {
        switch(interface) {
        case interfaces_t::UART0:
            HardwareRegister = (void *)0xE0001000;
            break;
            /* for UART1, etc. ... */
        }
    }
    void sendByte(uint8_t data) {
        /* lock Interface, send data, unlock Interface, etc. */ }

private:
    interfaces_t Interface;
    void * HardwareRegister;
};

To guarantee that the same driver instance is using a given peripheral, I use the following singleton:

class DevUartSingleton {
public:
    static DevUart & Instance(DevUart::interfaces_t interface) {
        static DevUart S{interface};
        return S;
    }

    DevUartSingleton() = delete;
    ~DevUartSingleton() = delete;
};

However, this singleton only supports for a single driver instance in the system, e.g., UART2:

DevUart & uart_driverC = DevUartSingleton::Instance(DevUart::interfaces_t::UART2);

If I wanted to also instantiate a driver for, e.g., UART0, I'd need a different solution.

Thus I'm looking for a solution to allow for creating multiple instances of that driver, but no more than a single driver for each physical interface. In other words, I'm looking for a singleton that returns different drivers for different interfaces but the same driver for the same interface. So that you can use the different peripherals, while guaranteeing that there will only ever be one driver instance that is representing the physical interface. I hope that makes sense to you.

Daniel K.
  • 919
  • 10
  • 17
  • 3
    IMHO everything that you are doing here is fighting to use an overcomplicated language. If you did all this in C then you would have finished the boiler plate ages ago and already be several days into working on your actual application. Every engineer has limited brain capacity. Why take up so much of it with all these lines of code that don't actually *do* anything? – Tom V May 02 '23 at 16:23
  • Not really a singleton, then. If you wrap the device drivers in singletons and then have a factory-of-sorts deliver those, you'll have what you want. But that's probably not as clean as a function that just creates and remembers. Maybe throw the created interfaces into an `unodrered_map`, maybe `static` variables in a `switch` if the situation's not that dynamic, Maybe you create all possible interfaces at start-up and dole out references on request. Tom's half right. You're not struggling with the language, you're struggling with trying to bash something into fitting the wrong design pattern – user4581301 May 02 '23 at 16:59
  • 1
    It seems you want a map between `DevUart::interfaces_t` and the corresponding singleton instance. One way to do this would be to template the singleton function on `DevUart::interfaces_t`. You also don’t need the separate struct just to do the singleton pattern. If you really want to restrict construction, make `DevUart:: DevUart` private then have a `template static DevUart& instance() { static DevUart inst{Instance}; return inst; }`. Then `DevUart::instance()` will give you the only possible UART0 instance. – Ben May 03 '23 at 00:37
  • @TomV is absolutely correct. In theory C++ can be used just fine for microcontroller programming, but in practice it requires _extreme_ discipline not to use the flood of more or less misguided features offered by the language. Alas some 95% of all C++ does not have the experience and discipline to tell what's useful, what's pointless fluff and what's actively harmful. So it is best to avoid C++. Also there's the severe issue of eternal loops in C++ that have turned broken by design due to incompetence in the ISO WG: https://stackoverflow.com/a/75173813/584518 – Lundin May 03 '23 at 07:34
  • More differences between C and C++ with examples where C++ is problematic beyond the main "feature bloat/poorly defined behavior overflow" arguments against C++: https://stackoverflow.com/a/75171239/584518 – Lundin May 03 '23 at 07:36

2 Answers2

1

This is a pretty classic problem in microcontroller programming and there's some "de facto standard" ways of implementing such drivers.

You have a register map somewhere and in that register map the first register of the UART peripheral has an address. Also all registers belonging to that one UART are very likely grouped together, and at the same address offset in relation to the first register, no matter which UART you are using.

So in the header file of your UART class you can do something like:

#define UART0  UART0_RX // name of first UART register for UART 0
#define UART1  UART1_RX // name of first UART register for UART 1
...

Inside the class, also create a list of all registers which this UART uses:

private:

typedef enum
{
  // some register names corresponding to your register map, in same order:
  UART_RX,
  UART_TX,
  ...
};

(In case of gaps in the register map, add dummy items like UART_DUMMY0, UART_DUMMY1.)

Then pass that as a parameter to your constructor (or equivalent init function). Assuming 32 bit registers then:

 private:
   volatile uint32_t* base;

 DevUart(volatile uint32_t* uart) : base(uart)
 {}

 ...

 void SendSomething (void) // some member function using the registers
 {
   // here do pointer arithmetic on the hardware registers starting at the first
   // the enum acts as offset for that pointer arithmetic
   base[UART_TX] = something;
   ...
 }

Note: this is no longer a singleton! Gone is the ugly switch and you can have one instance of the class per UART. Usage:

static DevUart uart0(UART0);
static DevUart uart1(UART1);
...
Lundin
  • 195,001
  • 40
  • 254
  • 396
1

This is the solution I'm using (which follows along the comment made by Ben).

Sticking to the idea of using a singleton, there has to be a singleton for each entry in DevUart::interfaces_t. This can be achieved by templating the singleton on this enum class. Further, there's no need to use a separate singleton class or struct. A function is just fine.

template<DevUart::interfaces_t interface>
auto & getUartInstance() {
    static DevUart instance(interface);
    return instance;
}

This is a "lazy initialization of static variable with local scope", see, e.g., here for more details.

You instantiate the driver as follows:

    DevUart & uart_driverX = getUartInstance<DevUart::interfaces_t::UART2>();
    DevUart & uart_driverY = getUartInstance<DevUart::interfaces_t::UART2>();
    DevUart & uart_driverZ = getUartInstance<DevUart::interfaces_t::UART0>();

    ASSERT(&uart_driverX == &uart_driverY);
    ASSERT(&uart_driverY != &uart_driverZ);

The asserts (or the debugger) will show that

  • for each entry in DevUart::interfaces_t, a separate singleton will be used and
  • for the same DevUart::interfaces_t, the same singleton will be instantiated and thus the same driver referred to.

Note: Users of this solution should check that their compiler is not creating separate singletons per compilation unit. This can be done by instantiation, e.g., DevUart & uart_driverX = getUartInstance<DevUart::interfaces_t::UART2>() in different compilation units and checking the returned reference.

Access control: For additional access control, one can make the constructor of class DevUart private and the singleton a friend function:

class DevUart {
  /* ... */
private:
    DevUart(interfaces_t interface) : Interface{interface}  {
     // implementation
    }
public:
    template<DevUart::interfaces_t interface>
    friend auto & getUartInstance();
}
Daniel K.
  • 919
  • 10
  • 17
  • Glad you like it! Personally, I'd make the getter a `static` member function, and I'd lean toward making `interfaces_t` be just an `enum` (not an `enum class`) so the caller doesn't have to say `interfaces_t`. Then usage would be `auto& uart = DevUart::instance();`. Also, I want to explicitly mention: This all works because `instance` is a template4 so `DevUart::instance()` is a different function from `DevUart::instance()`, etc., so `static` local variables within them are distinct. – Ben May 03 '23 at 12:06
  • @Ben, I was rather worried that the template mechanism would generate **different** static local variables for the **same** `DevUart::interfaces_t` when instantiated in [different compilation units](https://stackoverflow.com/questions/738933/multiple-singleton-instances). However, this does not seem the case (at least with gcc). – Daniel K. May 03 '23 at 12:59
  • 1
    You also could do it with runtime arguments, but you need to statically know all available options. But you could do `static instance(interfaces_t iface) { switch (iface) { case interfaces_t::UART0: { static auto inst = DevUart(iface); return inst; } case interfaces_t::UART1: { static auto inst = ... } ... } throw /*invalid interface*/ }`. – Ben May 03 '23 at 13:00
  • all templates are implicitly `inline` so the linker consolidates duplicates across translation units. – Ben May 03 '23 at 13:59