Show by Label

Monday, November 14, 2016

Demistifying Rotary Encoders (some more)

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 two schools of thought. You use the recognoition in the main loop construct, or you use 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).

Using an interupt to recognise 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 the heart of the decoder. 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 switch 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 this picture:


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 one of 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 by far the best solution, and 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.


Rotary Encoder with hardware debounce en direction decode

The following is an attempt that can be used on the rather slow Arduino, 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 these very cheap rotary encoders are mechanically inferiour, but good ones, typically the optical kind, are expensive. 

Even with all this effort, there are still spurious glitches every now and then, that need to be fixed in software. This extra hardware however reduces a lot of processing power for the micro-controller, and is not relying on interrupts. It could be used in a polling loop.



No comments:

Post a Comment