Easy Arduino Menus for Rotary Encoders

108,740

148

102

Introduction: Easy Arduino Menus for Rotary Encoders

Rotary encoders with centre-push buttons are cool input hardware for projects, especially if you want to be able to scroll through menus and select options. Hopefully this Instructable will give you the confidence to try a basic menu system out and combine it with a rotary encoder to give a professional feel to your next project!

Why menus and rotary encoders need code

I wanted to have a menu in an upcoming project and use a rotary encoder with a centre push button as my input hardware. This is really similar to the LCD controllers for RAMPS and Arduino MEGA 3D printers. The rotary encoder will allow me to scroll through the menu options, i.e. navigate the menu, select sub-menus and also change values within sub-menus/settings - very versatile for one hardware interface! A microcontroller is needed to manage all of this and microcontrollers need instructions, AKA code!

Other options

The problem I had with existing Arduino menu libraries and menu code is that for simple menus they were overly complicated. Another drawback of many alternatives was that the code was designed for LCD screens and momentary push buttons, not rotary encoders and adaptable to other display outputs. These menus were geared around selecting between a small number of modes and incrementing values relatively slowly. We know that rotary encoders are a great hardware input option because they afford relatively fast input value changes while retaining fine control at slow speed. I wanted to write code which would allow unambiguous top level menu navigation but also allow you to quickly scroll through a large range of values within each sub-menu/setting, exploiting the strengths of the rotary encoder.

The approach

I decided to follow some advice to use if() statements for a simple menu structure and keep it sketch-based. The resultant code builds on my previous Instructable which sought to reliably read the rotation pulses and direction. Please check it out for background.

In this sketch, we add the reading of the centre push button on the rotary encoder shaft, using code that Nick Gammon developed to record button state changes with debouncing and without relying on the Arduino's delay() function that prevents the microcontroller from executing other code and would potentially introduce noticeable delay in our sketch, e.g. slow display refresh rates. Button state change code is much more useful than just reading digital logic high or low when using a button to select something once, like a menu option, as it can help you prevent unintentional multiple selections for each button press.

Let's take a look at what you need to set up to use this example code in Step 1.

Step 1: Preparation

If you haven't yet, please see my other Instructable on rotary encoder reading to find out how to set up your hardware and Arduino IDE software.

Hardware

The additional hardware connections you need to make use of centre push button are shown in the pictures. I used Fritzing to draw the diagram but it didn't have a rotary encoder component which represented the most likely pin layout, so just use that diagram in conjunction with the notes and look at the photo of the rotary encoder to see what you are more likely to be looking for in terms of rotary encoder pin layout.

One of the two pins on one side of the rotary encoder (as opposed to the side with three pins) needs to be connected to ground and another to a digital pin on the Arduino. I have used D4 for the example sketch. If you choose a different pin, don't forget to change the value of buttonPin in the sketch.

Next comes the code in Step 2.

Step 2: Code

This is the code. By looking at the structure and the comments I hope you will find it easy to adapt for your specific needs!

/*******Interrupt-based Rotary Encoder Menu Sketch*******
 * by Simon Merrett, based on insight from Oleg Mazurov, Nick Gammon, rt and Steve Spence, and code from Nick Gammon
 * 3,638 bytes with debugging on UNO, 1,604 bytes without debugging
 */
// Rotary encoder declarations
static int pinA = 2; // Our first hardware interrupt pin is digital pin 2
static int pinB = 3; // Our second hardware interrupt pin is digital pin 3
volatile byte aFlag = 0; // let's us know when we're expecting a rising edge on pinA to signal that the encoder has arrived at a detent
volatile byte bFlag = 0; // let's us know when we're expecting a rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set)
volatile byte encoderPos = 0; //this variable stores our current value of encoder position. Change to int or uin16_t instead of byte if you want to record a larger range than 0-255
volatile byte oldEncPos = 0; //stores the last encoder position value so we can compare to the current reading and see if it has changed (so we know when to print to the serial monitor)
volatile byte reading = 0; //somewhere to store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent
// Button reading, including debounce without delay function declarations
const byte buttonPin = 4; // this is the Arduino pin we are connecting the push button to
byte oldButtonState = HIGH;  // assume switch open because of pull-up resistor
const unsigned long debounceTime = 10;  // milliseconds
unsigned long buttonPressTime;  // when the switch last changed state
boolean buttonPressed = 0; // a flag variable
// Menu and submenu/setting declarations
byte Mode = 0;   // This is which menu mode we are in at any given time (top level or one of the submenus)
const byte modeMax = 3; // This is the number of submenus/settings you want
byte setting1 = 0;  // a variable which holds the value we set 
byte setting2 = 0;  // a variable which holds the value we set 
byte setting3 = 0;  // a variable which holds the value we set 
/* Note: you may wish to change settingN etc to int, float or boolean to suit your application. 
 Remember to change "void setAdmin(byte name,*BYTE* setting)" to match and probably add some 
 "modeMax"-type overflow code in the "if(Mode == N && buttonPressed)" section*/

void setup() {
  //Rotary encoder section of setup
  pinMode(pinA, INPUT_PULLUP); // set pinA as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  pinMode(pinB, INPUT_PULLUP); // set pinB as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)
  attachInterrupt(0,PinA,RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below)
  attachInterrupt(1,PinB,RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)
  // button section of setup
  pinMode (buttonPin, INPUT_PULLUP); // setup the button pin
  // DEBUGGING section of setup
  Serial.begin(9600);     // DEBUGGING: opens serial port, sets data rate to 9600 bps
}

void loop() {
  rotaryMenu();
  // carry out other loop code here 
}

void rotaryMenu() { //This handles the bulk of the menu functions without needing to install/include/compile a menu library
  //DEBUGGING: Rotary encoder update display if turned
  if(oldEncPos != encoderPos) { // DEBUGGING
    Serial.println(encoderPos);// DEBUGGING. Sometimes the serial monitor may show a value just outside modeMax due to this function. The menu shouldn't be affected.
    oldEncPos = encoderPos;// DEBUGGING
  }// DEBUGGING
  // Button reading with non-delay() debounce - thank you Nick Gammon!
  byte buttonState = digitalRead (buttonPin); 
  if (buttonState != oldButtonState){
    if (millis () - buttonPressTime >= debounceTime){ // debounce
      buttonPressTime = millis ();  // when we closed the switch 
      oldButtonState =  buttonState;  // remember for next time 
      if (buttonState == LOW){
        Serial.println ("Button closed"); // DEBUGGING: print that button has been closed
        buttonPressed = 1;
      }
      else {
        Serial.println ("Button opened"); // DEBUGGING: print that button has been opened
        buttonPressed = 0;  
      }  
    }  // end if debounce time up
  } // end of state change

  //Main menu section
  if (Mode == 0) {
    if (encoderPos > (modeMax+10)) encoderPos = modeMax; // check we haven't gone out of bounds below 0 and correct if we have
    else if (encoderPos > modeMax) encoderPos = 0; // check we haven't gone out of bounds above modeMax and correct if we have
    if (buttonPressed){ 
      Mode = encoderPos; // set the Mode to the current value of input if button has been pressed
      Serial.print("Mode selected: "); //DEBUGGING: print which mode has been selected
      Serial.println(Mode); //DEBUGGING: print which mode has been selected
      buttonPressed = 0; // reset the button status so one press results in one action
      if (Mode == 1) {
        Serial.println("Mode 1"); //DEBUGGING: print which mode has been selected
        encoderPos = setting1; // start adjusting Vout from last set point
      }
      if (Mode == 2) {
        Serial.println("Mode 2"); //DEBUGGING: print which mode has been selected
        encoderPos = setting2; // start adjusting Imax from last set point
      }
      if (Mode == 3) {
        Serial.println("Mode 3"); //DEBUGGING: print which mode has been selected
        encoderPos = setting3; // start adjusting Vmin from last set point
      }
    }
  }
  if (Mode == 1 && buttonPressed) {
    setting1 = encoderPos; // record whatever value your encoder has been turned to, to setting 3
    setAdmin(1,setting1);
    //code to do other things with setting1 here, perhaps update display  
  }
  if (Mode == 2 && buttonPressed) {
    setting2 = encoderPos; // record whatever value your encoder has been turned to, to setting 2
    setAdmin(2,setting2);
    //code to do other things with setting2 here, perhaps update display   
  }
  if (Mode == 3 && buttonPressed){
    setting3 = encoderPos; // record whatever value your encoder has been turned to, to setting 3
    setAdmin(3,setting3);
    //code to do other things with setting3 here, perhaps update display 
  }
} 

// Carry out common activities each time a setting is changed
void setAdmin(byte name, byte setting){
  Serial.print("Setting "); //DEBUGGING
  Serial.print(name); //DEBUGGING
  Serial.print(" = "); //DEBUGGING
  Serial.println(setting);//DEBUGGING
  encoderPos = 0; // reorientate the menu index - optional as we have overflow check code elsewhere
  buttonPressed = 0; // reset the button status so one press results in one action
  Mode = 0; // go back to top level of menu, now that we've set values
  Serial.println("Main Menu"); //DEBUGGING
}

//Rotary encoder interrupt service routine for one encoder pin
void PinA(){
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; // read all eight pin values then strip away all but pinA and pinB's values
  if(reading == B00001100 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    encoderPos --; //decrement the encoder's position count
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00000100) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation
  sei(); //restart interrupts
}

//Rotary encoder interrupt service routine for the other encoder pin
void PinB(){
  cli(); //stop interrupts happening before we read pin values
  reading = PIND & 0xC; //read all eight pin values then strip away all but pinA and pinB's values
  if (reading == B00001100 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
    encoderPos ++; //increment the encoder's position count
    bFlag = 0; //reset flags for the next turn
    aFlag = 0; //reset flags for the next turn
  }
  else if (reading == B00001000) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation
  sei(); //restart interrupts
}
// end of sketch!

I have used "DEBUGGING" at the start of every comment on any line which isn't critical for the menu to do its thing. If you are happy with the menu's function, you might want to comment out or delete these lines for smaller compiled sketch size.

Be aware that a key part of menu navigation is feedback to the user while they are scrolling through the option and settings. Therefore if you choose not to include the DEBUGGING lines, you should probably use another visual indicator (e.g. LCD text display, LEDs) that encoder inputs are navigating the menu and changing settings.

If I comment out the DEBUGGING lines (noting that some visual feedback would still be needed for menu navigation) the compiled code is around 1,650 bytes for the Arduino Uno, hopefully leaving plenty of room on your ATMEGA328P for the more exciting parts of your sketch!

Go to Step 3 to find out how the menu system works.

Step 3: Operation and Conclusion

Operation

If you open the serial monitor in Arduino after uploading this sketch, and start to turn the encoder shaft, you should see the top level menu rotating through the number of sub-menus/options you have (limited using the modeMax variable). If you press the centre-push button you will see that the mode/sub-menu you have scrolled to is selected and now you have free reign to scroll through 0-255 values in that sub-menu. Now, if you press the centre-push button you will set that value to setting1 or setting2 or setting3 etc. The Arduino automatically and instantaneously returns you to the top level menu once this has happened.

While powered up, the Arduino remembers what you set each setting to and if you go back to the sub-menu for a setting you have already set a value to, it will start your encoder adjustments from the last value you chose!

Conclusion

I set out to write some sketch-based code that would allow rotary encoders to navigate a basic menu for Arduinos. I also tried to make it readable so that, unlike some alternatives, someone could see the menu structure and know what changes to the code they would need to make to tailor the menu to their needs.

This code is basic and generic, specifically to demonstrate the functionality while being easily adaptable to your own application. It uses the serial monitor as a basic debugging tool which also removes the need for a separate display if you want to see how the code works. I hope you find it useful and are inspired to edit, adapt and improve it!

Please share anything you do with it in the comments!

4 People Made This Project!

Recommendations

  • Back to School: Student Design Challenge

    Back to School: Student Design Challenge
  • Make it Move Challenge

    Make it Move Challenge
  • Fashion Challenge

    Fashion Challenge

102 Comments

0
bowlerhatman
bowlerhatman

Question 8 months ago

Hi Simon, for various reasons my project took a hiatus, and I'm just back to it now. The code as posted works great, but I'm struggling with a modification. My application has 4 menu items (settings), and uses 7 segment displays via a ht16k33. I'm wanting to use 2-letter abbreviations to show which setting I'm editing. The issue is that I can't figure out how to make the code display the letters when selecting the mode, but then show encoderPos as numbers when adjusting the actual variable. All my attempts have either changed nothing, or made it so only the letters appear, no digits. Do you have any thoughts on how to add this flexibility in?

Thanks, Frazer

0
Spa2036
Spa2036

1 year ago

Hello Simon,
Thank you very much for posting your code and explications on how to use and implement this. I am working on automating an incubator my father build 40 years ago with an arduino Nano, I am in need of a menu. I have tried a lot of menu's for Arduino but they were too slow, bulky in code, not running, didn't want to function with Serial initialized and so on. Found your code and I have been playing around with it.
On my oled display I have standard reading of Temp and Humidity and Days in process. I am trying to activate the menu with the push of the rotary switch and than make the selections for what specy to incubate and show in a second menu the parameters and eventualy modify some of the values, start initializing the incubator ( tamp and humidity) and start the process..
My problem is that I can access the menu with a press of the button, but I am no longer able to return to the main screen, or the menu is visible only when I press the button. I am missing something but I don't know what. If you or anyone else can give me a push in the right direction I will be a happy man. Thanks again.

0
SimonM83
SimonM83

Reply 1 year ago

Hi @Spa2036. It will be hard for anyone to help if you don't share details of your hardware setup or the exact code you're using. If you do post these, we might be able to replicate your issue and see what could solve it.

0
Spa2036
Spa2036

Reply 1 year ago

Hello Simon, thank you for your reply. You are right It is all in my head and I supose your can not read my mind :). Please find attached my sketch so far. I am using an arduino NANO and some control electronics for the heater ( 12v car heater controlled by a MOSFET), humidifier ( mister controlled same as heater), loudspeaker ( from an old notebook) and a LED to show the state of the incubator. In the sketch is some dutch language, what I speak best. I have disabled the eeprom update/write commands to not exhaust eeprom the memory of the NANO.
Thanks again, and if you need more specifications please let me know.

0
SimonM83
SimonM83

Reply 1 year ago

I don't know if this relates to the issues you observe but:
1) you don't appear to have any button debouncing in your main loop (although it is in the menu code, so can be copied from there or reused...), so you could be getting very confusing button input logic received by the microcontroller as a result. Here's my go-to for that: http://www.gammon.com.au/switches
2) You only seem to call openMenu() if you press the button, and that is where the encoderPos printing/updating happens, so I don't know if that's an issue. The updating of your setting variables from the encoder should be allowed to continue even if the button isn't pressed, once you have entered the menu, no?
3) You only seem to update the display when you enter the menu? But I haven't read it in more detail, just trying to give ideas about where to look.
4) Is your button logic consistent between the menu states? e.g. 0 is select in main loop, is that the same in e.g. the openMenu() function? Just wondering if it is entering an endless loop because of the state that variable leaves the functions in.
Just some ideas - I apologise if they are not useful.

0
Spa2036
Spa2036

Reply 1 year ago

@SimonM83 Thanks for your thoughts and ideas.

1. Ok no problem to add some debouncing. I guess 10 ms will be enough.
2. Yes I open the menu by pressing the encoder switch. Then I need to select the species and some sub menu's related to that species. The encoder shows the options and the switch selects the option.
3. Well I made a mess of the menu and started all over again for the xxth time. So I might have erased a little too much of my code.
4. openMenu() function is when I press the switch when the main program is running. So the top level menu is "0". After I have made a selection in the menu or not, I need to return to the main screen that indicates Temp Humidity and days ( if started).

I am thinking about doing something with switch and case, because there I have a break command to quit the menu and I need some submenu's. Also I am in need of a menu submenu counter that when break is used I return to the previous (sub)menu. I also need and a maximum possible menu option selection per subMenu. Ok back to the drawing board. Thanks again!

0
Boomer48
Boomer48

Reply 1 year ago

I haven't looked at your code but thought I would provide a couple of general pointers from my experience. First, switch debouncing may require more than 10ms. I typically use 50ms just to be sure. Second, whenever I want to use a single switch to get in/out of a function or to step through a function I do one of two things. To get in/out, I generally use an input pin tied to an interrupt handler. Just add a flag variable and toggle the flag each time the interrupt is handled. Then check the flag status in your "openMenu" function to determine if you are entering or exiting.

If I want to step through a function (like setting hours, then minutes of a clock) I detect the difference between long and short switch presses. For example, short presses to increment the hours, then a long press (typically one second) to move to the minutes setting. Hope this helps.

0
Spa2036
Spa2036

Reply 1 year ago

@Boomer48 Thank you for your input. At this moment I have the hardware interrupt assigned at the two rotary pins, but as I understand I need to change that to the switchpin of the rotary encoder. Which makes sense since the rotary encoder inputs are only needed when the menu is opened and not before.
For the debouncing it is easy to play with the variables as it is set as a variable.
Thank you for your input on how you handle switch presses. This certainly helps.

0
SimonM83
SimonM83

Reply 1 year ago

@Spa2036 you can add "Pin Change Interrupts" (PCI) to other pins on the ATMEGA328 based Arduinos. They are not quite as configurable as the hardware interrupts because they will fire every time the logic state changes (rather selecting only RISING or FALLING) but you can do a quick digitalRead in the interrupt service routine to confirm the pin's new state. The conclusion is that using a PCI will save you from having to change any of your existing wiring or interrupt-handling code.

0
Boomer48
Boomer48

Reply 1 year ago

I looked at the PIC micro code I wrote four years ago (in assembly language) for reading a rotary switch and saw that I had used the single External Interrupt pin for the encoder clock line. Once I was in the interrupt handler I simply read the high/low value of the encoder data line to determine the direction of movement. The ATMega328 has two external interrupts so you could use one for the encoder and one for the switch instead of needing to finesse a pin change input. Just a thought. I plan to do an Arduino project that will include a rotary switch so that's how I ended up here to begin with. I like figuring out stuff myself but I'm always interested in how other people attacked the problem.

0
Spa2036
Spa2036

Reply 1 year ago

@Boomer48 that would be the way to go, looking for left or right turning of the encoder, when entered into the menu. Well programming is a hobby of mine and I am certainly no coder, but step by step I am learning and getting a better understanding of how things work. And I like to find out things myself instead of copy and paste a sketch. So all the information provided by SimonM83 and you, helps me getting smarter and reach my goal.

0
Boomer48
Boomer48

Reply 1 year ago

I went ahead and translated my PIC assembly code for the cheap rotary switch to Arduino and it acts pretty much the same way. When you enter the interrupt handler for the clock, just do an if/else check on the data line. One key is that I use the 10k pull-up resistors provided on most rotary switch modules instead of the Arduino input pull-ups and I add 0.01uf capacitors to ground on the switch, clock, and data pins. That's a standard technique for eliminating bounce on the encoder lines.

0
SimonM83
SimonM83

Reply 1 year ago

@Boomer48, you could do that, yes. Someone in the comments here or, more likely, in my other instructible on reading rotary encoders has left a comment with a way to use only one external interrupt with the encoder, leaving the other external interrupt free for other uses.

0
Spa2036
Spa2036

Reply 1 year ago

@SimonM83 that is an interesting solution. I need to do some more reading and testing to get what I want. At least I am able to open the menu and close it again without resetting the Arduino, doing that.

0
Boomer48
Boomer48

Reply 1 year ago

I haven't looked at your code but thought I would provide a couple of general pointers from my experience. First, switch debouncing may require more than 10ms. I typically use 50ms just to be sure. Second, whenever I want to use a single switch to get in/out of a function or to step through a function I do one of two things. To get in/out, I generally use an input pin tied to an interrupt handler. Just add a flag variable and toggle the flag each time the interrupt is handled. Then check the flag status in your "openMenu" function to determine if you are entering or exiting.

If I want to step through a function (like setting hours, then minutes of a clock) I detect the difference between long and short switch presses. For example, short presses to increment the hours, then a long press (typically one second) to move to the minutes setting. Hope this helps.

0
bowlerhatman
bowlerhatman

1 year ago

Hi there, I'm trying to get your code working, but can't. There are a few things that I can see need improvement, for instance, when you attach the interrupts, the pin numbers you use don't correspond to the encoder pins, and you also use syntax that seems deprecated. I think the recommended syntax is attachInterrupt(digitalPinToInterrupt(pin), ISR, mode).

Having fixed that though, the code still doesn't behave as you describe, but I'm having a hard time figuring out why. Firstly, when I start it up, rotating the encoder doesn't scroll through the menu options, I get no serial output. If I press the button, I get:
Button closed
Mode selected: 1
Mode 1
0
Button opened
Rotating the encoder then still doesn't give any output.
Combinations of holding the button and rotating, or giving multiple presses and rotating, seem to yield almost random results.

Other than the attach interrupt syntax, and making sure the pin assignments are correct, I haven't changed your code at all. Do you have any advice that could help me get this working? From your description of the function, it does exactly what I need!

ETA: Also, when I am able to see the encoder changing a value, it only goes in one direction. My hardware is fine, as other sketches are able to see it go both ways. Hardware is a Leonardo, and I have debouncing caps on the encoder. pinA = 2, pinB = 3, buttonPin = 1 (all updated in the code). In addition, I've tried your first set of code, with and without the attachInterrupt modification, and get no output at all.

Thanks,
Fraz

0
SimonM83
SimonM83

Reply 1 year ago

Hey @bowlerhatman, of course there will be some necessary code changes for a chip that's not the ATMEGA328P - hopefully that was clear from the discussion about portability in my other instructible. Would you care to post images of your encoder and wiring?
My inclination would be to try connecting a multimeter in continuity mode (or oscilliscope if you have one) to ensure the voltage logic between detents is as expected with your current wiring.

0
bowlerhatman
bowlerhatman

Reply 1 year ago

Hi Simon, thanks for your speedy response! I confess, I can't see a discussion of portability in your other 'ible, other than that you mention it? As far as I can see though, 2 and 3 are interrupts on the Leonardo as well. I do see that they occupy the two least significant bits on PORTD, rather than the 3rd and 4th.

Can you expand a little on this:
...
static int pinA = 2; // Our first hardware interrupt pin is digital pin 2
static int pinB = 3; // Our second hardware interrupt pin is digital pin 3
attachInterrupt(0,PinA,RISING); // set an interrupt on PinA, looking for a rising edge signal and executing the "PinA" Interrupt Service Routine (below)
attachInterrupt(1,PinB,RISING); // set an interrupt on PinB, looking for a rising edge signal and executing the "PinB" Interrupt Service Routine (below)
reading == B00001100 && aFlag (and the other three instances)
...
I'm a little confused about why digital pins 2 and 3 are declared for the encoder pins, but then pins 0 and 1 are attached as the interrupt, but then the 3rd and 4th bits on PORTD are polled?

My wiring is attached. The Leonardo is represented by that connector, but the pins numbers match up. I have adjusted buttonPin to 1 to suit my connections.

I have tried adjusting the PIND section to:
reading = PIND & 0x3
and shifting the two polled bits down by two, thus:
reading == B00000011 && aFlag (and so on)
But still have no joy.

I've got the encoder, wired as is, running successfully with this library: https://www.pjrc.com/teensy/td_libs_Encoder.html
So I'm confident that the wiring is not the issue, but I'm clearly missing something else. Beyond the port assignments, I can't see anything in the code that should vary much from 'duino to 'duino?
Thank you for your help,
Frazer

Screenshot from 2020-05-15 08-30-03.png
0
SimonM83
SimonM83

Reply 1 year ago

Perhaps "mention" would have been better than "discussion". But the point was that with that code you should expect to have to change the port manipulation elements to port it to other chips.
Did you check against this tip in that instructible:
"Make sure you find out where the detent state is for your encoder and adapt your code accordingly. My example uses an encoder whose pins are both disconnected from ground and are pulled high by their respective input pullup resistors. This drives my selection of a RISING interrupt. If both pins were connected to ground when at detent, they would need code which was looking for FALLING pin voltage."?

Chips come with pins assigned in banks, called ports. Arduino assigns its own pin numbers to particular pins, so that they can match the peripheral functions across chips and achieve some commonality that e.g. PWM will be available on D5 for as many Arduinos as possible (to enable analogWrite() to be portable between Arduinos). Finally, there are hardware interrupts. The ATMEGA328P has two, starting at 0 and ending with 1. The ATMEGA32U4 appears to have 4 hardware interrupts (0-4). It just so happens that port pin number, arduino pin number and hardware interrupt number have very little to do with each other. Does that help?

If you are sure there are no other components involved in your wiring, the schematic looks good, as long as it is correct! What value capacitors have you used? Do you have a spec for your encoder - model number or photo?

Without going in to the pin assignments, port masking and interrupt setting, I think you would do well to firstly check that your encoder is contacting each pin to ground just using some form of continuity checker. If you don't have a multimeter, you could use a battery, suitable value resistor and LED to check this.

Once you have confirmed that you are definitely seeing both pin A and pin B make AND break contact with pin C for each detent, make note of whether the connection is open or closed at detent.

Then I think you should set up a simple sketch with only one of the A or B pins connected and set up the hardware interrupt. Then just make sure you can get e.g. Serial monitor or LED flash from a separate GPIO when the interrupt fires. Once you have this, move on to add the other pin B or A and its respective interrupt. Confirm now that both interrupts are firing.

From a quick look at the pinout of the Leonardo, you need to do something like this in setup (I'll leave it to you to set some suitable volatile variables to use as flags in your ISR):
pinMode(2, INPUT_PULLUP);
pinMode(3, INPUT_PULLUP);
attachInterrupt(0,yourRoutineA,CHANGE);
attachInterrupt(1,yourRoutineB,CHANGE);

Then write your ISRs called yourRoutine() to change the value of a flag variable.
Get your main loop to check for changes to the flag variable, reset it and notify you somehow (LED/serial monitor). See how you get on.

0
bowlerhatman
bowlerhatman

Reply 1 year ago

Hi again Simon, thanks! This is helpful I think, and is starting to clear some things up. I changed the interrupt mode to CHANGE, and the isrs to the below code and it's now working as described.
___________
void PinA(){
cli(); //stop interrupts happening before we read pin values
reading = PIND & 0x3; // read all eight pin values then strip away all but pinA and pinB's values
if(reading == B00000011 && aFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
encoderPos --; //decrement the encoder's position count
bFlag = 0; //reset flags for the next turn
aFlag = 0; //reset flags for the next turn
}
else if (reading == B00000010) bFlag = 1; //signal that we're expecting pinB to signal the transition to detent from free rotation
sei(); //restart interrupts
}
//Rotary encoder interrupt service routine for the other encoder pin
void PinB(){
cli(); //stop interrupts happening before we read pin values
reading = PIND & 0x3; //read all eight pin values then strip away all but pinA and pinB's values
if (reading == B00000011 && bFlag) { //check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
encoderPos ++; //increment the encoder's position count
bFlag = 0; //reset flags for the next turn
aFlag = 0; //reset flags for the next turn
}
else if (reading == B00000010) aFlag = 1; //signal that we're expecting pinA to signal the transition to detent from free rotation
sei(); //restart interrupts
}
___________

I think one thing that threw me is that the Arduino website give this advice "attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) (recommended)
attachInterrupt(interrupt, ISR, mode) (not recommended)
attachInterrupt(pin, ISR, mode) (Not recommended. Additionally, this syntax only works on Arduino SAMD Boards, Uno WiFi Rev2, Due, and 101.)"

It's not immediately clear from the comments in the code that your syntax is #2 in that list, and for someone rusty like myself, given that the other two options both suggest attaching by pin number rather than int number, it looked like the numbers were off. I see what's happening now, but might I suggest using the recommended syntax for clarity?

Those comments aside, it's now working perfectly, thank you for all your help! It's greatly appreciated.

Many thanks,
Frazer