As explained in this post, rotary encoders are notoriously unreliable. Well, the cheap ones are. Why bother?
But wait, could they be used at all? Most of the problems people complain about have to do with bouncing. But is bouncing as important if someone is turning a rotary encoder manually to increment a counter? Also, debouncing often slows down the response speed of the Arduino code enough to miss a few clicks. Is missing a few clicks important?
What I’m trying to implement is a simple way of incrementing or decrementing a counter to choose a stored patch value in a pedal board. Practically, this is done by manually turning a knob attached to a rotary encoder. Software will read the encoder and increment (or decrement) the value of a counter associated with a particular patch, or memory value.
The rotary encoder that I’m using came with a knob:
As you can see, the knob is huge! The speed at which one can twist a knob between one’s thumb and index finger is linked directly to the size of the knob, within limits. The bigger the knob, the slower the turning speed. This knob’s diameter is 25 mm (1 inch). The maximum speed that I can achieve is about one revolution per second, in two steps. The clicking speed is then about 40 clicks per second, or 25 ms per click. (I’m guessing the designers knew this)
Let’s see how that affects the debouncing that we might want to do. If we have 25 ms per click, we have to be pretty reasonable about the delay that we think is necessary waiting for the switch to stop bouncing. Actually, even this assumption is wrong. Referring to the previous post, each pin goes through a 0 to 1 then 1 to 0 transition between two clicks. Effectively, our debouncing has to happen twice per click per pin to be able to register every state change. Again, if we have 25 ms per click, there is not much time left to do anything! What to do? As you will see in my solutions, I’m only interested in a transition of one of the pins from 0 to 1. This means that I only have to debounce once per click.
The maximum speed that I could impart on that knob is one revolution per second. I can see myself reaching that speed if I want to jump between two values of my counter as fast as possible. So what if I miss a few clicks? The numbers would be going up fast enough that I couldn’t even see the missing steps (I tried). In practice, as I get close to the chosen value, I would slow down enough to be able to read each number and mentally process its value. As slow as 1/4 of a second per click, or 250 ms! Plenty of time to do a little debouncing!!
After much testing, reading and breadboarding, I chose to explore 3 methods that work well for what I’m trying to achieve.
Before presenting the solutions, I have to state that I rely on one assumption. With a rotary encoder, a transition on pin1 (A above) from 0 to 1 while pin2 (B above)= 1 means that a counter is increased by one. Otherwise, the counter decreases by 1. This is based on the graphic above (thanks to sagsaw). It is in line with the testing done in this post. In Arduino code, this translates to:
if (pin1 == HIGH) if (pin2 == HIGH) counter++; else counter--;
In fact, what I want to catch is the moment when pin1 becomes HIGH. There are a few programming techniques available to achieve this.
Method 1: Using the Arduino Bounce Library
#include <Bounce.h> // This code increments or decrements a counter based on // the status of a rotaty encoder #define pin1 2 #define pin2 3 #define LED 13 int counter = 0; // Instantiate a Bounce object with a 5 millisecond debounce time // Only pin1 needs to be debounced. It is assumed that pin2 // will be stable when reading pin1 Bounce bouncer1 = Bounce( pin1,5 ); void setup() { pinMode(pin1,INPUT); pinMode(pin2,INPUT); pinMode(LED,OUTPUT); Serial.begin(9600); } void loop() { // Update the debouncer bouncer1.update ( ); // Turn on or off the LED and // increment or decrement the counter if ( bouncer1.risingEdge()) { if (digitalRead(pin2)){ digitalWrite(LED, HIGH ); counter++; Serial.println(counter); }else{ counter--; Serial.println(counter); } } else { digitalWrite(LED, LOW ); } }
This sketch is based on the sample code of the new Arduino “Bounce” library, that is replacing the old “Debounce” library. I’m reading the rising edge value on pin1. I’m only interested in a pin1 transition from 0 to 1 (LOW to HIGH). The LED and the Serial commands are for testing only.
I am able to overflow this code if I remove the knob from the rotary encoder and twist the shaft as fast as I can, snapping it between my thumb and index finger. What happens then is that the counter might increase or decrease by one, randomly, until the rotational speed slows down a bit. If you read the counter values in the Arduino IDE, you can spot the artifact. But driving a real life counter, like a 3 digit LED display, you will not be aware of the incrementation skips.
This code will start misbehaving as the Arduino code gets more complex. If I add a delay in the main loop, say delay(100), the encoder readings becomes unreliable.With this in mind, I decided to try a version with an interrupt handler.
Method 2: External Interrupt Handler
The Arduino can accept external interrupts on some of its pins. The goal is again to catch the rising or falling edge on pin1.
/* Digital Pin 2 accepts external interrupts. Pin1 of a rotary encoder is attached to DigitalPin2. An interrupt routine will be called when pin1 changes state, including noise. This will be made more efficient with hardware debouncing. */ int pin1 = 2; int pin2 = 3; int counter; boolean goingUp = false; boolean goingDown = false; void setup() { counter = 0; //Serial prints for debugging and testing Serial.begin(9600); /* Setup encoder pins as inputs */ pinMode(pin1, INPUT); // Pin 2 pinMode(pin2, INPUT); // Pin 4 // encoder pin on interrupt 0 (pin 2) attachInterrupt(0, decoder, FALLING); } void loop() { //using while statement to stay in the loop for continuous //interrupts while(goingUp==1) // CW motion in the rotary encoder { goingUp=0; // Reset the flag counter ++; Serial.println(counter); } while(goingDown==1) // CCW motion in rotary encoder { goingDown=0; // clear the flag counter --; Serial.println(counter); } } void decoder() //very short interrupt routine //Remember that the routine is only called when pin1 //changes state, so it's the value of pin2 that we're //interrested in here { if (digitalRead(pin1) == digitalRead(pin2)) { goingUp = 1; //if encoder channels are the same, direction is CW } else { goingDown = 1; //if they are not the same, direction is CCW } }
This sketch works well because the interrupt routine is very short. I works even better if a bit of hardware debouncing is forced on the rotary encoder. With two 0.1 uF capacitors soldered to the encoder pins, the number of calls to the interrupt routine is dramatically reduced. This is important because too many calls to the interrupt routine will rob computing cycles from the main routine, negating the effects of using interrupts to save computing power. It has been recommended to never connect a bouncy switch directly to a controllers interrupt pins.
Method 3: Using a Timer Interrupt
/* * Example on how to configure the periodical execution of a user * defined function (Interrupt service routine) using Timer2. This * example will run the function every 1ms. */ #include <Bounce.h> /* Timer2 reload value, globally available */ unsigned int tcnt2; int pin1 = 2; int pin2 = 3; // Instantiate a Bounce object with a 5 millisecond debounce time // Only pin1 needs to be debounced. It is assumed that pin2 // will be stable when reading pin1 Bounce bouncer1 = Bounce( pin1,5 ); /* Setup phase: configure and enable timer2 overflow interrupt */ void setup() { pinMode(pin1, INPUT); pinMode(pin2, INPUT); Serial.begin(9600); /* First disable the timer overflow interrupt while we're configuring */ TIMSK2 &= ~(1<<TOIE2); /* Configure timer2 in normal mode (pure counting, no PWM etc.) */ TCCR2A &= ~((1<<WGM21) | (1<<WGM20)); TCCR2B &= ~(1<<WGM22); /* Select clock source: internal I/O clock */ ASSR &= ~(1<<AS2); /* Disable Compare Match A interrupt enable (only want overflow) */ TIMSK2 &= ~(1<<OCIE2A); /* Now configure the prescaler to CPU clock divided by 128 */ TCCR2B |= (1<<CS22) | (1<<CS20); // Set bits TCCR2B &= ~(1<<CS21); // Clear bit /* We need to calculate a proper value to load the timer counter. * The following loads the value 131 into the Timer 2 counter register * The math behind this is: * (CPU frequency) / (prescaler value) = 125000 Hz = 8us. * (desired period) / 8us = 125. * MAX(uint8) + 1 - 125 = 131; */ /* Save value globally for later reload in ISR */ tcnt2 = 131; /* Finally load end enable the timer */ TCNT2 = tcnt2; TIMSK2 |= (1<<TOIE2); } /* * Install the Interrupt Service Routine (ISR) for Timer2 overflow. * This is normally done by writing the address of the ISR in the * interrupt vector table but conveniently done by using ISR() */ ISR(TIMER2_OVF_vect) { /* Reload the timer */ TCNT2 = tcnt2; bouncer1.update(); if(bouncer1.risingEdge()){ if (digitalRead(pin2)) Serial.println("CW"); else Serial.println("CCW"); } } void loop() { delay(100); // Serial.println(millis()); }
I am still working on this version. Ideally, I would like to remove the call to the Bounce Library. But the timer interrupt subroutine is still extremely fast (tested at 12usec). The results are also unaffected by the length of the main loop. I tested it with a delay of 100 ms and the rotary switch was still decoded precisely.
Another version of the Timer function could be:
ISR(TIMER2_OVF_vect) { /* Reload the timer */ TCNT2 = tcnt2; state=(state<<1) | !digitalRead(pin1) | 0xe000; // Serial.println(state,HEX); if (state==0xf000){ state=0x0000; if(digitalRead(pin2)) counter++; else counter--; } }
This will increase or decrease the counter if 12 consecutive reads agree with our condition, but my switch seems to be too unreliable to constantly produce interesting results. I would save 4 usec using this routine.
__________________________________
So, the Timer Interrupt method is the best one. Studying the code of the new Arduino Bounce library (and testing with micros() ) confirmed that the library doesn’t rely on delay() for debouncing, effectively adding very little overhead to method 1 or 3.
This solution will be perfect for my initial version of the Rotary Encoder Arduino sub-project.
If you have read this so far, please keep in mind that your results may vary. I AM ASSUMING that pin2 is stable when I accept the debounced status on pin1. Otherwise, the switch is totally unreliable.
Also, if your application needs 100% confidence that the encoder will never miss a state change, or produce false positive, just buy a better encoder. I definitely would not trust my little encoder to drive a robot or a high precision machine. Take a look at this page and you will realise that my little encoder, at $1, really is at the bottom end of the quality scale. It was definitely made for manual operation, as a volume control or, like I’m doing here, to increase and decrease a counter.
Leave a Reply