I have also been running into this problem when programming for AVR microcontrollers. Avr-libc has header files (included through <avr/io.h>
that make available the register layout for each microcontroller by defining macros such as:
#define TCNT1 (*(volatile uint16_t *)(0x84))
This allows using TCNT1
as if it were a normal variable and any reads and writes are directed to memory address 0x84 automatically. However, it also includes an (implicit) reinterpret_cast
, which prevents using the address of this "variable" in a constant expression. And since this macro is defined by avr-libc, changing it to remove the cast is not really an option (and redefining such macros yourself works, but then requires defining them for all the different AVR chips, duplicating the info from avr-libc).
Since the folding hack suggested by Shafik here seems to no longer work in gcc 7 and above, I have been looking for another solution.
Looking at the avr-libc header files more closely, it turns out they have two modes:
- Normally, they define variable-like macros as shown above.
- When used inside the assembler (or when included with
_SFR_ASM_COMPAT
defined), they define macros that just contain the address, e.g.:
#define TCNT1 (0x84)
At first glance the latter seems useful, since you could then set _SFR_ASM_COMPAT
before include <avr/io.h>
and simply use intptr_t
constants and use the address directly, rather than through a pointer. However, since you can include the avr-libc header only once (iow, only have TCNT1
as either a variable-like-macro, or an address), this trick only works inside a source file that does not include any other files that would need the variable-like-macros. In practice, this seems unlikely (though maybe you could have constexpr (class?) variables that are declared in a .h file and assigned a value in a .cpp file that includes nothing else?).
In any case, I found another trick by Krister Walfridsson, that defines these registers as external variables in a C++ header file and then defines them and locates them at a fixed location by using an assembler .S file. Then you can simply take the address of these global symbols, which is valid in a constexpr expressions. To make this work, this global symbol must have a different name as the original register macro, to prevent a conflict between both.
E.g. in your C++ code, you would have:
extern volatile uint16_t TCNT1_SYMBOL;
struct foo {
static constexpr volatile uint16_t* ptr = &TCNT1_SYMBOL;
};
And then you include a .S file in your project that contains:
#include <avr/io.h>
.global TCNT1_SYMBOL
TCNT1_SYMBOL = TCNT1
While writing this, I realized the above is not limited to the AVR-libc case, but can also be applied to the more generic question asked here. In that case, you could get a C++ file that looks like:
extern char MY_PTR_SYMBOL;
struct foo {
static constexpr const void* ptr = &MY_PTR_SYMBOL;
};
auto main() -> int {
return 0;
}
And a .S file that looks like:
.global MY_PTR_SYMBOL
MY_PTR_SYMBOL = 0x1
Here's how this looks: https://godbolt.org/z/vAfaS6 (I could not figure out how to get the compiler explorer to link both the cpp and .S file together, though
This approach has quite a bit more boilerplate, but does seem to work reliably across gcc and clang versions. Note that this approach looks like a similar approach using linker commandline options or linker scripts to place symbols at a certain memory address, but that approach is highly non-portable and tricky to integrate in a build process, while the approach suggested above is more portable and just a matter of adding a .S file into the build.
As pointed out in the comments, there is a performance downside though: The address is no more known at compile time. This means the compiler can no more use IN, OUT, SBI, CBI, SBIC, SBIS instructions. This increases code size, makes code slower, increases register pressure and many sequences are no more atomic, hence will need extra code if atomic execution is needed (most of the cases).