Quadratrack: Using Mechanical Rotary Encoders
12-Feb-2001
I've been having a lot of fun with my LAB-X3 lately. This time I wanted to figure out how to read rotary encoders. If you aren't familiar with them, a rotary encoder looks like a potentiometer, except that instead of a variable resistor it an encoder wheel. Also, unlike a potentiometer they don't have a "stop" point generally. Thus you can turn them around and around and around. They have become very popular on electronic equipment because you can create a "soft" control that is digital from the start. Further, because they have become fairly popular the costs have gone down such that the one I used from Grayhill is only $4.70 from Digikey (GH3071-ND) qty 1. The Data sheet is on Grayhill's site.
These mechanical encoders generate a "quadrature" signal. I don't know the origin of the term quadrature but basically it means there are four states that this device can be in. Further, transition from one state to the next is well defined so with a simple circuit or some software you can translate the pulses into rotation movement.
The three pins on the device are A, B, and Common. Since they are mechanical they are simply switches that connect the A pin, the B pin, and then both the A and B pin to the C pin. A simple circuit for hooking this up is shown below.
As you can the outputs will appear to be 5V when the encoder is not connecting either A or B to C and they will be at ground potential (logic 0) when they are being connected.
The output of the encoder is a two bit gray code, specifically it has the sequence
Clockwise Rotation -> |
00 01 11 10 00 |
<- Counter Clockwise Rotation |
Or more specifically, if the output is 00 and it goes to 01 you know that the encoder has moved one "tick" clockwise, if it is 00 and goes to 10 then you know it moved one tick counter clockwise. If it goes from 00 to 11 you know you missed an intermediate tick. It can be useful to flag this case so that you know your input isn't accurate, but generally its safe to ignore it as if the knob didn't move.
Now there are a couple of ways you can read this device, the simplest is to set your microcontroller to interrupt when ever the state of the two pins changes. Then by knowing the previous state and the current state you can tell what happened. This is easy to do with the PIC which has the 'change on PORTB' interrupt mode. An alternative is to hook the two pins up to input capture pins of the Motorola 68HC11. When you capture a rising or falling edge on either input you can update the position state. Finally, there is an even easier way (but its a bit risky) which is to sample the pins every n microseconds and see if their state has changed.
I chose to use the sampling technique and sample at 1Khz (1 mS per sample). My reasoning was as follows:
1) |
We're talking a human here who is turning this knob, and if they spin it as hard as they can they worse they can do is miss one state. Normal use is unaffected. |
2) |
If there is any "bounce" in the switch (and I've not detected any) then the mS sampling will cover for it. |
The code to read these is fairly straight forward, I'll save you from digging from the full file by just pulling out the relevant bits. If you want the whole thing email me and I'll send it to you.
I'm using the PIC16F628 in a LAB-X3 proto board. The encoder was soldered into the prototyping area with the appropriate pull up resistors. Note that you could use the 'weak pullups' feature on PORTB but I didn't want to turn those on for all the pins. Anyway, as I'm not using the serial port in this example I jumper the Encoder to pins RB1 and RB2 on the PIC.
The code to set up a 1Khz interrupt from TMR0 on a 4Mhz system is as follows:
; * * * * * * ; * BANK 1 Operations ; * * * * * * BSF STATUS,RP0 ; Set Bank 1 MOVLW B'0000010' ; Set TMR0 prescaler to 8 MOVWF OPTION_REG ; Store it in the OPTION register CLRF TRISB ; B all outputs BSF TRISB,QUAD_A ; Except for Quadrature inputs BSF TRISB,QUAD_B ; * * * * * * * * * * * ; * BANK 0 Operations * ; * * * * * * * * * * * CLRF STATUS ; Back to BANK 0 BSF INTCON, T0IE ; Enable Timer 0 to interrupt BCF INTCON, T0IF ; Reset interrupt flag BSF INTCON, GIE ; Enable interrupts |
||
Listing 1) Initializing for Interrupts |
Then you have the following in the interrupt service routine:
; Interrupt Service Routine Pre-amble, save state, ; reset status to BANK 0 INTR_PRE: MOVWF TMP_W ; Copy W to temp register SWAPF STATUS,W ; Swap Status and move to W MOVWF TMP_STATUS ; Copy STATUS to a temp CLRF STATUS ; Force Bank 0 ; ; State is saved, and we've expended 3 Tcy plus the ; 3 Tcy (4 worst case) of interrupt latency for a total ; of 6(7) Tcy. ; ; Now loop through until we've satisfied all the ;pending interrupts. ; ISR_0: ; ... test bit to see if it is set BTFSS INTCON,T0IF ; Timeer0 Overflow? GOTO ISR_1 ; No, check next thing. ; ; Else process Timer 0 Overflow Interrupt ; BCF INTCON, T0IF ; Clear interrupt MOVLW D'133' ; Reset 1khz counter MOVWF TMR0 ; Store it. CALL QUAD_STATE ; Check Quadrature Encoders. GOTO ISR_1 ; Nope, keep counting ISR_1: ; ; Exit the interrupt service routine. ; This involves recovering W and STATUS and then ; returning. Note that putting STATUS back ; automatically pops the bank back as well. ; This takes 6 Tcy for a total overhead of 12 Tcy for sync ; interrupts and 13 Tcy for async interrupts. ; INTR_POST: SWAPF TMP_STATUS,W ; Pull Status back into W MOVWF STATUS ; Store it in status SWAPF TMP_W,F ; Prepare W to be restored SWAPF TMP_W,W ; Restore it RETFIE |
||
Listing 2) The Interrupt Service Routine |
As you can see the TMR0 is
reloaded first to insure an accurate tick rate (also TMR0 is the first
interrupt checked!) Once you know you're set to get the next
"tick" on time, then you can check the quadrature state. In the
ISR I call QUAD_STATE
and this is written as follows:
; ; QUAD State ; ; A quadrature encoder traverse a couple of states ; when it is rotating these are: ; 00 | Counter ; 10 | Clockwise ; 11 | ^ ; 01 V | ; 00 Clockwise | ; ; QUAD_STATE: BCF STATUS,C ; Force Carry to be zero MOVF PORTB,W ; Read the encoder ANDLW H'6' ; And it with 0110 MOVWF Q_1 ; Store it RRF Q_1,F ; And rotate it right. RLF Q_NOW,F ; Rotate Q_NOW Left RLF Q_NOW,W ; by two IORWF Q_1,W ; Or in the current value MOVWF QUAD_ACT ; Store at as next action MOVF Q_1,W ; Get last time MOVWF Q_NOW ; And store it. ; ; Computed jump based on Quadrature pin state. ; MOVLW high QUAD_STATE MOVWF PCLATH MOVF QUAD_ACT,W ; Get button state ADDWF PCL,F ; Indirect jump RETURN ; 00 -> 00 GOTO DEC_COUNT ; 00 -> 01 -1 GOTO INC_COUNT ; 00 -> 10 +1 RETURN ; 00 -> 11 GOTO INC_COUNT ; 01 -> 00 +1 RETURN ; 01 -> 01 RETURN ; 01 -> 10 GOTO DEC_COUNT ; 01 -> 11 -1 GOTO DEC_COUNT ; 10 -> 00 -1 RETURN ; 10 -> 01 RETURN ; 10 -> 10 GOTO INC_COUNT ; 10 -> 11 +1 RETURN ; 11 -> 00 GOTO INC_COUNT ; 11 -> 01 +1 GOTO DEC_COUNT ; 11 -> 10 -1 RETURN ; 11 -> 11 INC_COUNT: INCF COUNT,F MOVLW D'201' SUBWF COUNT,W BTFSS STATUS,Z RETURN DECF COUNT,F RETURN DEC_COUNT DECF COUNT,F MOVLW H'FF' SUBWF COUNT,W BTFSS STATUS,Z RETURN INCF COUNT,F RETURN |
||
Listing 3) Maintaining an internal count |
The INC_COUNT and DEC_COUNT limit the resulting value to between 0 and 200 because that was what my application called for, however it could just as easily have kept an 8, 16, or even 32 bit absolute value. Clearly this code would also work with quadrature encoders on wheels of a robot but there you have to take into account that if your robot is moving quickly you will definitely want to adjust the sample rate accordingly!
An alert reader pointed out that I could just write a subroutine for the quadrature encoder that returned +1 or -1 or 0 and then added that to the value. To that end, the following subroutine was created:
; ; Return the value of what we should do to the ; value to adjust it. ; QUAD_ACTION: ; ; Computed jump based on Quadrature pin state. ; CLRF PCLATH ; Must be in page 0!!! ADDWF PCL,F ; Indirect jump RETLW H'00' ; 00 -> 00 RETLW H'FF' ; 00 -> 01 -1 RETLW H'01' ; 00 -> 10 +1 RETLW H'00' ; 00 -> 11 RETLW H'01' ; 01 -> 00 +1 RETLW H'00' ; 01 -> 01 RETLW H'00' ; 01 -> 10 RETLW H'FF' ; 01 -> 11 -1 RETLW H'FF' ; 10 -> 00 -1 RETLW H'00' ; 10 -> 01 RETLW H'00' ; 10 -> 10 RETLW H'01' ; 10 -> 11 +1 RETLW H'00' ; 11 -> 00 RETLW H'01' ; 11 -> 01 +1 RETLW H'FF' ; 11 -> 10 -1 RETLW H'00' ; 11 -> 11 |
||
Listing 4) The modified compuation loop |
Now, instead of calling INC_COUNT or DEC_COUNT, I just call this routine with the computed value of QUAD_ACT and the add W to the COUNT variable. After the addition I test for over flow or underflow and update the value accordingly. Given that the top 4 bits of PORT B can be configured for "interrupt on change" I've got to assume this is how they implement computer mice with two quadrature encoder wheels.
In a final bit of cleverness, if you skip a state, and you remembered that you were turning clockwise or counter-clockwise, you could choose to increment or decrement by 2. That would keep the count accurate.
Using mechanical rotary encoders is easy to do and they provide an excellent "frob knob" for your projects. Software can set them to be anything from simple on/off switches, to linear or log potentiometers, or arbitrary value adjusters.
The amount of code needed to use them is comparable to the 'pseudo analog' techniques of pulling the line low then high and counting ticks until it crosses the logic 1 threshold (example the POT command on the Stamp). They do consume 2 digital I/O's rather than an analog I/O which is a detriment, but with an R2R ladder on the output they could use an A/D input.
Finally if you're using quadrature encoders on your wheels, you've probably already got the code in your system to deal with them and they are then nearly a total win.