These articles will contain information about the tests that I‘ve done on using of multiple rotary encoders in Arduino and beyond.
Finally we will develop a module based on Altera CPLD that will allow simultaneous reading up to 32 encoders.
Encoders that we used in these experiments are the type EC12 and EC11.

RotaryEncoders – EC12 and EC11
There are many methods, projects and libraries that aim to manage rotary encoders in Arduino enviroment.
These can be categorized into two major classes:
- Polling methods
- Interrupts methods
Polling methods
The microcontroller continuously monitors the input from the rotary encoder(s). The main drawback of this method is that microcontroller have to check very fast for pulses so don’t miss it. And the microcontroller does not have time to perform other tasks.
Interrupts methods
One or two pins of the rotary encoder(s) are connected at interrupt pins of microcontroller. The main drawback of this method is that external interrupts in microcontrollers are limited resources.
For example ATmega8 has two external interrupts: INT0 and INT1. Atmega32 has three: INT0, INT1, INT2. Atmega328 has INT0, INT1, PCI0, PCI1 and PCI2.
The correct method to read rotary encoders:
Before speaking of the “correct method” and why it is “correct”, we should see how it works effectively these encoders. These encoders (with which we will work further) are actually “incremental rotary encoders”. This type of encoders provides sequential output at pins A and B when the encoder is rotated.
There are 3 pins on on EC12, two for the Channel A and Channel B outputs and Common (which usually is connected to GND). EC11 has 2 extra pins that we will ignore for now (they correspond to a simple push button).
EC12 has 24 fixed position “detents” on 360º rotation. EC11 has 20 detents.
As we can see from the image above, in fixed positions(detents) both switches are open.We make a convention:
- MSB (most significant bit) = Channel A
- LSB (less significant bit) = Channel B
- OPEN = 0
- CLOSE = 1.
Transition from one detent to next detent will follow sequence:
- CW (clockwise): 00→10→11→01→00
- CCW(counterclockwise):00→01→11→10→00
If we try to implement this convention(which is very intuitive) we have the following (big)problem:
We need to connect common pin to VCC (high level signal). And we need pull-down resistors because we can’t leave input pins of microcontroller floating.
But AVR microcontrollers have already internal pull-up resistors. We want to use this and not add external pull-down resistors. We will connect common pin to ground and we will change our convention in according to this. These are values that we will work further:
- MSB = Channel A
- LSB = Channel B
- OPEN = 1 (pull-up resistor will keep level high when switch is open)
- CLOSE = 0 (when switch is closed, input pin of microcontroller will be connected to GND)
Our transitions will change as follows:
- CW (clockwise): 11→01→00→10→11
- CCW(counterclockwise) 11→10→00→01→11
As we can see for each state of the encoder there are only two possible values that can change its current state.
Because we want to detect CCW or CW movements we consider not 4 but 7 states.
CCWFinal← CCWSt3 ← CCWSt2 ← CCWSt1 ← START → CWSt1 → CWSt2 → CWSt3 → CWFinal
CCWFinal = START = CWFinal
The following diagram will clarify this more (or not?).
Although the diagram looks a bit non intuitive because we expect to have four states, if we analyze each of the states we see that it takes into consideration every possible movement, including incomplete movements. CW_Flag=0 and CCW_Flag=0 are default for all transitions less those marked with RED.
Next we move on to the actual implementation of the algorithm that results from the diagram / table .
Implementation
We have 7 states: START, CW_Step1, CW_Step2, CW_Step3, CCW_Step1, CCW_Step2, CCW_Step3. We put these states intro arbitrary order, but we will take care to maintain this order in the table which we will build.
1 2 3 4 5 6 7 |
#define START 0 #define CW_Step1 1 #define CW_Step2 2 #define CW_Step3 3 #define CCW_Step1 4 #define CCW_Step2 5 #define CCW_Step3 6 |
The columns will be placed in ascending order so that we can directly use as a index in the table. Since all possible values of our inputs are increasing sequence starting with 0 (00,01,10,11) we do not need to map these inputs to another index.
1 2 3 4 5 6 7 8 9 10 11 |
const byte transition_table[7][4] = { /* 00 01 10 11 */ /* -----------------------------------------------------*/ /*START | */ {START, CCW_Step1, CW_Step1, START}, /*CW_Step1 | */ {CW_Step2, START, CW_Step1, START}, /*CW_Step2 | */ {CW_Step2, CW_Step3, CW_Step1, START}, /*CW_Step3 | */ {CW_Step2, CW_Step3, START, START}, /*CCW_Step1 | */ {CCW_Step2, CCW_Step1, START, START}, /*CCW_Step2 | */ {CCW_Step2, CCW_Step1, CCW_Step3, START}, /*CCW_Step3 | */ {CCW_Step2, START, CCW_Step3, START} }; |
Our table is incomplete at this time because we not taken into account the most important thing CW_Flag and CCW_Flag. Usually we would have needed another table (or another method to select corresponding outputs). 7 states occupy only 3bits. In each cell, we have 5 bits available to store additional data. For our flags (CW_Flag and CCW_Flag) we need 2 bits.We define these flags using the most significant bits (arbitrary choice).
1 2 |
#define CW_Flag 0b10000000 #define CCW_Flag 0b01000000 |
Our table will take this final form:
1 2 3 4 5 6 7 8 9 10 11 |
const byte transition_table[7][4] = { /* 00 01 10 11 */ /* -----------------------------------------------------*/ /*START | */ {START, CCW_Step1, CW_Step1, START}, /*CW_Step1 | */ {CW_Step2, START, CW_Step1, START}, /*CW_Step2 | */ {CW_Step2, CW_Step3, CW_Step1, START}, /*CW_Step3 | */ {CW_Step2, CW_Step3, START, START | CW_Flag}, /*CCW_Step1 | */ {CCW_Step2, CCW_Step1, START, START}, /*CCW_Step2 | */ {CCW_Step2, CCW_Step1, CCW_Step3, START}, /*CCW_Step3 | */ {CCW_Step2, START, CCW_Step3, START | CCW_Flag} }; |
Updating current state is done as follows:
1 |
current_state = transition_table[current_state & 0b00000111][pins_input]; |
Example of using this in interrupt routine:
1 2 3 4 5 6 |
void isr_encoder(){ byte pins_input = (bitRead(PIND,3) << 1) | bitRead(PIND,2); current_state = transition_table[current_state & 0b00000111][pins_input]; if ((current_state & CW_Flag) && (counter <127)) counter++; if ((current_state & CCW_Flag) && (counter >0)) counter--; } |
Here is a complete example in Arduino using Atmega32, which will put it all together.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
/* Connect encoder pins to encoderPin1, encoderPin2, and GND. by silvius / http://openhardware.ro 06-Nov-2014 */ #include <LiquidCrystal.h> /* ====== definitions for encoder FSM (finite state machine) ===== */ /* *** states *** */ #define START 0x0 #define CW_Step1 0x1 #define CW_Step2 0x2 #define CW_Step3 0x3 #define CCW_Step1 0x4 #define CCW_Step2 0x5 #define CCW_Step3 0x6 /* *** flags *** */ #define CW_Flag 0b10000000 // ClockWise flag (bit 7) #define CCW_Flag 0b01000000 // CounterClokWise flag (bit 6) // transition table for encoder FSM const byte transition_table[7][4] = { /* 00 01 10 11 */ /* -----------------------------------------------------*/ /*START | */ {START, CCW_Step1, CW_Step1, START}, /*CW_Step1 | */ {CW_Step2, START, CW_Step1, START}, /*CW_Step2 | */ {CW_Step2, CW_Step3, CW_Step1, START}, /*CW_Step3 | */ {CW_Step2, CW_Step3, START, START | CW_Flag}, /*CCW_Step1 | */ {CCW_Step2, CCW_Step1, START, START}, /*CCW_Step2 | */ {CCW_Step2, CCW_Step1, CCW_Step3, START}, /*CCW_Step3 | */ {CCW_Step2, START, CCW_Step3, START | CCW_Flag} }; /* ====== END definitions for encoder FSM (finite state machine) ===== */ byte current_state; // global variable for encoder state // read-write only in ISR volatile byte counter; // global variable // read-write in ISR, read in main loop // these pins corespond to INT0 and INT1 on ATMEGA32 // must be changed depending on the Arduino board you use #define encoderPin1 10 // corespond to PORT D bit 2 - INT0 #define encoderPin2 11 // corespond to PORT D bit 3 - INT1 // pins for LCD // must be changed depending your configuration #define p_RS 18 #define p_EN 19 #define p_D4 12 #define p_D5 13 #define p_D6 14 #define p_D7 15 LiquidCrystal lcd(p_RS, p_EN, p_D4, p_D5, p_D6, p_D7); void setup() { lcd.begin(16, 2); lcd.clear(); lcd.print("ENCODER FSM"); pinMode(encoderPin1, INPUT); digitalWrite(encoderPin1, HIGH); // Turn on internal pullup resistor pinMode(encoderPin2, INPUT); digitalWrite(encoderPin2, HIGH); // Turn on internal pullup resistor attachInterrupt(0, isr_encoder, CHANGE); // use same ISR for INT0 attachInterrupt(1, isr_encoder, CHANGE); // and INT1 counter=0; current_state=START; } // interrupt service routine for INT0 and INT1 void isr_encoder(){ // reading inputs from encoderPin1 and encoderPin2 // not used digitalRead for speed reason byte pins_input = (bitRead(PIND,3) << 1) | bitRead(PIND,2); // update currents state current_state = transition_table[current_state & 0b00000111][pins_input]; //bitmask 0b00000111 is used to consider only 3 less significant bits // of current state (values 0,1, ... 6) //otherwise current_state not work as an index in table // (when CW_Flag or CCW_Flag is set) // testing flag CW and update counter if ((current_state & CW_Flag) && (counter <127)) counter++; // testing flag CCW and update counter if ((current_state & CCW_Flag) && (counter >0)) counter--; } // in main loop only display counter value void loop() { lcd.setCursor(0, 1); lcd.print("Counter:"); lcd.print(counter); lcd.print(" "); } |
This article will not stop here. Other sections will follow:
- Reading multiple rotary encoders with Arduino.(ATMEGA1284 DIP40).
- Verilog design for multiple rotary encoders with Altera FPGA development board.
- Downgrade to CPLD and PCB design for final project.
This sections are not ready yet for reasons of logistics.
I have already ordered 3 pieces ATMEGA1284 … about 1 week to delivery …arrived
Ordered ALTERA FPGA Development Board … about 3 weeks to delivery…arrived
Ordered TQFP100 carrier board for ALTERA CPLD.(Already have MAX II Cpld EPM570T100C5N) about 3 weeks to delivery. …arrived
If you’re interested in the evolution of the project, check for updates once a week
Later edit (25.feb.2015):
Part 2 it’s now ready Rotary encoders experiments – part 2.
I wonder if this could be decoded with a more simplistic state machine.
Referring to this diagram
http://openhardware.ro/wp-content/uploads/RotaryEncoderSignal-958×483.jpg
Whenever the readout is 00 (at a dent) you just have to know the state S before.
If S was 01 the the encoder was turned CW and if it was 10 then it was turned CCW.
You could put the XOR of channel A and B on an interrupt to get all events you need.
Am I wrong?
It depends… 🙂
I guess It will work correctly in 99% of cases.
But it will not “catch” correctly cases when the user “changes his mind” from “intermediate positions”. So … if you do not mind this behavior… It will work like you said.
Also, this (complicated) version will filter more better some noises.
There is a way using a Half-Step table that only produces values in 00 state (or 11, but not both).
This code using the FSM is the best for dealing with rotary encoders, but is which uses the most memory. It works for polling or interrupt without loosing any position and fully debounces them.
Anyway if interrupts are used i recommend using some capacitors to filter the encoder outputs. It reduces the bouncing enough to not interrupt the microcontroller too much when it’s switching. A RC filter is recomended instead only a C but it works (i tested personally).
The idea is to reduce the bouncing, not remove it completely. You don’t want to trigger the interrupts 100 times before the encoder sets (that takes CPU time), when with a simple filter you can reduce them to a minimum (or even eliminate all). If there is any bouncing remaining, the FSM will managate it.
Important: RC values for the low-pass filter are critical. In general R=10K and C=1nF are fine for the most of designs.
At first, thank you for the feedback! I must say that I gave up deliberately with hardware debouncing… because my intention is to read large numbers of rotary encoders… and I relate to this MCU as a dedicated MCU.
Consider it as a specialized MCU (even is not) that have one purpose… to read this encoders and put values on other MCU. In that way, this (pseudo)specialized MCU gave me opportunity to get rid of these extra components… and greatly simplified the PCB.
I did a project on a test board with 15 rotary encoders… it involves 30 capacitors and 30 resistors … I was glad to get rid of them… soldering them in addition to what already was there… to much mess on that board.
But you have a point! And what a on test board seemed difficult, on a PCB it’s not a big deal… So yes, I will redesign a the PCB that I made a some time ago…
I must say that this algorithm work flawless without RC filter… trust me… 🙂 I have done real tests …
But RC filter likely will optimize the use of MCU, especially if this MCU also has something else to do.
Hence in the end, thanks for your suggestion! I will incorporate it into my project. Luckily I did not get to order PCBs yet. I’ll do after redesign.