Rotary Encoder - 16f628

Discussion in 'Programmer's Corner' started by TCOP, Apr 27, 2011.

  1. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    I need help on using a rotary encoder with 16F628A. I have set the pic running with the internal oscilator on 4mhz. I have a loop constantly checking Rb1 and Rb2 for movent and i properly get the direction of the encoder. My problem is that when i turn the encoder very fast i miss some transition bits. The encoder sends 50 full pulses per rotation (200 tansitions) and it is supposed to turn with up to 300 rpm. My code needs no more that 50 cycles to check the encoder state and loop back but still it seems that i get slippage errors. Do you have any suggestions? Do you think i should use interrupts and if so how can i trap the 0 state and not only the 1?
     
  2. SgtWookie

    Expert

    Jul 17, 2007
    22,182
    1,728
    Use the interrupt on change feature on PORTB.

    From section 5.2 in the datasheet:
    Four of PORTB’s pins, RB<7:4>, have an interrupt-on-change feature. Only pins onfigured as inputs can cause this interrupt to occur (i.e., any RB<7:4> pin configured as an output is excluded from the interrupt-on-change comparison).

    The input pins (of RB<7:4>) are compared with the old value latched on the last
    read of PORTB. The “mismatch” outputs of RB<7:4> are OR’ed together to generate the RBIF interrupt (flag latched in INTCON<0>).
    This interrupt can wake the device from Sleep. The user, in the interrupt service routine, can clear the interrupt in the following manner:
    a) Any read or write of PORTB. This will end the mismatch condition.
    b) Clear flag bit RBIF.
    A mismatch condition will continue to set flag bit RBIF.
    Reading PORTB will end the mismatch condition and allow flag bit RBIF to be cleared.
     
  3. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    Thanks for your reply. I tried it already with the IDE simulator and it seems that it works ok but i haven't tried it yet on the bread board. Do you think that i will solve the slippage problem using ints ?
     
  4. t06afre

    AAC Fanatic!

    May 11, 2009
    5,939
    1,222
    I am not sure of this. Can you run on higher clock speed also. I think you can go up to 8 MHz with internal osc. Feel free to post your code also if you still have trouble.
     
  5. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    You can read the encoder MUCH faster than 50 PIC instructions, see this page; http://www.romanblack.com/trackball.htm

    It discusses encoders and provides a method for reading an encoder;

    1. Check if the inputs have changed, then;
    2. If newA NOT= oldB; direction is +
    3. else direction is -

    Here is C code to read the rotary quadrature encoder;
    Code ( (Unknown Language)):
    1.  
    2. //----------------------------------------------------
    3. // check for any change of encoder pins
    4. newX = (PORTB & 0b11000000);   // only keep the 2 encoder inputs; ABxxxxxx
    5. if(newX != lastX)   // if changed
    6. {
    7.   // now check direction (if newA != oldB)
    8.   if(newX.F7 != lastX.F6) x_value++;
    9.   else x_value--;
    10.   lastX = newX;
    11. }
     
  6. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    Thank you both for the replies...
    THE_RB
    Well the code you proposed will result to many more lines when you'll convert it to pic assembly. In addition, i need more instructions because i have to hold the total meters. Perhaps i can optimize it a make it smaller but not much smaller. By the way i use the same techniqe by xoring old and new values of the right hand and the left hand bits.
    t06afre
    I think you are right about speeding up...
    I did some calculations...
    300 RPM x 200 trassitions/60= 1000transitions/sec
    So i should check the ecnoder at least once every 1msec. right???
    If i need 50 cycles in 4mhz then every cycle is 1μsec.
    So 50 cycles x 1000 times = 50000μsec=50ms.

    Thus, i need 50ms to process the encoder status on full speed while i should be able to check it every 1ms. So even if i use a 20mhz crystal i will stil miss some bits cause 50/4 will result to 12,5 ms.

    Is my thinking right?
    Any suggestions?
     
  7. t06afre

    AAC Fanatic!

    May 11, 2009
    5,939
    1,222
    What encoder do you use. Do you have some datasheet you can post.
     
  8. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    It's an ELTRA encoder which gives out 50 quadrture pulses in every rotation. see image
     
  9. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    anyone here?
     
  10. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224

    I did some calculations...
    300 RPM x 200 trassitions/60= 1000transitions/sec
    So i should check the ecnoder at least once every 1msec. right???

    Theoretically right, but I'd check it several times faster if possible.

    If i need 50 cycles in 4mhz then every cycle is 1μsec.

    No, you mean "50 assembly instructions, where each one takes 1usec."

    So 50 cycles x 1000 times = 50000μsec=50ms.

    That's 50msecs for encoder processing, per second, or 1/20 of a second. You have plenty of time available!

    Thus, i need 50ms to process the encoder status on full speed while i should be able to check it every 1ms. So even if i use a 20mhz crystal i will stil miss some bits cause 50/4 will result to 12,5 ms.
    Is my thinking right?
    Any suggestions?

    You're calculating the wrong thing.

    If this were my program, I would set up a timer routine to trigger at a rate faster than the encoder transitions--say 5KHz. Then I'd read the encoder by polling, not by an interrupt. You could do it like this (code modified from http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino):

    Code ( (Unknown Language)):
    1.  
    2. const int8 enc_states[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
    3.  
    4. void read_encoder(void)
    5. {
    6.   static int8 old_AB;
    7.   int8 i;
    8.  
    9.   old_AB <<= 2;                   //remember previous state
    10.   old_AB |= ( ENC_PORT & 0x03 );  //add current state
    11.   old_AB &= 0xF;
    12.  
    13.   position += enc_states[old_AB];
    14.  
    15.         /* Alternative, which may run faster especially
    16.          if position is multibyte quantity:
    17.  
    18.   i = enc_states[old_AB];
    19.   if (bit_test(i, 7))                      // Test for -1
    20.     position--;
    21.   else if (bit_test(i, 0))                // Else test for +1
    22.     position++;
    23.        */
    24. }
    25.  
     
    Last edited: Apr 28, 2011
  11. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    Great idea. So, i can hook the interrupt with the internal timer and set it in a prescale so as to have 5-10 khz triggers and from inside the interrupt routine, i should check say Rb1 and Rb2 for change. If these are changed then i should update a file register with the right value.
    From my main loop i should check the file register only and report the total meters.
    I think i get it. Thanks very much
     
  12. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,387
    1,605
    I've never used a rotary encoder but I've used a lot of PICs. Let me see if I understand the issue.

    Encoder turns 300RPM or 5RPS. Each turn gives 50 pulses. So in each second either output transitions 5*100 = 500 times.

    The PIC running with a 4MHz will process 1E6 instructions per second.

    Thus we have 1E6 / 500 = 2000 instructions to process the information for each pulse from either A or B.

    Looking at Roman Blacks figure:

    For A +edge and B low, rotation is Clockwise.
    For A -edge and B high, rotation is Clockwise.

    For A -edge and B low, rotation is Counter Clockwise.
    For A +edge and B high, rotation is Counter Clockwise.

    Or...

    For B +edge and A high, rotation is Clockwise.
    For B -edge and A low, rotation is Clockwise.

    For B -edge and A high, rotation is Counter Clockwise.
    For B +edge and A low, rotation is Counter Clockwise.

    Since the encoder is an asynchronous input an interrupt driven scheme will use up the least processor resources (though the processor will spend most of it's time doing NOPs anyway) while making it simple to guarantee catching each and every transition as long as out IRS time is less then 2000 instructions, and that is simple to check.

    One thing to note is there is no reason to catch the edge of both pulses, the edge of either pulse will give you the whole story.

    So set up either A or B for an interrupt on change. Here is some pseudo code to track the position:

    Code ( (Unknown Language)):
    1.  
    2. ISR:
    3. {
    4.   Save_Context;
    5.   if (ISR_Source = PinChange)
    6.   {
    7.     if (PinA == 1)                  // we have a + edge
    8.     {
    9.       if (PinB == 0)                // For A +edge and B low, rotation is Clockwise.
    10.       {
    11.         Rotation = Rotation + 1;    // Rotation increments on Clockwise rotation
    12.       }
    13.       else
    14.                                     // For A +edge and B high, rotation is Counter Clockwise.
    15.         Rotation = Rotation - 1;    // Rotation decrements on Counter Clockwise rotation
    16.       }
    17.     }
    18.     else    // (PinA == 0)          // we have a - edge
    19.     {
    20.       if (PinB == 0)                // For A -edge and B low, rotation is Counter Clockwise.
    21.       {
    22.         Rotation = Rotation - 1;    // Rotation decrements on Counter Clockwise rotation
    23.       }
    24.       else
    25.                                     // For A -edge and B high, rotation is Clockwise.
    26.         Rotation = Rotation + 1;    // Rotation increments on Clockwise rotation
    27.       }
    28.     }
    29.     Clear_ISR_Flag
    30.   }
    31.   // other ISR sources here (if any)
    32.   Restore_Context;
    33. }
    34.  
     
  13. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    Well...
    thanx again...
    i was just reading the datasheet about setting tmr0 interrupt when i saw your post. So...you say that i should trap directly the encoder pin with an int and not with the timer. That's easier for me but is it better? dunno. Can you justify it?
    You also say to trap only one pin with an interrupt and then check the other pin's state. But the encoder gives 0,0,1,1 for A and 1,0,0,1 for B when turning CW. So, if i trap the first pin , how could i get the transition from 0 to 0 or from 1 to 1 ??
     
  14. John P

    AAC Fanatic!

    Oct 14, 2008
    1,634
    224
    This question of whether to interrupt on an event (encoder motion, etc) or just on time, depends on personal style. I'm a firm believer in using just the timer in any processor code, because I want to see the clock ticking at exactly the right frequency, never blocked by any other event. If you do that and you aren't catching every transition, then you need a faster clock, and maybe a faster processor! I'd treat the serial port the same way: never an interrupt on arriving characters, but the closk must run fast enough to catch everything that comes in.
     
  15. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    Actually it compiles down to about 10 asm instructions. It's main benefit is speed as in any test condition where the encoder has not changed it will only require about 5uS (at 1uS per inst). Since you said your encoder "checking" takes 50 cycles this takes about 5 to 10.

    What do you mean total meters? It aleady incs (or decs) a variable for every encoder state change.
     
  16. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0


    Well, true it does...but if you need to count 12000 cm then you need some extra code and additional file registers
     
  17. ErnieM

    AAC Fanatic!

    Apr 24, 2011
    7,387
    1,605

    Oops, gonna get confusing as we seem to have multiple ways to do this, and all are probably equally as valid.

    Using a timer you have to fire the timer at the fastest rate the encoder may pulse and process each tick; firing much faster (oversampling?) may be necessary to insure no pulse gets left behind. Thus you are spending most of your processing time looking for pulses that may not even be there.


    By using the interrupt on change you only process pulses when and if they happen. As you know your max rate of pulses as long as your ISR is shorter then that time you are guaranteed to catch each and every one of them.


    I am basing my reply off Roman's figure reproduced here:
    [​IMG]

    If we just look at the edges of A, when A goes hi B is low for CW, B is high for CCW. Also when A goes low, B is low for CCW and high for CW. Using the interrupt on change feature we just get told that an edge occurred, bot which edge. So once we enter the ISR we know we have an edge and have to check both A&B's state:

    A B Direction
    0 0 CCW
    0 1 CW
    1 0 CW
    1 1 CCW

    So by checking the states of A&B inside the ISR we get the direction, and I now notice that A==B for CW and A!=B for CCW, which is a simpler test then my pseudo code.

    The only drawback I can see is how you use the total Rotation count. If you check the count anywhere but in the ISR you can have a problem. As Rotation count will probably be bigger then a byte it will take several instructions to even copy from one variable to another, and it is a certainty that one day you will be reading it when the ISR comes along and changes it.

    It's the same problem as multi-thread synchronization on a PC program. You can work with the Rotation count outside of the ISR if you use some sore of semaphore flag. Let your IRS always set a flag when it updates the Rotation count. When your main loop wants to check the Rotation count first reset that flag and then copy the value. After the copy, if the flag is still reset you have a valid copy; if the flag is set your copied value is corrupted so try again.

    BTW, you can add in the Timer0 interrupt too to get a tick count which could be used to give you a rate of change if need be.

    Here I would define "Better" as being certain no pulse gets left behind. That is exactly what the ISR will do, while leaving you all the time in between pulses to do other work.
     
  18. THE_RB

    AAC Fanatic!

    Feb 11, 2008
    5,435
    1,305
    That figure is not for quadrature decoding, it gives a much lower resolution decoding (that may or may not be better).

    I'm not keen on using the interrupt on change for rotary encoders in quadrature due to them being edge-noisy, especially for things like motor use.

    My preference (if simplicity is a must) would be to sample at a slow rate that is slightly higher than needed for the max speed, so this eliminates any fast edge noise (and acts like a debounce).

    Or if the motor needs to be run very slowly then simplicity goes out the window and it needs a proper debounced system...
     
  19. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    Ok...
    i hooked the timer and get the program to work onthe simulator but not in actual use. here is it:
    LIST p=16F628A ;tell assembler what chip we are using
    include "P16F628A.inc" ;include the defaults for the chip
    __config _INTOSC_OSC_NOCLKOUT & _LVP_OFF & _WDT_OFF & _PWRTE_OFF & _BOREN_OFF & _MCLRE_OFF
    cblock 0x20 ;start of general purpose registers
    tmp ;20
    tmp2 ;21
    Old ;22
    New ;23
    RCounter ;24
    FRotation;25
    dataL;26
    W_TEMP;27
    STATUS_TEMP;28
    endc

    org 0x0000 ;org sets the origin, 0x0000 for the 16F628,this is where the program starts running
    goto main

    org 0x0004
    movwf W_TEMP ; push W
    swapf STATUS,W ;push Status without affecting flags. The save value is swapped
    BCF STATUS,RP0
    movwf STATUS_TEMP
    BTFSS INTCON,T0IF
    goto exitint
    bcf INTCON,T0IF ;clear int flag
    Call Encoder
    exitint:
    swapf STATUS_TEMP,W ; swap back status to W
    movwf STATUS ; pop original value to status
    swapf W_TEMP,F ;swap back W value
    swapf W_TEMP,W ;pop W
    retfie

    main:
    movlw 0x07
    movwf CMCON ;turn comparators off and enable pins for I/O functions
    bsf STATUS, RP0 ;select bank 1
    movlw 0xff ;set PortB all inputs
    movwf TRISB
    movlw 0x00
    movwf TRISA ;set PortA all outputs
    MOVLW B'00000001' ; Set TMR0 prescaler to 4
    MOVWF OPTION_REG
    bcf STATUS,RP0 ;select bank 0
    movlw 1
    movwf RCounter

    CLRF INTCON
    bsf INTCON,GIE
    BCF INTCON,T0IF
    bsf INTCON,T0IE

    Loop:
    movf RCounter,0
    movwf PORTA
    goto Loop

    Encoder:
    movf PORTB,0 ; read port B to W
    andlw 0x3 ; Keep only the first 2 bits
    movwf tmp ; keep it to tmp
    movf New,0 ; mov the previous "New" value
    movwf Old ; to OLD file register
    movf tmp,0 ; then move the new value
    movwf New ; to "New" file register
    Clrf STATUS
    xorwf Old,0 ; xor old and new
    btfss STATUS,2 ;if it is zero then bit 2 of status reg is set. if so call return
    Call EncoderStatus ; else call encoderstatus
    Return

    EncoderStatus:
    movf Old,0 ; move the right
    andlw 0x01 ; bit of the OLD value to tmp2
    movwf tmp2
    movf tmp,0
    andlw 0x02 ; move the left bit of the
    movwf tmp ; new value to tmp
    rrf tmp,0; rotate one bit tmp
    Clrf STATUS
    xorwf tmp2,0 ; xor them. if it is zero decrease else increase
    btfss STATUS,2
    Goto INCRC
    Goto DECRC

    INCRC:
    INCF RCounter
    Return

    DECRC:
    Decf RCounter
    return
    end

    It does not decrease the counter but only increases it. I am not sure what is wrong. I am checking it a couple of hours and cant get it
     
    Last edited: Apr 30, 2011
  20. TCOP

    Thread Starter Member

    Apr 27, 2011
    89
    0
    sorry for 2nd insertion...
     
Loading...