Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a constexpr pointer to a register on embedded system

I want to be able to configure a class to be able to access hardware in its member functions. Let's assume we have an avr device, where we simply can access hardware like PORTA = 0x00; which writes a 0x00 to the io memory space. The problem is generic for every kind of embedded memory io access, not specific to avr.

But if I want to now use a class which can be parametrized, it seems C++ has closed all the doors because it seems to be simply impossible to define any kind of pointer types and give them a constexpr value anymore.

Some compiler versions prior, we were able to run such code like: constexpr reference to avr port address

But now all tries to assign a value to pointer for a constexpr value fails, because reinterpret_cast can not longer be used in that case.

As a dirty hack I tried and failed:

struct ONE 
{
    static constexpr volatile uint8_t* helper=nullptr;
    static constexpr volatile uint8_t* portc=&helper[100];
};

fails with:

x.cpp:6:57: error: arithmetic involving a null pointer in '0'
    6 |     static constexpr volatile uint8_t* portc=&helper[100];

Also fails:

 // for AVR PORTB is defined in io.h like:
 #define PORTB (*(volatile uint8_t *)((0x05) + 0x20))

 constexpr volatile uint8_t* ptr=&PORTB;

Fails with:

x.cpp: In function 'int main()':
x.cpp:15:37: error: 'reinterpret_cast<volatile uint8_t* {aka volatile unsigned char*}>(56)' is not a constant expression
   15 |     constexpr volatile uint8_t* ptr=&PORTB;

which directly let me find reinterpret_cast<volatile uint8_t*>(37)' is not a constant expression. But also without any solution!

My target is very very simple: Write some class which can be configured to use a specific register like:

template < volatile uint8_t* REG>
class X
{
    public:
        X() { REG = 0x02; }
};

If we can not longer define pointer values as constexpr values, we can not use them in templates nor directly. This means, we only have run time variables, which can no longer be optimized and always needs space in ram and flash. This is not acceptable for very small embedded systems.

If that is really the fact, the only way to work is really using c-macros? I can't believe that none of my code will work anymore... and never will work in the future without using C macros.

I currently use avr-g++ (Fedora 10.2.0-1.fc33) 10.2.0 but it seems, that from all my readings it is correct behavior if used in C++17 mode.

like image 400
Klaus Avatar asked Jun 30 '26 06:06

Klaus


2 Answers

Here's an example of a template using a lambda for the port address.

The template could be defined as:

template <GetPortAdr PORT_ADR>
struct Register
{
    // Call lambda to get the address and de-reference the pointer.
    static inline void set()  { *PORT_ADR() = 2; } 
};

... where the type GetPortAdr would be defined as a function pointer:

typedef volatile uint8_t * (*GetPortAdr)();

Assuming a port definition provided by a HAL or vendor:

#define PORTA(*(volatile uint8_t *)(0x05000020))

... this could be instantiated and used like this:

Register<[](){return &PORTA;}> port_a;
port_a.set();

The ugly lambda-related syntax adds clutter but the PORTA #define provided by the vendor can be used unmolested otherwise.

2 caveats:

  • c++20 is required, otherwise I get errors stating lambda-expression in template-argument only available with '-std=c++20'
  • Optimization of at least -O1 is required otherwise flash-consuming bloated assembly is produced. With -O1, the generated code is equivalent to a regular C-style cast.

The https://godbolt.org/z/5aqTnGa68 example shows a C-style #definition for comparison with the template approach.

like image 166
Jim Fred Avatar answered Jul 03 '26 03:07

Jim Fred


I don’t think this is possible at all in C++17. But there is a possible workaround if you’re willing to switch to C++20. Instead of forging a constexpr pointer directly, create a custom type to represent it, which will convert to a pointer at first opportunity, when the constexpr context is left:

#include <cstdint>

template <typename T>
struct fixed_ptr
{
    std::uintptr_t m_value;

    inline constexpr explicit fixed_ptr(std::uintptr_t p) :
        m_value { p }
    {}

    inline operator T * () const
    { return reinterpret_cast<T *>(m_value); }
};

Then instead of specifying a type for the template parameter directly, constrain it by a concept:

#include <utility>
#include <cstdint>

template <typename T, typename U>
concept pointerish = std::is_same_v<U &, decltype(*std::declval<T>())>;

template <pointerish<volatile std::uint8_t> auto REG>
class X {
    public:
        X() { *REG = 0x02; }
};

volatile std::uint8_t x;

auto p = X<fixed_ptr<volatile std::uint8_t>(5)>();
auto q = X<&x>();

At -O1 there should be no overhead of using the class wrapper over a direct pointer: inlining will take care of it.

Now, what to do with your existing preprocessor macros? You cannot cast the addresses back to uintptr_t, because that constitutes a reinterpret_cast, which is forbidden in a constexpr context. You can run gcc -E -dM over the header file to extract the macros as an extra build step:

echo '#include <avr/io.h>' |
    gcc -E -dM - | 
    sed -ne '
        s!#define \(PORT[A-Za-z0-9_]*\) ( *\* *( *\(.*\) *\*)\(.*\))!DEF_PORT(\1, \2, \3)!p' \
    > avr-io.hh

Then you can use the generated file to create your own C++-compatible header like this:

#define DEF_PORT(name, type, addr) \
    constinit fixed_ptr<type> name = addr;
#include "avr-io.hh"
#undef DEF_PORT

But if you would rather not add a build step, there is always dirty preprocessor trickery to the rescue:

#define volatile ), (
#define UNWRAP_ADDR2__(x) 
#define UNWRAP_ADDR1__(x) UNWRAP_ADDR2__ x
#define UNWRAP_ADDR0__(x, y) y
#define UNWRAP_ADDR(x) UNWRAP_ADDR1__(UNWRAP_ADDR0__ x)

constexpr std::uintptr_t ADDR_PORTA = UNWRAP_ADDR(PORTA);
constexpr std::uintptr_t ADDR_PORTB = UNWRAP_ADDR(PORTB);

#undef volatile

Here we take advantage of the fact that register macro definitions are of the form (*(volatile XXX *)(YYY)). Defining volatile as ), ( splits the register macro into two macro arguments to UNWRAP_ADDR0__, which gets rid of the ‘first argument’ containing the dereference operator, while UNWRAP_ADDR1__ and UNWRAP_ADDR2__ strip away the rest of the type cast. You are left only with the numeric address.

like image 24
user3840170 Avatar answered Jul 03 '26 02:07

user3840170



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!