Lab: Using a Rotary Encoder

Introduction

Rotary encoders are sensors which sense the rotation of a central shaft. Unlike a rotary potentiometer, encoders can turn infinitely, covering the full 360 degrees of the shaft’s rotation. Many have built-in pushbuttons as well. Encoders are often used to measure the rotation of a vehicle’s wheel or axle as well. In this lab, you’ll learn how to use a rotary encoder as an input to a microcontroller.

What You’ll Need to Know

To get the most out of this Lab, you should be familiar with the following concepts beforehand. If you’re not, review the links below:

Things You’ll Need

Figures 1-4 show the parts you’ll need for this exercise. Click on any image for a larger view.

Photo of an Arduino Nano 33 IoT module. The USB connector is at the top of the image, and the physical pins are numbered in a U-shape from top left to bottom left, then from bottom right to top right.
Figure 1. Microcontroller. Shown here is an Arduino Nano 33 IoT, but an Uno will work as well
Photo of flexible jumper wires
Figure 2. Jumper wires. You can also use pre-cut solid-core jumper wires.
Photo of a solderless breadboard
Figure 3. A solderless breadboard
Photo of a rotary encoder
Figure 4. A rotary encoder with built-in pushbutton.

How the Sensor operates

Rotary encoders measure rotation using two internal switches very close together, and a rotating disk. The disk has holes in it, and when the it passes each switch, the switch opens then closes. The switches open and close out of phase with each other, and the resulting pattern of open and close lets you detect the rotation. This is called quadrature encoding.Figure 5 shows a quadrature wheel and the positions of two switches relative to the wheel.

Figure 5. An encoder wheel. Several holes are spaced around the wheel near the edge. In this case, each hole has a 15-degree sweep, and is separated from the next hole by another 15 degrees. Two switches, separated by less than the sweep, are positioned so that the holes will pass them as the wheel turns.

Paul Stoffregen’s encoder library page has a nice animation illustrating this pattern under the heading Understanding Quadrature Encoded Signals.

The sensors in an encoder may be mechanical or optical. Optical sensors (photodiodes) are often used to reduce mechanical wear on the sensor. Optical encoder wheels were common in early computer mice. You can see one in Figure 6, which comes from teachengineering.com

Inside a mechanical computer mouse.
Figure 6. Inside a mechanical computer mouse. The wheel inside hangs out the bottom of the mouse. It rotates against the surface of your desk, turning two encoder wheels, one for the vertical direction and one for the horizontal.

An encoder doesn’t tell you its absolute position, but it does tell you whether it’s rotating left or right, depending on the pattern of the sensor changes. The amount of rotation, and therefore the resolution of the encoder, depends on how many holes the encoder has. The more holes, the more finely you can read the change in rotation.

Encoders are often used as knobs that can turn a full 360 degrees, but they are also used in other applications to sense a changing rotation. Wheel encoders and shaft encoders can be used to count revolutions of a vehicle’s axle, for example. In this lab you’ll see how to use a knob-style encoder.

The Circuit

To use an encoder, you connect its two switches to two digital input pins of your microcontroller and look for changes. Because they can happen very fast, it’s wise to use inputs which can be hardware interrupts, meaning that they can read change as soon as the pin changes, not just when you use the digitalRead() command. There are several libraries for reading interrupts. One of the easiest is Paul Stoffregen’s Encoder library, which is used in the code below. To install it, search the Arduino IDE’s library manager for Encoder by Paul Stoffregen.

Figures 7 through 10 show how to connect an encoder with a pushbutton to an Arduino Nano 33 IoT or an Arduino Uno. For the Nano 33 IoT, any pin can act as a hardware interrupt pin. This lab uses pins 2 and 3 for compatibility with the Uno’s interrupts.

Schematic view of a rotary encoder with a pushbutton connected to an Arduino Nano 33 IoT
Figure 7. Schematic view of a rotary encoder with a pushbutton connected to an Arduino Nano 33 IoT. A typical encoder will have through-hole pins, with two pins on one side and three on the other. The side with three holes is the encoder, and the side with two holes is the pushbutton. The encoder’s two outer pins are attached to digital inputs 2 and 3. The encoder’s middle pin is attached to ground. One of the pushbutton’s two pins is attached to digital input 4. The other pin of the pushbutton is attached to ground.
Breadboard view of a rotary encoder with a pushbutton connected to an Arduino Nano 33 IoT.
Figure 8. Breadboard view of a rotary encoder with a pushbutton connected to an Arduino Nano 33 IoT. This encoder and pushbutton is connected as described in Figure 7. The encoder’s two outer pins are attached to digital inputs 2 and 3. The encoder’s middle pin is attached to ground. One of the pushbutton’s two pins is attached to digital input 4. The other pin of the pushbutton is attached to ground.

The wiring for an Arduino Uno is similar to the Nano 33 IoT, but the Nano has only two hardware interrupts, pins 2 and 3. It’s best to use those.

Figure 9. Schematic view of a rotary encoder with a pushbutton connected to an Arduino Uno. A typical encoder will have through-hole pins, with two pins on one side and three on the other. The side with three holes is the encoder, and the side with two holes is the pushbutton. The encoder’s two outer pins are attached to digital inputs 2 and 3. The encoder’s middle pin is attached to ground. One of the pushbutton’s two pins is attached to digital input 4. The other pin of the pushbutton is attached to ground.
Figure 10. Breadboard view of a rotary encoder with a pushbutton connected to an Arduino Uno. This encoder and pushbutton is connected as described in Figure 7. The encoder’s two outer pins are attached to digital inputs 2 and 3. The encoder’s middle pin is attached to ground. One of the pushbutton’s two pins is attached to digital input 4. The other pin of the pushbutton is attached to ground.

The Code

To read an encoder with the Encoder library, you include the library at the top, make an instance of the library in a variable in which you define the pins for the encoder. Then when you want to read the encoder, you call the .read() command. Here’s a basic example:

#include <Encoder.h>

// encoder on pins 2 and 3
Encoder myEncoder(2, 3);
// previous position of the encoder:
int lastPosition  = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  long newPosition = myEncoder.read();
  if (newPosition != lastPosition) {
    lastPosition = newPosition;
    Serial.println(newPosition);
  }
}

When you run this, you’ll see that the position value goes up in one direction and down in the other. There is no absolute position; unlike a potentiometer, which measures an absolute resistance at a particular position of the wiper, an encoder reads only change in one direction or another. It doesn’t know where it is in the rotation, only how many steps it’s taken since it started counting steps.

Most knob-style encoders change by four steps for every click of the shaft. This is typical with many encoders. These clicks are called detents, and they are built into the shaft to give you a sense of its movement. The encoder is reading the four possible states of the two switches. To a person turning the shaft, though, this can seem strange. Shouldn’t each click be one step? You can make this happen by changing your loop like so. First add a new global variable at the top:

// steps of the encoder's shaft:
int steps = 0;

Then change the loop to increment the number of steps for every four changes of the encoder’s position, like so:

void loop() {
  int newPosition = myEncoder.read();
  // compare current and last encoder state:
  int change = newPosition - lastPosition;
  // if it's changed by 4 or more (one detent step):
  if (abs(change) >= 4) {
    // get the direction (-1 or 1):
    int encoderDirection = (change / abs(change));
    steps += encoderDirection;
    Serial.println(steps);

    // save knobState for next time through loop:
    lastPosition = newPosition;
  }
}

When you run the sketch this time, you should see that the step value is changing once for each detent of the encoder shaft.

If you want to make the step count reset itself when it reaches a maximum number of steps, you can do that by adding a line after line 10 above. This line resets the step counter every 24 steps by using the modulo, or remainder, operator (%):

// compare current and last knob state:
  int knobChange = knobState - lastKnobState;
  // if it's changed by 4 or more steps (one detent):
  if (abs(knobChange) >= 4) {
    // get the direction (-1 or 1):
    int knobDirection = (knobChange / abs(knobChange));
    steps += knobDirection;
    // if you want to make the steps rollover every 24 steps, use this:
    steps = steps % 24;
    Serial.println(steps);

    // save encoder state for next time through loop:
    lastKnobState = knobState;

Rollover like this can be useful if you want the value to reset once per rotation. You can also reset the encoder’s position count like so:

myEncoder.write(0);

The Pushbutton and Internal Pullup Resistors

Most knob-style encoders have a pushbutton built in as well, and you wired the encoder’s pushbutton in the diagram above. You may have noticed that it’s wired differently than you did in the digital in and out lab. There’s no pulldown resistor, and the pushbutton is wired to ground. What’s going on?

Many microcontrollers (including all Arduino models) have internal pullup resistors on the input pins. These function in the opposite way of a pulldown resistor: they pull the pin up to voltage when there’s no other connection. When you use an internal pullup resistor, you wire your pushbutton to ground instead of to voltage, and the pin goes low when you press the button. This saves you one resistor; all you have to do is to wire your pushbutton to ground from the digital input pin, and declare the pin’s mode like so: digitalWrite(buttonPin, INPUT_PULLUP); You’ll see it in the example below:

void setup() {
  // set the encoder's pushbutton to use internal pullup resistor:
  pinMode(4, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int buttonState = digitalRead(4);
  Serial.println(buttonState);
}

Upload this code to your board with the circuit as shown in figures 7 through 10, then open the serial monitor. Press the encoder push the encoder’s pushbutton to see how the state changes.

Putting It all Together

Using an encoder and its pushbutton together gives you a great way to let a user choose from a range of choices. By reading the changing encoder state, you can get the range, and by reading the pushbutton, you can get the one they chose. Here’s an example that reads both the encoder and the pushbutton to do just that:

#include <Encoder.h>

// encoder on pins 2 and 3
Encoder myEncoder(2, 3);
// previous position of the encoder:
int lastPosition = 0;

// steps of the encoder's shaft:
int steps = 0;


const int buttonPin = 4;    // pushbutton pin
int lastButtonState = LOW;  // last button state
int debounceDelay = 5;       // debounce time for the button in ms

void setup() {
  Serial.begin(9600);
  pinMode(buttonPin, INPUT_PULLUP);
}

void loop() {
  // read the pushbutton:
  int buttonState = digitalRead(buttonPin);
  //  // if the button has changed:
  if (buttonState != lastButtonState) {
    // debounce the button:
    delay(debounceDelay);
    // if button is pressed:
    if (buttonState == LOW) {
      Serial.print("you pressed on ");
      Serial.println(steps);
    }
  }
  // save current button state for next time through the loop:
  lastButtonState = buttonState;

  // read the encoder:
  int newPosition = myEncoder.read();
  // compare current and last encoder state:
  int change = newPosition - lastPosition;
  // if it's changed by 4 or more (one detent step):
  if (abs(change) >= 4) {
    // get the direction (-1 or 1):
    int encoderDirection = (change / abs(change));
    steps += encoderDirection;
    // if you want to make the steps rollover, use this:
    if (steps < 0) steps = 23;
    steps = steps % 24;
    Serial.println(steps);

    // save encoder position for next time through loop:
    lastPosition = newPosition;
  }
}

Upload this code to your Arduino. When you turn the knob, you’ll run through 24 possible values (0 to 23), and when you press the pushbutton, you’ll be told what the knob value was when you pressed. Used like this, encoders can be another way to get a range of values, when a potentiometer doesn’t feel right for your application.

More Info

For more on encoders, see: