Why does this ISR fail with return type void?

Hi all,

I have the following code that works perfectly!! I know, thanks ;-}

int Header_Liquid_Level;
int *pointToHLL = &Header_Liquid_Level;
int Header_High_Pin = 2;


void setup() {
  Serial.begin(9600);
  pinMode(Header_High_Pin, INPUT);
  int Header_Liquid_Level = digitalRead(Header_High_Pin);
  attachInterrupt(digitalPinToInterrupt(Header_High_Pin), Level_ISR(Header_High_Pin, pointToHLL), CHANGE);
}

void loop() {

  Serial.print("Header_Liquid_Level= ");
  Serial.println(Header_Liquid_Level, DEC);

  delay(500);
}

int Level_ISR(int Level_Pin, int *pointerToLevel) {

  if (digitalRead(Level_Pin) == 0)
    *pointerToLevel = 0;
  else
    *pointerToLevel = 1;
}

My question is why does Level_ISR need to have the return type defined as int when no value is returned? It won’t compile with the type set to void …

As a cheeky 2nd question, have I followed “good practice” with this code? Although it’s more complex than it needs to be for just one sensor, I will have 10 sensors in the final sketch.

Everyday is a school day!

Where do the ISR arguments come from?

@anon56112670 Thanks for replying. The values are passed from the attachInterrupt definition. Is that what you meant?

In this line: attachInterrupt(digitalPinToInterrupt(Header_High_Pin), Level_ISR(Header_High_Pin, pointToHLL), CHANGE);

It feels like that's not really what you're asking though ...

An ISR can have neither a return type or arguments.

From Using Interrupts on Arduino
There are no input variables or returned values. All changes have to be made on global variables.

2 Likes

Think about how a bare ISR is called.
Literally between two machine instructions - now, where do those arguments come from?

Had me worried for a second there. I'm feeling better now :slight_smile:

warning: invalid conversion from 'int' to 'void (*)()' [-fpermissive]
attachInterrupt(digitalPinToInterrupt(Header_High_Pin), Level_ISR(Header_High_Pin, pointToHLL), CHANGE);

So it will compile, but with clear warnings, which should indicate to the programmer that something isn't right. In this case the function prototype for the ISR is wrong.

have I followed “good practice” with this code?

No.

  1. Comply with function prototypes
  2. Do not ignore compiler warnings.
  3. Always use brackets around "if" statements.
2 Likes

@groundFungus My intention was to avoid having 10 ISRs, each hard coded to a specific Interrupt.

I plan to have something like:

attachInterrupt(digitalPinToInterrupt(Pin_1), Level_ISR(Liquid_Level_1, pointer_1), CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_2), Level_ISR(Liquid_Level_2, pointer_2), CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_3), Level_ISR(Liquid_Level_3, pointer_3), CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_4), Level_ISR(Liquid_Level_4, pointer_4), CHANGE);

Is this fundamentally a prohibited way to perform the task?

I'm not sure what the phrase "There are no input variables or returned values." means? Clearly, I have passed input variables to the ISR and it receives them and processes them correctly. Does that mean I have written an incorrect ISR?

I understand that there are no returned values, which is why I was wondering why void wasn't valid ...

As for "All changes have to be made on global variables." Does that mean that my use of pointers is also prohibited?

Thanks for taking the time to answer :smiley:

Clearly?

It's not that void isn't valid. It is! The problem is that an ISR prototype is "void ISR_Func(void)" and you are doing something completely different! If you restructure your ISR to be the way it should, it will compile just fine without any warnings. If you need to access external data in your ISR, then that's one of the few acceptable reasons to use global variables.

[edit]
I don't know the C/C++ compiler well enough to know why your code would work, but my guess is that the code generated is pushing those variables onto the stack and the ISR is somehow managing to retrieve them. Maybe. There's a lot of weird stuff that C does which works until it just doesn't. But that's not a reason to rely on unspecified behavior.

Is this not passing input variables to the ISR?

Ah, now that makes sense!

So, perhaps my question should be: Should I write n x ISR functions each explicitly updating one or more global variables?

That's clearly your intent, but there is no mechanism for actually passing those to an ISR.

I can't deduce your actual intent from reading the code, but here is a possible solution which at least compiles without warnings and may be what you were trying to accomplish.

int Header_Liquid_Level;
int *pointToHLL = &Header_Liquid_Level;
int Header_High_Pin = 2;

int pointer_1 = 0;
int pointer_2 = 0;
int pointer_3 = 0;
int pointer_4 = 0;

int Liquid_Level_1 = 0;
int Liquid_Level_2 = 0;
int Liquid_Level_3 = 0;
int Liquid_Level_4 = 0;

struct ISR_MAP
{
   int pin;
   int levelPtr;
};

const int N_MAPS = 4;
volatile ISR_MAP pinMapping[N_MAPS] =
{
  {Liquid_Level_1, pointer_1},
  {Liquid_Level_2, pointer_2},
  {Liquid_Level_3, pointer_3},
  {Liquid_Level_4, pointer_4}
};

void Level_ISR() 
{
  for (int i = 0; i < N_MAPS; i++)
  {
    if (digitalRead(pinMapping[i].pin) == 0)
    {
       pinMapping[i].levelPtr = 0;
    }
    else
    {
       pinMapping[i].levelPtr = 1;
    }
  }
}

void setup() {
  Serial.begin(9600);
  pinMode(Header_High_Pin, INPUT);
  int Header_Liquid_Level = digitalRead(Header_High_Pin);
  attachInterrupt(digitalPinToInterrupt(Header_High_Pin), Level_ISR, CHANGE);
}

void loop() {

  Serial.print("Header_Liquid_Level= ");
  Serial.println(Header_Liquid_Level, DEC);

  delay(500);
}
1 Like

@cedarlakeinstruments

Hi, thanks for taking the time to work up that code, it really is appreciated.

I'm going to look at to carefully and see if it's going to be a good fit for me.

Hi @cedarlakeinstruments

The set up I have is that I have 5 tanks of liquid. Each tank has a liquid sensor near the top and another near the bottom. Each sensor is connected to a different input pin on the Arduino. The task for the code is to detect any time, any of the sensors changes status (High -> Low) (Low -> High) and update the appropriate global variable. The further processing of that information is not relevant to this question.

With your code you are only responding to an interrupt on a single pin whereas I need to respond to 10. I completely understand that you couldn’t have known this.

What I have now come to understand is that the phrase “There are no input variables” means THOU SHALL NOT even think about passing any input variables to the ISR.

The really confusing thing is that the code works exactly as I had expected and the ISR uses the passed values correctly.

Where it gets even more confusing (for me) is that if I add code like this:

attachInterrupt(digitalPinToInterrupt(pin_1), Level_ISR(pin_1, pointerToVar_1), CHANGE);
attachInterrupt(digitalPinToInterrupt(pin_2), Level_ISR(pin_2, pointerToVar_2), CHANGE);

Then the ISR fires twice when pin_1 changes state even when there is no change to the status of pin_2

So when my ISR is modified to this:

int Level_ISR(int Level_Pin, int *pointerToLevel) {

  Serial.print("Fired: ");
  Serial.println(Level_Pin);

  if (digitalRead(Level_Pin) == 0)
    *pointerToLevel = 0;
  else
    *pointerToLevel = 1;
}

I see :

Fired: 2
Fired: 3

On the serial monitor for a single change of state on pin_1 …

If @desp166 is compiling for an ESP8266 / ESP32, it can be done with "FunctionalInterrupt":

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

int Header_Liquid_Level;
int *pointToHLL = &Header_Liquid_Level;
int Header_High_Pin = 2;
void Level_ISR(int Level_Pin, int *pointerToLevel);

void setup() {
  Serial.begin(9600);
  pinMode(Header_High_Pin, INPUT);
  int Header_Liquid_Level = digitalRead(Header_High_Pin);

  std::function<void(void)> isrLambda = []() {
    Level_ISR(Header_High_Pin, pointToHLL);
  };
  attachInterrupt(digitalPinToInterrupt(Header_High_Pin), isrLambda, CHANGE);
}

void loop() {
  Serial.print("Header_Liquid_Level= ");
  Serial.println(Header_Liquid_Level, DEC);
  delay(500);
}

void Level_ISR(int Level_Pin, int *pointerToLevel) {
  if (digitalRead(Level_Pin) == 0)
    *pointerToLevel = 0;
  else
    *pointerToLevel = 1;
}

Thanks @gfvalvo, but sadly I'm using a Mega :roll_eyes:

Yes.

Instead, use:

attachInterrupt(digitalPinToInterrupt(Pin_1), Level_ISR_1, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_2), Level_ISR_2, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_3), Level_ISR_3, CHANGE);
attachInterrupt(digitalPinToInterrupt(Pin_4), Level_ISR_4, CHANGE);

void Level_ISR_1(void) {
  Level_ISR(Liquid_Level_1, pointer_1)
}
void Level_ISR_2(void) {
  Level_ISR(Liquid_Level_2, pointer_2)
}
void Level_ISR_3(void) {
  Level_ISR(Liquid_Level_3, pointer_3)
}
void Level_ISR_4(void) {
  Level_ISR(Liquid_Level_4, pointer_4)
}

I agree that ignoring compiler warning is not good, but I had none. Nothing. Zip. Nada :rofl:

I'm using IDE 2.0.3 writing to a Mega 2560 rev3 ...

Your point 3. Noted :wink:

Thanks @johnwasser

That looks good and I'll give it a go!

Just feels wrong to be repeating so much code ... Perhaps I've just got to change my attitude :wink: