1

In embedded systems, you often have a memory location which is not within the program memory itself but which points to some hardware registers. Most C SDKs provide these as #define statements. According to the following article, https://arne-mertz.de/2017/06/stepping-away-from-define/ one method of transitioning from #define statements (as used by C SDKs) to something more C++ friendly, is to create a class which forces reinterpret_cast to occur at runtime.

I am trying to go about this in a slightly different way because I want to be able to create "type traits" for the different pointers. Let me illustrate with an example.

#define USART1_ADDR 0x1234
#define USART2_ADDR 0x5678

template <typename T_, std::intptr_t ADDR_>
class MemPointer {
public:
    static T_& ref() { return *reinterpret_cast<T_*>(ADDR_); }
};

class USART {
public:
    void foo() { _registerA = 0x10; }

private:
    uint32_t _registerA;
    uint32_t _registerB;
};

using USART1 = MemPointer<USART, USART1_ADDR>;
using USART2 = MemPointer<USART, USART2_ADDR>;

template <typename USART_>
class usart_name;

template <>
class usart_name<USART1> {
public:
    static constexpr const char* name() { return "USART1"; }
};

template <>
class usart_name<USART2> {
public:
    static constexpr const char* name() { return "USART2"; }
};

Each USART "instance" in this example is its own, unique type so that I am able to create traits which allow compile-time "lookup" of information about the USART instance.

This actually seems to work, however, I wanted to create some test code as follows

static USART testUsart;

#define TEST_USART_ADDR (std::intptr_t)(&testUsart);

using TEST_USART = MemPointer<USART, TEST_USART_ADDR>;

Which fails with the following error:

conversion from pointer type 'USART*' to arithmetic type 'intptr_t' {aka 'long long int'} in a constant expression

I believe I understand the source of the problem based upon Why is reinterpret_cast not constexpr?

My question is, is there a way to make my MemPointer template work for test code like above as well?

EDIT

One solution is to have a separate class for each "instance" has follows

class USART1 : public USART {
public:
    static USART& ref() { return *reinterpret_cast<USART*>(USART1_ADDR); }
};

class USART2 : public USART {
public:
    static USART& ref() { return *reinterpret_cast<USART*>(USART2_ADDR); }
};

I would prefer some sort of template + using combination though so that I don't need to write a bunch of classes. But perhaps this is the only option.

Patrick Wright
  • 1,401
  • 7
  • 13
  • Can you change it to : `static constexpr USART testUsart;` ? – marcinj Sep 07 '21 at 20:12
  • 1
    Do you have a compelling reason to make these addresses template parameters, instead of runtime parameters? Your issue seems to center on making the runtime address of a variable a compile-time constant. – Drew Dormann Sep 07 '21 at 20:13
  • Unrelated: Why have the `#define`s instead of `constexpr std::uintptr_t USART1_ADDR = 0x1234;` `constexpr std::uintptr_t USART2_ADDR = 0x5678;` ? – Ted Lyngmo Sep 07 '21 at 20:16
  • 2
    @TedLyngmo The defines come from the C SDK that is provided by the embedded system manufacturer. Basically, I am writing a light C++ layer around some of the code that is already provided. But I agree, ideally you would use constexpr directly. – Patrick Wright Sep 07 '21 at 20:18
  • @DrewDormann The way you phrased that makes a lot of sense. In essence, the peripheral address is run-time as well. I had originally created a separate class for every single USART "instance" with a static instance() function returning the casted version of the address. I was trying to do this as a template to avoid all the duplicate class code. – Patrick Wright Sep 07 '21 at 20:24
  • Does the code depend on the `TEST_USART` *being* and instantiation of `MemPointer` and not just *looking like* one? – numzero Sep 07 '21 at 20:37
  • @numzero I suppose, since I am using the "class-overlay" technique, TEST_USART (and the real hardware registers) are not actually "instances" of the class in the traditional sense but merely "act like" the class (i.e., have the same memory layout). – Patrick Wright Sep 07 '21 at 20:44
  • Well TEST_USART is not a class instance, it is a *class* which is a template instance. If there is no requirement on it being a `MemPointer` instance, you can add a `TestMemPointer` template that *acts like* `MemPointer` but holds the test data instead (like a reference to the emulated register; template argument *can* be a reference). – numzero Sep 07 '21 at 21:10
  • Why would you want a cast to happen in runtime? What's the problem being solved here? Is your register access too fast, program performing too well? I don't get it. – Lundin Sep 09 '21 at 06:43
  • Now the correct way to do this would be to include the register pointers as private class members. And they absolutely need to be `volatile`. All the C++ meta programming fluffery broke them. I strongly advise to drop C++ since it is evidently actively harmful and causing beginner-level bugs hidden in meta slop. – Lundin Sep 09 '21 at 06:47
  • Related, here's a beginner tutorial: [How to access a hardware register from firmware?](https://electrical.codidact.com/posts/276290) – Lundin Sep 09 '21 at 06:52

1 Answers1

3

is there a way to make my MemPointer template work for test code like above as well?

You could just stop insisting that the address be an intptr_t. You're going to cast it to a pointer anyway, so why not just allow any type for which that conversion exists?

template <typename T_, typename P, P ADDR_>
class MemPointer {
public:
    static T_& ref() { return *reinterpret_cast<T_*>(ADDR_); }
};

using USART1 = MemPointer<USART, std::intptr_t, USART1_ADDR>;
using USART2 = MemPointer<USART, std::intptr_t, USART2_ADDR>;

static USART testUsart;
using TEST_USART = MemPointer<USART, USART*, &testUsart>;

Follow-up notes:

  • if this were for a library to be used by others, I'd consider adding a static_assert(std::is_trivial_v<T_>) inside MemPointer to catch annoying errors

  • there are a few potential issues around things like padding & alignment, but I assume you know what your particular embedded platform is doing

  • you should volatile-qualify your register members, or the whole object (eg. you can return std::add_volatile_t<T_>& from MemPointer::ref)

    This is so the compiler knows that every write is an observable side-effect (ie, observable by the hardware even if your program never reads it back), and that every read may produce a different value (because the hardware can update it even if your program doesn't).

Useless
  • 64,155
  • 6
  • 88
  • 132