Rotary Encoder - 16f628

Thread Starter

TCOP

Joined Apr 27, 2011
94
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?
 

SgtWookie

Joined Jul 17, 2007
22,230
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.
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
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 ?
 

t06afre

Joined May 11, 2009
5,934
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.
 

THE_RB

Joined Feb 11, 2008
5,438
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;
Rich (BB code):
//----------------------------------------------------
// check for any change of encoder pins
newX = (PORTB & 0b11000000);   // only keep the 2 encoder inputs; ABxxxxxx
if(newX != lastX)   // if changed
{
  // now check direction (if newA != oldB)
  if(newX.F7 != lastX.F6) x_value++;
  else x_value--;
  lastX = newX;
}
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
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?
 

John P

Joined Oct 14, 2008
2,026

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):

Rich (BB code):
const int8 enc_states[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

void read_encoder(void)
{
  static int8 old_AB;
  int8 i;

  old_AB <<= 2;                   //remember previous state
  old_AB |= ( ENC_PORT & 0x03 );  //add current state
  old_AB &= 0xF;

  position += enc_states[old_AB];

        /* Alternative, which may run faster especially 
         if position is multibyte quantity:

  i = enc_states[old_AB];
  if (bit_test(i, 7))                      // Test for -1
    position--;
  else if (bit_test(i, 0))                // Else test for +1
    position++;
       */
}
 
Last edited:

Thread Starter

TCOP

Joined Apr 27, 2011
94
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.
ok...

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.
ok...i meant the same

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

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.
Obviously!...now i get it

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):

Rich (BB code):
const int8 enc_states[16] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
 
void read_encoder(void)
{
  static int8 old_AB;
  int8 i;
 
  old_AB <<= 2;                   //remember previous state
  old_AB |= ( ENC_PORT & 0x03 );  //add current state
  old_AB &= 0xF;
 
  position += enc_states[old_AB];
 
        /* Alternative, which may run faster especially 
         if position is multibyte quantity:
 
  i = enc_states[old_AB];
  if (bit_test(i, 7))                      // Test for -1
    position--;
  else if (bit_test(i, 0))                // Else test for +1
    position++;
       */
}
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
 

ErnieM

Joined Apr 24, 2011
8,377
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:

Rich (BB code):
ISR:
{
  Save_Context;
  if (ISR_Source = PinChange)
  {
    if (PinA == 1)                  // we have a + edge
    {
      if (PinB == 0)                // For A +edge and B low, rotation is Clockwise.
      {
        Rotation = Rotation + 1;    // Rotation increments on Clockwise rotation
      }
      else
                                    // For A +edge and B high, rotation is Counter Clockwise.
        Rotation = Rotation - 1;    // Rotation decrements on Counter Clockwise rotation
      }
    }
    else    // (PinA == 0)          // we have a - edge
    {
      if (PinB == 0)                // For A -edge and B low, rotation is Counter Clockwise.
      {
        Rotation = Rotation - 1;    // Rotation decrements on Counter Clockwise rotation
      }
      else
                                    // For A -edge and B high, rotation is Clockwise.
        Rotation = Rotation + 1;    // Rotation increments on Clockwise rotation
      }
    }
    Clear_ISR_Flag
  }
  // other ISR sources here (if any)
  Restore_Context;
}
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
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 ??
 

John P

Joined Oct 14, 2008
2,026
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.
 

THE_RB

Joined Feb 11, 2008
5,438
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. ...
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.

In addition, i need more instructions because i have to hold the total meters.
What do you mean total meters? It aleady incs (or decs) a variable for every encoder state change.
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
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.


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

ErnieM

Joined Apr 24, 2011
8,377
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 ??

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:


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.
 

THE_RB

Joined Feb 11, 2008
5,438
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...
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
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:
Top