Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

ASM - encoder routine for high PPR optical encoders - ideas please?

Status
Not open for further replies.

augustinetez

Active Member
Rather than load up my timer preload thread with this, I've started this seperately (but it is all related).

For the cheap and nasty mechanical detented encoders, I've got a few routines that work well enough, but are basically hopeless for high (400) ppr optical encoders, even after taking out the divide by 4 bit for the detents - no detents in the encoder I'm using.

Bottom line is that all the stuff I've either found here or elsewhere on the 'net suffer from the inability to keep up with the encoder - they either jitter back and forth between CW and CCW too much, or the most common problem - the faster you spin the encoder, the slower the returned count or they start showing a reversed count ie instead of counting up, they (the code) start counting down and vice versa.

At the moment, I am polling the encoder because everything else in the program does nothing until the encoder moves, so interrupt driven routines aren't needed (yet).

Here are a couple or three bits of code I have been playing with - bottom line, all I need at the end of the routine is to set a direction bit (dir,0 in my program).

Ultimately, the aim is to produce a bit of code that will automatically change the frequency step size of a DDS chip dependent on how fast the encoder is moving.

A modification of Mike K8LH's bit of code - works until you crank up the speed:

Code:
movf    PORTA,W        ; load switch data
    andlw    b'00000011'    ; mask encoder B and A switches
    xorwf    enc_old,W    ; same as last reading?
    btfsc    STATUS,Z    ; yes, branch (no change), else
    goto    poll_encoder
    xorwf    enc_old,W    ; restore encoder bits in W
    rrf    enc_old,f    ; prep for B-old ^ A-new
    xorwf    enc_old,f    ; ENCOLD bit 0 = direction
    rrf    enc_old,f    ; now Carry bit = direction
    movwf   enc_old        ; update ENCOLD (new BA bits)

;****************** For optical encoder *****************************************
;       Prevent encoder slip from giving a false change in direction.

    movf    STATUS,w    ; Carry bit = direction
    andlw    b'00000001'
    movwf    next_dir    ; Save result (in W) as direction
    xorwf    dir,w        ; See if direction is same as before
    btfsc    STATUS,Z    ; Zero flag set? (i.e, is direction same?) 
    goto    enc_exit    ; Yes, same direction so no slip; keep going
    movf    next_dir,w    ; No Zero-flag, so direction changed       
    movwf    dir        ; Update the direction indicator
    goto    poll_encoder    ; Try again
;********************************************************************************         
;
;  set <up> or <dn> switch flag bits based on bit 0 in next_dir
;
enc_exit
    bcf    dir,0        ; set <dn> switch flag for Main
    btfsc    next_dir,0
    bsf    dir,0        ; set <up> switch flag for Main

    return

From a DDS program by Curtis W. Preuss - also works if you don't crank up the spin speed too much:

Code:
        movf    PORTB,w        ; Get the current encoder value
    andlw    b'00000011'    ; mask encoder B and A switches
    movwf    enc_read    ; Save it
    movlw    b'00000011'    ; Get encoder mask (to isolate RB0 and RB1)
    andwf    enc_read,w    ; Isolated encoder bits into W
    movwf    enc_new        ; Save new value
    xorwf    enc_old,w    ; Has it changed?
    btfsc    STATUS,Z    ; Check zero-flag (zero if no change)
    goto    poll_encoder    ; No change, keep looking until it changes
                ; Else, Zero-flag is not set, so continue on 

; It changed. Now determine which direction the encoder turned.

    bcf    STATUS,C    ; Clear the carry bit to prepare for rotate 
    rlf    enc_old,f    ; Rotate old bits left to align "Right-Bit" 
    movf    enc_new,w    ; Set up new bits in W                      
    xorwf    enc_old,f    ; XOR old (left shifted) with new bits      
    movf    enc_old,w    ; Put XOR results into W also               
    andlw    b'00000010'    ; Mask to look at only "Left-Bit" of pair
    movwf    next_dir    ; Save result (in W) as direction (bit=UP)  
    xorwf    last_dir,w    ; See if direction is same as before 
       
;****************** For optical encoder *****************************************
;       Prevent encoder slip from giving a false change in direction.

    btfsc    STATUS,Z    ; Zero flag set? (i.e, is direction same?)  
    goto    enc_continue    ; Yes, same direction so no slip; keep going
    movf    next_dir,w    ; No Zero-flag, so direction changed        
    movwf    last_dir    ; Update the direction indicator            
    movf    enc_new,w    ; Save the current encoder bits (now in W)  
    movwf    enc_old        ; for next time                           
    goto    poll_encoder    ; Try again
;********************************************************************************
;
enc_continue
    clrf    last_dir    ; Clear last_dir (default is DN)
    bcf    dir,0
    btfsc    enc_old,1    ; Are we going UP?             
    goto    enc_up        ; Yes, go process it.
                ; Else, we are goiong down
    goto    enc_movement    ; Indicate that the encoder has moved

enc_up 
    movlw    b'00000010'    ; Get UP value
    movwf    last_dir    ; and set in last_dir
    bsf    dir,0

enc_movement            ; Arrive here when encoder is being turned
    movf    enc_new,w    ; Get the current encoder bits
    movwf    enc_old        ; Save them in ren_old for the next time
    bsf    flags,2        ; Set encoder changed flag

    return            ; Return to the caller

And one from Leon - which has an error in it - this is extraneous as in doesn't do anything -> MOVWF Q_NOW
This one misbehaves the most:

Code:
;
; 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   0x03            ; And it with 0011
    MOVWF   Q_1             ; Store it
       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   

    end
 
Terry,

What pins do you have the encoder attached to?

I've got an idea, using interrupts, that should manage over 250k pulses per second.

Mike.
Edit, hopefully on PORTB as PORTA doesn't have IOC.
Edit2, looking at your previous code, was on B,0 and B,1.
 
Last edited:
Yep B,0 and B1.

Below is the current code for the encoder with the variable timing rate included.
It works but needs a bit of fine tuning in it's corresponding routine outside of this loop.

There is also the call to the RIT function in this loop that needs to be polled.

Code:
poll_encoder
    btfss    CAL_sw        ; Test if in calibrate mode
    BRA    read_encoder

    bcf    flags,6        ; Clear Timer2 active flag
    bcf    flags,2        ; Clear encoder changed flag

read_rit
    call    rit        ; Test RIT, skip to encoder polling if no change
    btfsc    flags,1        ; flags,1 = 0 - RIT = same as last time
    return            ; flags,1 = 1 - RIT = changed
;
read_encoder
    movf    enc_old,w    ; read the old jump vector
    andlw    0x03        ; and it with 0011 to lose the penultimate state
    movwf    enc_old        ; ready to shift the previous state
    bcf    STATUS,C    ; force carry to be zero
    rlf    enc_old, f
    bcf    STATUS,C    ; force carry to be zero
    rlf    enc_old, f
    ;sort new value
    movf    PORTB,w        ; read the encoder (newest state )
    andlw    0x03        ; and it with 0011
    iorwf    enc_old,F    ; Store it
    
    ;
    ; Computed jump based on Quadrature pin state.
    ;
    movlw    high read_encoder
    movwf    PCLATH
    movf    enc_old,w    ; get button state
    addwf    PCL,f        ; indirect jump
    goto    poll_encoder    ; 00 -> 00
    goto    enc_dn        ; 00 -> 01 -1
    goto    enc_up        ; 00 -> 10 +1
    goto    poll_encoder    ; 00 -> 11
    goto    enc_up        ; 01 -> 00 +1
    goto    poll_encoder    ; 01 -> 01
    goto    poll_encoder    ; 01 -> 10
    goto    enc_dn        ; 01 -> 11 -1
    goto    enc_dn        ; 10 -> 00 -1
    goto    poll_encoder    ; 10 -> 01
    goto    poll_encoder    ; 10 -> 10
    goto    enc_up        ; 10 -> 11 +1
    goto    poll_encoder    ; 11 -> 00
    goto    enc_up        ; 11 -> 01 +1
    goto    enc_dn        ; 11 -> 10 -1
    goto    poll_encoder    ; 11 -> 11
        
enc_up   
    bsf    dir,0   
    goto    enc_exit
    
enc_dn   
    bcf    dir,0

;enc_exit
;    bsf    flags,2        ; Set encoder changed flag
;    return

enc_exit
    btfss    flags,6
    call    start_timer
    incf    enc_count0,f
    btfss    PIR1,TMR2IF
    BRA    read_encoder
    bcf    T2CON,TMR2ON
    bsf    flags,2        ; Set encoder changed flag
    bcf    PIR1,TMR2IF
    return

start_timer
    bsf    flags,6        ; Set Timer2 active flag
    bsf    T2CON,TMR2ON
    return
 
I was thinking more along the lines of an Interrupt driven routine.
This ISR is less than 20 clock cycles (2.5uS at 32MHz) so should keep up with 400kHz pulses,
Code:
      ORG    0x0004
      ;automatic context save on this chip
      btfss  INTCON,INTF    ;is it encoder interrupt
      goto   otherIrq       ;no so check power down
      bcf    INTCON,INTF    ;clear the interrupt flag
      movlb  0              ;select bank 0
      btfss  PORTB,1        ;is it up or down
      goto   decrement      ;down
      incf   count+1,f      ;up
      incfsz count,f        ;do 16 bit increment
      decf   count+1,f
      goto   otherIrq       ;check other interrupts
decrement
      movf   count,f        ;do 16 bit decrement
      skpnz
      decf   count+1,f
      decf   count,f
      decf   count,f
otherIrq
      btfss  PIR2,C2IF      ;has the comparator interrupt fired
      retfie                ;no, done
      ;do your power down save here
      ;and loop forever
here 
      goto   here           ;wait forever so no more interrupts
It increments/decrements a 16 bit counter. After your 5mS, disable interrupts, copy count, zero count, enable interrupts. Then use count as desired, the interrupt will still keep track of movement whilst you process the previous count.
The setup is just 32MHz clock and enable INT interrupt. You'll also need to setup comparator 2 interrupt.
Code:
      banksel OSCCON
      movlw   b'11110000'     ;select 32MHz clock = 8MHz instruction cycle 
      movwf   OSCCON          ;Set PIC oscillator frequency        
      banksel OPTION_REG
      movlw   b'01000000'     ;WPUs on, INT = positive edge
      movlw   b'11010000'
      movwf   INTCON          ;enable INT pin interrupts

Mike.
BTW, above untested as don't have chip or encoder.
Edit, commented code. Count should be in common area.
 
Last edited:
Got to testing that code - it works but still getting the frequency change of direction problem if I give the encoder a good twist.

Having it tested in a setup that is fully geared to see if it still does it with the encoder geared down (saves me having to do some mechanical work).
That means your software isn't running fast enough.. If you get three turns within a second, that'll be 400x3 = 1200 pps.. 4 state changes = 208us... I think that code has @ 22clock cycles so if the rest of your code is @180 clock cycles + ?? Is your gearbox 6:1 or 1:6 ??? You could be producing many pulses
 
You have just reinvented my concept from post #9 - with the code added :D :D :D
Indeed I did. I was rather surprised when I realised it should run fine at up to 400kHz.
I'm not sure about staying in the interrupt when the power fail comparator triggers as that could be just a power glitch that should be recoverable from. Maybe, save, clear interrupt and return so the program won't freeze - just miss a few counts.
Maybe the final bit of the code should be,
Code:
otherIrq
      btfss  PIR2,C2IF      ;has the comparator interrupt fired
      retfie                ;no, done
      bcf     PIR2,C2IF     ;clear interrupt flag
      ;do your power down save here
      ;and loop forever
      retfie                ;get out of here

Mike.
 
Yes, I'm using interrupts for something else but not ruling it out for the encoder as well, just prefer not to if possible at this stage.

The interrupt is only being used to save data at power down currently so I'm guessing that interrupt flag testing would make it possible to do both.
Using multiple interrupts is normal and commonplace, most of my recent projects use two for serial port 1, two for serial port 2, two or three for timer interrupts, varying numbers for key inputs, and one or two for signal inputs.
 
6:1 - 6 turns of the input shaft = 1 turn of the encoder.


Good to know, I'm not that up with interrupts so learning plenty here.
Wow.. somethings wrong then... 1 turn is 66 PPR As I said.. I can turn mine at 50Hz..
Mine is 64PPR...

What circuitry have you in place... caps??? resistors???
 
OK, I'm having a major seniors moment trying to visualise how this (code below) is incorporated in to my code segment ref post #23 - that segment is but only a small section of the total program.

Because the interrupt can be triggered anywhere in the polling loop (well, anywhere in the whole program), it can jump to the interrupt at any point so not sure how/where timer2 is supposed to be started.

So, should the encoder interrupt be enabled only while it is within the polling loop?

Also don't understand this :-

"After your 5mS, disable interrupts, copy count, zero count, enable interrupts. Then use count as desired, the interrupt will still keep track of movement whilst you process the previous count."

The PIC being capable of only doing one thing at a time, how can it keep track of a moving encoder at the same time the software is off processing something else (besides interrupting the current process)?

I was thinking more along the lines of an Interrupt driven routine.
This ISR is less than 20 clock cycles (2.5uS at 32MHz) so should keep up with 400kHz pulses,
Code:
      ORG    0x0004
      ;automatic context save on this chip
      btfss  INTCON,INTF    ;is it encoder interrupt
      goto   otherIrq       ;no so check power down
      bcf    INTCON,INTF    ;clear the interrupt flag
      movlb  0              ;select bank 0
      btfss  PORTB,1        ;is it up or down
      goto   decrement      ;down
      incf   count+1,f      ;up
      incfsz count,f        ;do 16 bit increment
      decf   count+1,f
      goto   otherIrq       ;check other interrupts
decrement
      movf   count,f        ;do 16 bit decrement
      skpnz
      decf   count+1,f
      decf   count,f
      decf   count,f
otherIrq
      btfss  PIR2,C2IF      ;has the comparator interrupt fired
      retfie                ;no, done
      ;do your power down save here
      ;and loop forever
here
      goto   here           ;wait forever so no more interrupts
It increments/decrements a 16 bit counter. After your 5mS, disable interrupts, copy count, zero count, enable interrupts. Then use count as desired, the interrupt will still keep track of movement whilst you process the previous count.
The setup is just 32MHz clock and enable INT interrupt. You'll also need to setup comparator 2 interrupt.
 
The processor has many different peripherals. The encoder makes use of the INT pin to generate an interrupt. Timer 2 can also generate interrupts but doesn't. Comparator 1 also generates an interrupt. All these things are happening all the time unless you stop them.

To incorporate this into your code, you need to do the following,
Switch the oscillator speed to 32MHz and enable the INT and comparator 1 interrupts,
Code:
      banksel  OSCCON
      movlw    b'11110000'     ;select 32MHz clock = 8MHz instruction cycle    
      movwf    OSCCON          ;Set PIC oscillator frequency           
      banksel  OPTION_REG
      movlw    b'01000000'     ;WPUs on, INT = positive edge
      movlw    b'00010000'
      movwf    INTCON          ;enable INT pin interrupts
      banksel  PIE2
      bsf      PIE2,C1IE
The above assumed that CM1CON1/2 are already setup.

You need (should already have) an interrupt routine (ISR) at location 4. As the code will start at location zreo, it's usual to have a jump to after your interrupt.
Code:
      ORG     0
      goto    start
      ORG     0x0004
      ;automatic context save on this chip
      btfss   INTCON,INTF    ;is it encore interrupt
      goto    otherIrq       ;no so check power down
      bcf     INTCON,INTF    ;clear the interrupt flag
      movlb   0              ;select bank 0
      btfss   PORTB,1        ;is it up or down
      goto    decrement      ;down
      incf    count+1,f      ;up
      incfsz  count,f        ;do 16 bit increment
      decf    count+1,f 
      goto    otherIrq       ;check other interrupts
decrement 
      movf    count,f        ;do 16 bit decrement
      skpnz
      decf    count+1,f
      decf    count,f
      decf    count,f
otherIrq
      banksel PIR2
      btfss   PIR2,C2IF      ;has the comparator interrupt fired
      retfie                 ;no, done
      bcf     PIR2,C2IF      ;clear interrupt flag
      ;do your power down save here
      retfie                 ;get out of here
                   
start
      //your setup and run code goes here
I assume you already have the power down code to insert where it's indicated.
Once everything is done (setup) enable the interrupts.
Code:
      bsf     INTCON,PEIE
      bsf     INTCON,GIE
Now, the interrupt will fire whenever RB0 goes possitive.
So, in your code,
clear count​
start the timer.​
when the 5mS is up (timer interrupt flag set),​
clear the timer interrupt flag​
disable interrupts​
copy the value of count to another 16 bit variable​
zero count​
reenable interrupts​
The encoder will again trigger interrupts while you can do what you wish.
You have 5mS to use count to do as you wish - probably add it (it's signed) ,suitabley scaled, to a larger count variable.
Wait for the timer flag to be set again.
Note, 5mS is a very long time at 32MHz and you will have 40,000 instruction cycles to play with.
Also, the encoder shouldn't move more than 32,000 clicks during the 5mS or count will overflow.

HTHs

Mike.
Edit, the ISR only takes <20 clock cycles (2.5uS) to run so the main code won't notice and the pic will appear to be doing two things at once.
 
So, in your code,

clear count
start the timer.
when the 5mS is up (timer interrupt flag set),
clear the timer interrupt flag
disable interrupts
copy the value of count to another 16 bit variable
zero count
reenable interrupts
Sorry for being thick, but this is the part that is confusing me - is 'my code' inside the interrupt routine or outside of it.

Re all the other about having interrupts set up and power down code, yes that's done and comparator 2 is used for the power down trigger.
 
In the meantime, I've been playing with the polled code and after some trimming and modification it works well enough.

Code:
read_encoder
    movf    PORTB,W        ; load switch data
    andlw    b'00000011'    ; mask encoder B and A switches
    xorwf    enc_old,W    ; same as last reading?
    btfsc    STATUS,Z    ; yes, branch (no change), else
    goto    poll_encoder
    xorwf    enc_old,W    ; restore encoder bits in W
    rrf    enc_old,f    ; prep for B-old ^ A-new
    xorwf    enc_old,f    ; ENCOLD bit 0 = direction
    rrf    enc_old,f    ; now Carry bit = direction
    movwf   enc_old        ; update ENCOLD (new BA bits)

;****************** For optical encoder *****************************************
;       Prevent encoder slip from giving a false change in direction.

    movf    STATUS,w    ; Carry bit = direction
    andlw    b'00000001'
    xorwf    dir,w        ; See if direction is same as before
    btfsc    STATUS,Z    ; Zero flag set? (i.e, is direction same?) 
    goto    enc_exit    ; Yes, same direction so no slip; keep going
                ; No, direction changed
    bcf    dir,0        ; set <dn> flag
    btfsc    STATUS,C
    bsf    dir,0        ; set <up> flag
    goto    poll_encoder    ; Try again

;********************************************************************************         
;
;  set <up> or <dn> switch flag bits based on direction in C
;
enc_exit
    bcf    dir,0        ; set <dn> switch flag for Main
    btfsc    STATUS,C
    bsf    dir,0        ; set <up> switch flag for Main

    bsf    flags,2        ; Set encoder changed flag
    return
 
Your code is outside the interrupt.
Say you're using the timer 2 code in other thread then your code would be,
Code:
        clrf    count
        clrf    count+1
wait    btfss   PIR1,TMR2IF     ;wait for timer to timeout
        goto    wait
        bcf     PIR1,TMR2IF     ;clear the interrupt flag
        bcf     INTCON,GIE      ;dissable interrupts
        movfw   count           ;copy count
        movwf   countCopy
        movfw   count+1
        movwf   countCopy+1
        clrf    count           ;clear count
        clrf    count+1
        bsf     INTCOM,GIE      ;reenable interrupts
        return
This will,
wait until the time is up (5mS)
return with countCopy(16 bit) containing a signed count of the encoder movement in that (5mS) period. (±32768)
You can use this value however you wish and during this time the interrupt will be keeping track of encoder movement ready for the next time the above code is entered.

Mike.
Edit, the first two lines should only be done once when the timer is setup and started.
 
Terry,

Here's a simpler version that doesn't use interrupts.

The setup is pretty much the same only without enabling interrupts,
Code:
setup   
      banksel OSCCON
      movlw   b'11110000'     ;select 32MHz clock = 8MHz instruction cycle
      movwf   OSCCON          ;Set PIC oscillator frequency       
      banksel OPTION_REG
      movlw   b'01000000'     ;WPUs on, INT = positive edge
      movlw   b'00000000'     ;no interrupts
      movwf   INTCON
And here's the code that will read the encoder and quit once the timer has passed 5mS,
Code:
readEncoder
      movlb   0               ;select bank 0
      bsf     T2CON,TMR2ON    ;turn timer on   
      clrf     count
      clrf     count+1
loop
      btfsc   PIR1,TMR2IF
      goto    timeUp
      ;now check if the encoder has moved
      btfss   INTCON,INTF
      goto    loop
      bcf     INTCON,INTF     ;added
      btfss   PORTB,1         ;is it up or down
      goto    decrement       ;down
      incf    count+1,f       ;up
      incfsz  count,f         ;do 16 bit increment
      decf    count+1,f
      goto    loop            ;repeat reading
decrement
      movf    count,f         ;do 16 bit decrement
      skpnz
      decf    count+1,f
      decf    count,f
      decf    count,f
      goto    loop
timeUp
      ;this will return with count containing the number of moves
      ;the encoder has made with a ± number in count(16 bit)
      bcf     T2CON,TMR2ON    ;timer 2 off
      bcf     PIR1,TMR2IF
      return
You will need to add the timer 2 and comparator setup to the first bit of code.

The bad bit about the above is that when not in the readEncoder routine the encoder is being ignored.

Mike.
Edit, all the SFRs used in readEncoder are in bank 0 so there's no need for the bankselect instructions.
Edit2, added clear count at beginning of readEncoder.
Edit3, forgot to clear the INTF flag - commented "added".
 
Last edited:
Status
Not open for further replies.

New Articles From Microcontroller Tips

Back
Top