Improved Arduino Rotary Encoder Reading

105,401

213

99

Published

Introduction: Improved Arduino Rotary Encoder Reading

Rotary encoders are great input devices for electronics projects - hopefully this Instructable will inspire and help you use one in your next project.

Why write rotary encoder code?

I wanted to use a low cost rotary encoder as an input mechanism for one of my upcoming projects and was initially bewildered by the code options available to take readings from the rotary encoder and determine how many "detents" or cycles the encoder had clicked past and in what direction. I think my main sketch will need to use most of my Arduino's memory so I am avoiding the various available encoder libraries, which seemed to be difficult to make work when I tried a couple of them. They also appear to use far more of the code budget than the sketch-based code approaches discussed from here on.

If you just want to bypass the thinking behind my approach and get straight into the Instructable, feel free to skip ahead to Step 1!

Other Approaches

Several of the main sketch-based (i.e. they don't use a library) approaches are discussed in rt's blog post where they write rotary encoder code that makes the cheapest encoders usable as Arduino inputs. They also have a good example of they logic signal that the encoder produces. rt found that a timer interrupt system worked best for them but I'm concerned that the polling frequency would detract from screen update speed in the main loop of my project sketch. Given that the rotary encoder will be moving for a tiny proportion of the time I want the screen to be updating, this seems a poor match for my application.

I chose to start off using Steve Spence's code here, which was fine on it's own but appeared to really slow down when I incorporated the rest of my sketch code (which involves writing display updates to a small TFT screen). Initially I wondered if it could be because the main loop contains a debounce statement.

I then read Oleg's rotary encoder article on an interrupt service routine version of his previous post, I also thought it might be a good idea to use direct port manipulation to read both pins simultaneously and as soon as the interrupt fires. His code can be used on any input pin if the port manipulation code is rewritten. In contrast, I decided to use only the hardware interrupts on digital pins 2 and 3, so we can set interrupts to only fire on a rising edge of the pin voltage, rather than on pin voltage change, which includes falling edges. This reduces the number of times the ISR is called, distracting from the main loop.

Oleg's code uses a lookup table to reduce compiled code size to a really small size but I couldn't get reliable results which would catch very slow rotation as well as reasonably fast rotation. Bear in mind that hardware debouncing (see Step 2) can help a lot with reliability of readings but I was after a software solution to simplify the hardware build and be as portable to other hardware applications as possible.

This concludes the introduction of my challenge and considerations. In Step 2 we'll take a look at the encoder hardware, terminology and some practical considerations when you want to integrate a rotary encoder into your project.

Step 1: A Bit About Rotary Encoders

Why are rotary encoders so cool?

  1. Unlike a variable resistor/potentiometer they have infinite travel in any direction and because they produce a digital "Gray code" you can scale their readings to whatever range you like.
  2. The dual direction makes them useful for increasing or decreasing a value within a variable or navigating menus.
  3. Finally, many of these rotary encoders come with a centre push button, which can be used to select menu items, reset a counter or do anything you can think of that might suit a momentary push button.

Terms

  1. PPR: pulses per rotation - typically 12, 20 or 24. You might also see specifications for maximum rotation in rpm etc. This is probably determined by the encoder's propensity to "bounce" contacts - see below.
  2. Detent: the little click of the action as it springs to a natural rest point between pulses. There may be one detent per pulse/cycle (not equal to a rotation of the shaft) or two.
  3. Bounce: mechanical contacts inside the encoder literally bounce enough to jump off and back on a contact when rotating, potentially leading to too many readings attributed to that phase of the travel between detents.
  4. Debounce: This can be either done in hardware, perhaps with a low value ceramic capacitor between each pin and Ground, or in software, perhaps with a delay. In either case, the aim is to create a system which ignores bouncing contacts.

Tips

  1. Look out for a threaded section near the base of the shaft and a matching nut if you want to mount your encoder in a panel or enclosure.
  2. Many knobs are available for rotary encoders, with the most easily available coming in 6mm diameter shafts.
  3. Pay attention to whether your encoder shaft uses a flat face or splines to achieve a proper fit with the knob.
  4. The body of the rotary encoder may also come with a raised pin/stub, intended to mate with a small indent/hole in your panel (probably hidden by your knob) and prevent your encoder from rotating when you turn the knob. You might find you want to remove this if you can create enough friction to prevent encoder body rotation using the mounting bolt to screw the encoder in the panel or enclosure.
  5. 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.

Step 2: The Circuit

The circuit is so simple. You will need:



• An ATMEGA328P based Arduino, such as the Uno, Pro Mini or Nano.
• A mechanical (as opposed to optical) quadrature rotary encoder - this is the most common kind so don't worry too much if it isn't specified. eBay and Aliexpress listings will often mention Arduino in the description and this is a good indicator that one is suitable.
• Hook-up wire/jumper leads.
• Optional: a prototyping breadboard.


First of all, look for a collection of three pins on one side of the encoder. These are the three for measuring rotation with our code. If there are two pins together on another side, these are likely to be for the centre push button. We'll ignore these for now.

Out of the three pins together, the encoder ground pin is connected to Arduino ground pin. Either of the other two pins is connected to digital pin 2 and the remaining on is connected to digital pin 3. If your direction of rotation isn't the way you'd like, just swap the two non-ground pins over.

Pins 2 and 3 are important because on the ATMEGA328P-based Arduinos they are the only pins which have the ability to detect RISING and FALLING pin change interrupts. The MEGA 2560 boards etc. have other hardware interrupt pins which can do this.

Note: In the diagram the ground pin is one of the end pins. In reality, the ground pin is often the centre pin but this is not always the case so read the datasheet or test your encoder to find out which pin is ground.

Another note: ArneTR made a good comment about not having a separately wired connection to the logic positive voltage (e.g. 5V or 3.3V) for the rotary encoder circuit shown. The Arduino can't read the rotary encoder without both a ground signal (which we have connected a wire to) and the logic voltage (sometimes annotated as Vcc or Vdd), so how can the Arduino read the logic from this encoder without a positive voltage wire? The answer is that the ATMEGA328P chip in the Arduino has a special mode you can set on the digital pins (which we are using) where a pin is automatically pulled "high" to the logic voltage by an internal resistor. Look in the code for "pinMode(pinX, INPUT_PULLUP)" to see us telling the Arduino that we want to take advantage of this mode. Once set, we only need to provide the encoder with a ground wire as the sensing wires from the digital pins are already providing the logic voltage.

ONE MORE THING... Githyuk found that a particular branded encoder didn't work with this way of doing things (ie the code below). Please see the comments section for details but in general, trying a different encoder would be a good debugging step when you have exhausted the easier/faster/cheaper steps.

Step 3: The Code

If you are not familiar with programming Arduinos, please get up to speed with this resource from Arduino themselves.

This code is free for your use (as in no cost and to be modified as you please), please attribute where you should.


/*******Interrupt-based Rotary Encoder Sketch*******
by Simon Merrett, based on insight from Oleg Mazurov, Nick Gammon, rt, Steve Spence
*/

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

void 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)
Serial.begin(115200); // start the serial monitor link
}

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
}

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
}

void loop(){
if(oldEncPos != encoderPos) {
Serial.println(encoderPos);
oldEncPos = encoderPos;
}
}

That's it!

Step 4: Conclusion

I hope you find this code useful for your next project which uses a rotary encoder or that it has inspired you to consider a rotary encoder as an input for your next project.

Summary of the Aims

I have tried to write some code which achieves a good balance of:

  • Portability (port manipulation code is the compromise when moving to other chips)
  • Speed (port manipulation really helps)
  • Low compiled code size (port manipulation and bitmath helps)
  • Reliably records slow and fast manual rotation
  • Reduced nugatory interrupt service routine calls (using RISING interrupt and temporarily disabling interrupts)

Caveats and Ideas for Improvement

This code isn't perfect by any means and you might like to change it to use other pins. I tested this code with the sketch which was causing the most delay and least reliable readings with the other approaches discussed - I certainly haven't compared it with timers to see whose code produces fewer nugatory interrupt service routines, takes the least time to execute or filters out the highest percentage of contact bounces. Perhaps someone might like to do a benchmark test against the other approaches out there.

Share

Recommendations

  • Make it Move Contest

    Make it Move Contest
  • Casting Contest

    Casting Contest
  • Planter Challenge

    Planter Challenge
user

We have a be nice policy.
Please be positive and constructive.

Tips

Questions

0

Hi Simon i have a newbie Questions.

I am controlling the output voltage of an external DAC 16 BIT With I2C to produce 0 to 5 V. i wanna adjust this voltage by using the code of ur rotary encoder. how can i merge this code to make the rotary encoder controls this voltage for me? thanks

99 Comments

What would be the best, smoother and realiable Rotary encoder for a jog wheel to have presicion and fine tuning increments and decrements?
which PPR? thanks!

1 reply

Hi BlueM7, sorry I didn't get a notification for your comment. I haven't used this yet but I have bought something like this for my new jog controller https://m.banggood.com/100PPR-6-Terminal-Eletronic-Hand-Wheel-Manual-Pulse-Encoder-Generator-For-CNC-Machine-p-1121930.html?

You may also like my approach to a jog controller https://hackaday.io/project/29627-grbl-man-in-the-middle-cnc-jog-pendant

Hello Simon,
Do u have an code to check if the angle exeeds more than the permited angle?

1 reply

Not specifically. You'd have to convert encoder position to an angle (perhaps use map() function), then compare the angle value to your limit/threshold (perhaps use if() function). Then you can do what you want to inside the if() function. You can do all that inside your loop () function.

Do you have a code that would count the number of turns of the encoder? Lets say, add n = 1 for everytime the encoder gives a value of 0.

2 replies

TylerM173, can you explain what you're trying to achieve with a bit more detail and context? Are you talking about a 360 degree rotation of the encoder or just one detent? Or half to a quarter of a detent (as there are two zero phases and they overlap)?

The code already adds/subtracts 1 to/from encoderPos if you rotate through the contacts for one detent. If you're talking about a full 360 rotation, you can just set an if() statement which resets the variable encoderPos to zero when it goes above the same number of detents per rotation.

Thanks. Your encoder sketch works great. I am using an Arduino Uno and a CTR magnetic encoder with a vex gearbox.
The encoder has 4 phases per rotation. I choose an encoder value of 150 for my IF statement. in the statement I add to the counter and display the number of rotations in the serial monitor.

Hello Simon,

Many thanks for your instruction and code.

Got your Interactive menu working on a Mega per the comments below about the Mega.

I was trying to use your menu for a Wemos D1 Mini but got in trouble real fast with the Interrupt stuff and a OLED display.

So I decided to use a Mega and a ESP-01 (I should received it in about a month).

Right now, I am able to enter the IP, Gateway and Subnet with the encoder and show it on a .96 SSD1306 OLED.

Next step is entering the SSID and PASSWORD.

Maybe later, I will find a way to use the Wemos.

Thanks again.

Pierre VE2CFB

1 reply

Hi Pierre, thanks for taking the time to let people know how you are using the code. Ideas of the possible applications are always useful, especially when someone can confirm it's feasible.

The code in the my Instructables uses some language elements that are specific to the ATMEGA328P (which is why you need different code for the MEGA2561). You might be able to spot how to adapt the external interrupts to work with the Wemos D1 mini from this blog post https://techtutorialsx.com/2016/12/11/esp8266-external-interrupts/

Please come back and tell us how you get on.

Thanks Simon, I am working on an LCD Menu and hunting some examples of the rotary encoder control, your code was rock solid in performance with my salvaged encoder. One thing I really enjoyed was that there were no 'glitchy' points as I've had with other examples.

1 reply

kd6oji, that's encouraging to hear and thanks for taking the time to let us know!

Hi Simon, I just recently acquired some EC11 rotary encoders and apart from wiring the middle pin to GND they work well with your code. I would be interested in a h/ware + s/ware approach to limit the processor workload. Nick Gammon is a brilliant experimenter in this area. Accolades to you and all the other gurus that inspired this Instructable

1 reply

My electronics knowledge isn't great but you might be able to reduce the workload for the processor using an op amp as a comparator and/or (!) some logic gates. Perhaps some latching would be required. I'm not working on anything like this but if you find something, please post a link!

Hi Simon,

your encoder sketch works great with this encoder (OMRON E6B2-CWZ6C) on a Arduino Uno. I would like to get some assistance if I could to have this code work for my application. I am a Transmitter Hunter for a local Ham Radio Club and I have a beam that I want to track 0 to 360 degree rotation. I also have a Z-index on this encoder that I would like the routine to reset the counter to zero (Reset to zero) everytime it hits the z-index position. This will allow the encoder to stay accurate to the front of the vehicle this is mounted to. One more thing that would be great is an led that would blink everytime the z-index hit zero this led would blink. I am just learning arduino programming and any help I can get to accomplish this project would be greatly appreciated. Update, I have the 0 to 360 tracking working with no negative numbers. I also have the reset to zero button working so that the initial calibration for 0 (front of the vehicle) works. My issue now is the Z index portion (I gave it a shot but doesn't work) and getting the onboard LED to turn on when the encoder is around zero (Index position). This could be a +/- 3 degrees as zero would be hard to see all the time as it came around. here is the code so far:

//*******Interrupt-based Rotary Encoder Sketch*******

// Wiring connections for my encoder:

// Brown : VCC = 5 to 24VDC

// Blue: 0V(Common) = GND

// Shield: GND

// Black: outA = Digital pin 2

// White: outB = Digital pin 3

// Orange: outZ = Digital pin 4 (Index)

// buttonPinNr = Digital pin 5

// Onboard LED from digital pin 13 to ground for zero degree condition

// With these outA/outB/outZ connections and the interrupt code below

// clockwise rotation gives positive encoder counts

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

static int pinC = 4; // Our third hardware interrupt pin is digital pin 4

const int buttonPinNr = 5; //reset button to zero

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 cFlag = 0; // let's us know when we're expecting a rising edge on pinC to signal that the encoder has arrived at a detent (Indexed location for zero(reset))

volatile int 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

void 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)

pinMode(pinC, INPUT_PULLUP); // set pinC as an input, pulled HIGH to the logic voltage (5V or 3.3V for most cases)

pinMode(buttonPinNr, OUTPUT); //set buttonPinNR as output

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)

attachInterrupt(2,PinC,RISING); // set an interrupt on PinC, looking for a rising edge signal and executing the "PinC" Interrupt Service Routine (below)

Serial.begin(115200); // start the serial monitor link

}

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, pinB's & pinC'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

if (encoderPos < 0)

{

encoderPos = (encoderPos + 360);

}

cFlag = 0; //reset flags for the next turn

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

}

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, pinB's & pinC'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

if (encoderPos >= 360)

{

encoderPos = (encoderPos - 360);

}

cFlag = 0; //reset flags for the next turn

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

}

void PinC(){

cli(); //stop interrupts happening before we read pin values

reading = PIND & 0xC; //read all eight pin values then strip away all but pinA, pinB's & pinC's values

if (reading == B00001100 && cFlag) { //check that we have pin C at detent (HIGH) and that we are expecting detent on this pin's rising edge

encoderPos = 0; //reset the encoders Z-Index to zero

cFlag = 0; //reset flags for the next turn

bFlag = 0; //reset flags for the next turn

aFlag = 0; //reset flags for the next turn

}

sei(); //restart interrupts

}

void loop(){

if (oldEncPos != encoderPos) {

Serial.println(encoderPos);

oldEncPos = encoderPos;

}

if (digitalRead(buttonPinNr) == HIGH){ //reset button to zero

encoderPos = 0;

}

}

3 replies

I think I already discovered one flaw in my thinking with this sketch and that is trying to run this on an Arduino Uno because of the limitation of digital pins usable for interrupts (pins 2 & 3). I would be better off working with a Mega2560 that has 6 digital pins available (pins 2,3,18,19,20, 21). I would also like to be able to do a direct pin write of the 0 to 360 degree output (I believe would be 9 pins) to another Arduino for additional calculations and LED readout. Any thoughts? Bryan

Bryan, from what you've said I believe the Uno is absolutely capable of meeting your needs. Just use a Pin Change Interrupt (as per the Nick Gammon guide I linked to in my other reply) for the Z reset. You can use a display with an I2C or SPI interface if you want or set up a software serial pair of Rx/Tx pins to talk to another device (but I would like to think your further calculations can be done on the first Uno). Depending on your display interface, that will steer you to what pins remain for your Pin Change Interrupt. Keep at it. Even if you decide to order a Mega 2560, try the pin change interrupt on the Uno while you're waiting. You can even get a neopixel ring of colour changing leds and use those to display the angle of the encoder readout. That would only require one digital pin as well as power and ground. There's loads of guidance on neopixel and arduino on the Internet. Keep going!

Hi Bryan, thanks for posting in the comments. Before we get to your main issue, I want to say you've done a good job changing encoderPos to a larger size than a Byte but you might also want to change oldEncoderPos in the same way.

So, the problem is that you are trying to put a hardware interrupt on a pin which doesn't support it (digital pin 4). Only pins 2 and 3 support the kind of interrupt code we've used here. However, you can add pin change interrupts to the other pins (beware that it involves a whole port and therefore up to 8 pins - suggest you may want to look at using the analog pins if you want to reserve pin 13 as your LED pin. The best guide I know to all kinds of interrupts is by Nick Gammon (who is attributed in my code) and his section on pin change interrupts is here - good luck: http://gammon.com.au/forum/?id=11488&reply=6#reply6

hi Simon, im newbie in arduino.

can I use this code for reading 2 rotary encoder? i have try with mega2560 (pin 18,19 for encoder 1, then pin 20,21 for encoder 2) but not work. the code is exactly same with your post, just rename variable. i im very confused about it, can you please help me?

many thanks, Nurochim.

2 replies

Hi Nurochim, this has been asked and answered partly in the comments below and the comments for my menu instructable. If you're a newbie and you want to make this work, you have a little bit of learning to do but that's great if you're doing this for a school project.

Here's the text from some relevant comments. You might need the datasheet for the ATMEGA2560 and/or a port manipulation guide and pinouts for the ATMEGA2560:

"Hi, I haven't tested this but I expect the reason it doesn't work directly with the mega2560 is because the interrupt service routine relies on port manipulation and the mega2560's hardware interrupt pins aren't on the same ports as the atmega328. My code above could be easily adapted for the mega2560 if you change PIND to the correct port, eg PINE, to read port E. It looks like arduino interrupt pins are on 2 and 3, which appears to equate to PE4 and PE5. That means you also need to change the B0000XX00 to bit shifting/ masking/ comparing B00XX0000 (PE0 will be the right hand bit, PE7 will be the left hand bit) to deal with the readings from your encoder. Good luck with the edit - I think it's definitely doable. Please post the code you change once you get it working so other people can use this on the mega2560"

"SuppeschluerferSimonM83Reply9 months ago
Mega 2560 - Pin 2(INT0) and 3(INT1):

PIND & 0xC change to PINE & 0x30
B00001100 change to B00110000
B00000100 change to B00010000
B00001000 change to B00100000"

thanks so much, it work on my project. but I still not understand with

B00001100 etc, what is the meaning of this code? and if I will use pin 20 and 21 for encoder, what should I change?