Demystifying Rotary Encoders (one more time) Part 1-2

Paulv

New Member
I’m new at PICAXE, and I will probably be accused of beating an already decaying horse, but, I’m hoping to bring some additional information and clarity to the table, ahem forum.

My current project (still in design) was getting a bit complex for only using discreet logic and I also wanted to see if I could reduce the cost somewhat. The easiest way to do that was with a micro controller. I have not used them for several decades so I quickly zeroed in on the PICAXE because of its really complete design environment and I also did not want to resuscitate my decades old memory of (by now archaic) C, or start to learn assembler for yet another chip again. (been there, done that, no more)

My new design is an instrument that needs two selection switches, one for a frequency range selection (25 selections in a 1-2-5 sequence) and another selection switch for a DC or pulse output amplifier with calibrated outputs (15 positions, also in a 1-2-5 sequence). Rotary switches for 25 positions are pretty rare and expensive, so I wanted to switch to Rotary Encoders. Both will feed a counter, and two separate 14M2 chips will process the data, drive the selection hardware and as a bonus also display the settings on one line each of an OLED display (AXE133Y) to save on the front panel engravings.

If you have not used a rotary encoder before, look here for more information :http://www.electro-labs.com/rotary-encoders-understanding-practical-implementation/

In any case, I needed some software for the Rotary Encoders, so I started to look at this forum, and also Google “around”.
There are many different solutions available, some simple, some overly complex and some that simply did not work reliably when I tried them. After quite some time, I zeroed in on one design I liked by forum member Christophe (vertigo) which worked really well for me.

Here is the post : http://www.picaxeforum.co.uk/showthread.php?13590-Rotary-encoder-usage

This is by no means the “one-and-only” solution. But, as I said, it took me a long time (way too long) to find one that works reliably. I found that some approaches were very theoretically based and some overly complex with complete state machines. Some were designed for maximum speed and therefore complex. Then others where tailored to other embedded processors, and they are different enough from the PICAXE family to avoid them as well.

In my application, I needed a reliable and moderately fast solution for my selection criteria. I have no need to “spin” the knob to quickly go from one selection to the next, or from one end of the scale to the other. So, I decided to document this particular solution with enough details to show how it works, and why.
The solution I picked out of the many is simple, very simple. It also lends itself to be used as part of a larger program, eliminating the need for a dedicated (08M2?) controller to just encode the switch.

So, with this post I’m trying to show why I selected this method and to try to explain it using a scope to show timing diagrams. With the aim to help others going through the same selection process. Once you understand the details, you can tweak the code or, decide to hunt for other solutions, based on what you learned.

So why is this solution simple? We’ll if you look at the datasheet, it is easy to get tempted into a design that will have an overly complex solution. This is not needed. The author of the code, Christophe, calls his solution “early detection” and it is indeed just that. Let me try to explain this in simple terms. The two inputs from the rotary encoder, A and B, are switches that feed pulses in an order (time wise) that are depending on the rotary direction. If you turn the knob of the encoder one indent (a click) to the right (clockwise), the A switch will close first, and a little while later, the B switch closes. If you turn one indent to the left (anti-clockwise), the B switch closes first, and the A switch follows a little later.

It is really as simple as that.

Now then, the timing between the closure of the switches is depending on the speed of rotation, so the faster you turn the knob, the quicker the closure of the switches follow each other, and therefore the duration of the pulses get smaller and smaller.

The following scope screenshot of a move from one indent to the next shows this.
Rot_Encoder-Sigs-1.JPG
There are two things I would like to point out here. First note that the signals are not always as symmetrical as the datasheet lets you believe. It depends on how “even” the speed of your turning is and also on the quality of the mechanical design of the encoder.

The other thing to note is the noise on the leading edge of both switches. A lot depends on the mechanical construction of the encoder, so you may want to check that. Some solutions I found use extra hardware to eliminate this switch bounce, but for most if not all applications, there really is no need for that. First of all, the pulses stay in the “high” area, don’t even go near ground, and the rest can be done in software.

The program from Christophe (vertigo) that we will discuss here uses an interrupt to start the decoding process, as soon as switch A changes position. Note that this is regardless of the direction of the rotation. I also elected to use a positive going transition, and I used the schematic below to document the design of the circuit around the encoder, but it is simple to go to an active low pulse.
Rotary Encoder Test Circuit.jpg
It cannot get much more simple. The resistor values are not very important, 10K is fine, and so is 4K7. Lower values will use more current, higher values could add more “bounce” noise.

Here is the code I used for this post. I modified it slightly for the 14M2, and added some more comments, but it follows the original code almost completely, although I “instrumented” it to track the timing on a scope. Because this is part of a much larger program, some of the structure and definitions are left in. Sorry.
Code:
; Rotary Encoder Test
;
#picaxe 14M2
#no_data
; 
; INPUT C.0 A input, C.1 B input of the Rotary Encoder
DIRSC = %00000000 ; 0=input, 1=output
;
; OUTPUT B.1 Used as an indicator for the scope
DIRSB = %00000010
;
; used variables
; bit vars
SYMBOL getBits  = b0 	; C.0=A and C.1=B of the rotary decoder
; byte vars
SYMBOL dir      = b4	; direction (left|right)
SYMBOL savepos 	= b5	; previous value of rotary position
SYMBOL fpos		= b6	; frequency counter & position
; 
; interrupt definition
setint %00000001, %00000001 ,C ;set interrupt on C.0 (rot enc A) going high

main: 
	
	GOSUB init

	DO
		; wait for an interrupt coming from the rotary decode

	LOOP ; forever

END

init:
	;
	OUTPINSC = %00000000
	; set all to low, the pulse for the scope will be positive
	OUTPINSB = %00000000
	;
	; PIC CPU Clock Settings
	SETFREQ M8 ; m1, m2, m4(default), m8, m16, m32
	PAUSE 3000	; only needed if not at M4
	;
return	


; Rotary Encoder
;
; http://www.picaxeforum.co.uk/archive/index.php/t-13590.html
;
interrupt:
; on entry, the interrupt is disabled, need to set it again at the end
;
        PAUSE 1 ; workaround for a bug in the microcode
	PULSOUT B.1, 2	; show the entry of the routine on the scope
	PINSC = getBits ;read the rotary encoder pins
	; move A and B inputs to b0
	bit1 = pinC.1	; B input
	bit0 = pinC.0 	; A inout

	getBits = getBits & %000000011 ;isolate the two rotary encoder pins
	
	if getBits <> 0 then ;if both pins are low, direction is undetermined : discard
		dir = bit1 * 2 ; dir(ection): determined on the B input
					   ; if bit2=low=0  then 2 x 0 = 0 -> dir=0=right(up)
					   ; if bit2=high=1 then 2 x 1 = 2 -> dir=2=left(down)
		fpos = fpos - 1 + dir ;change a "tracking" counter with -1 or +1
		; The following is a structure from my larger program, 
		; I left it in to keep some of the timing intact
		if dir = 0 then ; up/right
			PULSOUT B.1, 2 ; show this point in time on the scope
		else			; down/left
			PULSOUT B.1, 2 ; show this point in time on the scope
		endif
	
		;now wait for the encoder to go to the next indent position
		;to finish the full "click" cycle
		do while getBits <> 0 
			getBits = PINSC & %00000011 
		loop
	endif
	
	setint %00000001, %00000001 ,C ;restore the interrupt on C.0 going high
	
	PULSOUT B.1, 2 	; show this point in time on the scope,
					; as we are ready again for the next indent
return

I'm out of room and attachments, so I'll continue on the next post.
 
Last edited:
Demystifying Rotary Encoders (one more time) Part 2-2

So how does the program work? Simple, if the knob turns to the right, A goes high followed by B, meaning that when A goes high, B is still low. If the knob turns to the left, B goes high, A is still low.
So as soon as a rising edge is detected on switch A (input C.0), an interrupt service routine is called. Both switch positions get recorded and if A is high and B is still low, the direction is to the right, and if A is high and B is (still) high, the direction is to the left. After the direction has been determined, the code waits for the next activity of the switches to finish-up with the complete indent cycle, and is ready for the next.

I instrumented the code to follow the progress and the timing. Here’s what’s happening:
Rot_Enc_Test-1.JPG
The top trace (A) shows the activity of the A switch, and the bottom trace (B) shows the effect of the instrumentation of the code. So, as soon as A goes high, the interrupt gets triggered and we enter the interrupt service routine. Note that this works a little different on a PICAXE, because it’s not a real interrupt, but rather an “internal” polling action between instructions. The lag-time therefore is much larger then you would expect from other microcontrollers, but here we can actually use it to our advantage.

The first pulse on trace B happens when we enter the interrupt service routine, and due to the polling, it took about 1.3 mSec. As you can see, this is way beyond any switch bounce, so we will read a reliable high of switch A. The second pulse on trace B happens when the code actually figured out what direction the knob was turned, and the third pulse shows when we leave the interrupt service routine and are ready for the next interrupt.

Note that after the second pulse, the code is actually waiting in a loop for the complete cycle to finish.

The whole sequence from A going high and getting ready for the next takes about 7,2 mSec, and we have just over 2 mSec of free processing time before the next cycle starts. This is measured with a 14M2 running at 8MHz, and the knob turning pretty fast through several indents.

This is NOT my typical usage, but it does not hurt to see what’s really happening. In any case, I suggest that running the code at the default of 4MHz is too slow, even if you don’t yank on the encoder knob. If, in my case, I want to rather quickly go from one selection to one that is several indents away, you want a reliable process, and not skip positions. That’s not a good user interface.

Obviously, the faster you let the chip run, the more time your program will have to do the processing of the switch activity.

Here is a screenshot of the B switch activity into the complete timing sequence, just to finish up:
IMG_2922.JPG
The top trace (A) shows the B switch activity, and the rotation is again to the right (A then B). I can’t show the A switch, (I only have two channels to capture), but remember that the first pulse on the lower trace (B) happens when the code enters the interrupt service routine, and that is about 1.3mSec after switch A went high. You can see that the B input is solidly low at this point, so we can read it reliably.

The second pulse on the lower trace indicates when the direction has been determined.

And finally, the third pulse shows when we leave the interrupt service routine. The time between pulse 2 and 3 is the result of the software loop polling for the end of the indent cycle. The time spent here is depending on two elements. First the rotational speed of the encoder, and second, the execution speed of the code in that final loop.
That final loop just waits for any activity on A or B, so this piece of code is independent of the rotational direction. Remember that after we decided the rotational direction, both A and B inputs are high. The cycle ends when either A or B goes low. When you turn right, you have A followed by B, so a falling B ends the cycle, or when you turn left, B is followed by A, and a falling A finishes the cycle.

In this particular case, the period between pulse 2 and 3 lasts about 2.5 mSec, and this is mostly determined by the code reading the A and B inputs (C.0 and C.1), then deciding to end the loop if one went low and then enabling the interrupt on the A input (C.0) again.

Again a faster chip clock speed will shorten this cycle.

In the end, selecting the clock speed all depends on your particular application and your desire to spin the knob quickly while tracking it precisely.

With this, I hope I have been able to help you demystifying rotary encoders by clarifying some things in more detail and by showing the code and hardware in action to make your own selection a bit easier and faster than my own.

Enjoy!
 
Dear @Paulv,

Thank you for the impressive research and writeup.

I have successfully used the cheap type of encoder with the code below. The pause 1000 is switch debounce and will have to be reduced if you are running below 32MHz and maybe increased for 64MHz. ...BtnPin variables are three pins connected to the encoder and ...BtnPress are sub-procedures to execute depending on what was done with the knob. This and the heaps of code around it to run the display are sitting in slot 1. In slot0, where the actual program runs, the display would say something like "press the knob to enter setup" and then it lands here. While sometimes you cannot avoid the complexity of interrupts (or more specifically getting out of one), this approach is suitable for many projects.

Code:
main:
  do
    if SelBtnPin = 1 then : pause 1000 : gosub SelBtnPress : endif
  loop until UpBtnPin = 1 or DnBtnPin = 1          'wait for something to happen
  if UpBtnPin = 1 then                                         'in case the something was channel A ...
    do : loop until DnBtnPin = 1                           'wait for channel B to go high as well
    do : loop while DnBtnPin = 1                          'now wait for it go back low again
    gosub UpBtnPress
   elseif DnBtnPin = 1 then
    do : loop until UpBtnPin = 1
    do : loop while UpBtnPin = 1
    gosub DnBtnPress
  endif
goto main

Edmunds
 
Hi Paul,

A belated welcome to the forum and thanks for that informative and worthwhile addition to the knowledge base on rotary encoders. They're not something that I've used myself, but I've bookmarked the thread for future use! Also worthwhile copying to the "Finished Projects : Code Snippets" section of the forum, or perhaps hippy will do it for you. ;)

Less than 70 bytes of code when the "test" pulses are removed, but if/when you do that, beware of a potential "issue" with some M2 devices - see the "Revision History" tab on this page :

ISSUE - VERY FIRST LINE OF INTERRUPT: ROUTINE SHOULD NOT USE VARIABLES (LET, INC, DEC, IF VAR = etc.) A 'inc' or 'dec' or certain expressions may not work correctly if it is used as the very first line in the 'interrupt:' subprocedure. To workaround this issue add another command, such as 'pause 1', before the let/if command is used within 'interrupt:' - See more at: http://www.picaxe.com/Hardware/PICAXE-Chips/PICAXE-14M2-microcontroller/#sthash.AU0Pjw47.dpuf

It's not an issue I've directly observed myself, but I was unaware of it when I did have some unresolved problems with an interrupt routine. :(

Cheers, Alan.
 
Here is another way of doing it: read the inputs at some minimum frequency (enough to catch the fastest move you want to track), and use current and last state to see what - if anything - happened.
Can be quickly decided with a on-goto

(I like to have many routines that executes quickly in some kind of frequent loop for concurrent multitasking, and this suits such designs)

http://www.picaxeforum.co.uk/showth...Encoder-Tester&p=263863&viewfull=1#post263863
also read the two posts after it.
 
Last edited:
Top