Actions

Reading Rotary Encoders

From Just in Time

Many rotary encoders work in quadrature mode. This means that their signals will look somewhat like what is shown on the right

typical quadrature signal

. Note that the ones you turn by hand normally have one noticable 'click' for every full cycle (phases 1-4 in the image). Whether the pins are low-active, or high-active and which pin is which is normally not important, though it does influence which direction is considered 'up' and which is 'down'.

AVR code

The following code runs in a timer interrupt. Most timer frequencies will do, as long as it's fast enough to see all states in the interrupt. Of course, you can also use a pin change interrupt if you want to be sure. The code assumes a read function that reads the state of the two encoder output pins and puts them in the bottom two bits of the return value. Any two adjacent bits will do if the constant 0x02 is adapted. Do take care that timer.state in this example counts up or down for every state change, which means that the counter is increased four times for every click.

/// Timer interrupt
ISR( TIMER1_COMPA_vect)
{
	// previously seen value of encoder bits.
	static uint8_t previous = 0;

	// get the encoder state 
	uint8_t current = read(encoder); // this reads the A and B line values in the lower 2 bits.
	if (current != previous)
	{
		// there's a change, decide whether we count up or down.
		if ((previous ^ (current << 1)) & 0x02)
		{
			--timer_state.value;
		}
		else
		{
			++timer_state.value;
		}
		timer_state.value_changed = 1;

		//timer_state.value = current;
		previous = current;
	}

}

The code works by comparing line A at time t with line B at time t-1. Looking at the drawing above, you can see that when we move to the right in this diagram, the state of line A seems to follow that of line B, which means that A(t) == B(t-1) if the encoder moves to the right. Conversely, if the encoder moves to the left A(t) will always be the opposite of B(t-1). An exclusive OR of A(t) and B(t-1) is used to compare the two values.

SX code

The following SX28 assembly contains two virtual pheripherals:

  • Reading a rotary encoder
  • driving 4 7-segment led displays

Reading the encoder starts at label ReadEncoder.

4x7segments 003.jpg

The main program just loops and does nothing. The first vp communicates directly with the second. This code is an adapted version (made into a vp) of sources found on sxlist.

;=======================================================================
 
 ;TITLE:         4x7segments.src
 ;
 ;PURPOSE:       Read pulses from a rotary encoder, adapt a counter
 ;			and output to 4 7-segment led displays
 ;
 ;AUTHOR:        Danny Havenith
 ;
 ;REVISIONS:
 ;  <mm/dd/yy> - <details of revision>
 ;               <more details of same revision>
 ;
 ;CONNECTIONS:
 ;	ra.0 and ra.1: encoder inputs
 ;	rc: led segment outputs
 ;	rb.0-4: led column outputs
 ;
 ;=======================================================================
 
 
 ;-------------------------- DEVICE DIRECTIVES --------------------------
 
 		DEVICE		SX28,OSC4MHZ,TURBO
 		DEVICE		STACKX, OPTIONX
 		IRC_CAL		IRC_SLOW
 
 		RESET		Initialize
 
 ;------------------------------ CONSTANTS ------------------------------
 
 TicksPerMs	EQU	20	; interrupts per ms
 CyclesPerTick	EQU	200	; cycles per interrupt
 
 
 ------------------------------ VARIABLES ------------------------------
 			ORG	$10
 
 BankLeds	= $
 
 MSTimer		DS	1
 CurrentDigit	DS	1
 CurrentColumn	DS	1
 Digits		DS	4
 
 CounterA	DS	1
 CounterB	DS	1
 CounterC	DS	1
 
 BankEncoder	= $
 EncoderState	DS 	1
 temp		DS	1
 
 
 ColumnPort	EQU 	rb
 RowPort		EQU	rc
 
 EncoderPort	EQU	ra 	; port for the rotary encoder
 EncoderMask	EQU	$03	; which bits to use.
 
 ;---------------------------- DEBUG SETTINGS ---------------------------
 
 		FREQ	4_000_000
 	
 ;		WATCH	<Symbol>,<bit count>,<format>
 
 WKED_W		equ	$0A		;Write MIWU/RB Interrupt edge setup, 0 = falling, 1 = rising
 WKEN_W		equ	$0B		;Write MIWU/RB Interrupt edge setup, 0 = enabled, 1 = disabled
 ST_W		equ	$0C		;Write Port Schmitt Trigger setup, 0 = enabled, 1 = disabled
 LVL_W		equ	$0D		;Write Port Schmitt Trigger setup, 0 = enabled, 1 = disabled
 PLP_W		equ	$0E		;Write Port Schmitt Trigger setup, 0 = enabled, 1 = disabled
 DDIR_W		equ	$0F		;Write Port Direction
 
 RA_latch	equ	%00000000		;SX18/20/28/48/52 port A latch init
 RA_DDIR		equ	%11111111		;see under pin definitions for port A DDIR value
 RA_LVL		equ	%00000000		;SX18/20/28/48/52 port A LVL value
 RA_PLP		equ	%11111111		;SX18/20/28/48/52 port A PLP value
 
 RB_latch	equ	%00000000		;SX18/20/28/48/52 port B latch init
 RB_DDIR		equ	%11110000		;SX18/20/28/48/52 port B DDIR value
 RB_ST		equ	%11111111		;SX18/20/28/48/52 port B ST value
 RB_LVL		equ	%00000000		;SX18/20/28/48/52 port B LVL value
 RB_PLP		equ	%11111111		;SX18/20/28/48/52 port B PLP value
 
 RC_latch	equ	%00000000		;SX18/20/28/48/52 port C latch init
 RC_DDIR		equ	%00000000		;SX18/20/28/48/52 port C DDIR value
 RC_ST		equ	%11111111		;SX18/20/28/48/52 port C ST value
 RC_LVL		equ	%00000000		;SX18/20/28/48/52 port C LVL value
 RC_PLP		equ	%11111111		;SX18/20/28/48/52 port C PLP value
 
 ;-------------------------- INTERRUPT ROUTINE --------------------------
 		ORG	$0
 		
 ;	break Interrupt
 Interrupt
 ; timer
 	bank BankLeds
 	mov w, #TicksPerMs
 	dec MSTimer
 	snz
 	mov MSTimer, w
 	sz
 	jmp EndInterrupt
 
 	mov w, #Digits
 	add w, CurrentDigit
 	mov fsr, w
 	mov w, IND
 	call Decode
 	clr ColumnPort
 	mov RowPort, w
 	mov ColumnPort, CurrentColumn
 
 	; next column
 	clc
 	rr CurrentColumn	; rotate column left
 	mov w, #%00001000
 	inc CurrentDigit		; increase digit counter
 	snc 		; reset if digit = 4
 	mov CurrentColumn, w
 	snc 
 	clr CurrentDigit
 
 :EndLeds
 
 	break ReadEncoder
 ReadEncoder
         mov w, EncoderPort           ;get change between current and previous 
                             ;encoder state in w
         xor w, EncoderState    ;
         
         xor EncoderState, w    ;update state and preserve difference in w
 
         and w, #EncoderMask         ;check if there is change
         snz
          jmp :EndEncoder       ;no change, read encoder again
 
         ;if both bits changed, this will be an error, but we ignore it here
         ;xor w, #$03
         ;skpnz
         ; jmp enc_error
 
         ;calculate direction in temp.1
         mov temp, w       
         mov w, <<EncoderState
         xor w, EncoderState
         xor temp, w
 
         sb temp.1
          call EncoderIncrease       ;Encoder moved up
         snb temp.1
          call EncoderDecrease       ;Encoder moved down
 
 :EndEncoder
 
 EndInterrupt
 	mov w, #-CyclesPerTick		
 	retiw
 
 EncoderIncrease
 	inc	Digits + 3
 	cjne	Digits + 3, #10, :EndInc
 	clr 	Digits + 3
 
 	inc	Digits + 2
 	cjne	Digits + 2, #10, :EndInc
 	clr 	Digits + 2
 
 	inc	Digits + 1
 	cjne	Digits + 1, #10, :EndInc
 	clr 	Digits + 1
 
 	inc	Digits 
 	cjne	Digits, #10, :EndInc
 	clr 	Digits
 :EndInc
 	retp
 
 EncoderDecrease
 	dec	Digits + 3
 	cjne	Digits + 3, #$FF, :EndDec
 	mov	Digits + 3, #09
 
 	dec	Digits + 2
 	cjne	Digits + 2, #$FF, :EndDec
 	mov	Digits + 2, #09
 
 	dec	Digits + 1
 	cjne	Digits + 1, #$FF, :EndDec
 	mov	Digits + 1, #09
 
 	dec	Digits
 	cjne	Digits, #$FF, :EndDec
 	mov	Digits, #09
 :EndDec		
 	retp
 
 Decode
 	and w, #$0F
 	jmp PC+w
 	retw %00000101 ;0
 	retw %11011101 ;1
 	retw %10000110 ;2
 	retw %10010100 ;3
 	retw %01011100 ;4
 	retw %00110100 ;5
 	retw %00100100 ;6
 	retw %10011101 ;7
 	retw %00000100 ;8
 	retw %00010100 ;9
 	retw %00001100 ;A
 	retw %01100100 ;b
 	retw %00100111 ;C
 	retw %11000100 ;d
 	retw %00100110 ;E
 	retw %00101110 ;F
 
 ;------------------------ INITIALIZATION ROUTINE -----------------------
 
 Initialize
 		; Configure all ports
 		mov 	m, #ST_W			;point MODE to write ST register
 		mov     w,#RB_ST            	;Setup RB Schmitt Trigger, 0 = enabled, 1 = disabled
 		mov	!rb,w		
 		mov     w,#RC_ST            	;Setup RC Schmitt Trigger, 0 = enabled, 1 = disabled
 		mov	!rc,w	
 		mov 	m, LVL_W			;point MODE to write LVL register
 		mov     w,#RA_LVL            	;Setup RA CMOS or TTL levels, 0 = TTL, 1 = CMOS
 		mov	!ra,w		 
 		mov     w,#RB_LVL            	;Setup RB CMOS or TTL levels, 0 = TTL, 1 = CMOS
 		mov	!rb,w		
 		mov     w,#RC_LVL            	;Setup RC CMOS or TTL levels, 0 = TTL, 1 = CMOS
 		mov	!rc,w	
 
 		mov 	w,#RA_PLP            	;Setup RA Weak Pull-up, 0 = enabled, 1 = disabled
 		mov	!ra,w		 
 		mov     w,#RB_PLP            	;Setup RB Weak Pull-up, 0 = enabled, 1 = disabled
 		mov	!rb,w		
 		mov     w,#RC_PLP            	;Setup RC Weak Pull-up, 0 = enabled, 1 = disabled
 		mov	!rc,w	
 
 		mov 	m, #DDIR_W			;point MODE to write DDIR register
 		mov	w,#RA_DDIR		;Setup RA Direction register, 0 = output, 1 = input		
 		mov	!ra,w	
 		mov	w,#RB_DDIR		;Setup RB Direction register, 0 = output, 1 = input
 		mov	!rb,w			
 		mov	w,#RC_DDIR		;Setup RC Direction register, 0 = output, 1 = input
 		mov	!rc,w			
 
 		mov     w,#RA_latch          	;Initialize RA data latch
 		mov     ra,w		
 		mov     w,#RB_latch         	;Initialize RB data latch
 		mov     rb,w		
 		mov     w,#RC_latch          	;Initialize RC data latch
 		mov     rc,w		
 
 ; zero all ram (SX28)
 		clr	fsr			;reset all ram banks
 :zero_ram	sb	fsr.4			;are we on low half of bank?
 		setb	fsr.3			;If so, don't touch regs 0-7
 		clr	ind			;clear using indirect addressing
 		incsz	fsr			;repeat until done
 		jmp	:zero_ram
 
 :init_leds	bank BankLeds
 		mov CurrentDigit, #0
 		mov CurrentColumn, #1
 
 ;---------------------------- MAIN PROGRAM -----------------------------
 
 Main
 
 	mov Digits, #1
 	mov Digits + 1, #2
 	mov Digits + 2, #3
 	mov Digits + 3, #4
 	mov	!option, #%10011111	;enable rtcc interrupt
 
 :loop	
  	jmp :loop
 
 
 
 
 	
 ;----------------------------- SUBROUTINES -----------------------------
 
 ;<GlobalLabel>
 ;<detailed description of routine>
 
 ;		<inst>	<op1>,<op2>		;<time>	<comment>