Introduction

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.

Description

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.

Source Code

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!

Late breaking news:

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.

Conclusions

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.

Return to the Projects Page

Return to the Notebook