attachInterrupt blows up in my simple 8266 sketch

I've been trying to figure out interrupt handling using my 8266. It never comes back from the attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE); call and it just goes into a rolling Serial Monitor spew of stuff like this:

>>>stack>>>

ctx: cont
sp: 3fffff50 end: 3fffffd0 offset: 0010
3fffff60:  00000000 feefeffe 00000005 40202555  
3fffff70:  0001c200 0000001c 00000000 feefeffe  
3fffff80:  0000000f 00000000 3ffee574 4020111b  
3fffff90:  00000005 feefeffe feefeffe feefeffe  
3fffffa0:  feefeffe feefeffe feefeffe 3ffee5f4  
3fffffb0:  3fffdad0 00000000 3ffee5c8 40201c1c  
3fffffc0:  feefeffe feefeffe 3fffdab0 40100e75  
<<<stack<<<

--------------- CUT HERE FOR EXCEPTION DECODER ---------------

This is the simple code I'm using to test. I'm using the NodeMCU 12E board.

const int testPin = D1;
volatile bool flag = false;

void setup() {
  Serial.begin(115200);
  pinMode(testPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE);
  Serial.println("Setup complete");
}

void loop() {
  if (flag) {
    Serial.println("Interrupt Triggered!");
    flag = false;
  }
  delay(1000);
}

void testISR() {
  flag = true;
}

TIA for any guidance.

I found references to IRAM_ATTR for ESP...

Wow! Thanks for that. Initially it still broke but in a different way. I found from the example that the interrupt service routine, apparently, has to be included prior to the attachInterrupt call in Setup. Here's my fixed code. Thanks again!

const int testPin = D1;
volatile bool flag = false;

void IRAM_ATTR  testISR() {
  flag = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(testPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE);
  Serial.println("Setup complete");
}

void loop() {
  if (flag) {
    Serial.println("Interrupt Triggered!");
    flag = false;
  }
  delay(1000);
}


Just to explain. In C/C++, the compiler wants to know what a function looks like before it can 'use' it. So it wants to know what type of arguments are required and what the function will return.

So you have to either put the function before its first use (as you did) or add a prototype before the function's first use.

One of the processes (often referred to as the arduino builder) in the Arduino IDE does a reasonable job at adding the prototype before the compile is started but it does sometimes fail to do it correctly.

The prototype in your case would be

void IRAM_ATTR  testISR() ;

After that the function itself can be anywhere in your code.

Your modified code would look like

const int testPin = D1;
volatile bool flag = false;

void IRAM_ATTR testISR();

void setup()
{
  Serial.begin(115200);
  pinMode(testPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE);
  Serial.println("Setup complete");
}

void loop()
{
  if (flag)
  {
    Serial.println("Interrupt Triggered!");
    flag = false;
  }
  delay(1000);
}

void IRAM_ATTR testISR()
{
  flag = true;
}

What happens without the function prototype or if you don't move the function to the top of your code is that the arduino builder does not generate the prototype, more than likely due to the IRAM_ATTR attribute.

For your original code, the below is the code that is actually compiled after the builder did its job.

#include <Arduino.h>
#line 1 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
const int testPin = D1;
volatile bool flag = false;

#line 4 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void setup();
#line 11 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void loop();
#line 19 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void testISR();
#line 4 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void setup() {
  Serial.begin(115200);
  pinMode(testPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE);
  Serial.println("Setup complete");
}

void loop() {
  if (flag) {
    Serial.println("Interrupt Triggered!");
    flag = false;
  }
  delay(1000);
}

void testISR() {
  flag = true;
}

Please note the below in above

#line 19 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void testISR();

If you now add the IRAM_ATTR to your ISR function, the builder will not generate the prototype and hence the compile fails.

#include <Arduino.h>
#line 1 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
const int testPin = D1;
volatile bool flag = false;

#line 4 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void setup();
#line 12 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void loop();
#line 4 "C:\\Users\\bugge\\AppData\\Local\\Temp\\.arduinoIDE-unsaved2024925-22056-188eqqd.d8an\\sketch_oct25a\\sketch_oct25a.ino"
void setup()
{
  Serial.begin(115200);
  pinMode(testPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(testPin), testISR, CHANGE);
  Serial.println("Setup complete");
}

void loop()
{
  if (flag)
  {
    Serial.println("Interrupt Triggered!");
    flag = false;
  }
  delay(1000);
}

void IRAM_ATTR testISR()
{
  flag = true;
}

Manually adding the prototype makes that the compiler now knows about the testISR() function before its first 'use'.


You might encounter a similar problem in the future for other things.

You can find the file that is actually compiled (yourSketch.ino.cpp) if you enable verbose output during compilation under file/preferences in the IDE.
After a compile, copy the content of the output to a text editor and search for .ino.cpp; you will find a path to the file.

Thanks for this. I always prefer to put all the subordinate, so to speak, code below the main loop. I wasn't aware of the prototype deal. If I had run into that code in the wild I would have expected that an error would have been generated for two of the same function names in the code. This good to know.

It's a known issue; see Prototype not generated for function with inline attribute · Issue #2697 · arduino/arduino-cli · GitHub.

Arduino tries to make life easy for non-programmers so it does (or tries to do) that stuff for you.

Most people in the Arduino world do that.

I recommend getting out of that habit. It's not normal, outside the Arduino world. Most other IDEs & languages don't allow it. Think of it like having stabilisers/training wheels on a bicycle.