Thursday, March 31, 2011

Decoding a Rotary Encoder

I recently acquired a rotary encoder to use as an input device for my hardware project. Knowing that such encoders make use of Gray code, I cracked open my Digital Design textbook and began coding a sketch for my Arduino. It wasn't as straightforward as I had hoped.

This isn't going to be a lesson in Gray code or rotary encoders — you can click the Wikipedia links above for that. This is intended to be a record of the lessons learned while trying to get a reliable, responsive read on the rotary encoder (herein simply "encoder" for brevity) that I have. Google turned up a lot of resources for reading an encoder with an Arduino, but there were a number of problems with them, including:
  • too complicated;
  • too much code (bytes of assembly);
  • too long (cycles); and
  • too unreliable.
Others were just flat out wrong.

From what I read, I decided that an Interrupt Service Routine (ISR) based on the changing levels from the 2 outputs from the encoder was the most reliable way of reading the encoder. Using basic Boolean algebra, I was able to come up with a simple expression to determine if the encoder was rotating clockwise. Where A and B are the previous state bits read from the encoder pins, and C and D are the current bits, the expression representing a clockwise movement can be represented in this truth table:
AB
00011011
000010
C011000
D100001
110100
The table can be reduced to the following boolean expression:
    (A XOR D) AND NOT (B XOR C)

The Arduino code looks like this:
 1:  volatile boolean rotating = false;  
 2:  static boolean rotating_cw = false;  
 3:    
 4:  void rotEncoder() {  
 5:   static byte old = 0;  
 6:   byte in = PIND & B1100;  
 7:   rotating_cw = (!!(old & B0100) ^ !!(in & B1000)) & ~(!!(old & B1000) ^ !!(in & B0100));  
 8:   rotating = true;  
 9:   old = in;  
10:  }  
11:    
12:  void setup() {  
13:   DDRD |= B1100;  
14:   PORTD |= B1100;  
15:   attachInterrupt(0, rotEncoder, CHANGE); // ISR for rotary encoder  
16:   attachInterrupt(1, rotEncoder, CHANGE); // ISR for rotary encoder  
17:  }  
By way of a short explanation, the encoder is hooked up to digital pins 2 and 3 on the Arduino, and I'm using direct port manipulation for faster reads than digitalRead(). The bitwise math should be pretty easy to understand if you have done any real programming; the !! expression, however, is a little trick to normalize a truthy expression (non-zero) to 1 with minimal instructions.

Whenever you need to read the encoder, you just check the rotating variable, and, if true, then read the rotating_cw variable to see which direction the encoder is rotating. After reading, set rotating to false to get ready for the next read. It's small, fast, simple, reliable, and usable in almost any programming application (save for those that require PD2 or PD3; but you can change the code to use any pin that can be used as an interrupt).

Here's the problem: it's too fast and too reliable! I was puzzled why my encoder was generating multiple rotations for each detent on the switch, even with fairly long delays between reads. I agonized over the math, the code, and my own sanity. Finally, by sending each read to serial output, I could see that, although the switch had 24 detents per rotation, each detent clocked a full 4 cycles of Gray code! What I really needed was to generate a rotating signal only once per detent.

It turns out the code for doing that is even faster, simpler, and smaller than the above code:
 1:  volatile boolean rotating = false;  
 2:  static boolean rotating_cw = false;  
 3:    
 4:  void rotEncoder() {  
 5:   byte in = PIND & B1100;  
 6:   rotating_cw = in == B1100;  
 7:   rotating = in & B100;  
 8:  }  
 9:    
10:  void setup() {  
11:   DDRD |= B1100;  
12:   PORTD |= B1100;  
13:   attachInterrupt(0, rotEncoder, CHANGE); // ISR for rotary encoder  
14:   rotating = false; // ignore the first reading, which will be false
15:  }  
Basically, I need only check for the ending state of the switch after landing on the detent. This requires checking only one of the lines for a signal change and ignoring all others when checking for rotation.

The moral of this story is that, despite using the same basic principles, different rotary encoders work differently, so check your data sheets or conduct signal tests like I ultimately ended up doing. If you're interested in a function to handle the reading of the encoder without inspecting and manipulating the rotating and rotating_cw variables, here it is:
1:  int readEncoder() {  
2:   int dir = rotating ? (rotating_cw ? 1 : -1) : 0;  
3:   rotating = false;  
4:   return dir;  
5:  }  
This will work with either of the two routines above. A call to readEncoder() will return 0 if the encoder hasn't rotated, 1 for a clockwise direction, or -1 for a counter clockwise direction. This makes it really handy to keep a running counter based on the rotation of the encoder.

No comments:

Post a Comment