2

I've written code for an Arduino Nano that when a button is pressed, it sends an IR command. It is set so that if no button press has been received for a period of 10 seconds, it will enter sleep mode until a button press is received. The button to send the IR command is the same button being used to send the wake up interrupt (button connected to pin D2). When testing the code the Arduino is able to go to sleep and wake up from the interrupt 1-3 times before it will suddenly receive an interrupt signal just before entering sleep mode. When this happens it gets stuck in sleep mode and will no longer accept the wake up interrupt. I am currently using Serial Monitor outputs and the built-in LED to determine when the Arduino is in sleep mode or not.

#include <IRLib.h>
#include <avr/sleep.h>
#define interruptPin 2

IRsend My_Sender;

const int button = 2;
int buttonState = 0;
int timeSinceCommand = 0;
const unsigned long eventInterval = 1000;
unsigned long previousTime = 0;

void setup() {
  Serial.begin(9600);
  pinMode(button, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);         //delete once code finalized and working
  pinMode(interruptPin, INPUT_PULLUP);
  digitalWrite(LED_BUILTIN, HIGH);      //delete once code finalized and working
  Serial.println("---------------------------New Run---------------------------");
}

void loop() {
  while (timeSinceCommand < 10) {
    unsigned long currentTime = millis();
    buttonState = digitalRead(button);
    if (buttonState == HIGH) {
      My_Sender.send(SONY, 0xC90, 12);
      timeSinceCommand = 0;
      previousTime = currentTime;
    }
    if (currentTime - previousTime >= eventInterval) {
      timeSinceCommand++;
      previousTime = currentTime;
    }
  }
  if (timeSinceCommand >= 10) {
    Going_To_Sleep();
    timeSinceCommand = 0;
  }
}

void Going_To_Sleep() {
  sleep_enable();
  attachInterrupt(0, wakeUp, RISING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  digitalWrite(LED_BUILTIN, LOW);             //delete once code finalized and working
  Serial.println("Arudino going to sleep!");  //delete once code finalized and working
  delay(1000);                                //delete once code finalized and working
  sleep_cpu();
  Serial.println("Arudino has waken up!");    //delete once code finalized and working
  digitalWrite(LED_BUILTIN, HIGH);            //delete once code finalized and working
}

void wakeUp() {
  Serial.println("Interrupt received!");      //delete once code finalized and working
  sleep_disable();
  detachInterrupt(0);
}

Here is the Serial Monitor output from a couple of tests:

---------------------------New Run---------------------------
Arudino going to sleep!
Interrupt received!
Arudino has waken up!
Arudino going to sleep!
Interrupt received!
Arudino has waken up!
Interrupt received!
Arudino going to sleep!
---------------------------New Run---------------------------
Arudino going to sleep!
Interrupt received!
Arudino has waken up!
Interrupt received!
Arudino going to sleep!
2
  • 1
    Try clearing the interrupt flag before executing attachInterrupt(). For the pin 2 external interrupt on a Uno class board it will be something like: EIFR = bit (INTF0);
    – 6v6gt
    Commented Jan 20, 2023 at 7:31
  • I copied that line of code and put it right before attachInterrupt() and it seems to have fixed my issue. Everything is working properly now. Thanks!
    – 99natmar99
    Commented Jan 20, 2023 at 20:20

2 Answers 2

2

There are a couple of tricky things going on here... First, you should be aware of a few facts:

  1. Buttons are prone to mechanical bounce. You press a button once, and quite often the microcontroller senses multiple transitions on the input pin.

  2. When INT0 is configured to sense rising edges, any rising edge on the pin will set the INTF0 interrupt flag, whether the interrupt is enabled or not. Once the flag is set, it will fire an interrupt request as soon as the interrupt is enabled.

  3. When the CPU receives an interrupt request, it finishes executing the current instruction before servicing the request.

With that in mind, here is an explanation of what goes on:

When you wake up the Arduino with the push button, it often triggers multiple signal transitions. The first rising edge wakes up the Arduino and triggers the interrupt. The second rising edge only sets the INTF0 interrupt flag: the interrupt is “on hold”, because interrupts are not themselves interruptible. However, before exiting, wakeUp() disables servicing that interrupt, which means the flags just stays set. It is a pending interrupt of sorts.

Some time later, Going_To_Sleep() is called again. attachInterrupt() enables the interrupt and, since the flag is still set, the (pending) interrupt fires immediately. That's when you see the message “Interrupt received!” printed right before the Arduino goes to sleep. The problem is that the interrupt is disabled by wakeUp(). One second later, sleep_cpu() puts the CPU to sleep, and now our wake-up source is disabled, so the Arduino is just locked into its dreams.

There is still one issue with this scenario. The interrupt handler supposedly disabled sleeping, so how come the Arduino went to sleep anyway? The answer is that sleeping was accidentally re-enabled by set_sleep_mode() because of a data-race. Here is a disassembly of set_sleep_mode():

in   r24, 0x33 ; read SMCR (Sleep Mode Control Register) into r24
andi r24, 0xF1 ; clear the Sleep Mode Select bits
ori  r24, 0x04 ; select the Power Down mode
out  0x33, r24 ; write back SMCR

The first instruction in this sequence comes right after the instruction in attachInterrupt() that enables the INT0 interrupt. When that instruction generates an interrupt request (because the interrupt flag was already set), the interrupt request reaches the CPU while it is already executing the in instruction above, and this instruction is completed before servicing the interrupt request. This means that we have now in register r24 a copy of the Sleep Mode Control Register from before the interrupt is serviced. In this copy, the SE bit (Sleep Enable) is set. Even though wakeUp() clears that bit in the actual SMCR register, it stays set in our copy in r24. Then, the out instruction re-enables sleeping. I experimented putting a single nop instruction right before set_sleep_mode() and that was enough to prevent the data race.

So here you have an explanation of the observed behavior. I may come up with some suggestions for fixing the problem, but it need to take some sleep before that (do not worry, I will make double sure I have one interrupt source enabled before executing sleep).

1
  • 1
    Nice explanation especially about the data race risk which I hadn't thought about. When I use sleep mode, with wakeup on either external or pin change interrupt, I usually, if compatible with the application, (a) have an empty interrupt service routine and (b) don't detach the interrupt which is anyway attached in setup(). The sleep_disable() command immediately follows the sleep_cpu() command then any other code to respond to the wake up.
    – 6v6gt
    Commented Jan 21, 2023 at 5:08
1

I'm taking this opportunity to include a simple basic sleep sketch which eliminates the possible race condition risk that Edgar Bonet identified in the standard approach to using the sleep mode for AVR class MCUs. See the reference in the sketch to further optimise, if required, to switch off the ADC, Brown out detection etc.

/*
   Sleep mode demonstration (Uno/Nano ATmega328P)
   based on https://www.gammon.com.au/power Sketch J 
   but stripped down for demonstration purposes to exclude some optimisation (BOD and ADC etc.)
   See also: https://www.nongnu.org/avr-libc/user-manual/group__avr__sleep.html

   Connect a button switch between pin 2 and ground
   Press the button. It wakes, blinks the led and then sleeps.

*/

#include <avr/sleep.h>

const byte LED = 13;  // built in led

void wake()
{
  // nothing here. All done in the loop
}

void setup()
{
  Serial.begin( 115200 ) ;
  pinMode( LED, OUTPUT );
  pinMode( 2, INPUT_PULLUP ) ; 
  attachInterrupt ( digitalPinToInterrupt(2), wake, FALLING);
  set_sleep_mode (SLEEP_MODE_PWR_DOWN);
}

void loop()
{
  Serial.println( "waking . . . ") ;

  // your own code could go here instead 

  for ( int i = 0; i < 4; i++ ) {
    digitalWrite (LED, HIGH);
    delay (50);
    digitalWrite (LED, LOW);
    delay (50);
  }
  delay(3000) ;
  

  Serial.println( "Sleeping. . . " ) ;
  Serial.flush() ;
  delay(20) ; // allow message to be written

  sleep_mode();
  // *** we wake here ***
}
2
  • 1
    Note that the calls to sleep_enable(), sleep_cpu() and sleep_disable() can be replaced by a single call to sleep_mode(). The three functions you used are most useful when you want to atomically test some condition before going to sleep. Commented Jan 22, 2023 at 15:26
  • @EdgarBonet. Good point. I've updated to include these because the objective was to provide a "simple as possible" but working example.
    – 6v6gt
    Commented Jan 23, 2023 at 12:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.