Rotary Encoder - 16f628

Markd77

Joined Sep 7, 2009
2,806
You should be able to delete your posts now (or at least edit them). Just click the edit button by the post. While you are there, if you click "go advanced", highlight the code and click the button that looks like "#" it will make it look neater.
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
I added some comments on the previous code to make it easily readable.
Any ideas so far? I acts as it supposed to act under the simulator when i turn on or off, rb0 and rb1. On the breadboard it only increases and never decreases. i even checked the breadboard but everything is ok. It has to be something with the pulses...dunno
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
Ok...i figured it out....
Now the results...
I tried both Timer Int and RB7:Rb4 ints. When hooking the Rb pins everything worked flowlessly. I got no slippage errors even on max speed.
On the contrary using the timer i still got slippage errors. I guess this is because of the frequency.
On a 1:2 prescale with a 4mhz internal oscilator freq should be
4mhz/4 =1 mhz thus 1μs for every cycle ...thus 500μs or 0,5 ms for every int triggering
it turns out though that the controller needs a little longer than this but dunno the reason.
If you add my code's cycles it turns out that this is not enough.
 

Markd77

Joined Sep 7, 2009
2,806
Could you change a few things to make it more readable please?
Anything like:
movf var, 0
to
movf var, F or movf var, W

anything like

btfss STATUS, 0
to
btfss STATUS, C or btfss STATUS, Z

It might not fix anything but more people will look at it.

Also you should move W_TEMP and STATUS_TEMP into the memory which is available to all banks: 0x70 to 0x7F

Just put:
Rich (BB code):
cblock 0x70
W_TEMP
STATUS_TEMP
endc
after the first cblock (and remove them from the first one).
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
Could you change a few things to make it more readable please?
Anything like:
movf var, 0
to
movf var, F or movf var, W

anything like

btfss STATUS, 0
to
btfss STATUS, C or btfss STATUS, Z

It might not fix anything but more people will look at it.

Also you should move W_TEMP and STATUS_TEMP into the memory which is available to all banks: 0x70 to 0x7F

Just put:
Rich (BB code):
cblock 0x70
W_TEMP
STATUS_TEMP
endc
after the first cblock (and remove them from the first one).
ok..thanks
i'll change it and upload the final code. I just got an idea. Perhaps i should also preset the timer to FF so as not to let it count for 500 cycle but just for 2 in order to make it trigger faster.
 

Markd77

Joined Sep 7, 2009
2,806
You can preset it in the interrupt, but not too close to 255, you need a few cycles to return from the interrupt and run through the non interrupt code. At a guess 245 would be a good value to try.
(Just changed my guess after seeing the size of non interrupt code).
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
Rich (BB code):
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
endc
cblock 0x70 
W_TEMP;70
STATUS_TEMP;71
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 saved value is swapped
movwf STATUS_TEMP
BTFSS INTCON,T0IF 
goto exitint
Call Encoder
exitint
BCF STATUS,RP0
movlw 0xF0
movwf TMR0
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
bcf INTCON,T0IF ;clear int flag 
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'10000000' ; Set TMR0 prescaler to 2
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 FRotation,W
movwf PORTA
goto Loop
 
Encoder
movf PORTB,W ; read port B to W
andlw 0x30 ; Keep only the first bits 5,4
movwf tmp ; keep it to tmp
rrf tmp,F 
rrf tmp,F 
rrf tmp,F 
rrf tmp,F 
movf New,W ; mov the previous "New" value 
movwf Old ; to OLD file register
movf tmp,W ; then move the new value
movwf New ; to "New" file register
Clrf STATUS 
xorwf Old,W ; 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,W ; move the right
andlw 0x01 ; bit of the OLD value to tmp2
movwf tmp2
movf tmp,W
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,W ; xor them. if it is zero decrease else increase
btfss STATUS,2
Goto INCRC
Goto DECRC
 
INCRC
INCF RCounter
Clrf STATUS
movf RCounter,W
xorlw d'201'
btfsc STATUS,2
Goto IFROT
Return
IFROT
INCF FRotation
movlw 0x01
movwf RCounter 
return
 
DECRC
Decf RCounter
Clrf STATUS
movf RCounter,W
xorlw d'0'
btfsc STATUS,2
Goto DFROT
return
DFROT
DECF FRotation
movlw d'200'
movwf RCounter 
return 
end
end
That's the final code using the timer and reading pins rb4 & rb5. I preset the timer to F0 to speed up the interrupt. Works fine now.
Thanks everyone for your help. I managed to hook the encoder without any problems using both the ways you suggested and both worked fine. I use pins Rb4 and 5 to easily switch my routine from timer int to RB4:RB7 int.
Feel free to comment the code
 
Last edited by a moderator:

Thread Starter

TCOP

Joined Apr 27, 2011
94
I sould tell you that i count the rotations in the above code. every rotation is 200 transitions or 50 quadrture full pulses.
 

ErnieM

Joined Apr 24, 2011
8,415
Well congratulations on getting this to work!

I spent some time yesterday morning getting interrupt driven code to do this, with built in on the fly deglitching. It seems to sim well, is completely tolerant of any glitch or either input and will keep a 32 bit variable updated as to total rotation.

However, I shall not steal your thread to post. (If anyone is interested, PM me and I will start a new thread.)
 

Thread Starter

TCOP

Joined Apr 27, 2011
94
Well congratulations on getting this to work!

I spent some time yesterday morning getting interrupt driven code to do this, with built in on the fly deglitching. It seems to sim well, is completely tolerant of any glitch or either input and will keep a 32 bit variable updated as to total rotation.

However, I shall not steal your thread to post. (If anyone is interested, PM me and I will start a new thread.)
Not at all. i am currently working on increasing and decreasing a 32bit value so feel free to share your work here.
 

ErnieM

Joined Apr 24, 2011
8,415
OK, since you asked so nicely. <grin>


Again I must state I have never worked with a physical rotary encoder so I'm learning them as I go. However, looking at the physical layout I would conclude that yes they could glitch an output pin but I would expect this glitch to occur as the wheel spins in a hesitant fashion, meaning if is moving slowly due to a human hand. A motor would give a smooth uniform monotonic change of angle and I would not expect a mechanical glitch; if you have a glitch using a motor due to electrical reasons (poor shielding, bad coupling) you have plain old bad data and good luck pulling a valid signal from that mess.


But in either case a glitch from rotation would occur on the edge of an interrupter slot and hence be concentrated on only one output; if the slot edge of A is at the detector then B should be solid, and vice versa. So to detect a valid change of input we have some work to do.


First we must now check both A & B for changes so enable them both for IOC (Interrupt On Change). When we detect a change we must do the following:


1 - See if the current input state of A&B is a new state, as it is possible for an input to quickly flash a new state but go back to where it was before we can read it. Thus we “filter” out fast inconsistent inputs.


2 - See if this change is on the same pin as last seen. If it is, we ignore it.


3 – The interesting part: if we have both a new pin state and a change on the other pin than last time then we have a valid input change. So we save the new input state (using LastInState) and the new pin to change (in LastChange). And we update the _Rotation variable that counts valid rotation inputs. Here we update _Rotation 4 times per change, or 4 * 50 = 200 times per rotation. That's the best resolution we can get from the sensor.




The updates all occur asynchronously inside the interrupt routine. I also added some main loop code that mostly waits for new data to appear and when it does kicks off code to process the new value. The first problem here is to guard from using the _Rotation value while it is being updated; so Rotation_Flag is set when the value changes. If Rotation_Flag gets set while we are making a copy of _Rotation then we reset the flag and try it again.


I also use a NewData_Flag flag to mark when there is new data ready to be handled. Right now I just reset this flag as I don't know what you do with your data.


The code is written using the BoostC compiler, which you can download a free demo version from SourceBoost Technologies at http://www.sourceboost.com/CommonDownload.html. This is an excellent compiler that integrates right into MPLAB letting you run the debug simulator on the C code.



Here is the code:
Rich (BB code):
 // Sample code for rotary encoder decoding  
 // PIC16F628A running at 4MHz
 // Encoder connected as A->RB6, B->RB7
 // BoostC compiler (License type: free) under MPLAB
 // http://www.sourceboost.com
 

 #include <system.h>
 

 #pragma CLOCK_FREQ 400000       // let compiler know our 4 Mhz clock frequency  
                                             
 #pragma DATA _CONFIG, _BOREN_OFF & _CP_OFF & _DATA_CP_OFF & _PWRTE_OFF &  
                                             _WDT_OFF & _LVP_OFF & _MCLRE_OFF & _INTOSC_OSC_NOCLKOUT
 

 char LastInState;               // last valid state of encoder outputs
 char CurrentState;              // holds current reading  
                                 //  (to prevent multiple read of port)
 char LastChange = 0;            // last input to change  
 char CurrentChange;             // current input changing
                         
 long _Rotation = 0;             // current valid rotation counts
 long Rotation = 0;              // internal copy of current rotation counts
 volatile bit Rotation_Flag = 0; // flag to guard reading _Rotation variable
                                 // clear this flag,then copy Rotation variable
                                 // if flag now set, clear flag & repeat
 volatile bit NewData_Flag = 0;  // flag to notify we have a new Rotation value
 

 #define ENCODER_MASK 0x30       // Keep only the first bits 5,4
 #define ENCODER_A    4             // encoder A pin
 #define ENCODER_B    5             // encoder B pin
 #define A_CHANGE  1
 #define B_CHANGE  2
 

 void main(void)
 {
   // init PIC
   LastInState = portb & ENCODER_MASK;
   intcon = 0b10001000;  // enable PORTB IOC
     
   while (1)
   {
     // while we wait for an interupt   
     // let's get a copy of the rotation counnts
     do
     {
       NewData_Flag = Rotation_Flag;   // copy in case there is a pernding update  
       Rotation_Flag = 0;              // reset flag
       Rotation = _Rotation;           // copy value to safe place
       if (Rotation_Flag == 1)         // flag set when we have corrupt data
         NewData_Flag = 1;             // flag we have new data
     } while (Rotation_Flag ==1);      // flag set if copy corrupted, so try again
      
     // see if this is new data and do something interesting with it
     while (NewData_Flag == 1)
     {
       // we have new data to process
       // add code to handle new value here
       NewData_Flag = 0;               // clear flag for the new data
     }
   }
 }
 

 void interrupt(void)
 {
   if (intcon.RBIF == 1)    
   {
     // we have our encoder interupt
     CurrentState = portb & ENCODER_MASK;    // get current pins
     if (CurrentState != LastInState)
     {
       // we had an input change!
       // is this the same change we saw last time?
       if (CurrentState.ENCODER_A == LastInState.ENCODER_A)
         CurrentChange = B_CHANGE;           // if old A == new A then B changed
       else
         CurrentChange = A_CHANGE;           // if old A != new A then A changed
       if (LastChange != CurrentChange)      //is this a new change?
       {
         // we got a new valid input change
         LastChange = CurrentChange;         // preserve change
         LastInState = CurrentState;         // preserve state
         Rotation_Flag = 1;                  // set change flag
         if (CurrentState.ENCODER_A == CurrentState.ENCODER_B)
           if (CurrentChange == A_CHANGE)
             _Rotation--;   
           else
             _Rotation++;   
         else
           if (CurrentChange == A_CHANGE)
             _Rotation++;   
           else
             _Rotation--;   
       }
     }
     intcon.RBIF = 0;    // ready to exit, clear our IOC flag
   }
   // handle other int sources here
 }
 

John P

Joined Oct 14, 2008
2,063
I claim that glitches, no matter how fast, can't cause inaccuracy with a program that checks the encoder on a timer basis.

If the glitch is so fast that it occurs between timer intervals, the processor never sees it at all, so no problem.

If the processor catches the glitch (let's say one channel is "high" very briefly) then the next time the encoder is tested, that channel will be "low" again and the result will be one step forward and one step backward, which is correct, so again, no problem.

On the other hand, if you use the interrupt-on-change approach, the first change might still be getting processed when the second one occurs. It's possible to write code to deal with this, but it's also possible to fail to deal with it.

Yes, I have worked with encoders and processors. I did achieve success, but I also had some learning experiences along the way.
 

THE_RB

Joined Feb 11, 2008
5,438
I agree JohnP and I said the same thing in post #18.

If he must use an interrupt make it a regular timer interrupt and just check the encoder inputs there.

And I have no idea why ErnieM has used such a clunky way of detecting rotation when I already posted an extremely simple and elegant C procedure in post #5.
 

ErnieM

Joined Apr 24, 2011
8,415
I claim that glitches, no matter how fast, can't cause inaccuracy with a program that checks the encoder on a timer basis.
Sure, as long as you can guarantee a sample rate faster then the data change rate all should be well with a polling method. It would work just fine and there is no need to beat that dead horse.

However, you have to do a lot of work even when nothing is happening, and you have to keep doing it over and over and over. That keeps us from say putting the processor to sleep until the encoder starts turning again, or doing something else useful with a processor that needs to spend a lot of time checking the inputs.

There is also a synchronization issue to handle if you do this check the encoder in a timer interrupt.
On the other hand, if you use the interrupt-on-change approach, the first change might still be getting processed when the second one occurs. It's possible to write code to deal with this, but it's also possible to fail to deal with it.
Go ahead and replace "might" with "will sooner or later." PIC hardware and the code above would handle this as the port mismatch condition that fires the interrupt is set on a port read statement and the port is only read once. That means if another change occurs during the ISR (after the one and only port read) then the flag gets set and the return from interrupt just generates another interrupt, thus nothing "steady state" is missed. It may miss some very fast glitches but that is the whole point of a deglitcher, isn't it?.

So it's been dealt with.

The advantage of going to an interrupt driven measurement is you only spend time processing events when an event happens. That leaves the maximum processor time to do something else useful, and also allows the possibility of doing nothing and go to a low power sleep mode that will awake on... an interrupt event(!) so you can save power by sleeping when the encoder is not turning. It is something to keep in mind for say a generator + battery/super cap powered bicycle speedometer + odometer; it would only draw power when the bike is moving but still keep total distance between trips.

Which method is "better"? An engineering project has been described as a "successful series of compromises." For every part or technique you employ, there are costs to be weighed back and fourth. There is no abstract universal "best" method, there are only those methods that cost the least in a given application.

For this project it is entirely up to TCOP (the OP here) to weigh options and make his choice.

Finally, why would I post this technique? Mostly the OP asked me to do so. It is my understanding this board is to share ideas, maybe to learn something, maybe to teach something. I would hope to find a more hospitable atmosphere here in the future.
 

John P

Joined Oct 14, 2008
2,063
There's a fair amount of programming that comes down to a person's individual style, so I won't argue this too much. I just like using a timer as sole interrupt, so I know what's happening when. Others may do it their way, and I'm sure it'll work.

One thing that makes me nervous about the interrupt-on-change feature in PIC processors is what's talked about in this thread:
http://www.microchip.com/forums/tm.aspx?m=530484&mpage=&print=true

If this can't be totally trusted, and there's a work-around, I'll take the work-around. Or am I the victim of superstition?

As for the issue of saving the processor's time so it can do other things, I have a sense that that's running into danger. If this part of the program that doesn't usually take much time (reading the encoder, I mean) suddenly does require a burst of action, is some other part of the code going to fail catastrophically? I actually find it more comforting to keep the loading even, so whether the encoder is spinning fast or slow or not at all, the time taken for processing is roughly the same. If the program is written so that fast motion can be accommodated, then there must be slack somewhere where the processor is twiddling its thumbs. If we have time being wasted somewhere, then it's no worse to have the processor responding to timer interrupts in which nothing much happens. Anyway, we almost always want a clock running. It can do several different things once the interrupt happens.

But I'm not claiming to know the One True Way. I do it the way it looks right.
 

ErnieM

Joined Apr 24, 2011
8,415
JohnP: Oh yeah, it is unlikely but still possible to miss an interrupt on change when doing anything (even a write) to the port as that could reset the change detector state just as it changes state. That is a very bad thing.

The good news is all the work-arounds actually do work. You can usually just force the interrupt with:
Rich (BB code):
    bsf      INTCON,RBIF   ;  'force' the IOC interrupt. (assembly version)
    INTCON.RBIF = 1;      // 'force' the IOC interrupt. (C version)
After a quick check I don't see that warning on higher lever cores (18F and up).
 

Markd77

Joined Sep 7, 2009
2,806
I think both methods can be made to work. I haven't really looked at the IOC code so you might have already dealt with the case where the port value changes in between the interrupt happening and the read of the port.
If it can happen it will happen.
 

THE_RB

Joined Feb 11, 2008
5,438
...
Finally, why would I post this technique? Mostly the OP asked me to do so. It is my understanding this board is to share ideas, maybe to learn something, maybe to teach something. I would hope to find a more hospitable atmosphere here in the future.
I owe you an apology ErnieM! Having re-read my words, they do indeed sound "inhospitable" and furthermore sound downright arrogant. I'm sorry. It was not my intention to be either of those things, it was a social clumsyness on my part where I typed in a hurry and was trying to express a genuine question of why you would use that particular pin testing procedure instead of one that was already posted and much smaller and (to me) cleaner. You have put in a lot of effort in to this thread providing long informative posts to help the OP and it was wrong of me to criticise your work in such an arrogant sounding manner.

Regarding the other issue of using pin-change interrupts, I still dislike using those for mechanical inputs like encoders. Edge glitches do occur, and can cause problems like triggering an interrupt while one is still happening or make 2 close interrupts for 1 glitched edge event. This requires additional checking and/or debounce control to prevent this situation.

I would much prefer to use a timer interrupt at an exact period, which guarantees automatic immunity to edge glitching. Furthermore because it is timed at an exact recurring frequency you can do additional processes like updating real time clocks or counting encoder edges against actual time (number of interrupts) to easily determine things like encoder RPM. I just think that is safer and provides more useful options then interrupt on pin-change.
 

ErnieM

Joined Apr 24, 2011
8,415
The RB: Apology completely accepted along with my thanks for your gentlemanly response.

Though we still seem to disagree on both methods being completely acceptable I will not discuss it here any further as we're seriously hijacking the OP's post and into the land of off topic-ville.

<semi serious comment meant to be funny>

Do feel free to start another post where we can have a no holds barred code war. Two methods enter! One method leaves!

</semi serious comment meant to be funny>

And a beautiful spring day to all!
 
Top