Show by Label

Monday, November 14, 2016

Demistifying Rotary Encoders (updated)


For an Arduino based project, I wanted to use a rotary decoder to control a menu based structure.

As you are probably aware, there are dozens of solutions available, typically one more complex than the other, in an attempt to make it reliable and fast.

There are basically three schools of thought. One set of die-hards believe in a pure software solution,  and others in a hardware solution, the remaining ones, the more practical ones?, use a combination of both.

Then there are two ways to get the information from the encoder. You obtain the switch recognition in the main loop construct, or you use 1 or 2 interrupts. In the first case, you need to carefully design the main loop, because if you don't get the timing right, the recognition of a twist (a click) of the decoder will be slow, or can be missed. This makes for a very poor user interface (U/I) because it does not seem responsive.

Using an interrupt to recognize movement can be more responsive, unfortunately, almost all interrupt based solutions use two interrupts. On the Arduino Nano or Mini-Pro, there are only two external interrupts available, so using both can be a problem. The good news is that you really don't always need two interrupts.

If you look at the datasheet, you are presented with perfectly modeled waveforms of the two switches that are central to these mechanical decoders. I'm not discussing the much more expensive optical versions here. Here is a picture of the typical waveforms.



Image result for rotary encoder switch


First off, the real wave-forms are not perfectly symmetrical, the output is depending on the mechanical construction and the rotation speed. The other important bit of information is that practically, you rotate the switch from indent to indent.

Here is a screen shot from a one indent move clockwise, captured with a Logic Analyzer:


And here is the screen shot of moving one indent anti-clockwise, or back.


Notice the different pulse width of the A and the B switches in both cases.

The challenge is to not only detect a rotation movement, a click, but also the direction, and then in such a way that you can also rotate the switch very fast and always be correct.

Using one interrupt

In the following Arduino sketch, I use one interrupt on the rising edge of the A-switch, and then sample the level of the B-switch. As you can see above, a clock-wise (to the right) rotation will cause the A-switch to become high before the B-switch. If you turn the other way, the B-switch is already high when the A-switch becomes high.

In order to track and visualize what is going on, I added some statements in the code that will generate a trigger pulse so the Logic Analyzer or scope will help us with the timing relationships.

Here is the sketch:

/* Software Debouncing - Mechanical Rotary Encoder */

#include <FaBoLCD_PCF8574.h>             //include the i2c bus interface and LCD driver code

//---- initialize the i2c/LCD library
FaBoLCD_PCF8574 lcd;                     //with this, there are no further code changes writing to the LCD

#define encoderPinA 2                    //encoder switch A
#define encoderPinB 4                    //encoder switch B
#define encoderPushButton  5             //encoder push button switch
#define Trigger 6                        //Trigger port for Logic Analyzer or Scope

volatile int encoderPos = 0;
volatile int oldencoderPos = 0;

void setup() {
  pinMode(Trigger, OUTPUT);
  pinMode(encoderPinA, INPUT);
  pinMode(encoderPinB, INPUT);
  pinMode(encoderPushButton, INPUT); 
  attachInterrupt(digitalPinToInterrupt(encoderPinA), rotEncoder, RISING); //int 0 
  lcd.begin(16, 2);                      //set up the LCD's number of columns and rows
  lcd.clear();                           //clear dislay
  lcd.setCursor(0,0);                    //set LCD cursor to column 0, row O (start of first line)
  lcd.print("Rotary Encoder");
  lcd.setCursor(0,1);                    //set LCD cursor to column 0, row 1 (start of second line)
  lcd.print(encoderPos);
}

void rotEncoder(){
  boolean rotate;
  delayMicroseconds(300);                //approx. 0.75 mSec to get past any bounce
                                         //delay() does not work in an ISR
  //send entry Trigger pulse
  digitalWrite(Trigger, HIGH);
  delayMicroseconds(10);
  digitalWrite(Trigger, LOW);
 
  rotate = digitalRead(encoderPinA);           //Read the A-switch again
  if (rotate == HIGH) {                        //if still High, knob was really turned
    if (rotate == digitalRead(encoderPinB)) {  //determine the direction by looking at B
      encoderPos--;
    } else {                                  
      encoderPos++;
    }
  }
  //send exit Trigger pulse
  digitalWrite(Trigger, HIGH);
  delayMicroseconds(10);
  digitalWrite(Trigger, LOW); 
}


void loop() {
  //loop until we get an interrupt that will change the encoder position counter
  if (encoderPos != oldencoderPos) {
    lcd.setCursor(0,1);
    lcd.print(encoderPos);
    lcd.print("      ");
    oldencoderPos = encoderPos;
  }
}

And here is a screen shot of a forward indent with the trigger pulses:


As you can see from this data, it takes the Arduino 755 uSec from the A-switch rising edge recognition to the entry into the Interrupt Service Routine (ISR). It then only needs 14.2 uSec to do the rotation recognition.

To put this into perspective, so you get an idea of the relative "blinding" speed of a 16MHz Arduino in relation to slow moving switches:


 Turning the knob as fast as I can produces the picture below:


This solution works pretty good, but is not perfect.

A more reliable method, using two interrupts

This is my most favorite solution. I have used this method with a lot of success, but it uses two interrupts to determine the direction and the clicks. Unfortunately, the number of interrupts on the Arduino is limited, so you may not always have the room to implement this.

In the global area, you need this:

// Rotary Encoder setup
static int enc_A = 2; // D2 No filter caps used!
static int enc_B = 3; // D3 No filter caps used!
static int enc_But = 7; // D7 No filter caps used!
volatile byte aFlag = 0; // expecting a rising edge on pinA encoder has arrived at a detent
volatile byte bFlag = 0; // expecting a rising edge on pinB encoder has arrived at a detent (opposite direction to when aFlag is set)
volatile byte encoderPos = 0; //current value of encoder position (0-255). Change to int or uin16_t for larger values
volatile byte oldEncPos = 0; //stores the last encoder position to see if it has changed
volatile byte reading = 0; //store the direct values we read from our interrupt pins before checking to see if we have moved a whole detent


In the setup code, you need this:

  // setup the rotary encoder switches and ISR's
  pinMode(enc_A, INPUT_PULLUP); // set pinA as an input, pulled HIGH
  pinMode(enc_B, INPUT_PULLUP); // set pinB as an input, pulled HIGH
  attachInterrupt(0, enc_a_isr,RISING);
  attachInterrupt(1, enc_b_isr,RISING);


Here are the two Interrupt Service Routines, one for each switch:

/*
 * Rotary decoder ISR's for the A and B switch activities.
 */
void enc_a_isr(){
  cli(); //stop interrupts
  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
    if (encoderPos <= 0){
      encoderPos = 0;
    }else{
      encoderPos --;
    }
    bFlag = 0; //reset flags
    aFlag = 0; //reset flags
  }
  else if (reading == B00000100) bFlag = 1; //we're expecting pinB to signal the transition to detent from free rotation
  sei(); //restart interrupts
}

void enc_b_isr(){
  cli(); //stop interrupts
  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
    if (encoderPos >= 255){
      encoderPos = 255;
    }else{
      encoderPos ++;
    }
    bFlag = 0; //reset flags
    aFlag = 0; //reset flags
  }
  else if (reading == B00001000) aFlag = 1; //we're expecting pinA to signal the transition to detent from free rotation
  sei(); //restart interrupts
}

This is a much better solution, and even needs no extra hardware parts. I have not been able to detect any false "claims". However, I have not tried it on faster processors so be aware. 

In the meantime, I modified this code to add acceleration, and used it on an ESP32. The code and details can be found in my DC Dynamic Load project here.

There are other debounce solutions in software that only use one interrupt. And then there are solutions that rely on the fact that if you trigger on the edge of one of the pins, the level on the other pin is in the middle of it's cycle and therefore stable. If you "transpose" that level in place of the one you triggered on, you have effectively eliminated the bounce. Very clever! Have a look here : Special Solution


Rotary Encoder with hardware debounce en direction decode

The following is an attempt that uses hardware debounce en encoding to reduce the processing overhead (less false triggers) and can be used on the 8 or 16MHz clocked Arduino's, and also with faster processors like the Raspberry Pi or the ESP processors.

Here is the schematic diagram.


The idea is to determine the direction with the aid of a Flip-Flop. First the switches are debounced with an R/C combination and then fed to Schmitt-triggers to get clean logic transitions again, This circuit creates a clock pulse for every indent and a directional level that changes only when the direction changes from clockwise to anti-clockwise. 

I created a small PCB, and here is the result of my soldering.


However, is it perfect? Unfortunately no. The switches of very cheap China rotary encoders as the one above are mechanically inferior, but really good ones, typically the optical kind, are expensive. After some testing, I reduced the values of the capacitors to 10nF. In combination with the 10K resistors they are sufficient.

This extra hardware reduces a lot of processing power for the micro-controller, because it reduces the amount of false triggers and is not relying on interrupts, although you could use one on the CLK signal. It could be used in a polling loop as well.

However, when you have bounces like the ones below, all bets are off. These are taken from a cheap no-name rotary switch. In this case, I just used a 10K pull-up resistor to 5V to the switching pin and used GND on the common pin.


Below is an example with a simple and typical 10K/10K/10nF r/c filter like the one in the schematic diagram above. It adequately filters out most of the bounce noise. This is actually taken from an ALPS STEC12E08 version, which is a lot better than the cheap no-name ones. At €2,34 its not that expensive.


However, even these much better quality rotary switches are not perfect, see below. The blue trace is taken directly from the switch, the yellow trace after the simple debounce circuit. In my opinion, the only solution in that case is to use a combined hardware/software solution. You would have to use pretty long delays after the trigger to attempt a software solution. 



It shows that you cannot simply use one-fits-all solutions, you really should try it out, based on your switch and your application.


If you like what you see, please support me by buying me a coffee: https://www.buymeacoffee.com/M9ouLVXBdw