Getting Pin Change Interrupts working together with Timer interrupts / sleep(XX ms) on Arduino Pro Mini



  • Hi everyone,

    this is a recap of another recent thread to get this into the correct subforum and to clean up the info.
    For my minimalistic battery powered node (Minimal LiPo powered Arduino Pro Mini + RFM95 MySensors node) I am (of course:-) using MySensors, together with its sleep() function. Now I am looking to get timer wake ups and additional wake ups via Pin Change Interrupts working.

    The minimal hardware set up to test this consists of an Arduino (I use a Pro Mini 3.3V/8MHz) with a simple wire connected to A1, which can then be connected to ground to simulate a button press. I believe debouncing etc. is not necessary for this example, as I just want to wake up the Arduino (mind you, which should be sleeping via sleep(XX ms), not indefintely) successfully and will figure out the rest later on.

    The most simplistic example code I could come up with:

    #include <Arduino.h>
    
    #define MY_RADIO_RFM95
    #include <MySensors.h>
    
    volatile int value = 1000, counter = 0;
    volatile char PinRegLast, ChangedPin, PinRegister;
    volatile unsigned long last_debounce_time=0;
    volatile boolean interrupted;
    
    ISR(PCINT1_vect)
    {
        interrupted=true;
        counter++;
    }
    
    void setup()
    {
    noInterrupts();
    PCICR |= 0b00000011; // Enables Ports B and C Pin Change Interrupts
    PCMSK1 |= 0b00000110; //PCINT9 & PCINT10 = A1 & A2
    interrupts();
    pinMode(A1, INPUT_PULLUP); pinMode(A2, INPUT_PULLUP);
    Serial.begin(38400);
    }
    
    void presentation() {}
    
    void loop()
    {
      noInterrupts();
      if ( interrupted ) {
        Serial.println("Interrupted / Woken up by Pin");
        interrupted = false;
      }
      else  { Serial.println("Timer wake up / Loop");  }
    
      Serial.print("  ");  Serial.print(" Counter: ");Serial.println(counter); 
      interrupts();
      sleep((uint32_t) 10*60*1000);
      //LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); //not compiling
    }
    

    This misbehaves in a strange way. First of all, it seems to do a timed sleep()/wake up correctly. But simulating button presses does not wake the node immediately, but with varying repetitions of button presses which depend on the duration of the requested sleep(XXms) time.

    With sleep((uint32_t) 10 * 60 * 1000), the node wakes up after approx 35 button presses and I get:

    Timer wake up / Loop
       Counter: 0
    Interrupted / Woken up by Pin
       Counter: 77
    Interrupted / Woken up by Pin
       Counter: 152
    

    With sleep((uint32_t) 60 * 1000) on the other hand, much less repetitions are needed (mostly 4 button presses) and I get:

    Interrupted / Woken up by Pin
       Counter: 11
    Interrupted / Woken up by Pin
       Counter: 20
    Interrupted / Woken up by Pin
       Counter: 30
    Interrupted / Woken up by Pin
       Counter: 40
    Interrupted / Woken up by Pin
       Counter: 49
    

    The interval of 9-10 counter increments is pretty stable (but not always the same) with this sleep duration, as are the counter increments of 75-80 with the longer sleep time above.

    So depending on the requested sleep time, the delay resp. the iterations before the ISR wakes up the node to get into the loop() is longer or shorter. What might cause this?

    Thanks for any help,

    Joost

    BTW: thanks again to @electrik , @mfalkvidd, @BearWithBeard from my first thread for helping. Sadly, the Low-Power lib approach is not working (anymore) as id does not compile in conjunction with MySensors 2.3.2 (Github issue).



  • Are you sure your contacts are not bouncing? This could trigger the interrupt routine many times (though yours is very high)



  • Hi,

    yes, this is no bouncing issue. I'll try to rephrase / explain exactly what's happening.

    Scenario 1, sleep time 60*1000ms:

    • Node goes to sleep(60*1000)
    • I close and open contact for the first time (1) -> no wakeup
    • Wait for a second or so
    • Closing/Opening secord time (2) -> no wakeup
    • Waiting ... Closing/Opening third/fourth/fifth time
    • Now the node wakes up, gives me "Interrupted / Woken up by Pin" and a counter of 9 to 11 (slightly varying)

    In scenario 2 with longer sleep time of say 10 minutes, I have to close and open the contact/button for 30 to 40 times until the node wakes up, but then giving me the message that counter got incremented 77 times - which means the ISR got triggered that often, but the node did not resume into loop() all those wakeups. Why it decides to resume loop() at that point is beyond me. It should just resume loop() at the first ISR call.
    This seems so strange...

    I did another test, which I sure know is wrong usage of ISRs, but shows that the ISR is triggered: if I call loop() in my ISR (I know this is wrong...), every button press is immediately registered.



  • So I did some further testing. If I set the sleep(time) to

    1 Minute: I have to close the contact for 4 times before the ISR returns to loop(). Variable counter then contains 8.

    2 Minutes: The contact needs to be closed 8 times before the ISR retuns to loop(); counter is then 16 (= rising and falling change of each contact closure; also meaning there is absolutely no bouncing at all)

    4 Minutes: 16 contact closures are needed, counter as now expected gets up to 32.

    What's happening here??



  • Pin-change-interrupt is not supported by the Mysensors library. I have used some diodes my self when I need multiple external interrupt sources, but there is also a workaround here: https://forum.mysensors.org/topic/6352/pin-change-interrupt-on-any-pin



  • Exactly. I think the interrupt routine is executed, but it just doesn't wake up the micro.



  • Thanks guys!

    I've been experimenting with a small addition to the ISR

    ISR(PCINT1_vect)
    {
        _wokeUpByInterrupt = 0xFE;
        interrupted=true;
        isrcounter++;
    }
    
    

    for the last hour and am cautiously optimistic this might work. I believe that's also the main thought of the thread you linked to @olka , thanks for that! Will let you know if this works out in a stable fashion,

    Joost

    PS: using diodes is a great idea as well, but as my PCB is already finished and on my table right now I'd prefer getting around with a proper software solution 🙂 ...



  • This is the complete sketch:

    #include <Arduino.h>
    
    #define MY_RADIO_RFM95
    #include <MySensors.h>
    
    volatile int isrcounter = 0, timercounter=0;
    volatile char PinRegLast, ChangedPin, PinRegister;
    volatile unsigned long last_debounce_time=0;
    volatile boolean isr_interrupted;
    
    ISR(PCINT1_vect)
    {
        PinRegister = PINC;
        ChangedPin = PinRegister ^ PinRegLast;
        PinRegLast = PinRegister;
        _wokeUpByInterrupt = 0xFE;
        isr_interrupted=true;
        isrcounter++;
    }
    
    void setup()
    {
    noInterrupts();
    PCICR |= 0b00000011; // Enables Ports B and C Pin Change Interrupts
    PCMSK1 |= 0b00000110; //PCINT9 & PCINT10 = A1 & A2
    PCIFR  = B00000001; // Reset Interruptflag
    pinMode(A1, INPUT_PULLUP); pinMode(A2, INPUT_PULLUP);
    PinRegister = PINC;
    interrupts();
    Serial.begin(38400);
    }
    
    void presentation() {}
    
    void loop()
    {
      noInterrupts();
      if ( isr_interrupted ) {
        isr_interrupted = false;
        switch (ChangedPin){
        case (1 << PINC1):
          if (~PinRegister & (1 << PINC1)) { Serial.println("Interrupted / Woken up by Pin A1"); } 
          else Serial.println("Falling edge of Pin A1"); 
          break;
        case (1 << PINC2):
          if (~PinRegister & (1 << PINC2)) { Serial.println("Interrupted / Woken up by Pin A2"); } 
          else Serial.println("Falling edge of Pin A2"); 
          break;
        default:
          break;
        }
      }
      else  { Serial.println("Timer wake up / Loop"); timercounter++; }
      interrupts();
    
      Serial.print("  ");  Serial.print(" ISR Counter: ");Serial.print(isrcounter);  Serial.print("   Timer counter: "); Serial.println(timercounter);
      sleep((uint32_t) 60*1000);
    }
    

    Does anyone spot obvious mistakes in here?
    In basic testing this seems to work fine, though I haven't "fuzzed" it (like fast repeating button switching for example).



  • 
    
    // Enable debug prints to serial monitor
    #define MY_DEBUG
    
    // Enable and select radio type attached
    #define MY_RADIO_RF24
    #define MY_RADIO_RF24_PA_MAX
    // #define MY_RF24_CE_PIN 8 // used when Radio is wired non conventional
    // #define MY_RF24_CS_PIN 7 //
    #include <MySensors.h>
    
    #define MY_NODE_ID 10 // if this works you can create your own NODES
    #define SKETCH_NAME "Remote Control"
    #define SKETCH_MAJOR_VER "1"
    #define SKETCH_MINOR_VER "5"
    // define your children here
    #define PRIMARY_CHILD_ID 7
    #define SECONDARY_CHILD_ID 8
    #define PRIMARY_CHILD2_ID 9
    #define SECONDARY_CHILD2_ID 10
    // construct your button pins here for the analog inputs you have created
    const int PRIMARY_BUTTON3_PIN_OUT = 5;
    const int SECONDARY_BUTTON4_PIN_OUT = 7 ;
    // These are the primary and secondary main buttons
    #define PRIMARY_BUTTON_PIN 2   // Arduino Digital I/O pin for button/reed switch
    #define SECONDARY_BUTTON_PIN 3 // Arduino Digital I/O pin for button/reed switch
    // these are the 1ast 2 analog pin chosen
    #define PRIMARY_BUTTON3_PIN 4   // Arduino Digital I/O pin for button/reed switch
    #define SECONDARY_BUTTON4_PIN 6   // Arduino Digital I/O pin for button/reed switch
    
    #if (PRIMARY_BUTTON_PIN < 2 || PRIMARY_BUTTON_PIN >7)
    #error PRIMARY_BUTTON_PIN must be either 2 or 3 for interrupts to work
    #endif
    #if (SECONDARY_BUTTON_PIN < 2 || SECONDARY_BUTTON_PIN > 7)
    #error SECONDARY_BUTTON_PIN must be either 2 or 3 for interrupts to work
    #endif
    #if (PRIMARY_BUTTON_PIN == SECONDARY_BUTTON_PIN)
    #error PRIMARY_BUTTON_PIN and BUTTON_PIN2 cannot be the same
    #endif
    #if (PRIMARY_CHILD_ID == SECONDARY_CHILD_ID)
    #error PRIMARY_CHILD_ID and SECONDARY_CHILD_ID cannot be the same
    #endif
    // create you messages here
    // Change to V_LIGHT if you use S_LIGHT in presentation below
    MyMessage msg(PRIMARY_CHILD_ID, V_TRIPPED);
    MyMessage msg2(SECONDARY_CHILD_ID, V_TRIPPED);
    MyMessage msg3(PRIMARY_CHILD2_ID, V_TRIPPED);
    MyMessage msg4(SECONDARY_CHILD2_ID, V_TRIPPED);
    // ********************************************
    ISR (PCINT2_vect)
    {
      // handle pin change interrupt for D0 to D7 here
      if (PIND & bit (4))  // if it was high
        PORTD |= bit (5);  // turn on D5
      else
        PORTD &= ~bit (5); // turn off D5
    
      if (PIND & bit (6))  // if it was high
        PORTD |= bit (7);  // turn on D7
      else
        PORTD &= ~bit (7); // turn off D7
    }  // end of PCINT2_vect
    //*************************************************
    void setup()
    {
      // Setup the buttons
      pinMode(PRIMARY_BUTTON_PIN, INPUT_PULLUP);
      pinMode(SECONDARY_BUTTON_PIN, INPUT_PULLUP);
      //**********************************************************
      // pin change interrupt (D4)
      PCMSK2 |= bit (PCINT20);  // want pin 4
      PCIFR  |= bit (PCIF2);    // clear any outstanding interrupts
      PCICR  |= bit (PCIE2);    // enable pin change interrupts for D0 to D7
      pinMode(PRIMARY_BUTTON3_PIN, INPUT_PULLUP);
      pinMode (PRIMARY_BUTTON3_PIN_OUT, OUTPUT);
    
      // pin change interrupt (D6)
      PCMSK2 |= bit (PCINT22);  // want pin 6
      PCIFR  |= bit (PCIF2);    // clear any outstanding interrupts
      PCICR  |= bit (PCIE2);    // enable pin change interrupts for D0 to D7
      pinMode (SECONDARY_BUTTON4_PIN, INPUT_PULLUP);
      pinMode (SECONDARY_BUTTON4_PIN_OUT, OUTPUT);
    
    //**********************************************************************
    
    // Send the sketch version information to the gateway and Controller
    sendSketchInfo(SKETCH_NAME, SKETCH_MAJOR_VER "." SKETCH_MINOR_VER);
    
    // Register binary input sensor to sensor_node (they will be created as child devices)
    // You can use S_DOOR, S_MOTION or S_LIGHT here depending on your usage.
    // If S_LIGHT is used, remember to update variable type you send in. See "msg" above.
    present(PRIMARY_CHILD_ID, S_DOOR);
    present(SECONDARY_CHILD_ID, S_DOOR);
    present(PRIMARY_CHILD2_ID, S_DOOR);
    present(SECONDARY_CHILD2_ID, S_DOOR);
    }
    // Loop will iterate on changes on the BUTTON_PINs
    void loop()
    {
      uint8_t value;
      static uint8_t sentValue = 2;
      static uint8_t sentValue2 = 2;
    
      static uint8_t sentValue3 = 2;
      static uint8_t sentValue4 = 2;
      // short delay to allow buttons to properly settle
      delay(5);
    
      value = digitalRead(PRIMARY_BUTTON_PIN);
      if (value != sentValue) {
        // Value has changed from last transmission, send the updated value
        send(msg.set(value == HIGH));
        Serial.print("Value of Primary Button > ");
        Serial.println(digitalRead(value));
        sentValue = value;
      }
    
      value = digitalRead(SECONDARY_BUTTON_PIN);
      if (value != sentValue2) {
        // Value has changed from last transmission, send the updated value
        send(msg2.set(value == HIGH));
        Serial.print("Value of Secondary Button > ");
        Serial.println(digitalRead(value));
        sentValue2 = value;
      }
    
      value = digitalRead(PRIMARY_BUTTON3_PIN);
      if (value != sentValue3) {
        send(msg3.set(value == HIGH));
        Serial.print("Value of Primary Button 3 > ");
        Serial.println(digitalRead(PRIMARY_BUTTON3_PIN_OUT));
        sentValue3 = value;
      }
    
      value = digitalRead(SECONDARY_BUTTON4_PIN);
      if (value != sentValue4) {
        send(msg4.set(value == HIGH));
        Serial.print("Value of Secondary Button 4 > ");
        Serial.println(digitalRead(SECONDARY_BUTTON4_PIN_OUT));
        sentValue4 = value;
      }
      // Sleep until something happens with the sensor
      sleep(PRIMARY_BUTTON_PIN - 2, CHANGE, 0, SECONDARY_BUTTON_PIN - 2, CHANGE, 0);
        }
    


  • Thanks, will recap your code for identifying the pressed button, seems pretty concise.
    Though this code in general will in my experience not work together with a "timed" sleep() - you have sleep(...,0), and my goal is to get PCInterrupts and Timer interrupts working together.

    After some ongoing testing, the sketch I posted above works in achieving both.
    Now I still have to figure out a way how to sleep() for the remaining sleep interval after the node was awoken with a button press /PCInt (say, I want a sensor reading strictly every 4 hours (sleep(4 * 60 * 60 * 1000), but wake the node after 90minutes with a button press. Now I'd need a way to figure out the remainder of the sleep interval (2.5h)).
    Thanks,

    Joost



  • @Joost RTC?



  • @zboblamont Ah, ok.
    To be honest, I had hoped there'd be a counter/register (for the timer interrupt - how does the Arduino keep track of itself otherwise?) I could read and then sleep() for the remainder...

    I'll browse around later and see if I can dig up something on the web.
    Thanks!



  • How does MySensors accomplish long sleep() times? Is it like
    sleep() with Watchdog Timer set for 8s -> calculate remainder -> sleep() again?
    If so, I could maybe find the var in which MySensors itself keeps track of sleep time.



  • @Joost There has been an attempt to do this a few years ago. I don't know if they ever got it working though: https://forum.mysensors.org/topic/7197/sleep-time-and-external-interrrupts

    Besides that, you could request the current time from the controller on interrupts and calculate the remaining sleep duration from there or, as mentioned before, use an external RTC.


  • Mod

    @Joost I implemented a way to get the remaining sleep time, albeit only roughly on AVR: https://forum.mysensors.org/topic/9595/interrupted-sleep/11



  • @Joost My only thought on the RTC is separation of the timing aspect allowing the processor to go deep-sleep forever until Interrupt. I have one set up to hourly scan my water tank, but I can press a button in parallel with the interrupt to ground for ad-hoc update.



  • @zboblamont Thanks, will think about that if a pure software approach fails! Though I'd really like to keep complexity down; I'm planning on fabricating at least 10-20 nodes of that kind (by hand).



  • @Yveaux Thanks, very cool and promising, will take a more in depth look tomorrow!
    I just threw that in my code quickly tonight, so far I got a 5 digit number back, but that was always the same also with different wake up delays via PCInt after going to sleep().


  • Mod

    @Joost as documented it can be roughly 8 seconds off on avr. When an avr is set to sleep for 8 seconds (the maximum watchdog sleep time) and it is woken by an interrupt, it will not know how much time is remaining and always return the same time remaining.
    Try sleeping for a minute, then interrupt after 1 second and after half a minute. The remaining time should be different.



  • @Yveaux said in Getting Pin Change Interrupts working together with Timer interrupts / sleep(XX ms) on Arduino Pro Mini:

    @Joost as documented it can be roughly 8 seconds off on avr. When an avr is set to sleep for 8 seconds (the maximum watchdog sleep time) and it is woken by an interrupt, it will not know how much time is remaining and always return the same time remaining.
    Try sleeping for a minute, then interrupt after 1 second and after half a minute. The remaining time should be different.

    @Yveaux Hi, yeah, I was already expecting the 8s inaccuracy (which is no problem at all, I am planning to cover intervals of several hours).
    I tried yesterday night with sleep times of 1-5 minutes and could not get it to work at that point (always receiving identical numbers though the time to awakening the node with PCInt differed), but this might be because I just hacked it in an otherwise more complicated sketch.

    I'll start now with making up a minimal test sketch to get the hang of it!
    Thanks, J



  • @Yveaux & others:
    So this works great! Here is an example sketch to get Pin Change Interrupts working together with timer-based sleep(). There will be identification of the triggered pin as well as discrimination between rising and falling edge. To test, just add a jumper wire to pin A1 or/and A2 to a Arduino Pro Mini and short it to Ground to wake the Arduino out of a timed sleep() (a connected Radio, here a RFM95, is needed to get the sketch started).

    #include <Arduino.h>
    
    #define MY_RADIO_RFM95
    #include <MySensors.h>
    
    volatile int isrcounter = 0, timercounter=0;
    volatile char PinRegLast, ChangedPin, PinRegister;
    volatile boolean isr_interrupted;
    uint32_t remainingSleepTime = 0;
    
    ISR(PCINT1_vect)
    {
        PinRegister = PINC;
        ChangedPin = PinRegister ^ PinRegLast;
        PinRegLast = PinRegister;
        _wokeUpByInterrupt = 0xFE;
        isr_interrupted=true;
        isrcounter++;
    }
    
    void setup()
    {
    noInterrupts();
    PCICR |= 0b00000011; // Enables Ports B and C Pin Change Interrupts
    PCMSK1 |= 0b00000110; //PCINT9 & PCINT10 = A1 & A2
    PCIFR  = B00000001; // Reset Interruptflag
    pinMode(A1, INPUT_PULLUP); pinMode(A2, INPUT_PULLUP);
    PinRegister = PINC;
    interrupts();
    Serial.begin(38400);
    }
    
    void presentation() {}
    
    void loop()
    {
      noInterrupts();
      if ( isr_interrupted ) {
        isr_interrupted = false;
        switch (ChangedPin){
        case (1 << PINC1):
          if (~PinRegister & (1 << PINC1)) { 
            Serial.print("Interrupted / Woken up by Pin A1, remaining sleep() time: ");  
            remainingSleepTime = getSleepRemaining();
            Serial.println( remainingSleepTime );}
          else { 
            Serial.println("Falling edge of Pin A1"); 
           } 
          break;
        case (1 << PINC2):
          if (~PinRegister & (1 << PINC2)) { 
            Serial.print("Interrupted / Woken up by Pin A2, remaining sleep() time: "); 
            remainingSleepTime = getSleepRemaining();
            Serial.println( remainingSleepTime );}
          else { 
            Serial.println("Falling edge of Pin A2"); 
           }
          break;
        default:
          break;
        interrupts();
        }
      }
      else  { 
        interrupts();
        Serial.println("Timer wake up / Loop"); 
        timercounter++; 
        }
    
      Serial.print("  ");  Serial.print(" ISR Counter: ");Serial.print(isrcounter);  Serial.print("   Timer counter: "); Serial.println(timercounter);
      sleep((uint32_t) 60*1000);
    }
    

    Thanks a lot! Joost



  • @Joost I like your solution to be able to use other pins as pin change interrupts together with Timer interrupts in the MySensors library.

    I did use the default sleep function of MySensors with the foreseen D1 and D2 pins where D1 is already taken by the radio, so only D2 is usable for custom use.
    See https://www.mysensors.org/download/sensor_api_20#sleeping



  • I think this is the same thing as you were trying to accomplish.

    Sleep for 6 minutes or when interrupted. I have a reed switch on pin 2

    pinMode(REED_SWITCH, INPUT);   // set the reed switch digital pin as input
    //digitalWrite(REED_SWITCH, HIGH);
    pinMode(REED_SWITCH, INPUT_PULLUP);
    
    
    sleep(digitalPinToInterrupt(REED_SWITCH), CHANGE, 360000); //3600000 hour
    

Log in to reply
 

Suggested Topics

49
Online

11.4k
Users

11.1k
Topics

112.6k
Posts