0

I have written a program for a kitchen timer. The timer uses just one single rotary encoder with a push button on its axis. It has modes for counting up and for counting down. After counting down it switches into alarm state. When I then push the button the alarm stops and the device goes into sleep mode. To wake it up I need to either push the button again or twist the encoder.

The problem: When I push the button during alarm the device does go into sleep mode but since that very same button is used to wake it up again, the timer switches into the wrong state and freezes in sleep mode. At least that is my interpretation of the problem:

Pushing the button during alarm triggers sleep mode ( goToSleep() ). During goToSleep I attach to that very button the interrupt that wakes the device.

The solution must be simple, I just can't see it. I just need to ignore any button input beween calling goToSleep() and sleepcpi();. Right?

How can I make this work?

#include <avr/sleep.h>
#include <Rotary.h>
#include <Bounce2.h>

unsigned long timerSeconds = 0;   // Remaining countdown time in seconds. An unsigned int would be enough for more than 18 HOURS!
unsigned long alarmTime = 0;      // The absolute point in time of the alarm

// Variables for updates
unsigned long currentMillis = 0;
unsigned long previousMillis = 0;
unsigned long currCountMillis = 0;
unsigned long prevCountMillis = 0;

// Setup Pins & Variables
Rotary rotary = Rotary(3, 4);     // Needs to be pin 2 or 3 in order to use as interrupt
const byte encBtnPin = 2;         // Needs to be pin 2 or 3 in order to use as interrupt
const byte beepPin = 5;

byte encBtnState;
byte prevEncBtnState;
bool encBtnPushed = false;
Bounce debouncer = Bounce();

// Setup state machine
typedef enum { NONE, SETTING, COUNTING_DOWN, COUNTING_UP, WAS_COUNTING, ALARM } states;
states state = NONE;

void setup() {
  Serial.begin(9600); //remove after testing

  pinMode(encBtnPin, INPUT_PULLUP);
  debouncer.attach(encBtnPin);
  debouncer.interval(10);
  pinMode(beepPin, OUTPUT);

  // Turn off ADC
  ADCSRA = 0; 
}

void loop() {
            Serial.println("Top of loop"); // Remove after testing
            Serial.print("State: "); // Remove after testing 
            Serial.println(state); // Remove after testing
  switch (state) {
    case NONE:
      goToSleep();
      break;
    case SETTING:
      setCountdown();
      break;
    case COUNTING_DOWN:
      startCountdown();
      break;
    case COUNTING_UP:
      startCounter();
      break;
    case ALARM:
      alarm();
      break;
  }
}

void readEncButton() {
  encBtnState = digitalRead(encBtnPin);     // Check if button state has changed
  if (encBtnState != prevEncBtnState) {
    if (encBtnState == LOW) {
      encBtnPushed = true;
      beep();
    }
  }
  prevEncBtnState = encBtnState;

  if (encBtnPushed) {                     // If Button has been pushed ...
    encBtnPushed = false;                 // ... reset button and do one of the following ...
    switch (state) {                      // Core state machine (state changes)
      case NONE:  
        break;
      case SETTING:
        if (timerSeconds != 0) {          // Start countdown only if timer is not = 0 ...
          state = COUNTING_DOWN;
              Serial.print("State: "); // Remove after testing 
              Serial.println(state); // Remove after testing
        }
        else {                            // If timer = 0, go to sleep
          state = NONE;
              Serial.print("State: "); // Remove after testing 
              Serial.println(state); // Remove after testing        
        }
        break;
      case COUNTING_DOWN:
        timerSeconds = (timerSeconds / 10) * 10;        // Set last digit to zero ...
        state = SETTING;                                // ... then go back to setting time
        break;
      case COUNTING_UP:
        timerSeconds = 0;
        state = SETTING;
        break;
      case ALARM:
        state = NONE;
        break;
    }
  }
}

void setCountdown() {
  // Read rotary encoder input
  unsigned long timeOut = millis();
  while (state == SETTING) {
    unsigned char result = rotary.process();
    if (result == DIR_CW) {
      timerSeconds += 10L;
      timeOut = millis();
      writeDisplay();     
    } 
    else if (result == DIR_CCW) {
      timerSeconds -= 10L;
      timeOut = millis();
      writeDisplay();  
    }
    if ( millis() >= timeOut + 30000L ) {         // Go to sleep after 30 seconds idle
      state = NONE;
    }
    // Read rotary encoder button
    readEncButton();
  }
}

void startCountdown() {  
  alarmTime = millis() + (timerSeconds * 1000L);  // Determine absolute alarm point in time in milliseconds (only once on countdown start)
  while (state == COUNTING_DOWN) {
    updateDisplay();
    timerSeconds = ( alarmTime - millis() ) / 1000L;            // Calculate new remaining number-of-seconds
    readEncButton();
  }
}

void startCounter() {
  while (state == COUNTING_UP) {
    currCountMillis = millis();
    updateDisplay();
    if (currCountMillis - prevCountMillis > 999) {              // Update counter once per second
      prevCountMillis = currCountMillis;
      timerSeconds ++;
    }
    readEncButton();
  }
}

void updateDisplay () {  
  currentMillis = millis();

  // Only once every 1000ms ...
  if (currentMillis - previousMillis > 999) {
    previousMillis = currentMillis;                           // Reset the timer for display updates
    writeDisplay();
  }
}

void writeDisplay() {
  int cHours = timerSeconds / 3600L;                        // Update variables to display
  int cMinutes = timerSeconds % 3600L / 60;
  int cSeconds = timerSeconds % 3600L % 60;
  // Format and print display data
  if (cHours == 0 || cHours < 10) {Serial.print("0");}      // Add leading "0" if necessary
  Serial.print(cHours);
  Serial.print(":");
  if (cHours == 0 || cMinutes < 10) {Serial.print("0");}    // Add leading "0" if necessary
  Serial.print(cMinutes);
  Serial.print(":");
  if (cSeconds == 0 || cSeconds < 10) {Serial.print("0");}  // Add leading "0" if necessary
  Serial.println(cSeconds);
  // Alarm trigger follows here since alarm is supposed to sound AFTER the nillth second has been displayed
  if (state == COUNTING_DOWN && timerSeconds == 0) {
    state = ALARM;
  }
}

void beep() {
  tone(beepPin, 1000, 120);
}

void alarm() {
  int alarmRep = 0;
  attachInterrupt(digitalPinToInterrupt(encBtnPin), endAlarm, LOW);  
  while (state == ALARM && alarmRep < 30) {
    for (int i = 0; i < 4; i ++) {
      tone(beepPin, 2000, 100);
      delay(120);
    }
    delay(500);
    alarmRep ++;
  }
  goToSleep();          // Go to sleep after alarm (state is changed to NONE in sleepmode-setup)
  //state = NONE;       // Need condition to prevent state = NONE in case previous while-loop was left due to state change 
}

void endAlarm() {
  detachInterrupt(digitalPinToInterrupt(encBtnPin));
  state = NONE;
}

/* Sleep mode and stuff *
************************/

void wakeToSet() {
  sleep_disable();
  detachInterrupt(digitalPinToInterrupt(3));          // Hard-coded to pin 3
  detachInterrupt(digitalPinToInterrupt(encBtnPin));  // Encoder button can no longer wake up CPU
  state = SETTING;
}

void wakeToCount() {
  sleep_disable();
  Serial.println("Detaching interrupts ...");         // Remove after testing
  detachInterrupt(digitalPinToInterrupt(3));          // Hard-coded to pin 3
  detachInterrupt(digitalPinToInterrupt(encBtnPin));  // Encoder button can no longer wake up CPU
  beep();
  Serial.println("Waking up to count ...");           // Remove after testing
  state = COUNTING_UP;
}

void goToSleep() {
  Serial.println("Starting goToSleep()");             // Remove after testing
  timerSeconds = 0;                                   // Reset everything
  alarmTime = 0;
  state = NONE;
  sleep_enable();
  attachInterrupt(digitalPinToInterrupt(3), wakeToSet, LOW);             // Hard-coded to pin 3
  attachInterrupt(digitalPinToInterrupt(encBtnPin), wakeToCount, LOW);   // Encoder button can wake up machine
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  Serial.print("State: "); // Remove after testing 
  Serial.println(state); // Remove after testing
  Serial.println("Going to sleep now..."); // Remove after testing
  delay(250);
  sleep_cpu();
}
  • 1
    You need to change to the NONE state on button release, instead of press. Of have the goToSleep function wait till the button is released (while(dititalRead(encBtnPin)==LOW){delay(10);}). I used a PRE-SLEEP state in one of my projects. Then when the button is released and you are in the PRE-SLEEP state, move to the SLEEP (NONE) state. – Gerben Feb 18 at 16:50
  • 1
    Thank you. That one line (delay) fixed the whole thing. Thanks. I'll keep the PRE-SLEEP state in mind for future projects. – fertchen Feb 24 at 15:51
-1

couple of thoughts: a fritzing Diagram of your Connections may help. as far as i can tell you connect the (?data?) lines of the Encoder to the Interrupt lines? do you want to trigger an Interrupt on Input value Change? like from 4 minutes to 5minutes? did you Thing of debouncing your switches? (just saw you do it in Software... consider a simple RC lowpass instead. Maybe the debouncing uses a timer and then collides with the remaining logic...) do you really Need to now the previous Button state? another Thing i see is you just have an awful lot of Code. consider taking out a piece of paper and draft a state Diagram for the statemachine your About to realise. and then share it here (makes life a lot easier!)

  • Thank you for your time looking into this. To reply to your questions in order: 1) Trigger an interrupt on input value change? Yes. 2) No. Only during sleep mode. 3) yes but this solution seems easier to implement. 4) Yes, I really need to know. state diagram: I'll do it next time it is necessary (I hope not). Thank you. – fertchen Feb 24 at 15:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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