Debouncing a mechanical rotary encoder

As an active member of the Arduino user community, I felt I should keep up with my library updates. So after some planning and “careful” coding, I’ve added a new digital rotary encoder class to my phi_interfaces library. The phi_interfaces library includes buttons, rotary encoders, analog buttons, joysticks, matrix keypads, serial ports, etc. as input devices and one can use getKey() to sense any of them.

The original rotary encoder class already works well. This addition expands the scope, supporting both normally open and normally closed encoders, and also supporting encoders that have twice the detents as they have complete pulses (have some hardware but haven’t tested yet). Also, this addition paves the way of an analog encoder class that will help people using nano/ATMEGA328P-AU save two pins, which has two analog-only pins with no digital functions.

Along the way I met some difficulties that I thought was useful to share. Although it stems from library development, the nature of the problem is actually misunderstanding of hardware.

Hardware I used: Bourns PEC11 series 18 pulses/rev. encoder with 18 detents./rev. (no obsolete, replaced by PEC11R)

So here is what I discovered: the mechanical rotary encoder that I have has a delay between channel A closing and channel B closing about 14 ms, then delay between channel A opening and channel B opening about 8 ms. Not only is opening asymmetric with closing, but they are also faster than my expectation. I started programming the new class expecting to debounce the encoder. I first used 20ms for debouncing, like what I do for tactile switches. That didn’t yield good results. I thought I need longer time, 50 ms, not good, or about the same, only 1/3 detents got detected. I then tried shorter debouncing like 10 ms and still few detents got detected. Running the original rotary encoder code I developed initially, without debouncing, was find, with every detent registered. So I checked the encoder with a logic analyzer. Wow the action was faster than I expected, 14ms and 8 ms. I also looked at the bouncing of the contacts. It was around 0.5 ms after initial contact was made, many bouncing occurred. So if I’m using millis() to debounce, I’d rather not debounce at all.

Here is one part of the pulse when channel A closes then channel B closes, about 14 ms:

Here is another part of the pulse when channel A opens then channel B closes, about 8 ms:

Here is a typical bouncing of the contacts, in the order of 0.5 ms:

An afterthought, I read the spec sheet, which says at 15RPM, close to how fast I was turning the shaft, bouncing is 5ms max. I’ve seen bouncing around that length in time. If I read the spec sheet, I’ve probably avoided the whole learning experience smiley

Here is diagram showing symmetric pulses on the spec sheet:

So, take the spec sheet with a grain of salt.

BTW, I’ve recently designed a system that uses one analog pin to sense a mechanical rotary encoder. The purpose was to save pins. The ATMEGA328P-AU has two analog pins that have no digital functions. Why wasting them if I can hook a rotary encoder to one of them?

Rotary encoder

A rotary encoder is a digital device in the shape of a knob. You can turn it in either clockwise direction or counter-clockwise direction infinite turns, without any limit. This is what a potentiometer can’t do. In the old days most knobs are potentiometers or rotary switches, such as those on stove top, washers and dryers. Potentiometer is much easier in the concept to sense than a rotary encoder so here we discuss how to sense a digital rotary encoder.

How does it work?

A simple rotary encoder has two channels or switches. If you turn the encoder’s shaft, the two switches will open and close repeatedly much like two tactile buttons. Depending on the direction of the rotation, the sequence of the open and close will be different. Say we have two channels A and B that open and close as we turn the encoder and if we turn the encoder clockwise, we will see:

A open->B open->A close->B close

If we turn counter clockwise, we see:

B open->A open->B close->A close

So to tell which way the encoder is turning, we just need to tell which sequence occurs. There are many ways to tell which sequence occurs but I will focus on the easiest way, which is also efficient enough.

First, you combine the status of the switches A and B into a 2-bit binary number, A being the low bit and B being the high bit. Let’s define open as 1, and close as 0. Here is how to combine the status bits:

stat=(digitalRead(EncoderChnB)*2) + digitalRead(EncoderChnA);

When both channels are closed, we get 0. When both are open, we get 3. When only a is open, we get 1. When only b is open, we get 2.

Now we can translate the sequences into numbers:

A open->B open->A close->B close ==>> 1,3,2,0

B open->A open->B close->A close ==>> 2,3,1,0

Note that in the first sequence when B opens after A opens, they are both open, so that corresponds to 3.

Now we can make an array to store both sequences with a pointer:

byte stat_seq[]={0,1,3,2,0,1,3,2,0};

byte stat_seq_ptr=4;

The pointer initially points to a status of both switches closes, which is a detent position for the encoder. You turn your encoder and it clicks into detent positions. If your detent position is when both switches are open, you may use {3,2,0,1,3,2,0,1,3} instead.

Now, every time you read the two channels, you compare the stat with the array element before and after the current pointer’s position. If the stat represent the array element after the current pointer, move the pointer there, same for the other way around. When the array pointer hits the left end of the array, with a 0, you have completed one CCW turn, report this turn and reset the pointer to the middle, 4. Same for CW turn when the pointer reaches the right end of the array.

This should be easier to understand than other ways of tracking the encoder and is much more robust. Here is the complete code:

The function returns the number of CW or CCW turns stored in its buffer. Every time you call it to ask number of CW turns, it returns the turns and decrements its internal count by one. All you need to do is to keep calling this function every loop by supplying which direction you want to read back. If the read back is greater than zero, a turn is made by the user. You can decide what to do, maybe advance in a menu. If the return is zero, there is no turn in that direction, you need to do nothing. The green section is sensing the encoder and the blue section is responding the the caller.

byte read_encoder(byte direction) // 1 for asking CW counts, -1 for asking CCW counts. Returns counts and decrements the counts till 0.
  static byte stat_seq[]={3,2,0,1,3,2,0,1,3}; // For always on switches use {0,1,3,2,0,1,3,2,0};
  static byte stat_seq_ptr=4;
  static byte cw_counter=0;
  static byte ccw_counter=0;

  byte stat_int=(digitalRead(EncoderChnB)<<1) | digitalRead(EncoderChnA);
  if (stat_int==stat_seq[stat_seq_ptr+1])
    if (stat_seq_ptr==8)
  else if (stat_int==stat_seq[stat_seq_ptr-1])
    if (stat_seq_ptr==0)
if (direction==1)
    if (cw_counter>0)
      return cw_counter+1;
    else return 0;

  else if (direction==255)
    if (ccw_counter>0)
      return ccw_counter+1;
    else return 0;

  else return 0;

%d bloggers like this: