This post is for hardware hackers interested in adding dials to their projects. It discusses what we learned about rotary encoders and provides info about how to add one to your Arduino project.
For the original
timetravel.fm prototype, we used thumbwheel potentiometers (pots for short) as the dials for the radio. While these components worked well for the hackathon, they have a few limitations:
- They have limited range
- The mechanics of the component and Arduino limit precision
- They are generally unattractive
Because the experience of using the device is extremely important to us, we place a lot of value on this element being (1) flexible, (2) pleasurable to use, and (2) attractive. While there are numerous types of pots, including ones that can rotate up to two full turns and allow a knob to be attached, a component called a
rotary encoder performs better on each of the metrics that are important to us. Since I hadn't previously heard of a rotary encoder, I started out blindly searching Google using queries like "unlimited turn dial" (which I find humorous in retrospect). Eventually, I discovered the rotary encoder and pieced together what we'd need to experiment with them.
The first thing to note about rotary encoders is that they come in numerous different styles. While browsing the options at my favorite supplier,
Digi-Key, it took me a while to figure out what all the features meant (actually, I didn't fully understand until I received my first order of encoders). Looking at an
example encoder, some noteworthy characteristics include:
- Encoder Type: The method with which the encoder determines rotation. The example encoder is mechanical, which is a good choice for basic projects.
- Output Type: How rotation is encoded in the output. The example encoder uses quadrature, which means the output is represented by out-of-phase waves.
- Pulses per Revolution: The number of times the output changes per full revolution of the dial. The example encoder will cycle through the output phases 12 times per full turn of the dial.
- Actuator Type: This contains the diameter of the encoder's dial and the style of shaft. This information will help determine which knobs will fit.
- Detent: Whether the dial "clicks" or rotates smoothly as you turn it.
- Built in Switch: Some rotary encoders have a built-in switch that is triggered by pressing down on the shaft.
- Mounting Type: This indicates how you mount the component onto your device. The example includes "PCB Through Hole", which means it has legs that allow you to lock it into place on the PCB, and "Panel", which mean that it has threaded metal at the bottom of the shaft that allows it to be locked into place on a device using a nut (see the example component).
- In addition to the above, I'd also recommend looking at the part's datasheet to see the length-wise parameters of the shaft to make sure it fits the dimensions you're looking for and that any knobs you intend to use will fit properly.
To try the encoder out, I wanted to plug it into a solderless breadboard. However, I found that the PCB mounting legs prevented it from fitting. To deal with this, I drilled two small holes in the breadboard and the encoder snapped into place:
With the encoder set up on the breadboard, I was ready to create an Arduino project. I started by writing the following for Arduino UNO R3:
// For Arduino UNO, only pins 2 & 3 work for interrupts
int _encoderDialPin1 = 2;
int _encoderDialPin2 = 3;
volatile int _changeInTicks = 0;
// Don't need to be volatile since only accessed from single interrupt handler
int _lastBit1 = 0;
int _lastBit2 = 0;
void setup() {
Serial.begin (9600);
pinMode(_encoderDialPin1, INPUT);
pinMode(_encoderDialPin2, INPUT);
// Turn on pullup resistor
digitalWrite(_encoderDialPin1, HIGH);
digitalWrite(_encoderDialPin2, HIGH);
attachInterrupt(0, handleEncoderChange, CHANGE);
attachInterrupt(1, handleEncoderChange, CHANGE);
}
void loop() {
int changeInTicks = 0;
noInterrupts();
changeInTicks = _changeInTicks;
_changeInTicks = 0;
interrupts();
if (changeInTicks != 0) {
Serial.println(changeInTicks);
}
delay(250);
}
void handleEncoderChange() {
int bit1 = digitalRead(_encoderDialPin1);
int bit2 = digitalRead(_encoderDialPin2);
int code = (_lastBit1 << 3) | (_lastBit2 << 2) | (bit1 << 1) | bit2;
if (code == 0b0001 || code == 0b0111 || code == 0b1110 || code == 0b1000) {
_changeInTicks++;
} else if (code == 0b1011 || code == 0b1101 || code == 0b0100 || code == 0b0010) {
_changeInTicks--;
} else {
// For this case, the direction of the turn is indeterminate
}
_lastBit1 = bit1;
_lastBit2 = bit2;
}
This code warrants some explanation. Two things are key to understanding what's going on. The first is that we are using interrupts to signal that the dial on the encoder is turning. The second is how we interpret the output of the encoder.
To create interrupts, we are using Arduino's
attachInterrupt function. Different versions of Arduino work differently with this function. For the UNO, which the above code was written for, only pins 2 and 3 can be used for interrupts. The first parameter to attachInterrupt says which of these are used, meaning a value of 0 maps to pin 2 and a value of 1 maps to pin 3. The second parameter is the function to execute on the interrupt. The third parameter is when to trigger the interrupt. Here, we are asking for an interrupt to be triggered whenever the value of one of the pins changes.
Because we can't be certain about when a context switch might occur, we turn off interrupts before accessing
_changeInTicks in our main loop. Before calling an interrupt handler, Arduino disables interrupts, so we don't need to do this in the handler,
handleEncoderChange. What this gives us is that when the signal to pin 2 or 3 changes,
handleEncoderChange is called.
When
handleEncoderChange is called, we determine the rotation of the dial on the rotary encoder. Because the encoder we're using uses quadrature as it's output encoding, the signals to the pin will repeatedly iterate through the following pattern as the dial rotates to the right. When rotated to the left, it will go in reverse.
| step 1 | step 2 | step 3 | step 4 | repeat... |
pin1 | 0 | 0 | 1 | 1 | 0 |
pin2 | 0 | 1 | 1 | 0 | 0 |
result | 00 | 01 | 11 | 10 | 00 |
The logic in
handleEncoderChange observes the sequence of values read from the encoder to determine whether the dial is being rotated forward or backward. For several sequences, like 0011, we can't tell which direction the encoder was turning, because it is possible to generate them by rotating the dial in either direction. In testing the example encoder, I occasionally observed such values, which is why we purposely ignore them in the code. At this point, it's worth noting that writing to the serial line (e.g. using
Serial.print) in an interrupt handler doesn't always work properly. When I tried doing this, I found that Arduino's serial monitor would occasionally hang.
With this simple program ready to go, we can hook the encoder up to the Arduino.
Encoders are designed to allow a knob to be attached. As mentioned earlier, when looking for a knob, you'll want to make sure it fits the encoder. For this example, we'll use
this knob. When selecting a knob, make sure the specs for "shaft size" and "height" match the corresponding values for the shaft of the encoder. To get the precise dimensions, look at the part's datasheet.
It's not obvious from most pictures of knobs, but they typically come with a screw on the side that tightens down to lock onto the shaft of the encoder.
Attaching the knob to the encoder, gives us a nice dial for our project.
One last thing to cover here is the push-button feature of the example encoder. The following code, which can be combined with the previous code, turns on an LED when the encoder shaft is pushed down.
int _encoderBtnPin = 4;
int _ledPin = 13;
// Don't need to be volatile since only accessed from single interrupt handler
int _lastBit1 = 0;
int _lastBit2 = 0;
void setup() {
Serial.begin (9600);
pinMode(_ledPin, OUTPUT);
digitalWrite(_encoderBtnPin, HIGH);
digitalWrite(_ledPin, LOW);
}
void loop() {
int decompressed = digitalRead(_encoderBtnPin);
if (decompressed) {
digitalWrite(_ledPin, LOW);
} else {
digitalWrite(_ledPin, HIGH);
}
delay(250);
}
Now, we can connect the encoder button to the Arduino.
I've learned quite a bit of new things about encoders, knobs, and Arduino in figuring all this out. I hope sharing it helps accelerate your learning! Feel free to leave a comment if you have any questions.