In summary, there's a few things going on:
1. The `SPI` object isn't being compile-time initialized.
2. A "constant expression" in C++ can't use a `reinterpret_cast` (equivalent in this case to the explicit cast used in the code). See #18 here:
https://en.cppreference.com/w/cpp/language/constant_expression
The top of the list states, "A core constant expression is any expression whose evaluation
would not evaluate any one of the following".
Part 1 — Constant Expressions
Looking at the `SPI` variable initialization, if we replace this code:
Code:
SPIClass SPI((uintptr_t)&IMXRT_LPSPI4_S, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
with this code:
Code:
constexpr uintptr_t xxx = (uintptr_t)(&IMXRT_LPSPI4_S);
SPIClass SPI(xxx, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
you'll see immediately that the `xxx` line gives a compile error.
This code works (non-zeros in startup_late_hook()):
Code:
const uintptr_t xxx = (uintptr_t)(&IMXRT_LPSPI4_S);
SPIClass SPI(xxx, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
The expression:
Code:
(uintptr_t)&IMXRT_LPSPI4_S
is converted to a `reinterpret_cast` according to the rules found here:
https://en.cppreference.com/w/cpp/language/explicit_cast (the first one)
Specifically, because we're casting a pointer to a `uintptr_t`, an integral type, the compiler will choose point 1)d), a `reinterpret_cast`. That expression is therefore equivalent to this:
Code:
reinterpret_cast<uintptr_t>(&IMXRT_LPSPI4_S)
which we know isn't a constant expression.
Part 2 — Initialization
With Teensyduino 1.58.1 (Arduino IDE 2.1.1), this code works (non-zeros in startup_late_hook()):
Code:
const uintptr_t xxx = reinterpret_cast<uintptr_t>(&IMXRT_LPSPI4_S); // Equivalent to explicit cast form
SPIClass SPI(xxx, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
but this does not (zeros in startup_late_hook()):
Code:
uintptr_t xxx = reinterpret_cast<uintptr_t>(&IMXRT_LPSPI4_S); // Equivalent to explicit cast form
SPIClass SPI(xxx, (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
and neither does this (zeros in startup_late_hook()):
Code:
SPIClass SPI(reinterpret_cast<uintptr_t>(&IMXRT_LPSPI4_S), (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
I would argue that the last two of the three code blocks are the same. I would also say that the compiler might be initializing the "const xxx" version differently than the "non-const xxx" versions. This brings us to initialization ordering and the relationship to different translation units. See here:
Static Initialization Order Fiasco
Simply put, because
SPI.cpp and the code calling or defining `startup_late_hook()` are in different translation units, there's no guarantees about ordering or what's initialized when or who sees what. That means that the `SPI` object created inside
SPI.cpp may or may not be initialized before being accessed. This is one of the reasons a singleton pattern is useful, like this, where the variable isn't initialized until first use:
Code:
Object &instance() {
static Object o;
return o;
}
In other words, the `SPI` object being initialized is dependent on what the compiler feels like and nothing else here. Just because one form works and another, slightly different form, does not, it doesn't mean we can trust any sort of initialization order.
Note also that the `SPI` object is not compile-time initialized.
I know what you might be thinking here, and that's `ResetHandler2()`, the function that calls `startup_late_hook()`, isn't a static object that depends on the `SPI` initialization (per the "fiasco" link above). It's a function. I'll just say that it's declared as "section .startup", so maybe that's related? Is it possible that "section .startup" functions have the same ordering requirements as static objects?
Part 3 — constexpr Constructors
A `constexpr` constructor doesn't mean that an object will be initialized at compile time as part of a constant expression. It only means that it is
possible to initialize it at compile time as a constant expression. From here:
https://en.cppreference.com/w/cpp/language/constexpr
it says, "The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time."
This means that you need to declare an object as `constexpr` when defining it.
For example, the following code will compile; `simpleInstance` won't necessarily be created at compile time, and it's not constant:
Code:
class SimpleClass {
public:
constexpr SimpleClass(int mynum) : n(mynum) { }
int read() { return n; }
int n;
};
SimpleClass simpleInstance(1234);
What gives away that the previous instance isn't constant is the fact that the following won't compile:
Code:
class SimpleClass {
public:
constexpr SimpleClass(int mynum) : n(mynum) { }
int read() { return n; }
int n;
};
constexpr SimpleClass simpleInstance(1234);
const SimpleClass simpleInstance2(5678);
Can you see it? The `read()` function isn't marked as `const`, and so is mutable, meaning instances can't be declared const because a `read()` function call may mutate the object.
The following fixes it:
Code:
class SimpleClass {
public:
constexpr SimpleClass(int mynum) : n(mynum) { }
int read() const { return n; } // <-- Added const
int n;
};
constexpr SimpleClass simpleInstance(1234);
const SimpleClass simpleInstance2(5678);
Summary
In summary:
1. `constexpr` constructors don't mean that all instances are constant or compile-time initialized.
2. There's no guaranteed initialization order when accessing variables across translation units.
3. `reinterpret_cast` expressions can't be part of a constant expression.
4. `(uintptr_t)x`, where `x` is a pointer, is equivalent to `reinterpret_cast<uintptr_t>(x)`.
Recommendations
1. In any C++ code, change all explicit casts to an appropriate `static_cast` or `reinterpret_cast` for more code clarity.
2. Figure out a way to guarantee initialization before use. Unfortunately, due to Arduino's whole global variable schtick, it might not always be possible, other than through experimentation to see what works for a given compiler — for example, changing the `SPI` object initialization to use a `const`, previously declared, variable, per that `xxx` example.
3. Compile-time
constant initialization should help #2, but for that to work, the classes (eg. `SPIClass`) need to accommodate being non-mutable. For example, all functions need to be `const`, as do all the internal variables, possibly along with some other subtle points.
Note that this form does not work:
Code:
SPIClass SPI(reinterpret_cast<const uintptr_t>(&IMXRT_LPSPI4_S), (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);
It casts to a `const uintptr_t`, but it looks like (at least with this compiler) it's equivalent to the non-const `xxx` example.
Epilogue
C++ is hard and it's easy to misunderstand and misinterpret. I don't even trust my own knowledge in it sometimes. So I'll add a caveat to this post that, while I think I know what I'm doing, I may still have some of the details subtly wrong. Corrections and additions welcome.
