Rotary encoders - to interrupt or not?

hi all,

So i have a project that is going to be a part of much larger project, this includes on rotary encoder used to set up wake/sleep time of the 7 segment displays.
How it works: when encoder button is pressed i get 8 numbers on 2 7 segment 4 digit displays, when button is pressed again first 2 numbers from the left are active, i then rotate the encoder to set up “sleep start hours”, once done button is pressed again which then activates second 2 digits from the left “sleep start minutes”… next is “sleep end hours” and “sleep end minutes”. Final click of a button then exits the settings display and goes back to clock, values are then saved in EEPROM and i have my sleep/wake timer active. This all works as intended but after reading a lot about encoders, i am still not sure if i should use interrupts or not.

The way i have it now, both encoder decoding and writing the values on the display are done in the main loop, which i think is waste of resources and controller time. If i move to using interrupts, then encoder decoding would be moved outside of the main loop but i would still have to set values on the display inside the main loop…

the questions is, would this be a good approach/way to do it?

Also, as you can notice, i am using azButton to count button presses… is there any other simpler and easier way?

My main sketch code is attached as it was too long to post as code:

Encoder interrupt code i was thinking to implement, from arduino playground, written by “rafbuff”
https://playground.arduino.cc/Main/RotaryEncoders/#Example13

My original sketch works like a charm, no encoder position skips, it counts both direction accurately. I am also using RC filter for the encoder (10k+0.1uF).

Many thanks for any tips,
Alek

code.ino (9.15 KB)

Always use interrupts for quadrature encoders, unless the process is utterly trivial.

Manually rotated encoders don't require interrupt handling. Is debouncing required?

Thanks guys!
two posts above are great example of different opinions :slight_smile:

As for debouncing, i have H/W RC filter which seems to do the job.

I am now trying to incorporate the interrupt code into my skecth but having issues…

enum PinAssignments {
  encoderPinA = 2,   // right
  encoderPinB = 3,   // left
};

volatile unsigned int encoderPos = 0;  // a counter for the dial
unsigned int lastReportedPos = 1;   // change management
static boolean rotating = false;    // debounce management

// interrupt service routine vars
boolean A_set = false;
boolean B_set = false;


void setup() {

  pinMode(LED, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(CLOCK,INPUT);
  pinMode(DATA,INPUT); 



  pinMode(encoderPinA, INPUT);
  pinMode(encoderPinB, INPUT);
  digitalWrite(encoderPinA, HIGH);
  digitalWrite(encoderPinB, HIGH);
  // encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);
  // encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);
}
void loop() {

tasksEverySecond();

button.loop(); // MUST call the loop() function first
unsigned long count = button.getCount();





    
if (count>0) {
  
  encoder_alarm();}
}

//==============================================================================
// tasksEverySecond
//==============================================================================
void tasksEverySecond()
{
  if (second() != previousSecond)
  {
    previousSecond = second();

    displayRtcTime();
    ledTest();
  }
}

//==============================================================================
// ENCODER ALARM
//==============================================================================

void encoder_alarm(){

  button.loop(); // MUST call the loop() function first
  unsigned long count = button.getCount();

    // reset button count when we cycle through all digit groups and set an alarm
    if (count > 6) {
    button.resetCount();
    digitalWrite(LED, LOW);
  } 

  //--------------------- HOURS alarm STARTS at ------------------------//
  
  if (count == 2) {

    
    if (lastReportedPos != encoderPos) {
      if (encoderPos>23) {(encoderPos = 0);}
      if (encoderPos<0) {(encoderPos = 23);}

      tensofhours = encoderPos / 10;
      singlehours = encoderPos % 10; 

      lastReportedPos = encoderPos;
  }
      EEPROM.update(2,encoderPos);

  }

the issues is, my counter goes to 23 and then skips to 0 as supposed to do, but when counting down to zero, it does not jump to 23 as supposed to, i guess because of this variable
“volatile unsigned int encoderPos = 0;”

Also, on every arduino reset, once i press the button to start setting up first 2 digits, they re-set to zero… while they should keep the last saved value.

Again, as said my original code works like a charm but figured i should move to interrupts as that seems to be the way, or did i get that wrong?

Many thanks,
Alek

volatile unsigned int encoderPos = 0;  // a counter for the dial
      if (encoderPos < 0)

An unsigned integer can never be less than zero

UKHeliBob:

volatile unsigned int encoderPos = 0;  // a counter for the dial
      if (encoderPos < 0)

An unsigned integer can never be less than zero

Yes, figured it out and changed to “signed”
Now, any clues why my counter resets every time i reset arduino, that is, it stays the same on the display but if last value was 15, once i hit encoder it jumps to 0 and starts from there…
In original sketch, if last saved value was 15, once i hit encoder it starts from 15 and goes up or down depending on rotation direction. everything else seems ok for now.
many thanks,
Alek

Ok, figured it out, obvious mistake as i was no calling the correct value from EEPROM, thats why the value kept resetting to 00.

now, next issue, i have FOUR settings to change:

counter_hour_start
counter_minute_start
counter_hour_end
counter_minute_end

in the interrupt code i now have one value set to change "counter_hour_start":

// Interrupt on A changing state
void doEncoderA() {
  // debounce
  if ( rotating ) delay (1);  // wait a little until the bouncing is done

  // Test transition, did things really change?
  if ( digitalRead(encoderPinA) != A_set ) { // debounce once more
    A_set = !A_set;

    // adjust counter + if A leads B
    if ( A_set && !B_set )
      counter_hour_start += 1;

    rotating = false;  // no more debouncing until loop() hits again
  }
}

// Interrupt on B changing state, same as A above
void doEncoderB() {
  if ( rotating ) delay (1);
  if ( digitalRead(encoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if ( B_set && !A_set )
      counter_hour_start -= 1;

    rotating = false;
  }
}

not sure how do i add other values, without copying the entire interrupt code...

Many thanks,
Alek

Which Arduino board are you using ?

UKHeliBob:
Which Arduino board are you using ?

Nano V3.0 for this test setup, final project would be on Mega2560, and since i already use INT0 on pin2 for the final project, i was planning to use pins 18 and 19 on MEGA board.
Now using pins 2 and 3 on NANO.
Thanks,
Alek

got it working with other settings now BUT, i have a feeling this is very very dirty solution. Also, i had to add ezButton callout into the interrupt as it asks for it wherever you need to call the button count.

// Interrupt on A changing state
void doEncoderA() {

  button.loop(); // MUST call the loop() function first
  unsigned long count = button.getCount();
  
  // debounce
  if ( rotating ) delay (1);  // wait a little until the bouncing is done

  // Test transition, did things really change?
  if ( digitalRead(encoderPinA) != A_set ) { // debounce once more
    A_set = !A_set;

    // adjust counter + if A leads B
    if (( A_set && !B_set ) && (count == 2))
      counter_hour_start += 1;
    if (( A_set && !B_set ) && (count == 3))
      counter_minute_start += 1;
  
    rotating = false;  // no more debouncing until loop() hits again
  }
}
// Interrupt on B changing state, same as A above
void doEncoderB() {

  button.loop(); // MUST call the loop() function first
  unsigned long count = button.getCount();
  
  if ( rotating ) delay (1);
  if ( digitalRead(encoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if (( B_set && !A_set ) && (count == 2))
      counter_hour_start -= 1;

    if (( B_set && !A_set ) && (count == 3))
      counter_minute_start -= 1;

    rotating = false;
  }
}

This is pretty much as far as my knowledge on this goes. any hints or tips would be highly appreciated.

Many thanks,
Alek

elcrni:
now, next issue, i have FOUR settings to change:

in the interrupt code i now have one value set to change “counter_hour_start”:

not sure how do i add other values, without copying the entire interrupt code…

Arrays*. Make an array to hold the various timeValues. Use count as a selector to index into the array.

Make the ISR return a value for CW/CCW rotation/idle. If in set mode and rotation sensed, inc/dec the array value that count is currently pointing you to.

*IDE reference page

dougp:
Arrays*. Make an array to hold the various timeValues. Use count as a selector to index into the array.

Make the ISR return a value for CW/CCW rotation/idle. If in set mode and rotation sensed, inc/dec the array value that count is currently pointing you to.

*IDE reference page

Was this a question or a solution? hahahahah now i understand how my dog feels when i talk to him. Jokes aside, i know how to do it as in the example above, in my last post... but thats where my skill hangs
Thanks,
Alek

It's easier when the thing is broken into pieces. Make a test/experiment sketch to do nothing but get the values you need from the encoder. Use that sketch as a basis for one which can increment/decrement a variable - and display it. Make another to continually increment counter and display its value on the serial monitor. Make yet another that uses counter to do nothing but display some values you preloaded into an array.

And so on and etc. As you get a piece working incorporate it into the project.

again, i got it all working, i am just not sure this is the way to do it, as in the code bellow.

#include <EEPROM.h>
#include <LedControl.h>
#include <ezButton.h>
#include <TimeLib.h>
#include <RTClib.h>
#include <OneWire.h>
#include <DS1307RTC.h>
#define MAXIMCCLD 12         // output - CS/LOAD
#define MAXIMCCCLK 11        // output - CLOCK
#define MAXIMCCDATA 10       // output - DATA    
#define DS1307_I2C_ADDRESS 0x68 // define the RTC I2C address
ezButton button(7);  // create ezButton object that attach to pin 7;
LedControl MaximCC=LedControl(MAXIMCCDATA, MAXIMCCCLK, MAXIMCCLD, 1); // Define pins for Maxim 72xx and how many 72xx we use
int LED = 5;
int LED2 = 9;
volatile signed int counter_hour_start = EEPROM.read(2);
volatile signed int counter_minute_start = EEPROM.read(3);
volatile signed int counter_hour_end = EEPROM.read(4);
volatile signed int counter_minute_end = EEPROM.read(5);

// usually the rotary encoders three pins have the ground pin in the middle
enum PinAssignments {
  encoderPinA = 2,   // right
  encoderPinB = 3,   // left
};
  // a counter for the dial
signed int lastReportedPos_hour_start = 1;   // change management
signed int lastReportedPos_minute_start = 1;   // change management
signed int lastReportedPos_hour_end = 1;   // change management
signed int lastReportedPos_minute_end = 1;   // change management
// interrupt service routine vars
boolean A_set = false;
boolean B_set = false;
int previousSecond = 0;
int tensofhours, singlehours, tensofminutes, singleminutes,tensofhours1, singlehours1, tensofminutes1, singleminutes1;

//==============================================================================
// SETUP
//==============================================================================
void setup() {
  pinMode(LED, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(encoderPinA, INPUT);
  pinMode(encoderPinB, INPUT);
  // encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);
  // encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE); 
  MaximCC.shutdown(0,false);
  MaximCC.setIntensity(0,15);
  MaximCC.clearDisplay(0);
  button.setDebounceTime(50); // set debounce time to 50 milliseconds
  button.setCountMode(COUNT_FALLING);
  setSyncProvider(RTC.get); // the function to get the time from the RTC
  tensofhours = counter_hour_start / 10;
  singlehours = counter_hour_start % 10;
  tensofminutes = counter_minute_start / 10;
  singleminutes = counter_minute_start % 10;
  tensofhours1 = counter_hour_end / 10;
  singlehours1 = counter_hour_end % 10;
  tensofminutes1 = counter_minute_end / 10;
  singleminutes1 = counter_minute_end % 10; 
}
//==============================================================================
// LOOP
//==============================================================================

void loop() {
tasksEverySecond();
button.loop(); // MUST call the loop() function first
unsigned long count = button.getCount();   
if (count>0) {
  encoder_alarm();
  }
}
//==============================================================================
// tasksEverySecond
//==============================================================================
void tasksEverySecond()
{
  if (second() != previousSecond)
  {
    previousSecond = second();

    displayRtcTime();
    ledTest();
  }
}
//==============================================================================
// ENCODER ALARM
//==============================================================================
void encoder_alarm(){
  unsigned long count = button.getCount();
    // reset button count when we cycle through all digit groups and set an alarm
    if (count > 6) {
    button.resetCount();
    digitalWrite(LED, LOW);
  } 

  //--------------------- HOURS alarm STARTS at ------------------------//
    if (lastReportedPos_hour_start != counter_hour_start) {
      if (counter_hour_start>23) {(counter_hour_start = 0);}
      if (counter_hour_start<0) {(counter_hour_start = 23);}
      tensofhours = counter_hour_start / 10;
      singlehours = counter_hour_start % 10; 
      lastReportedPos_hour_start = counter_hour_start;
  }
      EEPROM.update(2,counter_hour_start);
  //--------------------- MINUTES alarm STARTS at ------------------------//
    if (lastReportedPos_minute_start != counter_minute_start) {
      if (counter_minute_start>59) {(counter_minute_start = 0);}
       if (counter_minute_start<0) {(counter_minute_start = 59);}
      tensofminutes = counter_minute_start / 10;
       singleminutes = counter_minute_start % 10; 
      lastReportedPos_minute_start = counter_minute_start;
       EEPROM.update(3,counter_minute_start);
  } 
  
 
  //--------------------- 7 Segment display  ------------------------//
  if (count == 1 || count == 6) {
    MaximCC.setChar(0,7,tensofhours,false);
    MaximCC.setChar(0,6,singlehours,true);
    MaximCC.setChar(0,5,tensofminutes,false);
    MaximCC.setChar(0,4,singleminutes,true);
    MaximCC.setChar(0,3,tensofhours1,false);
    MaximCC.setChar(0,2,singlehours1,true);
    MaximCC.setChar(0,1,tensofminutes1,false);
    MaximCC.setChar(0,0,singleminutes1,true);  
    digitalWrite(LED, HIGH);
  }
  if (count == 2) {
    MaximCC.setChar(0,7,tensofhours,false);
    MaximCC.setChar(0,6,singlehours,true); 
  } else if (count != 1 && count != 6){
    MaximCC.setChar(0,6,singlehours,false);
  }

  if (count == 3) {
    MaximCC.setChar(0,5,tensofminutes,false);
    MaximCC.setChar(0,4,singleminutes,true);
  } else if (count != 1 && count != 6) {
    MaximCC.setChar(0,4,singleminutes,false);
  }

  if (count == 4) {
    MaximCC.setChar(0,3,tensofhours1,false);
    MaximCC.setChar(0,2,singlehours1,true);
  } else if (count != 1 && count != 6) {
    MaximCC.setChar(0,2,singlehours1,false);
  }

  if (count == 5) {
    MaximCC.setChar(0,1,tensofminutes1,false);
    MaximCC.setChar(0,0,singleminutes1,true);
  } else if (count != 1 && count != 6) {
    MaximCC.setChar(0,0,singleminutes1,false);
  }
}
//==============================================================================
// displayRtcTime
//==============================================================================
void displayRtcTime()
{
  unsigned long count = button.getCount();
  
  if (count == 0){
  MaximCC.setChar(0, 7, (hour() / 10), false);
  MaximCC.setChar(0, 6, (hour() % 10), false); 
  MaximCC.setChar(0, 5, '-', false);
  MaximCC.setChar(0, 4, (minute() / 10), false);
  MaximCC.setChar(0, 3, (minute() % 10), false);
  MaximCC.setChar(0, 2, '-', false);
  MaximCC.setChar(0, 1, (second() / 10), false);
  MaximCC.setChar(0, 0, (second() % 10), false);
 }
}
//==============================================================================
// ledTest
//==============================================================================
void ledTest(){
  int midnight;
  int powersaving;
  int startTime = counter_hour_start * 100 + counter_minute_start;
  int endTime = counter_hour_end * 100 + counter_minute_end;
  int timeNow = hour() * 100 + minute();
  if (endTime == 0000 && startTime != 0000){
    endTime = 2359;
  }   
   if (timeNow >= startTime && timeNow < endTime)  {
    powersaving = true;
   } else {
    powersaving = false;
   }
  
  if (powersaving == true){
    digitalWrite(LED2, HIGH);
  } else {
    digitalWrite(LED2, LOW);
  }
}
// Interrupt on A changing state
void doEncoderA() {
  unsigned long count = button.getCount();
  // Test transition, did things really change?
  if ( digitalRead(encoderPinA) != A_set ) { // debounce once more
    A_set = !A_set;
    // adjust counter + if A leads B
    if (( A_set && !B_set ) && (count == 2))
      counter_hour_start += 1;
    if (( A_set && !B_set ) && (count == 3))
      counter_minute_start += 1;
    if (( A_set && !B_set ) && (count == 4))
      counter_hour_end += 1;
    if (( A_set && !B_set ) && (count == 5))
      counter_minute_end += 1;
  }
}
// Interrupt on B changing state, same as A above
void doEncoderB() { 
  unsigned long count = button.getCount();
  if ( digitalRead(encoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if (( B_set && !A_set ) && (count == 2))
      counter_hour_start -= 1;
    if (( B_set && !A_set ) && (count == 3))
      counter_minute_start -= 1;
    if (( B_set && !A_set ) && (count == 4))
      counter_hour_end -= 1;
    if (( B_set && !A_set ) && (count == 5))
      counter_minute_end -= 1;
  }
}

I am not sure if leaving too many if statements in the interrupt routine is OK? As i need different reading depending on the button count. the code looks dirty to me, i am sure there is a cleaner/better/proper solution, its just i can not figure it out.
Thanks,
Alek

Test A_set and B_set once at the start of the ISR and return if the condition is not met, then all you need to do is to test the value of count and act on it

UKHeliBob:
Test A_set and B_set once at the start of the ISR and return if the condition is not met, then all you need to do is to test the value of count and act on it

Thanks man, but after a while of thinking about what you said, i feel i know how it should look like but still no luck... cant figure out the code.
Thanks,
Alek

think i got it, something like this?

 if ( A_set && !B_set ) {
      if (count == 2)
        counter_hour_start += 1; 
      else if (count == 3)
        counter_minute_start += 1;
      else if (count == 4)
        counter_hour_end += 1;
      else if (count == 5)
        counter_minute_end += 1;
      else if (count == 0)
        brightness_count += 1;
  }

Thanks,
Alek

That's what I had in mind

Should something happen when count equal 1 ?

UKHeliBob:
That's what I had in mind

Should something happen when count equal 1 ?

Thanks for the hint man!
count 1 and count 6 are used as mid steps, count1 sets the display to show current wake/sleep timer values and count6 shows newly set timer, neither require any encoder input so not in the interrupt code.
Now, all this is working but i've realized that i may end up using timers ISR(TIMER1_COMPA_vect), as what i have now seems to mess up my radio signal decoding when i am manipulating the encoder, which is not often but still, its itching me.
Thanks for all the help!
Alek