Using Rotary Encoders

By

Dr. Jack Purdum, W8TEE

I want to start this by saying I'm a newcomer to QRP ARCI and attended my first FDIM conference a few months ago. I had a blast...a great event, friendly attendees, and generally just a good time. While there, I thought I'd pop over to Rex's buildathon and see what was happening. Rex introduced himself and five minutes later I was taking tickets and handing out kits. I'm still not sure exactly what happened during those first five minutes.

Anyway, I especially enjoyed the show-and-tell element and seeing how microcontrollers are creeping into our hobby. That's a good thing. Most modern rigs use microcontrollers in one way or another, so I thought I'd show how easy it is to add a rotary encoder in your microcontrollerprojects.

“But, I don't know how to program!”

Yeah...so what? When you got your first license, did you know what a Colpitts oscillator was? For you older hams, did you know Morse code when you started out? Me neither, but we learned what we needed to know to get our ticket. As to programming a microcontroller, it's a whole lot easier than designing electrical circuits or learning Morse code. Plus, most of the microcontrollers out there come from an Open Source background. What that means is that there is a ton of free software available that is literally plug-and-play.

I know you people are bright enough to spend a weekend with a programming tutorial and you're off and running. Besides, you're going to discover that there's a whole lot of fun to be had with these little devices.

“What Do I Need to Get Started?”

Not much. You'll need a microcontroller, a few components, and some programming tools. I'm a big fan of the Arduino family of microcontrollers. They are cheap, the programming tools are free, and there is about a bazillion lines of free program code available to you.

For this exercise, I prefer using the Arduino Nano microcontroller. The Nano has 32K of Flash memory, 2K of SRAM memory, and 1K of EEPROM memory. It has 14 I/O pins, 8 analog pins, zips along at 16MHz, and is smaller than your thumb. I bought 3 on the Internet for $7.50, and that included shipping! Make sure you get the Arduino Nano, V 3.0. The Arduino Pro Mini looks almost the same, but doesn't have the USB connector on it and, as a result, is a little more difficult to program. The downside of selecting a Nano instead of an Arduino Uno is that the Nano cannot accept plugin modules, called shields, like the Uno can. The code presented here will work with either choice.

About the Nano's Memory...

As I mentioned, there are three types of memory on the Nano. Flash memory is where your programs reside. Flash memory is non-volatile. That is, even removing power from the Nano leaves flash memory unaffected. It's much like a thumb, or flash, drive in that sense.

SRAM (Static Random Access Memory) is where your program data resides. SRAM memory is volatile. That is, if you remove power from SRAM, it goes stupid and forgets everything.

EEPROM (Electrically Eraseable Programmable Read Only Memory) is like Flash memory in that it is non-volatile. However, it is a little slower to read and write EEPROM memory and it has a finite write-cycle life. After about 100,000 writes, it can get a little flaky. (Actually, flash memory can also get corrupted over time, but we can ignore that for all practical purposes.) For that reason, EEPROM is usually reserved for data that isn't expected to change much over time. For example, if you wanted to stored the band edge frequencies for all of the various ham bands, EEPROM would be a good choice since that kind of data doesn't change very often.

Okay, so you have 32K of flash memory, 2K of SRAM, and 1K of EEPROM. In these days of PC's with a mega-munch of memory, what can I possibly do with a puny 32K of memory? Actually, a lot! The little demo program I'm about to show you takes 3,880 bytes of flash memory and 495 bytes of SRAM. That still leaves you with almost 28K of program memory and about 1.5K of data space.

Unlike a PC, microcontrollers have pretty small overhead memory requirements. A little less than 2K of flash memory is used for what is called the bootloader program, leaving you with about 30K of flash memory for your programs. The primary responsibility of the bootloader is to allow you to communicate with your PC. In fact, you are more likely to run out of data space (i.e., SRAM) before you run out of program space.

About the Encoder...

A rotary encoder is a mechanical device that can sense when its shaft is being turned. It senses the rotation via a series of contacts within the encoder housing that send out a pulse chain when the shaft is rotated. Most inexpensive encoders have detents that you can feel as you rotate the shaft. The encoder I used has 20 detents per revolution, which means that a pulse chain is sent every 18 degrees of rotation. (You can buy encoders with fewer or more detents per revolution.) By reading the pulse chain, it is possible to determine whether the encoder is being turned clockwise or counter-clockwise. (For additional information, see:

Figure 1 shows the KY-040 encoder that we are using. I purchased 5 of these on eBay for $5.00, including shipping. If you look closely at the figure, you can see two pins labeled CLK (clock) and DT (data), a middle pin labeled SW (switch), and then the + and GND pins.


Figure 1. The KY-040 encoder.

The KY-040 has a built-in switch that is activated by pushing on the shaft. The encoder on the left in Figure 1 has two 0.1uF caps attached to ground and to the clock and data lines. You can also see a 10K resistor tied between the pin and the positive voltage. The resistor ensures the switch pin does not float. (We can do away with the resistor by using the internal resistors available on the Nano.)

So what's the deal with the caps? Each time you rotate the encoder shaft and come to rest at a detent, the contacts that send the pulse chain tend to vibrate. The Nano is fast enough to read these vibrations, which can be interpreted as a false pulse chain. The caps are used to remove the false pulse chain caused by the contact vibrations. You could use an optical encoder, which does not use mechanical contacts, but such encoders are considerably more expensive.

Reading the Encoder

There are a number of ways that we can read the encoder. Perhaps the simplest way is to use a polling technique. Polling requires that you visit the device of interest at some regular interval to see if the state of the device has changed. In our case, we want to see if the encoder's state has changed due to a shaft rotation. Other devices would have different state changes. For example, suppose the device was a fire sensor instead of our encoder. Let's suppose you have 10 fire sensors. First you would poll sensor 1 and see if its state has changed. The two states are: 1) No Fire, and 2) Fire. If you read the sensor and find a No Fire state, you move on and poll sensor number 2. Again, if there is no fire, you continue to sensor number 3 and so on. After reading the 10th sensor, you repeat the procedure, starting with sensor number 1. This polling continues until power is remove, the state of one of the sensors changes (we do something to note the fire), or there is a component failure. If it takes a second to read each sensor, we poll all 10 sensors every 10 seconds.

Polling works well until we have a building like the Empire State building with 100 sensors on each floor and 102 floors. Even at 1 sensor per second, it's going to take 2.8 hours to read all of the sensors one time! Potentially, a fire could get almost a 3 hour head start before we even know there's a fire. Not good.

Because of this polling deficiency, most sensors are serviced by an interrupt service routine, or ISR. Anyone who's been around a 2 year old knows what an interrupt service routine is. Essentially, what it means is that any sensor can demand immediate attention the instant the state of the sensor changes. Fortunately, the Arduino family of microcontrollers have interrupt capabilities built into them. We will take advantage of these capabilities for our encoder. (It helps if you have a breadboard so you can plug the Nano and encoder into the breadboard and connect things with jumper wires.)

Figure 2 shows the pins on the Nano. If you look closely at the left side of Figure 2, you'll see a GND and 5V pins. Connect these two pins to the GND and + pins on the encoder. Connect the encoder switch pin (SW) to digital I/O pin (D7) on the Nano. Finally, connect the data pin on the encoder (DT) to pin D3 on the Nano and the clock pin (CLK) to pin D2. Pins D2 and D3 are the external interrupt pins for the Nano (and Uno). Your setup should look similar to Figure 3. (I used an Uno in Figure 3 because I don't have an icon for the Nano.) That's it. Now we can consider the software that controls the encoder and what it does.


Figure 2. The Nano pinouts.


Figure 3. Wiring the encoder to the Nano.

Encoder Software

Figure 4 shows a sample run of the encoder demo program. The top part of the screen shot shows the main menu (via a function call to the Splash() function) for the available options. The user types in a digit character in the textbox at the top of the figure and clicks the Send button. The program responds with a menu header and then displays the first item in the selected menu. Because I selected Favorite Frequencies, the first item in the favorites[] array, 7030, is displayed. Turning the encoder shaft CW to the first detent selects the second item in the array, or 7195. Another CW turn selects 14250. One more CW rotation selects 21300. Next, I turned the encoder CCW, which re-displays 14250. Another CCW turn shows 7195.

At that point, I pressed the encoder shaft, which causes the menu to advance to the next menu as held in the filters[] array and show the first entry in that array, 500Hz. Advancing too far for either the menu selection or the items in any individual array wraps around to the first element.


Figure 4. Sample run of the Demo program.

Listing 1 shows the complete source code for the demo program. The goal is to use the encoder to scroll through three menus, showing the various options offered by each menu. A more common use for the encoder is to serve as a tuning control for a radio, but the principles used here are the same.

Processing the signals from the rotary encoder is done by code found in the Rotary library, a free download at: A code library is a specific collection of data and functions that you can use in your own programs. There are literally hundreds of these free Arduino libraries out there and virtually all of them have sample programs you can run to try them out. (In fact, one of the first things I do whenever I start a new project is a quick Internet search so I don't end up reinventing the wheel.)

The three menu options are for selecting our favorite frequency, for selecting a filter, or setting the frequency increment (per each detent of the encoder). The choices are held in the following data arrays:

static long favorites[] = {7030, 7195, 14250, 21300, 28500};

static char *filters[] = {"500Hz", "1kHz", "1.5kHz", "2kHz", "3kHz"};

static char* incrementStrings[] = {"10", "20", "100", ".5", "1", "2.5", "5", "10", "100"};

The idea is that you use the encoder shaft presses to move between the 3 menus, and you rotate the encoder shaft to select items from within each menu. So, if you select option 1 from the Splash() menu, Favorite Frequencies, you will see 7030 displayed. If you then turn the encoder shaft CW to the first detent, you will see 7195 displayed. If you turn CW one more detent, you will see 14250 displayed. If you now turn CCW, you will again see 7195 displayed. If you scroll past the end of menu options, the code resets to the first menu option.

If you press the encoder shaft a second time, you will see 500Hz displayed. Rotate one detent CW and it changes to 1kHz. Rotate CCW one detent and 500Hz appears again. If you just take a few minutes to look at the code...even if you know nothing about programming...I'll bet you'll get an idea of how all the pieces-parts fit together.

Note that every Arduino program must have a setup() and a loop() function. The purpose of the setup() function is to establish the environment for the program and it is executed only once when the program first starts executing. Often you will see setup() fix the serial monitor baud rate (e.g., Serial.begin(115200)), initialize sensors and I/O pins, etc. The loop() function is where all the processing for the program takes place. The code in the loop() function is continually repeated until 1) power is removed, 2) there is a component failure., or 3) the reset button is pressed. Indeed, in our program, the Nano spends most of its time twiddling its thumbs waiting for you to do something.

Listing 1. The Encoder Demo Program.

/*

Rev 1.00: July 25, 2015, Jack Purdum

*/

#include <rotary.h>

#define ROTARYSWITCHPIN 7 // Used by switch for rotary encoder

// A macro that calculates the number of elements in an array.

#define ELEMENTCOUNT(x) (sizeof(x) / sizeof(x[0]))

// ISR variables

volatile int rotationDirection; // + is CW, - is CCW

int menuSelection; // Which menu is active

int menuIndex; // Which menu option is active

#define MAXMENUS 3 // We have the following menus

static char *filters[] = {"500Hz", "1kHz", "1.5kHz", "2kHz", "3kHz"};

static char* incrementStrings[] = {"10", "20", "100", ".5", "1", "2.5", "5", "10", "100"};

static long favorites[] = {7030, 7195, 14250, 21300, 28500};

Rotary r = Rotary(2, 3); // sets the pins the rotary encoder uses. Must be interrupt pins.

void setup() {

Serial.begin(115200); // Activate the Serial object. Make sure

// you set the Monitor to the same rate

pinMode(ROTARYSWITCHPIN, INPUT_PULLUP); // Use pullup resistors

Splash();

PCICR |= (1 < PCIE2); // Pin Change Interrupt Enable 2 to set

// Pin Change Interrupt Control Register

PCMSK2 |= (1 < PCINT18) | (1 < PCINT19); // Use pins 2 and 3 for

// the control mask

sei(); // Set interrupts flag

rotationDirection = 0;

}

void loop() {

static int switchState = 1;

if (Serial.available() > 0) { // Anything from the user??

menuSelection = Serial.read() - '0'; // Convert ASCII to int

if (menuSelection < 1 || menuSelection > MAXMENUS) {

menuSelection = 1; // If they can't follow directions, // use menu 1.

}

menuIndex = 0;

MenuHeader();

ShowCurrentSelection();

}

switchState = digitalRead(ROTARYSWITCHPIN);

if (switchState == LOW) { // Switch pressed?

delay(200); // Yep...

MenuHeader();

menuIndex = 0;

menuSelection++;

if (menuSelection > MAXMENUS)

menuSelection = 0;

ShowCurrentSelection();

}

if (rotationDirection != 0) { // Encoder rotated??

menuIndex += rotationDirection; // Yep...

ShowCurrentSelection();

rotationDirection = 0;

}

}

/*****

* This routine displays a simple menu header

*

* Parameter List:

* void

*

* Return value:

* void

*****/

void MenuHeader()

{

Serial.println();

Serial.println("======New Menu Selection =====");

Serial.println();

}

/*****

* This routine displays a simple menu selection sequence on the Serial monitor

*

* Parameter List:

* void

*

* Return value:

* void

*****/

void Splash()

{

Serial.println();

Serial.println("Rotary Encoder Routines");

Serial.println("Menu Selections: ");

Serial.println(" 1. Favorite Frequencies");

Serial.println(" 2. Filters");

Serial.println(" 3. Frequency Increments");

Serial.println("");

Serial.println("Enter 1, 2, or 3 above");

}

/*****

* This routine displays a menu item from the current menu selection

*

* Parameter List:

* void

*

* Return value:

* void

*

* CAUTION: This function assume menuIndex and menuSelection are set

* prior to the call.

*****/

void ShowCurrentSelection()

{

if (menuIndex < 0) { // Should not be negative

menuIndex = 0;

}

switch (menuSelection) {

case 1:

if (menuIndex >= ELEMENTCOUNT(favorites)) { // Rotated too far??

menuIndex = 0;

}

Serial.println(favorites[menuIndex]);

break;

case 2:

if (menuIndex >= ELEMENTCOUNT(filters))

menuIndex = 0;

Serial.println(filters[menuIndex]);

break;

case 3:

if (menuIndex >= ELEMENTCOUNT(incrementStrings))

menuIndex = 0;

Serial.println(incrementStrings[menuIndex]);

break;

default:

menuIndex = 0; // Start over with choice

menuSelection = 1;

Splash();

break;

}

}

/*****

* This is the Interrupt Service Routine for the rotary encoder

*

* Parameter List:

* PCINT2_vec Pin change interrupt request #2, pins D0 to D7

*

* Return value:

* void

*****/

ISR(PCINT2_vect) {

unsigned char result = r.process();

switch (result) {

case 0: // Nothing done...

return;

case DIR_CW: // Turning Clockwise

rotationDirection = 1;

break;

case DIR_CCW: // Turning Counter-Clockwise

rotationDirection = -1;

break;

default: // Should never be here

break;

}

}

The ISR is the last function presented in Listing 1. As you can see, there's not much going on there, which is a good thing. The reason is because you want to keep ISR's as short and fast as possible because, while your ISR is servicing your interrupt, no other interrupt can take place. In other words, your ISR might “block out” another interrupt.