Make ISR call a function and return to another (sub)function afterwards

For an aquarium-project i'm imitating sunrise, sunset, moonrise and moonset (each in a sub-function).

My setup: an Uno, an RTC (DS3231), an RGBW-strip, a (touch)button, a temp-sensor (DS18b20), and a OLED display (SSD1306 based).

On certain times (from the RTC) the main loop will call one of those four subfunctions to change the brightness of different colors. Each has several while-loops (because R,G,B and W are called after eachother in a specific order and with different intensities).

Now i want an interrupt to link a push on the button (pin 2 or 3) to displaying the water temperature for 5 seconds on the OLED display.
Because ISR's should be short and 'millis' is unavailable inside an ISR, i think i've to call another function ("buttonPush()") via the ISR to do this.

My main question: can an ISR call a function and after running that once return to what it was doing before the interrupt?
I can program a bit, but am not good with messing with registers (which seems a thing i have to do? Store the registers during the interrupt and return to them after the buttonPush-function).

In a next part i want the button to have several function (long press, short press, double press).
I have most pieces working, but putting them all together is difficult

Why do you think you need an interrupt? Just include a digitalRead in loop to check the button (0 or 1).

Interrupts always return to the place you where in the code when the ISR was invoked and you cannot change that!

By the way there are no subfunctions in C/C++

Mark

Indeed doesn't sound like you need to use an interrupt.

But to answer your question: no you can't call a function. The normal way is to set a flag (a boolean variable) in the interrupt, and test for that in your loop() function.

Vanegh:
Because ISR's should be short and 'millis' is unavailable inside an ISR, i think i've to call another function ("buttonPush()") via the ISR to do this.

You need to time from the moment that you enter the ISR to the moment that you leave the ISR to dedtermine if an ISR is short or long or ... . Calling a function in the ISR will not solve your problem, the ISR will run just as long (even a littale longer).

You have two options
1)
In the ISR, set a flag and check and clear the flag in your main program.
2)
Write your code in your main program in such a way that it is more responsive. ISRs are usually not the solution for button presses.

holmes4:
Interrupts always return to the place you where in the code when the ISR was invoked and you cannot change that!

Yes, you can change that. Modifying the stack is possible; it might be tricky in C/C++ but in assembly it's quite easy.

adwsystems:
Why do you think you need an interrupt? Just include a digitalRead in loop to check the button (0 or 1).

I thought that first, but the 4 function take a while (between 10 and 30 minutes -- i'm not sure yet). So my idea was to run them when the RTC-time reaches a certain value, but while they run i should be able to push the button to show the water temp (and possibly some other button-functions). That's why i want the interrupt-approach.

Vanegh:
I thought that first, but the 4 function take a while (between 10 and 30 minutes -- i'm not sure yet). So my idea was to run them when the RTC-time reaches a certain value, but while they run i should be able to push the button to show the water temp (and possibly some other button-functions). That's why i want the interrupt-approach.

As @adwsystems suggested you are better testing for a button press in your loop, you can still continue doing your other functions.
Interrupts should contain the bare minimum of code you should not start displaying things within them. You could use an IST to set a flag to say that the button had been pressed, however you would have to check that flag in the loop so you may as well have tested the button state there in the first place.
Take a look at this tutorial
https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay
It should be obvious how you can do (or appear to do) several things simultaneously.

What do your functions have to do in those 10..30 minutes? You can more than likely break them in smaller chunks and by repeatedly calling the function from loop till it is finished achieve the same effect.

That way, in loop(), you can regularly check if the button is pressed.

If you give us an example of one of those functions, we might be able to advise how to break it into smaller chunks.

Two links that might help in understanding the principles
Demonstration code for several things at the same time
Using millis() for timing. A beginners guide

Oh dear!

Whenever I see a post which includes the word "interrupt" and particularly with "buttons", I instinctively cringe. :astonished:

"Newbies" of course - do not really understand what an interrupt is. You may of course argue that you are experienced in programming but the fact is that you are now dealing with a microcontroller and real-time programming.

An interrupt - in this context - is a mechanism for performing a task that can be performed virtually instantaneously, that must be performed virtually immediately, and that is not in itself intended to affect the flow of the main program (or delay it) in any way. Servicing a pushbutton essentially tends to fail all three criteria. These terms "instantaneously" and "immediately" are you see, defined in microprocessor, not human context, that is in terms of (sub)microseconds. Just while your finger is in the process of pressing the button, the microcontroller has performed several million instructions.

Now the first problem with the buttons. Buttons suffer from contact bounce so in general, must be de-bounced. The potential consequence of bounce is multiple actuation of the related function. If you consider this unimportant, saying that your code does not permit this to happen, well that may be the case or it may come back to bite you. Your present approach is presumably to set a flag for every interrupt and leave the main code "loop" to sort it out later. Well one would hope you are doing that, it may be even worse! The problem there is that you may not know how much later. If for example, you happen to process the flag before the bounce has finished (a few milliseconds in general) and clear the flag, then it will be set again on the next bounce.

The proper way to service pushbuttons (or of course, any mechanical contact) is to poll them as you go through the loop. You consider a valid press or release only when a change from a previously accepted state is maintained on every poll while the millis() count advances a given increment, such as ten (milliseconds). Every time the change reverts to the previous within the prescribed de-bounce interval, the status is reset and a new starting millis() count will then be determined if and when a change from the previously accepted stable state is again found. I have "stock" code to do just this.

This is based on the main loop "spinning" rapidly - of the order of one or (many) more times each millisecond. The trick is to write "non-blocking" code. This prohibits the use of "While" loops or equivalent constructs (which automatically precludes "delay()"), but is instead, a chain of "If" constructs within the main loop(). Which incidentally, is a state machine. The important thing is that many different tasks are allowed to be performed apparently simultaneously given that each only does what it must at any given instant, promptly passing on to the next.

This is how your overall program - however many modules it includes - needs to be written.

sterretje:
If you give us an example of one of those functions, we might be able to advise how to break it into smaller chunks.

Thanks for the tips!
I - indeed - am used to a bit of VBA-programming, but the reason i wanted to use the button to interrupt (and not just poll) is to be able to put the arduino to sleep when i don't use it. It could then wake up with the button or on a set time (every hour to check the times).

The code below doesn't work all the way, i think because i had several parts working separately and tried to put them together. :sweat_smile:
The display showed the temperature (worked) and the light went smooth from blue over orange to white. So those parts worked. The button worked, but not in the ISR, as it is here.

// LIBRARIES
  #include <SPI.h>
  #include <OneWire.h> 
  #include <DallasTemperature.h>
  #include <Adafruit_SSD1306.h>


  #define ONE_WIRE_BUS 4 
    OneWire oneWire(ONE_WIRE_BUS); 
    DallasTemperature sensors(&oneWire);

    #define OLED_RESET 2
    Adafruit_SSD1306 display(OLED_RESET);

        
// I/0 PINS
  const int LEDstrip_W = 6;     //PWM white LEDS
  const int LEDstrip_R = 9;     //PWM red LEDS
  const int LEDstrip_G = 10;    //PWM green LEDS
  const int LEDstrip_B = 11;    //PWM blue LEDS
  const int button = 2;         //button interrupt pin
  const int SQW = 3;            //RTC interrupt pin

//TIMERS
  unsigned long sun_time = 300000;       //--- TEMP 5 MINUTES --- #seconds from dark to light and vv (will become 1/2 hour? 10minutes?)
//  unsigned long moon_time = 60;       //--- TEMP 1 MINUTE --- #seconds from dark to blue (will become 5 or 10 minutes?)
  volatile unsigned long timer_button = 0; //to return the millis from the button interrupt
  unsigned int cnt = 0;                 // counter for the sun_time loop
  unsigned int step_time = 0;           // each brightness step-time. 
  unsigned long prev_millis = 0;        

// COLORS
  int W = 0;
  int R = 0;
  int G = 0;
  int B = 0;
  float Q = 0;
  const int PWM_steps = 100;    //#steps from 0 >> 255
        int PWM_tot_steps = 0;  //
  int brightness = 0;

  
//ALARMS
  byte time_sunrise = 13; 
  byte time_sunset = 22; 
  byte time_moonrise = 6; 
  byte time_moonset = 00; 

// BUTTONS
  volatile boolean buttonPush;

// STATUS
  bool sun_rising = 0; 


void ISR_button() {
  buttonPush = true;
  timer_button = millis();
}

void setup() {
  Serial.begin(9600);
  
  sensors.begin();  //for temperature
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x32)
  display.clearDisplay();

//SET PINS 
  pinMode(LEDstrip_W, OUTPUT);
  pinMode(LEDstrip_R, OUTPUT);
  pinMode(LEDstrip_G, OUTPUT);
  pinMode(LEDstrip_B, OUTPUT);
  pinMode(buttonPush, INPUT_PULLUP); 
  
//INTERRUPTS
  attachInterrupt(digitalPinToInterrupt(button), ISR_button, FALLING);

//(RE)SET LEDS
  analogWrite(LEDstrip_R, 0);
  analogWrite(LEDstrip_G, 0);
  analogWrite(LEDstrip_B, 0);
  analogWrite(LEDstrip_W, 0);
  
//
  cnt = 0; 
  Q = (PWM_steps * (log10(2))) / (log10(255)); //because LEDS I/O is exponential
  PWM_tot_steps = PWM_steps * 3;      //the brightness curve i use takes 3x the steps it takes to go from 0 to 255 (because not all need to go to 255)
  step_time = sun_time / (3 * PWM_steps);  //each steps takes the total time (in ms) x 1000 (s) / 7 (phases) / #steps
}


void loop() {
  //wakeup, for when i use it with sleep mode

  if((time_sunrise == rtc.hour) || (sun_rising == 1){
    sun_rising = 1;
    SunRise(); 
  }
 

  if (buttonPush == 1)  tempSensor();
 
}


void SunRise() {
  if((cnt >= (PWM_tot_steps * 0 / 6))  && (cnt < (PWM_tot_steps * 1 / 6))) {
    B++ ;
    brightness = pow (2, (B / Q));
    analogWrite(LEDstrip_B, brightness);
  }  
  
  if((cnt >= (PWM_tot_steps * 1 / 6))  && (cnt < (PWM_tot_steps * 2 / 6))) {
    R++;
    brightness = pow (2, (R / Q));
    analogWrite(LEDstrip_R, brightness);
  }
    
  if((cnt >= (PWM_tot_steps * 2 / 6))  && (cnt < (PWM_tot_steps * 3 / 6))) {
    R++;
    brightness = pow (2, (R / Q));
    analogWrite(LEDstrip_R, brightness);

    B--;
    brightness = pow (2, (B / Q));
    analogWrite(LEDstrip_B, brightness);
    
    G++;
    brightness = pow (2, (G / Q));
    analogWrite(LEDstrip_G, brightness);
  }
  
  if((cnt >= (PWM_tot_steps * 3 / 6))  && (cnt < (PWM_tot_steps * 4 / 6))) {
    W++;
    brightness = pow (2, (W / Q));
    analogWrite(LEDstrip_W, brightness);
  }  

  if((cnt >= (PWM_tot_steps * 4 / 6))  && (cnt < (PWM_tot_steps * 5 / 6))) {
    W++;
    brightness = pow (2, (W / Q));
    analogWrite(LEDstrip_W, brightness);

    G--;
    brightness = pow (2, (G / Q));
    analogWrite(LEDstrip_G, brightness);
      
    R--;
    brightness = pow (2, (R / Q));
    analogWrite(LEDstrip_R, brightness);             
  }  

  if((cnt >= (PWM_tot_steps * 5 / 6))  && (cnt <= (PWM_tot_steps * 6 / 6)))  {
    R--;
    brightness = pow (2, (R / Q));
    analogWrite(LEDstrip_R, brightness);     
  }

  // run once per step_time: 
  while (millis() - prev_millis < step_time) delay(1);    
  prev_millis = millis();
  cnt++; 
 
  if((cnt == (PWM_tot_steps))  && (R == 0)  && (G == 0)  && (B == 0)){
      analogWrite(LEDstrip_R, 0);
      analogWrite(LEDstrip_G, 0);
      analogWrite(LEDstrip_B, 0);
      analogWrite(LEDstrip_W, 255);
      cnt = 0;
      sun_rising = 0;      
    }
    
}

void tempSensor() {
  if((buttonPush == 1) && (millis() - timer_button < 5000 )){ 
    sensors.requestTemperatures(); // Send the command to get temperature readings
      display.setTextSize(1);
      display.setTextColor(WHITE);
      display.setCursor(0,0);
      display.println("Water temperature");
      display.setTextSize(3);
      display.setTextColor(WHITE);
      display.print(sensors.getTempCByIndex(0));
        display.println("C");
      display.display();
      delay(500);
      display.clearDisplay(); 
    } 
  else{ 
      buttonPush = 0;
    }     
  }
while (millis() - prev_millis < step_time) delay(1);

A complicated way of writing delay(step_time)

sterretje:

while (millis() - prev_millis < step_time) delay(1);

A complicated way of writing delay(step_time)

Hmm. You're right! I tried to avoid delay(), but this is exactly the same, but more complicated.
My intention was to check the if-statements only once per step_time, so the increase in RGBW isn't every clock cycle....

Vanegh:
I - indeed - am used to a bit of VBA-programming, but the reason i wanted to use the button to interrupt (and not just poll) is to be able to put the arduino to sleep when i don't use it. It could then wake up with the button or on a set time (every hour to check the times).

That's a fair reason to use an interrupt.

Quickly going through your code I noticed a major error: variable buttonPush is used as flag (good) but also later used as pin number in a pinMode() call - and that's of course wrong.

Code is too long to scour completely - do post a minimal example of your button code (e.g. code that just puts the Arduino to sleep, wakes up on a button press, and lights an LED for a second or so before going back to sleep so you can see the button press is handled).

Still two approaches.
When the interrupt is handled, wake up from sleep, then either set a flag (as you do) and check for it in loop(), or leave it at that and do the normal polling process in loop().

If you use the external interrupt (as on pins 2 and 3 of the Uno) you can simply set the flag as you know which pin. In case of pin change interrupts, you may have multiple pins on the same ISR, and scanning of the pins is still needed.

I'm not sure why you wrote the SunRise() code in the sequence you did.

But I would start it with

void SunRise()
{
  // check the time
  if (millis() - prev_millis < step_time)
  {
    // if it's not time yet, do nothing
    return;
  }

  ...
  ...

and get rid of the earlier indicated while-loop.

With that, the SunRise() function uses only a few CPU cycles when it's not time yet. Now, for this, you don't need the ISR anymore and can just poll the button in loop().

An odd way of doing this. I always do those checks the exact opposite way, I find that better for readability:

void SunRise()
{
  // check the time
  if (millis() - prev_millis > step_time)
  {
    // Do whatever you want to do.
  }
  // Otherwise, do nothing.
}

Nonetheless, this you would normally do with an alarm by the RTC, connected to an interrupt to wake up the Arduino when it's time for that.

If one writes a function to do something when it's time, I would use your approach.

If the timing is part of the function (as in OP's code), I usually do as shown.

One thing I don't like is too much nesting and the shown approach removes on level of nesting. Just a personal preference, I guess.

Check out my phase cutting library and EC sensor library as examples on timer manipulation.

Do also download the ATmega328 data sheet so you have all the registers at hand.

wvmarle:
That's a fair reason to use an interrupt.

Yes, for that reason.

But simply - do not use it - the interrupt - for any other function. Just poll the button at the appropriate times.