Introduction: How to Interpret the Direction of Rotation From a Digital Rotary Switch With a PIC

The objective for this Instructable is to illustrate how to interface a digital (quadrature coded) rotary switch with a microcontroller. Don't worry, I'll explain what quadrature coded means for us. This interface and the accompanying software will allow the microcontroller to recognize the direction of rotation for each move from one detent to another.

I recently used this type of switch in a microcontroller project that required a pressure set point to be entered using a knob with 16 detents instead of up/down buttons. The idea was to allow the user to "dial in" the desired pressure. As a result, we had to develop a software routine to get the position information from the switch and deduce the rotation direction in order to increment or decrement the pressure set point for the main system.

In this Instructable, I'll cover the physical interface to the microcontroller, the theory of operation for the rotary switch, the theory of operation for the software as well as the deduction routine. Finally, I'll show you my application of the deduction routine. As we progress, I'll try to keep things somewhat generic so that the idea can be applied on as many platforms as possible but I'll also share what I did so you can see a specific application.

Step 1: Parts

In order to implement this, you'll need:

A rotary switch (quadrature coded)
Pull up resistors
Suitable microcontroller platform

For my project, I used a Grayhill 61C22-01-04-02 optical encoder. The data sheet for the rotary switch calls for 8.2k ohm pull up resistors on the two data lines coming from the switch. You'll want to check the data sheet for the encoder you opt to use. The rotary switch I used can also be ordered with an axial push button switch. It's a useful feature for committing selections that have been dialed in, etc. but I will not be discussing its interface here. I have a "suitable microcontroller platform" listed because (I think) this can be implemented on more than one platform. I have seen a lot of people using other microcontrollers for Instructables so I want to show the general approach as well. I wrote all the code in PIC Basic Pro for use with a Microchip PIC16F877A. Really, the key thing that you need on the microcontroller is the ability to interrupt when there is a logic change on either of two pins. On the PIC16F877A, this is called the PORTB change interrupt. There may be other names for it on other controllers. This microcontroller interrupt feature is part of what makes this implementation so elegant.

Step 2: Hardware Interface

A "simple" solution would be to have a "single pole-16 throw" switch with 16 connections to the microcontroller. Each switch output would then be tied to a pin on the microcontroller so that every dial position can be checked by the microcontroller. This is an excessive use of I/O pins. Things get even worse if we want more than 16 positions (detents) available to us on the switch. Each extra position on the switch would require an extra input to the microcontroller. This quickly becomes a very inefficient use of inputs on a microcontroller.

Enter the beauty of the rotary switch.

The rotary switch has only two outputs to the microcontroller listed as A and B on the data sheet. There are only four possible logic levels that these lines can take: AB = 00, 01, 10 and 11. This greatly reduces the number of input lines you must use in connecting the switch to the microcontroller. So, we've cut the number of input lines down to just two. Now what? It seems like we really need 16 different states but this new switch has only four. Have we shot ourselves in the foot? Nope. Read on. We'll cover a little bit of the theory behind the rotary switch operation to explain.

Step 3: Hardware Theory of Operation

Rotation direction sensing is possible using the aforementioned "single pole-16 throw" switch but it uses up a lot of inputs on the microcontroller. Using the rotary switch reduces the number of inputs to the microcontroller but now we need to interpret the signals coming from the switch and translate those to a rotation direction.

I mentioned earlier that the switch was quadrature coded. This is also one of the key elegances in this solution. This means that there is a 2-bit code the switch gives that corresponds to the position of the switch. You might be thinking: "If there is a two bit input to the microcontroller, how do we represent all 16 positions?" That's a good question. We don't represent them all. We just need to know the relative positions of the knob so we can determine the direction of rotation. The absolute position of the knob is irrelevant. For clockwise rotation, the code that the switch gives repeats every four detents and is grey coded. Grey coded means that there is only one bit change for every position change. Instead of the AB input counting up for clockwise rotation in binary like this: 00, 01, 10, 11, it changes like this: 00, 10, 11, 01. Notice that for the latter pattern, there is only one input changing between sets. The counterclockwise values for the AB input to the microcontroller will look like this: 00, 01, 11, 10. This is simply the reverse of the clockwise pattern with AB = 00 listed first.

Take a look at the diagrams for a more visual explanation.

Step 4: Software Theory of Operation

The routine that deduces the rotation direction is interrupt driven. The microcontroller that you select needs to be able to interrupt any time there is a change on either of (at least) two pins when the interrupt is enabled. This is called the PORTB change interrupt on the PIC16F877A. Anytime the switch is rotated, the microcontroller will be interrupted and the program execution will be sent to the Interrupt Service Routine (ISR). The ISR will quickly figure out which way the switch was rotated, set a flag appropriately and quickly return to the main program. We need this to happen quickly in case the user rotates the switch very fast.

We know the grey coded AB pattern repeats every four positions so if we make the routine work for transitions between those four positions it'll work for all of the others. Notice that in one four position cycle, there are four edges. A rising edge and a falling edge for the A input as well as the B input. The microprocessor will be interrupted each time there is an edge which means that the microcontroller will be interrupted any time the knob is turned. As a result, the ISR needs to figure out which way the knob was turned. To help us figure out how to do this, we turn to the waveform for clockwise rotation.

Notice that any time A has an edge, its new value is always different from that of B. When the knob goes from position 1 to 2, A transitions from logic-0 to logic-1. B is still 0 for this transition and does not match the new value of A. When the knob goes from position 3 to 4, A has a falling edge while B remains at logic-1. Notice again, that B and the new value of A are different. Right now, we can see that any time A causes the interrupt during clockwise rotation, its new value is different from that of B. Let's check B to see what happens. B has a rising edge when the switch transitions from position 2 to 3. Here, the new value of B is the same as A. Looking at the last remaining edge for clockwise rotation, B has a falling edge moving from position 4 to 5. (Position 5 is the same as position 1.) The new value of B is the same as A here as well! We can now make some deductions! If A causes the interrupt and the new value of A is different from that of B, the rotation was clockwise. In addition, if B causes the interrupt and the new value of B is the same as A, then the rotation was clockwise.

Let's quickly examine the case of counterclockwise rotation. Just like clockwise rotation, counterclockwise rotation will cause four interrupts in one cycle: two for input A and two for input B. Input A has a rising edge when the knob moves from position 4 to 3 and a falling edge moving from position 2 to 1. When the knob moves from position 4 to 3, the new value of A is the same as the value of B. Notice that when A moves from position 2 to 1 its new value is the same as that of B as well. Now, we can see that when A causes the interrupt and its new value matches that of B the rotation was counterclockwise. Quickly, we'll look at input B to verify everything. B will cause an interrupt when the knob moves from position 5 (which is the same as 1) to 4 and when the knob moves from position 3 to 2. In both of these cases, the new value of B does not match the existing value of A which is the opposite of the cases when B causes the interrupt for clockwise rotation. This is good news. Everything checks out like it should.

To summarize, if A causes the interrupt and its new value does not match the value of B or if B causes the interrupt and the new value of B matches the value of A we know there was clockwise rotation. We can check the other cases for counterclockwise rotation in software or we can assume that because it wasn't clockwise rotation it was counterclockwise. My routine simply made the assumption.

Step 5: Software

I did not use the built in interrupts in PIC Basic Pro. I used a couple of files that I included in my code from Darrel Taylor to drive the routine. This is where a huge credit to Darrel belongs! The files are free. Just visit his website for more information, other applications and to download the files. You can skip this part if you aren't using a PIC with Darrel Taylor interrupts. Just set up the interrupts as necessary on the platform you're using.

To get the Darrel Taylor (DT) interrupts set up there are two things to do:

1.) Include the DT_INTS-14.bas and ReEnterPBP.bas files in your code.

2.) Copy and paste this into your code.

ASM
INT_LIST macro ;IntSource, Label, Type, ResetFlag?
INT_Handler RBC_INT, _ISR, PBP, yes
endm
INT_CREATE
ENDASM

Insert tabs and spaces like the graphic at the end of the Instructable so you can see things a little easier in your code. You'll need to modify it slightly to fit your needs. Under Label, replace ISR with the name of the subroutine that is your ISR. Don't forget the underscore! You need it!

To get the interrupts working, there are two more things to do:

1.) Write the ISR. You'll write this just like you were going to write a PBP subroutine except that you will need to insert @ INT_RETURN at the end of the subroutine instead of RETURN. This will acknowledge the interrupt and return program execution to where it left off in the main loop.

Inside the ISR, you need to clear the interrupt flag so your program does not get caught in a recursive interrupt. Simply reading PORTB is all that needs to be done to clear the interrupt flag on the PIC16F877A. Each different microcontroller has a different way of clearing interrupt flags. Check the data sheet for your microcontroller.

2.) When you reach the point in your code that you want to enable the interrupt, use this line of code:

@ INT_ENABLE RBC_INT

When you want to disable the interrupt simply use:

@ INT_DISABLE RBC_INT

There's a lot of stuff packed into what I just covered so I'll summarize quickly. So far, your program should look something like this:

; Any needed set up or code

INCLUDE "DT_INTS-14.bas"
INCLUDE "ReEnterPBP.bas"

ASM
INT_LIST macro ;IntSource, Label, Type, ResetFlag?
INT_Handler RBC_INT, _myISR, PBP, yes
endm
INT_CREATE
ENDASM

; Any other needed set up or code

@ INT_ENABLE RBC_INT
; Code that needs to know which way the knob is rotating
@ INT_DISABLE RBC_INT

; Other code

END ; End of program

myISR:
;ISR code here
@ INT_RETURN

(Interrupt Handler Set Up Table)

I think this is where anyone who is not using a PIC or DT interrupts can join in again. Now, we need to actually write the ISR so the microcontroller knows which way the knob is rotating. Recall from the software theory section that we can deduce the direction of rotation if we know the input that caused the interrupt, its new value and the value of the other input. Here's the pseudocode:

Read PORTB into a scratch variable to clear the interrupt flag
Check if A caused the interrupt.
If true,
Compare A and B.
Check if different, if different,
It was clockwise rotation
Else,
It was counterclockwise
Endif

Check if B caused the interrupt.
If true,
Compare A and B
Check if different, if same,
It was clockwise rotation
Else,
It was counterclockwise
Endif

Return from interrupt

How do we know if a change on A or B caused the interrupt? Discovering the new value of the changed input and the other (unchanged) input is easy because we can read them inside the ISR. We need to know what the state of each one was before execution gets sent to the ISR. This happens in the main routine. The main routine sits and waits for a byte variable that we called CWflag to be set to 1 or cleared to 0 by the ISR. After each acknowledged change of the knob or if there is no knob activity, the variable is set to 5 to indicate an idle state. If the flag gets set or is cleared, the main routine immediately increments or decrements the set point pressure appropriately based on the rotation and then sets the CWflag variable back to 5 because the knob is now idle again. As the main routine is checking the CWflag, it is also documenting the state of the A and B rotary switch values. This is really simple and looks like this:

oldA = A
oldB = B

There really is nothing super fancy here. Just include those two lines at the beginning of the loop that checks the CWflag for rotation. We're just updating the logic values of the inputs from the rotary knob inside the increment/decrement loop in the main routine so that we can see what input caused the interrupt when the ISR is executed. Here is the ISR code:

ABchange:
scratch = PORTB ' Read PORTB to clear interrupt flag

' If A causes the interrupt, check B for direction of rotation
IF oldA != A THEN
' If A and B are different, it was clockwise rotation
IF A != B THEN
GOTO CW
' Otherwise, it was counter-clockwise rotation
ELSE
GOTO CCW
ENDIF
ENDIF

' If B causes the interrupt, check A for direction of rotation
IF oldB != B THEN
' If A and B are the same, it was clockwise rotation
IF A == B THEN
GOTO CW
' Otherwise, it was counter clockwise rotation
ELSE
GOTO CCW
ENDIF
ENDIF

CW:
CWflag = 1
@ INT_RETURN

CCW:
CWflag = 0
@ INT_RETURN

I've included the ISR code in a AB_ISR.bas file because the tabs in the code aren't showing up the way they should.

Now, because the ISR has the old values for inputs A and B it can determine which input caused the interrupt, compare it to the other (unchanged) input and determine the direction of rotation. All the main routine has to do is check the CWflag to see which direction the knob has turned (if it has) and increment or decrement a counter, set point or whatever you like or need.

I hope this helps and hasn't been too confusing. This type of interface is especially useful if your system is already using interrupts as this is only one more interrupt to add. Enjoy!