Teensy 4.1 does not boot when invoking SPI from external class

LucaMarius

New member
Hello All,
I updated the Teensy platform from 4.14.0 to 4.18.0 today. Afterwards I figured out that invoking the SPI class from my own driver class member leads to an issue. The Teensy 4.1 does not jump in setup/loop.
After removing all SPI.xy commands everything went well. In 4.17.0 everything works fine too. I have no compiler warnings.

My Class Header:
Code:
#ifndef DAC8568_h
#define DAC8568_h

#include "Arduino.h"
#include <SPI.h>

class DAC8568
{
private: 
  const bool LOGGING = 0;

My Class implementation
Code:
#include "DAC8568.h"

DAC8568::DAC8568(uint8_t csPIN)
{
  _CS_DAC8568 = csPIN;

  pinMode(_CS_DAC8568, OUTPUT);
  digitalWriteFast(_CS_DAC8568, HIGH);

  //SPI.begin(); <-- causes error
  _initDAC8568();
}


PACKAGES:
- framework-arduinoteensy @ 1.158.0 (1.58)
- tool-teensy @ 1.158.0 (1.58)
- toolchain-gccarmnoneeabi-teensy @ 1.110301.0 (11.3.1)


Thank you for your support.

Kind regards
 
Maybe you're using PlatformIO? Version 4.18.0 doesn't correspond to anything PJRC publishes.

First, make sure you really are using Teensy's SPI library and it's the most recent version. Wrong libraries even with the right main platform/framework/core is a common problem with PlatformIO. This is the SPI library for Teensy:

https://github.com/PaulStoffregen/SPI/

As a general rule, calling functions from other classes from a static constructor is risky. Some people call it C++ Static Initialization Order Fiasco. But usually this isn't a problem with SPI on Teensy because we use a constexpr constructor. (and you probably should consider using constexpr constructors for your own classes and provide a begin() method to actually start the hardware running)

If these comments don't solve your problem, I'm going to ask you to please reproduce the problem with the Arduino IDE and post a complete program which can be copied into Arduino IDE and uploaded to Teensy to reproduce the problem. It's entirely possible you've discovered some previously unknown bug in Teensy's SPI library or other library code. But I will only investigate when a complete test case for Arduino IDE is posted. I will not investigate Teensy's library code using PlatformIO. Test case must demonstrate the problems with Arduino IDE. A code fragment as you've shown, even if the missing part is "trivial", is not enough. It needs to be a whole program which reproduces the problem with zero guesswork. I do have a long history of investigating bug reports, but I also have a long history of wasting a lot of time not managing to reproduce the problem when the report has only a code fragment and I have to fill in the rest of the code, so I hope you can understand how essential a complete test case is.
 
How about this to reproduce the problem:
Code:
#include "Audio.h"

// Uncomment this to cause crash and re-boot
//AudioEffectDelayExternal dummy(AUDIO_MEMORY_23LC1024,100.0f);

void setup() 
{
  while (!Serial)
    ;
  Serial.println("Setup");
  Serial.println((uint32_t) &SPI.hardware(),HEX);
}

void loop() 
{
  Serial.println("Loop");
  delay(500);
}

extern "C" {
  void startup_late_hook(void)
  {
    while (!Serial)
      ;
    Serial.println("Late hook");
    Serial.println((uint32_t) &SPI.hardware(),HEX);
    if (CrashReport)
      Serial.print(CrashReport);
    delay(250);
  }
}
Note that I had to make SPIClass::hardware() public to demonstrate that the SPI hardware address is NOT set at startup_late_hook(), when static constructors are about to be called. If another static constructor which calls SPI.begin() is called before the hardware address is set, the NULL pointer is dereferenced and the Teensy crashes before ever getting to setup().

This issue was noted in https://forum.pjrc.com/threads/73030-AudioEffectDelayExternal-not-working-all-of-a-sudden, but the OP seemed to have difficulty getting my fixed AudioEffectDelayExternal installed. Also noted from https://forum.pjrc.com/threads/2927...-audio-library?p=327076&viewfull=1#post327076

@LucaMarius, for now you'll have to move the call to SPI.begin() out of your constructor...
 
Good morning @h4yn0nnym0u5e ,

Thank you for your response. I also figured out last evening after preparing my 4.1 board for my debugger.
I already cleaned out my constructor and moved it to the suggested begin() method. Now everything went well.

I also checked out the official SPI lib but it was the same one as cloned by platoformIO.

Thank you for your help!

Kind regards,
Luca
 
How about this to reproduce the problem:

I'm able to reproduce the problem.

Must admit, I don't understand why. My limited C++ understanding of constexpr constructors is the static instance is supposed to be fully initialized at compile time. Hopefully someone like Luni who knows C++ much better can chime in?

Added a simple constexpr constructor to the test case.

Code:
#include <SPI.h>

class SimpleClass {
  public:
    constexpr SimpleClass(int mynum) : n(mynum) { }
    int read() { return n; }
    int n;
};
SimpleClass SimpleInstance(1234);


void print_info(const char *title) {
  while (!Serial) ; // wait for serial monitor
  Serial.println(title);
  Serial.println((uint32_t) &SPI.hardware(), HEX);
  Serial.println((uint32_t) &SPI.port(), HEX);
  Serial.println(SimpleInstance.read());
  Serial.println();
}

extern "C" {
  void startup_late_hook() {
    print_info("startup_late_hook");
  }
}

void setup() {
  print_info("setup");
}

void loop() {
}

When I run on Teensy 4.1 (with SPI library edited to make those functions public) I get this:

Code:
startup_late_hook
0
0
1234

setup
20000360
403A0000
1234
 
After much fiddling, I discovered changing the instance from this:

Code:
SPIClass SPI([COLOR="#FF0000"](uintptr_t)&IMXRT_LPSPI4_S[/COLOR], (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);

to this makes it work.

Code:
SPIClass SPI([COLOR="#FF0000"]IMXRT_LPSPI4_ADDRESS[/COLOR], (uintptr_t)&SPIClass::spiclass_lpspi4_hardware);

I don't understand why this makes any difference, since IMXRT_LPSPI4_S is defined as:

Code:
#define IMXRT_LPSPI4_S          (*(IMXRT_LPSPI_t *)IMXRT_LPSPI4_ADDRESS)
 
I think the problem you are facing is that you hand over an already dereferenced object (which is provided by your macro) but your constructor expects a pointer.

Please correct me if I'm wrong.
 
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. :)
 
Last edited:
Yep, there's at least one error in my post.

This code:
Code:
class SimpleClass {
  public:
    constexpr SimpleClass(int mynum) : n(mynum) { }
    int read() { return n; }
    int n;
};
constexpr SimpleClass simpleInstance(1234);
const SimpleClass simpleInstance2(5678);

will compile unless `simpleInstance.read()` or `simpleInstance2.read()` is called.
 
Additionally, this will compile:
Code:
class SimpleClass {
  public:
    SimpleClass(int mynum) : n(mynum) { }
    int read() { return n; }
    int n;
};
const SimpleClass simpleInstance2(5678);

but this will not:
Code:
class SimpleClass {
  public:
    SimpleClass(int mynum) : n(mynum) { }
    int read() { return n; }
    int n;
};
constexpr SimpleClass simpleInstance(1234);

The reason is that to be created as a `constexpr`, the constructor must be `constexpr`.

The reverse isn't true. The object doesn't have to be `const` or `constexpr` if its constructor is marked `constexpr`.
 
I was not aware of using constexpr on the static instance.

So far we've just been using it inside the class definition on the constructor, with the assumption constexpr constructor would cause all instances of the class to be compile time initialized.
 
I was not aware of using constexpr on the static instance.

So far we've just been using it inside the class definition on the constructor, with the assumption constexpr constructor would cause all instances of the class to be compile time initialized.

For future readers not wishing to read my above treatise: declaring a constructor as `constexpr` won’t necessarily make instances const or constexpr unless those instances are also declared const or constexpr. I’m also reasonably certain that instances won’t necessarily be compile-time initialized either.
 
Last edited:
Thanks for the detailed explanation. Must admit, my understanding of C++ isn't so deep, so this really helps.

Looks like I should probably go through all the classes that are meant to be compile time initialized and add constexpr every static instance.
 
Thanks shwan for the detailed explanation of static initialization and constexpr.

Two small additions:
All dynamic initializations of C++ objects are done when calling __libc_init_array() after startup_late_hook() and just before calling main(). So there is a well-defined order in the startup code, startup_late_hook() is always executed before any dynamic initialization of C++ code.

Since C++20 there is the constinit specifier, which asserts that a variable has static initialization (but doesn’t imply const). However, to define constinit SPIClass SPI(...); it would be necessary to slightly modify SPIClass.
 
We're just updated to gcc 11.3.1 in Teensyduino 1.58 and we're moving from C++17 with Teensyduino 1.59 (today still beta).

Unknown when we'll migrate to C++20, but seems unlikely to be soon.
 
Thanks for the detailed explanation. Must admit, my understanding of C++ isn't so deep, so this really helps.

Looks like I should probably go through all the classes that are meant to be compile time initialized and add constexpr every static instance.

Judging by this thread, the Wire library is one instance where this is needed. I did have a go myself, but failed miserably, apart from to satisfy myself that the issue is there and is the same as the one for SPI...
 
Back
Top