A rotary or "shaft" encoder is an angular measuring device. It is used to precisely measure rotation of motors or to create wheel controllers (knobs) that can turn infinitely (with no end stop like a potentiometer has). Some of them are also equipped with a pushbutton when you press on the axis (like the ones used for navigation on many music controllers). They come in all kinds of resolutions, from maybe 16 to at least 1024 steps per revolution, and cost from 2 to maybe 200 EUR.
I've written a little sketch to read a rotary controller and send its readout via RS232.
It simply updates a counter (encoder0Pos) every time the encoder turns by one step, and sends it via serial to the PC.
This works fine with an ALPS STEC12E08 encoder which has 24 steps per turn. I can imagine it could fail with encoders with higher resolution, or when it rotates very quickly (think: motors), or when you extend it to accommodate multiple encoders. Please give it a try.
I learned about how to read the encoder from the file encoder.h included in the Arduino distribution as part of the AVRLib. Thanks to its author, Pascal Stang, for the friendly and newbie-proof explanation of the functionings of encoders there.here you go:
Oh, a few notes:
I'm not sure about the etiquette of this, but I'm just going to add onto this tutorial. Paul Badger
Below is an image showing the waveforms of the A & B channels of an encoder.
This might make it a little more clear how the code above works. When the code finds alow-to-high transition on the A channel, it checks to see if the B channel is high or low and then increments/decrements the variable to account for the direction that the encoder must be turning in order to generate the waveform found.
One disadvantage of the code above is that it is really only counting one fourth of the possible transitions. In the case of the illustration, either the red or the lime green transitions, depending on which way the encoder is moving.
Below is some code that uses an interrupt. When the Arduino sees a change on the A channel, it immediately skips to the "doEncoder" function, which parses out both the low-to-high and the high-to-low edges, consequently counting twice as many transitions. I didn't want to use both interrupt pins to check the other two classes of transition on the B channel (the violet and cyan lines in the chart above), but it doesn't seem much more complicated to do so.
Using interrupts to read a rotary encoder is a perfect job for interrupts because the interrupt service routine (a function) can be short and quick, because it doesn't need to do much.
I used the encoder as a "mode selector" on a synthesizer made solely from an Arduino chip. This is a pretty casual application, because it doesn't really matter if the encoder missed pulses, the feedback was coming from the user. Where the interrupt method is going to shine is with encoders used for feedback on motors - such as servos or robot wheels. In those applications, the microcontroller can't afford to miss any pulses or the resolution of movement is going to suffer.
One side note: I used the Arduino's pullup resistors to "steer" the inputs high when they were not engaged by the encoder. Hence the encoder common pin is connected to ground. The sketch above fails to mention that some pulldown resistors (10k is fine) are going to be needed on the inputs since the encoder common is attached to +5V.
/* read a rotary encoder with interrupts Encoder hooked up with common to GROUND, encoder0PinA to pin 2, encoder0PinB to pin 4 (or pin 3 see below) it doesn't matter which encoder pin you use for A or B uses Arduino pullups on A & B channel outputs turning on the pullups saves having to hook up resistors to the A & B channel outputs */ #define encoder0PinA 2#define encoder0PinB 4volatile unsigned int encoder0Pos = 0;void setup() { pinMode(encoder0PinA, INPUT); digitalWrite(encoder0PinA, HIGH); // turn on pullup resistor pinMode(encoder0PinB, INPUT); digitalWrite(encoder0PinB, HIGH); // turn on pullup resistor attachInterrupt(0, doEncoder, CHANGE); // encoder pin on interrupt 0 - pin 2 Serial.begin (9600); Serial.println("start"); // a personal quirk} void loop(){// do some stuff here - the joy of interrupts is that they take care of themselves}void doEncoder() { /* If pinA and pinB are both high or both low, it is spinning * forward. If they're different, it's going backward. * * For more information on speeding up this process, see * [Reference/PortManipulation], specifically the PIND register. */ if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) { encoder0Pos++; } else { encoder0Pos--; } Serial.println (encoder0Pos, DEC);}/* See this expanded function to get a better understanding of the * meanings of the four possible (pinA, pinB) value pairs: */void doEncoder_Expanded(){ if (digitalRead(encoder0PinA) == HIGH) { // found a low-to-high on channel A if (digitalRead(encoder0PinB) == LOW) { // check channel B to see which way // encoder is turning encoder0Pos = encoder0Pos - 1; // CCW } else { encoder0Pos = encoder0Pos + 1; // CW } } else // found a high-to-low on channel A { if (digitalRead(encoder0PinB) == LOW) { // check channel B to see which way // encoder is turning encoder0Pos = encoder0Pos + 1; // CW } else { encoder0Pos = encoder0Pos - 1; // CCW } } Serial.println (encoder0Pos, DEC); // debug - remember to comment out // before final program run // you don't want serial slowing down your program if not needed}/* to read the other two transitions - just use another attachInterrupt()in the setup and duplicate the doEncoder function into say, doEncoderA and doEncoderB. You also need to move the other encoder wire over to pin 3 (interrupt 1). */Careful when using Serial.Print inside an interrupt function, most of the time it will fail, but it works sometimes, the worst of programming bugs. It is documented in a number of places: "https://groups.google.com/a/arduino.cc/forum/#!topic/developers/HKzEcN6gikM""http://forum.arduino.cc/index.php?topic=94459.0""http://forum.jeelabs.net/node/1188.html"
Code for reading encoder using 2 interrupts on pin 2 & pin3
Note: the code below uses 2 interrupts to read the full resolution of the encoder. The code above used 1 interrupt. It read half the resolution by only checking EncoderPin A for position, but it freed up an interrupt pin.
#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
void setup() {
pinMode(encoder0PinA, INPUT); pinMode(encoder0PinB, INPUT);// encoder pin on interrupt 0 (pin 2)
attachInterrupt(0, doEncoderA, CHANGE);// encoder pin on interrupt 1 (pin 3)
attachInterrupt(1, doEncoderB, CHANGE); Serial.begin (9600);}
void loop(){//Do stuff here}
void doEncoderA(){
// look for a low-to-high on channel A if (digitalRead(encoder0PinA) == HIGH) { // check channel B to see which way encoder is turning if (digitalRead(encoder0PinB) == LOW) { encoder0Pos = encoder0Pos + 1; // CW } else { encoder0Pos = encoder0Pos - 1; // CCW } } else // must be a high-to-low edge on channel A { // check channel B to see which way encoder is turning if (digitalRead(encoder0PinB) == HIGH) { encoder0Pos = encoder0Pos + 1; // CW } else { encoder0Pos = encoder0Pos - 1; // CCW } } Serial.println (encoder0Pos, DEC); // use for debugging - remember to comment out}
void doEncoderB(){
// look for a low-to-high on channel B if (digitalRead(encoder0PinB) == HIGH) { // check channel A to see which way encoder is turning if (digitalRead(encoder0PinA) == HIGH) { encoder0Pos = encoder0Pos + 1; // CW } else { encoder0Pos = encoder0Pos - 1; // CCW } } // Look for a high-to-low on channel B else { // check channel B to see which way encoder is turning if (digitalRead(encoder0PinA) == LOW) { encoder0Pos = encoder0Pos + 1; // CW } else { encoder0Pos = encoder0Pos - 1; // CCW } }}
by mikkoh [01/2010]
wrapped above example (example with one interrupt) into a class and reduced code size from the doEncoder function a bit (hope it's still readable to the most of you). An example usage is contained in the class documentation.
#ifndef __ENCODER_H__#define __ENCODER_H__#include "WProgram.h"class Encoder { /* wraps encoder setup and update functions in a class !!! NOTE : user must call the encoders update method from an interrupt function himself! i.e. user must attach an interrupt to the encoder pin A and call the encoder update method from within the interrupt uses Arduino pullups on A & B channel outputs turning on the pullups saves having to hook up resistors to the A & B channel outputs // ------------------------------------------------------------------------------------------------ // Example usage : // ------------------------------------------------------------------------------------------------ #include "Encoder.h" Encoder encoder(2, 4); void setup() { attachInterrupt(0, doEncoder, CHANGE); Serial.begin (115200); Serial.println("start"); } void loop(){ // do some stuff here - the joy of interrupts is that they take care of themselves } void doEncoder(){ encoder.update(); Serial.println( encoder.getPosition() ); } // ------------------------------------------------------------------------------------------------ // Example usage end // ------------------------------------------------------------------------------------------------ */public: // constructor : sets pins as inputs and turns on pullup resistors Encoder( int8_t PinA, int8_t PinB) : pin_a ( PinA), pin_b( PinB ) { // set pin a and b to be input pinMode(pin_a, INPUT); pinMode(pin_b, INPUT); // and turn on pullup resistors digitalWrite(pin_a, HIGH); digitalWrite(pin_b, HIGH); }; // call this from your interrupt function void update () { if (digitalRead(pin_a)) digitalRead(pin_b) ? position++ : position--; else digitalRead(pin_b) ? position-- : position++; }; // returns current position long int getPosition () { return position; }; // set the position value void setPosition ( const long int p) { position = p; };private: long int position; int8_t pin_a; int8_t pin_b;};#endif // __ENCODER_H__Uses both External Interrupt pins, Counts in 1 Direction only.Editor's note: Although this code claims efficiency gains, note that it uses the digitalRead() library functions, which according to http://jeelabs.org/2010/01/06/pin-io-performance/ is 50 times as slow as direct port reads.
by m3tr0g33k
Paul Badger's work and the original post are enlightening and useful - you will need to understand what they have said before this makes sense to you (I hope it does!).
My project is a data logger where three analogue inputs are sampled each time a rotary encoder pulse steps clockwise. On an Arduino, time is of the essence to get this data sampled and saved somewhere (I have not included the 'saving somewhere' part of this project yet.) In order to save some processor cycles, I have slightly redesigned the interrupt system to maintain a pair of Boolean states outside of the interrupt loop.
The idea is to set a Boolean state for A or B when there is a positive going edge on encoder output A or B. So, when you get an interrupt from A, and it's positive going, you set A_set=true. Then you test if B_set is false. If it is, then A leads B which means a clockwise step (increment position counter).
Likewise, when you get an interrupt from B, and it's positive going, you set B_set=true. Then you test if A_set is false. If it is, then B leads A, which means a counter-clockwise step (decrement position counter).
An important difference from previous code examples is when there is an interrupt on A or B which is negative going, you just set A_set or B_set to false, respectively, no further work is needed, reducing the time spent servicing the interrupt.
Anyway, enough explanation of code in words, here is the code:
#define encoder0PinA 2#define encoder0PinB 3volatile unsigned int encoder0Pos = 0;unsigned int tmp_Pos = 1;unsigned int valx;unsigned int valy;unsigned int valz;boolean A_set;boolean B_set;void setup() { pinMode(encoder0PinA, INPUT); pinMode(encoder0PinB, INPUT); // encoder pin on interrupt 0 (pin 2) attachInterrupt(0, doEncoderA, CHANGE);// encoder pin on interrupt 1 (pin 3) attachInterrupt(1, doEncoderB, CHANGE); Serial.begin (9600);}void loop(){ //Check each second for change in position if (tmp_Pos != encoder0Pos) { Serial.print("Index:"); Serial.print(encoder0Pos, DEC); Serial.print(", Values: "); Serial.print(valx, DEC); Serial.print(", "); Serial.print(valy, DEC); Serial.print(", "); Serial.print(valz, DEC); Serial.println(); tmp_Pos = encoder0Pos; } delay(1000);}// Interrupt on A changing statevoid doEncoderA(){ // Low to High transition? if (digitalRead(encoder0PinA) == HIGH) { A_set = true; if (!B_set) { encoder0Pos = encoder0Pos + 1; valx=analogRead(0); valy=analogRead(1); valz=analogRead(2); } } // High-to-low transition? if (digitalRead(encoder0PinA) == LOW) { A_set = false; }}// Interrupt on B changing statevoid doEncoderB(){ // Low-to-high transition? if (digitalRead(encoder0PinB) == HIGH) { B_set = true; if (!A_set) { encoder0Pos = encoder0Pos - 1; } } // High-to-low transition? if (digitalRead(encoder0PinB) == LOW) { B_set = false; }}The rest of the code around the improved interrupt routines is just to demonstrate that it works. As I said, I only want to sample when going CW (which is forwards on my sampling trolley). When the encoder is going CCW, I just update the counter.
The loop{} prints the current encoder position and the corresponding sampled data values every second, but only if the encoder position has changed. You can have fun seeing how far you can rotate your encoder in a second! I have managed nearly 300 steps or 1.5 revolutions on my rotary encoder.
There is one issue with the logic in this code. If you are changing direction a lot then you may wish to know that if you change direction in the middle of a step, your counter will not update. This is a half-step hysteresis. Under most circumstances, this is not noticeable or important, but think whether it is important to you!
Hope this potential speed increase helps someone!
m3tr0g33k
You can reduce the size of the interrupt service routines considerably by looking at A_set == B_set to determine lag vs lead.
The ISR's are then just
// Interrupt on A changing statevoid doEncoderA(){ // Test transition A_set = digitalRead(encoderPinA) == HIGH; // and adjust counter + if A leads B encoderPos += (A_set != B_set) ? +1 : -1;}// Interrupt on B changing statevoid doEncoderB(){ // Test transition B_set = digitalRead(encoderPinB) == HIGH; // and adjust counter + if B follows A encoderPos += (A_set == B_set) ? +1 : -1;}Basically, if the pin that changed now matches the other pin, it's lagging behind it, and if the pin that changed is now different, then it's leading it.
Net result: two lines of code to process the interrupt.
The entire sketch is
enum PinAssignments { encoderPinA = 2, encoderPinB = 3, clearButton = 8};volatile unsigned int encoderPos = 0;unsigned int lastReportedPos = 1;boolean A_set = false;boolean B_set = false;void setup() { pinMode(encoderPinA, INPUT); pinMode(encoderPinB, INPUT); pinMode(clearButton, INPUT); digitalWrite(encoderPinA, HIGH); // turn on pullup resistor digitalWrite(encoderPinB, HIGH); // turn on pullup resistor digitalWrite(clearButton, HIGH);// encoder pin on interrupt 0 (pin 2) attachInterrupt(0, doEncoderA, CHANGE);// encoder pin on interrupt 1 (pin 3) attachInterrupt(1, doEncoderB, CHANGE); Serial.begin(9600);}void loop(){ if (lastReportedPos != encoderPos) { Serial.print("Index:"); Serial.print(encoderPos, DEC); Serial.println(); lastReportedPos = encoderPos; } if (digitalRead(clearButton) == LOW) { encoderPos = 0; }}// Interrupt on A changing statevoid doEncoderA(){ // Test transition A_set = digitalRead(encoderPinA) == HIGH; // and adjust counter + if A leads B encoderPos += (A_set != B_set) ? +1 : -1;}// Interrupt on B changing statevoid doEncoderB(){ // Test transition B_set = digitalRead(encoderPinB) == HIGH; // and adjust counter + if B follows A encoderPos += (A_set == B_set) ? +1 : -1;}More encoder links
this code did work better for me than most others, with good explanation
________________________________________________________________________________________________________________
Uses both External Interrupt pins, after the initial read, does not read the state of the pins.
Fast encoder reading: using just interrupts
I also had to face with the problem of reading encoder signals and, after many trials, I'm happy to let you know a new way to deal with them, inspired by all the previous suggestions. I tried it on AMT encoder and it work really good. Unlikely, the other methods failed, the counting rate was too fast. To escape reading Arduino's port programming, I thought it may be even faster just using the interrupt pins. Here is the code:
//PIN's definition#define encoder0PinA 2#define encoder0PinB 3volatile int encoder0Pos = 0;volatile boolean PastA = 0;volatile boolean PastB = 0;void setup() { pinMode(encoder0PinA, INPUT); //turn on pullup resistor //digitalWrite(encoder0PinA, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!! pinMode(encoder0PinB, INPUT); //turn on pullup resistor //digitalWrite(encoder0PinB, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!! PastA = (boolean)digitalRead(encoder0PinA); //initial value of channel A; PastB = (boolean)digitalRead(encoder0PinB); //and channel B//To speed up even more, you may define manually the ISRs// encoder A channel on interrupt 0 (arduino's pin 2) attachInterrupt(0, doEncoderA, RISING);// encoder B channel pin on interrupt 1 (arduino's pin 3) attachInterrupt(1, doEncoderB, CHANGE); }void loop(){ //your staff....ENJOY! :D}//you may easily modify the code get quadrature..//..but be sure this whouldn't let Arduino back! void doEncoderA(){ PastB ? encoder0Pos--: encoder0Pos++;}void doEncoderB(){ PastB = !PastB; }I hope this can help you.
by carnevaledaniele [04/2010]
_____________________________________________________________\\
Here's a complete library code for working with Encoders:
#include #include "HardwareSerial.h"// 12 Step Rotary Encoder with Click //// http://www.sparkfun.com/products/9117 //#define EncoderPinA 20// Rotary Encoder Left Pin //#define EncoderPinB 19// Rotary Encoder Right Pin //#define EncoderPinP 21// Rotary Encoder Click //// ======================================================================================= //class Encoder{public:Encoder() { pinMode(EncoderPinA, INPUT);digitalWrite(EncoderPinA, HIGH);pinMode(EncoderPinB, INPUT);digitalWrite(EncoderPinB, HIGH);pinMode(EncoderPinP, INPUT);digitalWrite(EncoderPinP, HIGH);Position = 0; Position2 = 0; Max = 127; Min = 0;clickMultiply = 10;}void Tick(void){ Position2 = (digitalRead(EncoderPinB) * 2) + digitalRead(EncoderPinA);;if (Position2 != Position){isFwd = ((Position == 0) && (Position2 == 1)) || ((Position == 1) && (Position2 == 3)) || ((Position == 3) && (Position2 == 2)) || ((Position == 2) && (Position2 == 0));if (!digitalRead(EncoderPinP)) { if (isFwd) Pos += clickMultiply; else Pos -= clickMultiply; }else { if (isFwd) Pos++; else Pos--; }if (Pos < > Max) Pos = Max;}Position = Position2;}int getPos(void){return (Pos/4);}void setMinMax(int _Min, int _Max) { Min = _Min*4; Max = _Max*4; if (Pos < > Max) Pos = Max;}void setClickMultiply(int _clickMultiply){clickMultiply = _clickMultiply;}private:int clickMultiply;int Max;int Min;int Pos;int Position;int Position2;int isFwd;};Uses both External Interrupt pins. Based on the Circuits@home code. Does not debounce. Notice that the Serial.print() functions can take milliseconds to return, so the interrupt code takes relatively long. This may change the behavior of the code when those statements are removed (as some bounces and even some transitions may be missed if the interrupt routine takes a comparatively long time). -Ed.
/* RotaryInterrupts - a port-read and interrupt based rotary encoder sketch Created by Joshua Layne (w15p), January 4, 2011. based largely on: http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino Released into the public domain.*/#define ENC_A 2#define ENC_B 3#define ENC_PORT PINDuint8_t bitShift = 2; // change to suit your pins (offset from 0,1 per port)// Note: You need to choose pins that have Interrupt capability.int counter;boolean ticToc;void setup(){ pinMode(ENC_A, INPUT); digitalWrite(ENC_A, HIGH); pinMode(ENC_B, INPUT); digitalWrite(ENC_B, HIGH); Serial.begin (115200); Serial.println("Start"); counter = 0; ticToc = false; // Attach ISR to both interrupts attachInterrupt(0, read_encoder, CHANGE); attachInterrupt(1, read_encoder, CHANGE);}void loop(){// do some stuff here - the joy of interrupts is that they take care of themselves}void read_encoder(){ int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; static uint8_t encoderState = 0; static uint8_t stateIndex = 0; static uint8_t filteredPort = 0; uint8_t filter = 0x03; // base filter: 0b00000011 filter