Rotary Encoder in ASM

Go To Last Post
22 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Well after 1 week at this, I have been unable to make my rotary encoder software work 100% bug free. I can make the direction output an flash 2 LEDs at the 10 Ms rate, but there are always some degree of falses on the other channel. I tried the interrupt on A ch and read B for direction, but no good as well. I've also looked here and did not find any examples in ASM only C. C is like reading Martian to me. Sure I could eventually understand, but time restraints preclude me from doing so. Any help on my enclosed software would be appreciated. Thanks. Partial hardware schematic also.

All delay loops are standard for me to select from, depending on final clock. I.E. 128 KHz or 8 MHz The final code of course does not include the unused ones.

Mute and GANG: it just a simple toggle of 2 pins to LEDs. Ignore them.



.include	"tn2313def.inc" 

.CSEG

.EQU	ENCA	=	1
.EQU	ENCB	=	0
.EQU	DN_LED	=	1
.EQU	UP_LED	= 	0
.EQU	TYPE	=	4
.equ	MuteIn	=	0
.equ	MuteOut	=	2

;INTERRUPT VECTORS:

.org 	$0000

  	rjmp 	RESET

.org 	$0001		; INT0 address

  	rjmp 	RESET	; INT0 reset vector

.org 	$0002		; INT1 address

  	rjmp 	TURN	; INT1 reset vector

.org	$0003		; INT2 address

	rjmp    RESET	; INT2 reset vector

.org	$0004		; INT3 address

	rjmp    RESET	; INT3 reset vector

.org	$0005		; INT4 address

	rjmp    RESET	; INT4 reset vector

.org	$0006		; INT5 address

	rjmp    RESET	; INT5 reset vector

.org	$0007		; INT6 address

	rjmp    RESET	; INT6 reset vector

.org	$0008		; INT7 address

	rjmp    RESET	; INT7 reset vector

.org	$0009		; INT8 address

	rjmp    RESET	; INT8 reset vector

.org	$000A		; INT9 address

	rjmp    RESET	; INT9 reset vector

.org	$000B		; INTA address

	rjmp    RESET	; INTA reset vector

.org	$000C		; INTB address

	rjmp    RESET	; INTB reset vector

.org	$000D		; INTC address

	rjmp    RESET	; INTC reset vector

.org	$000E		; INTD address

	rjmp    RESET	; INTD reset vector

.org	$000F		; INTE address

	rjmp    RESET	; INTE reset vector

.org	$0010		; INTF address

	rjmp    RESET	; INTF reset vector

.org	$0011		; INT10 address

	rjmp    RESET	; INT10 reset vector

.org	$0012		; INT11 address

	rjmp    RESET	; INT11 reset vector


RESET:

  	ldi  	R20,	LOW(RAMEND)  	
  	out  	SPL,	R20

  	ser  	R20			       		
  	out  	DDRB,	R20	; Set PORTB As Output

  	out  	PORTB,	R20	; Make All Outputs High
	out	PORTD,	R20	; All Pullups On

  	ldi  	R20,	$80	   
  	out  	GIMSK,	R20	; INT1 Enable (D3)

  	ldi  	R20,	$08
  	out  	MCUCR,	R20	; ISC01 Enable (The ;rising edge of INT1 generates an interrupt)


OFF:

	sbi  	PORTB,	DN_LED  	
	sbi  	PORTB,	UP_LED	
	sei

BOOTFLASHER:						; Start up LED flash (3 times)

	cbi		PORTB,	2				; 
	rcall	DELAY100_8
	sbi		PORTB,	2				; 
	rcall	DELAY100_8
	cbi		PORTB,	2				; 
	rcall	DELAY100_8
	sbi		PORTB,	2				; 
	rcall	DELAY100_8
	cbi		PORTB,	2				; 
	rcall	DELAY100_8
	sbi		PORTB,	2				; 

;===================================

Mute:

	sei
	in	R21,	PIND
	rcall	DELAY100_8	; 100Msec delay
	sbrc	R21,	0
	rjmp	Gang
	in	R22,	PORTB
	nop
	nop
	andi	R22,	0b00000100
	tst	R22
	brne	OnMute
	nop
	breq	OffMute

OffMute:

	sbi	PORTB,	2
	rcall	DELAY100_8	; 100Msec Delay
	rjmp	Gang

OnMute:		

	cbi	PORTB,	2
	rcall	DELAY100_8	; 100Msec Delay
	rjmp	Gang

Gang:

;	sei
	in	R21,	PIND
	rcall	DELAY100_8	; 100Msec delay
	sbrc	R21,	1
	rjmp	Mute
	in	R22,	PORTB
	nop
	nop
	andi	R22,	0b00001000
	tst		R22
	brne	OnGang
	nop
	breq	OffGang

OffGang:

	sbi	PORTB,	3
	rcall	DELAY100_8	; 100Msec Delay
	rjmp	Mute

OnGang:		

	cbi	PORTB,	3
	rcall	DELAY100_8	; 100Msec Delay
	rjmp	Mute


;===================================


TURN:

  	cli							; Stop all interrupts
	nop
	nop
	sbi		PORTB,	2			; Un-mute
	in		R20,	PIND	; Check B channel status
	nop
	nop
	nop
	rcall	DELAY20_8	; 20Msec delay
	andi	R20,	$04	; Knock off everything else but B channel
	cpi	R20,	$04	; Compare B channel, is it high?
	breq	UPOUT		; Then jump to Up LED pulse
	brne	DNOUT		; If not, then jump to Down LED pulse
	nop
;	ser	R20
;	out	EIFR,	R20

UPOUT:
	
	nop
	cbi  	PORTB,	UP_LED	; Turn on LED	PB0
	rcall	DELAY50_8	; 50Msec before changing state
	sbi  	PORTB,	UP_LED	; Turn off LED	PB0
	nop
;	ser	R20
;	out	EIFR,	R20
	ser	R20
	out	SPL,    R20 
	rjmp	Mute
	reti

;===================================

DNOUT:

	nop
	cbi  	PORTB,	DN_LED	; Turn on LED	PB1
	rcall	DELAY50_8	; Wait a 50th of a second before changing state
	sbi  	PORTB,	DN_LED	; Turn off LED	PB1
	nop
;	ser	R20
;	out	EIFR,	R20	
	ser	R20
	out	SPL,	R20
	rjmp	Mute
	reti

;===================================

DELAY250_8:
	
;	ret

;   2000000 cycles:

    ldi  	R17, 	$12

ALOOP0:

  	ldi  	R18, 	$BC

ALOOP1:

  	ldi  	R19, 	$C4

ALOOP2:

  	dec  	R19
    brne 	ALOOP2
    dec  	R18
    brne 	ALOOP1
    dec  	R17
    brne 	ALOOP0
    nop
    nop

	ret

; ============================= 

DELAY100_8:

;	ret

;   800000 cycles:

    ldi  	R17, 	$5F

BLOOP0:  

	ldi  	R18, 	$17

BLOOP1:

  	ldi  	R19, 	$79

BLOOP2:

  	dec  	R19
    brne 	BLOOP2
    dec  	R18
    brne 	BLOOP1
    dec  	R17
    brne 	BLOOP0
    ldi  	R17, 	$01

BLOOP3:  

	dec  	R17
    brne 	BLOOP3
    nop
    nop

	ret

; ============================= 

DELAY50_8:

;	ret

;  	400000 cycles:

    ldi  	R17, 	$97
CLOOP0:

  	ldi  	R18, 	$06

CLOOP1:

  	ldi  	R19, 	$92

CLOOP2:

  	dec  	R19
    brne 	CLOOP2
    dec  	R18
    brne 	CLOOP1
    dec  	R17
    brne 	CLOOP0
    nop

	ret

; ============================= 

DELAY20_8:

;	ret

;   159999 cycles:

   	ldi  	R17, 	$E1

DLOOP0: 

 	ldi  	R18, 	$EC

DLOOP1:

  	dec  	R18
    brne 	DLOOP1
    dec  	R17
    brne 	DLOOP0
    ldi  	R17, 	$08

DLOOP2:

  	dec  	R17
    brne 	DLOOP2

	ret

; ============================= 


DELAY10_8:

;	ret

;   80000 cycles:

	ldi  	R17, 	$86

ELOOP0:

  	ldi  	R18, 	$C6

ELOOP1:

  	dec  	R18
    brne 	ELOOP1
    dec  	R17
    brne 	ELOOP0
    nop
   	nop

	ret

; ============================= 

DELAY500_128:

;	ret

;   64000 cycles:

    ldi  	R17, 	$5A

FLOOP0:

  	ldi  	R18, 	$EC

FLOOP1:

  	dec  	R18
    brne 	FLOOP1
    dec  	R17
    brne 	GLOOP0
    ldi  	R17, 	$03

FLOOP2:

  	dec  	R17
    brne 	FLOOP2
    nop
	ret

; ============================= 

DELAY250_128:

;	ret

;   32000 cycles:

	ldi  	R17, 	$2D

GLOOP0:

  	ldi  	R18, 	$EC

GLOOP1:

  	dec  	R18
  	brne 	FLOOP1
  	dec  	R17
  	brne 	GLOOP0
  	ldi  	R17, 	$01

GLOOP2:

  	dec  	R17
  	brne 	GLOOP2
   	nop
   	nop
	ret

; ============================= 

DELAY100_128:

;	ret

;   12800 cycles:

	ldi  	R17, 	$12

HLOOP0:  

	ldi  	R18, 	$EC

HLOOP1:

  	dec  	R18
    brne 	HLOOP1
    dec  	R17
    brne 	HLOOP0
    nop
    nop

	ret

; ============================= 

DELAY50_128:

;	ret

;   6400 cycles:

    ldi  	R17, 	$09

ILOOP0:

  	ldi  	R18, 	$EC

ILOOP1:  

	dec  	R18
    brne 	ILOOP1
    dec  	R17
    brne 	ILOOP0
   	nop
	ret

; =============================

DELAY20_128:

;	ret

;   2560 cycles:

	ldi  R17, 	$04

JLOOP0:

  	ldi  R18, 	$D4

JLOOP1:

  	dec  R18
    brne JLOOP1
    dec  R17
    brne iLOOP0
    ldi  R17, 	$01

JLOOP2:

  	dec  R17
    brne JLOOP2
    nop
	ret

; ============================= 

DELAY10_128:

;	ret

;   1280 cycles:

    ldi  	R17, 	$02

KLOOP0:

  	ldi  	R18, 	$D4

KLOOP1:

  	dec  	R18
    brne 	KLOOP1
    dec  	R17
    brne 	KLOOP0
    nop
    nop

	ret

; ============================= 

[/img]

Attachment(s): 

Last Edited: Tue. Dec 2, 2008 - 04:39 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

That's a very complicated way to do it!

Here is some PIC code using a simple state machine that works:

;
; QUAD State
;
; A quadrature encoder traverse a couple of states
; when it is rotating these are:
;       00      |  Counter
;       10      |  Clockwise
;       11      |     ^
;       01      V     |
;       00  Clockwise |
;
;
QUAD_STATE:
    BCF     STATUS,C        ; Force Carry to be zero
    MOVF    PORTB,W         ; Read the encoder
    ANDLW   0x03            ; And it with 0011
    MOVWF   Q_1             ; Store it
   	IORWF   Q_1,W           ; Or in the current value
    MOVWF   QUAD_ACT        ; Store at as next action
    MOVF    Q_1,W           ; Get last time
    MOVWF   Q_NOW           ; And store it.
    ;
    ; Computed jump based on Quadrature pin state.
    ;
    MOVLW   high QUAD_STATE
    MOVWF   PCLATH
    MOVF    QUAD_ACT,W      ; Get button state
    ADDWF   PCL,F           ; Indirect jump
    RETURN                  ; 00 -> 00
    GOTO    DEC_COUNT       ; 00 -> 01 -1
    GOTO    INC_COUNT       ; 00 -> 10 +1
    RETURN                  ; 00 -> 11
    GOTO    INC_COUNT       ; 01 -> 00 +1
    RETURN                  ; 01 -> 01
    RETURN                  ; 01 -> 10 
    GOTO    DEC_COUNT       ; 01 -> 11 -1
    GOTO    DEC_COUNT       ; 10 -> 00 -1
    RETURN                  ; 10 -> 01
    RETURN                  ; 10 -> 10
    GOTO    INC_COUNT       ; 10 -> 11 +1
    RETURN                  ; 11 -> 00
    GOTO    INC_COUNT       ; 11 -> 01 +1
    GOTO    DEC_COUNT       ; 11 -> 10 -1
    RETURN                  ; 11 -> 11
INC_COUNT:
    INCF    COUNT,F
    MOVLW   D'201'
    SUBWF   COUNT,W
    BTFSS   STATUS,Z
    RETURN
    DECF    COUNT,F
    RETURN
DEC_COUNT
    DECF    COUNT,F
    MOVLW   H'FF'
    SUBWF   COUNT,W
    BTFSS   STATUS,Z
    RETURN          
    INCF    COUNT,F
    RETURN    

It should be quite easy to convert it into AVR assembly language.

Leon

Leon Heller G1HSM

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

leon_heller wrote:

It should be quite easy to convert it into AVR assembly language.

Instead learning the complicated PIC, its many times easier to convert C to assembler.

Following the generated assembler listing:

ISR( TIMER1_COMPA_vect )			// 1ms for manual movement
{
  a0:	1f 92       	push	r1
  a2:	0f 92       	push	r0
  a4:	0f b6       	in	r0, 0x3f	; 63
  a6:	0f 92       	push	r0
  a8:	11 24       	eor	r1, r1
  aa:	2f 93       	push	r18
  ac:	8f 93       	push	r24
  ae:	9f 93       	push	r25
  static int8_t last;
  int8_t new, diff;

  new = 0;
  if( PHASE_A )
  b0:	b0 9b       	sbis	0x16, 0	; 22
  b2:	02 c0       	rjmp	.+4      	; 0xb8 <__vector_3+0x18>
  b4:	23 e0       	ldi	r18, 0x03	; 3
  b6:	01 c0       	rjmp	.+2      	; 0xba <__vector_3+0x1a>
  b8:	20 e0       	ldi	r18, 0x00	; 0
    new = 3;
  if( PHASE_B )
  ba:	b1 9b       	sbis	0x16, 1	; 22
  bc:	02 c0       	rjmp	.+4      	; 0xc2 <__vector_3+0x22>
    new ^= 1;					// convert gray to binary
  be:	81 e0       	ldi	r24, 0x01	; 1
  c0:	28 27       	eor	r18, r24
  diff = last - new;				// difference last - new
  c2:	90 91 86 00 	lds	r25, 0x0086
  c6:	92 1b       	sub	r25, r18
  if( diff & 1 ){				// bit 0 = value (1)
  c8:	90 ff       	sbrs	r25, 0
  ca:	09 c0       	rjmp	.+18     	; 0xde <__vector_3+0x3e>
    last = new;					// store new as next last
  cc:	20 93 86 00 	sts	0x0086, r18
    enc_delta += (diff & 2) - 1;		// bit 1 = direction (+/-)
  d0:	80 91 9e 00 	lds	r24, 0x009E
  d4:	81 50       	subi	r24, 0x01	; 1
  d6:	92 70       	andi	r25, 0x02	; 2
  d8:	89 0f       	add	r24, r25
  da:	80 93 9e 00 	sts	0x009E, r24
  }
}
  de:	9f 91       	pop	r25
  e0:	8f 91       	pop	r24
  e2:	2f 91       	pop	r18
  e4:	0f 90       	pop	r0
  e6:	0f be       	out	0x3f, r0	; 63
  e8:	0f 90       	pop	r0
  ea:	1f 90       	pop	r1
  ec:	18 95       	reti

It must be called from a timer interrupt, e.g. every 1ms.
The variable "enc_delta" (SRAM address 0x009E) contain the counts.

sbis 0x16, 0
...
sbis 0x16, 1

checks the two pins, where the encoder was connected.

Peter

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Unfortunatly, both examples are of no help to me. Translating from PIC asm to AVR asm, leaves me confused. The second example is a disassembled C version which I have no idea where a rjmp .+4 goes without any reference.

The mute & gang functions listed in my code are working just fine. Just simple button-output toggling.

Now, if you could express yourself a little more clearer? And thanks for replying!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Sparky85 wrote:

The second example is a disassembled C version which I have no idea where a rjmp .+4 goes without any reference.

Seems, you need new eyeglasses, the destination address was named after the comment sign ; (in bytes).

Nevertheless its a fully valid syntax to give the destination address as displacement from the current line ("rjmp pc+2" on the Atmel AVR assembler).

You must only watch, the GCC assembler count all addresses in bytes, but the Atmel assembler in words.

Peter

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Sparky85 wrote:
Unfortunatly, both examples are of no help to me. Translating from PIC asm to AVR asm, leaves me confused. The second example is a disassembled C version which I have no idea where a rjmp .+4 goes without any reference.

The mute & gang functions listed in my code are working just fine. Just simple button-output toggling.

Now, if you could express yourself a little more clearer? And thanks for replying!


Here's an excerpt from some firmware I wrote to read a quadrature encoder. It's written to expect the encoder to be wired to bits 0 and 1 of PortB, but it could be adapted.
; Knobservice.inc - Example code for tracking the movements of a rotary knob
;
;
;
; Here are two RAM variables to support polling a quadrature-encoded
; rotary knob.  One holds the "state" of the knob-reading machine;
;               the other holds a model of the knob's angular position

                .DSEG
KnobState:      .BYTE 1
KnobPosition:   .BYTE 1

                .CSEG

        rjmp KnobInit


;===========================================
; Knob scanning state machine lookup table |
;===========================================
;
; The table format is:
;  bit4        indicates error (old & new states not adjacent)
;  bits 3,2 :  Contain shifted copy of "current state" to be loaded into "old"
;  bits 1,0 :  Contain 2-bit "delta" increment+1
;
KnobTable:
        .db 0b00001, 0b00101
        .db 0b01000, 0b11101
        .db 0b00001, 0b00101
        .db 0b11001, 0b01101
        .db 0b00010, 0b10101
        .db 0b01001, 0b01101
        .db 0b10001, 0b00101
        .db 0b01001, 0b01101



sampleKnob:
        lds ZH, KnobState  ; Read "last table value"
        andi ZH, 0xC      ; Keep only the "old knob bits"
        in ZL, PINB       ; Read the bits from the rotary encoder
        andi ZL, 3        ; Discard all but the two bits that we want
        or ZH, ZL         ; Combine "new" bits with old

        ; Now form a pointer into the correct byte of our lookup table:
        ;
        ldi ZL, low(2 * KnobTable)
        add ZL, ZH
        ldi ZH, high(KnobTable)
        adc ZH, ZH

        ; And then read the "next state" and "counts" from table
        ;
        lpm ZL, Z

        ; At this point, bits 0 and 1 of ZL contain a value that is one greater
        ; than the number of clicks that should be added to the knob's current
        ; "position", and bits 2 and 3 contain a copy of the values
        ; just read from the knob.
        ;
        sts KnobState, ZL    ; Update the current knob position and status
        andi ZL, 3          ; Keep only the delta bits
        dec ZL              ; Shift delta bits into  range from -1 to 1
        lds ZH, KnobPosition
        add ZH, ZL          ; And add detected movement to the modeled knob
        sts KnobPosition, ZH
        ret
        

        ; ----------------
        ; INITIALIZATION =
        ; ----------------

KnobInit:
        in r16, PORTB
        ori r16, 3
        out PORTB, r16        ; Turn on the pullups on Port B pins 0&1


; Initialize our state machine's "current state" bits:
;
        in r16, PINB        ; What state is the knob in now?
        andi r16, 3         ; We're only interested in bits 0 and 1
        lsl r16             ; But we want to store them in bits 2 and 3
        lsl r16             ;  of our official "state" variable:
        sts KnobState, r16

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Levenkay wrote:

Here's an excerpt from some firmware I wrote

Thanks for a clear reply. I don't do PIC. Have tried it, and woke up from that demented architecture and discoverd AVR. C I said is alien to me for now, and will be since I am now starting on the second half of a century on earth.

I see what your trying to do, however the code jumps to reset when I try to execute it and reaches the part of "store" sts KnobState, r16. I took great liberty in changing B to D. Any help is appreciated. Thanks.

.include	"tn2313def.inc"

.DSEG

KnobState:  

.BYTE 1

KnobPosition:

.BYTE 1

.CSEG

.org 	$0000

  	rjmp 	RESET

.org 	$0001							; INT0 address

  	rjmp 	RESET						; INT0 reset vector

.org 	$0002							; INT1 address

  	rjmp 	RESET						; INT1 reset vector

.org	$0003							; INT2 address

	rjmp    RESET						; INT2 reset vector

.org	$0004							; INT3 address

	rjmp    RESET						; INT3 reset vector

.org	$0005							; INT4 address

	rjmp    RESET						; INT4 reset vector

.org	$0006							; INT5 address

	rjmp    RESET						; INT5 reset vector

.org	$0007							; INT6 address

	rjmp    RESET						; INT6 reset vector

.org	$0008							; INT7 address

	rjmp    RESET						; INT7 reset vector

.org	$0009							; INT8 address

	rjmp    RESET						; INT8 reset vector

.org	$000A							; INT9 address

	rjmp    RESET						; INT9 reset vector

.org	$000B							; INTA address

	rjmp    RESET						; INTA reset vector

.org	$000C							; INTB address

	rjmp    RESET						; INTB reset vector

.org	$000D							; INTC address

	rjmp    RESET						; INTC reset vector

.org	$000E							; INTD address

	rjmp    RESET						; INTD reset vector

.org	$000F							; INTE address

	rjmp    RESET						; INTE reset vector

.org	$0010							; INTF address

	rjmp    RESET						; INTF reset vector

.org	$0011							; INT10 address

	rjmp    RESET						; INT10 reset vector

.org	$0012							; INT11 address

	rjmp    RESET						; INT11 reset vector


RESET:

  	ldi  	R20,	LOW(RAMEND)  	; stack pointer to top of ram
  	out  	SPL,	R20

   	rjmp 	KnobInit


;===========================================
; Knob scanning state machine lookup table |
;===========================================
;
; The table format is:
;  bit4        indicates error (old & new states not adjacent)
;  bits 3,2 :  Contain shifted copy of "current state" to be loaded into "old"
;  bits 1,0 :  Contain 2-bit "delta" increment+1
;

KnobTable:

.db 	0b00001, 	0b00101
.db 	0b01000, 	0b11101
.db 	0b00001, 	0b00101
.db 	0b11001, 	0b01101
.db 	0b00010, 	0b10101
.db 	0b01001, 	0b01101
.db 	0b10001, 	0b00101
.db 	0b01001, 	0b01101

sampleKnob:

    lds 	ZH, 	KnobState  	; Read "last table value"
    andi 	ZH, 	0xC      	; Keep only the "old knob bits"
   	in 		ZL, 	PINB       	; Read the bits from the rotary encoder
   	andi 	ZL, 	3        	; Discard all but the two bits that we want
   	or 		ZH, 	ZL         	; Combine "new" bits with old

; Now form a pointer into the correct byte of our lookup table:
;
   	ldi 	ZL, 	low(2 * KnobTable)
   	add 	ZL, 	ZH
   	ldi 	ZH, 	high(KnobTable)
   	adc 	ZH, 	ZH

; And then read the "next state" and "counts" from table
;
   	lpm 	ZL, 	Z

; At this point, bits 0 and 1 of ZL contain a value that is one greater
; than the number of clicks that should be added to the knob's current
; "position", and bits 2 and 3 contain a copy of the values
; just read from the knob.
;
   	sts 	KnobState, 	ZL    	; Update the current knob position and status
   	andi 	ZL, 	3          	; Keep only the delta bits
   	dec 	ZL              	; Shift delta bits into  range from -1 to 1
   	lds 	ZH, 	KnobPosition
   	add 	ZH, 	ZL          ; And add detected movement to the modeled knob
   	sts 	KnobPosition, ZH
   	ret
       
; ----------------
; INITIALIZATION =
; ----------------

KnobInit:

   in 		R16, 	PORTD
   ori 		R16, 	3
   out 		PORTD, 	R16        ; Turn on the pullups on Port D pins 0 & 1

; Initialize our state machine's "current state" bits:

   	in 		R16, 	PIND        ; What state is the knob in now?
   	andi 	R16, 	3         	; We're only interested in bits 0 and 1
   	lsl 	R16             	; But we want to store them in bits 2 and 3
   	lsl 	R16             	; of our official "state" variable:
   	sts 	KnobState, R16
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Don't know if you left out some of your code?
The RJMP to KnobInit ends without any jump to Main.
Is there a Main?
SampleKnob ends with RET.
Where is the RCALL SampleKnob?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Also...
ZH needs to be *2 also

; Now form a pointer into the correct byte of our lookup table:
;
      ldi    ZL,    low(2 * KnobTable)
      add    ZL,    ZH
      ldi    ZH,    high(KnobTable)
      adc    ZH,    ZH
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Lennart wrote:
Don't know if you left out some of your code?
The RJMP to KnobInit ends without any jump to Main.
Is there a Main?
SampleKnob ends with RET.
Where is the RCALL SampleKnob?

"I don't know. It's your code."
Levenkay's code. Corrrected
I copied and pasted into a test run.

I like to get specific "modules" working first before I get too involved. No call or jump to KnobInit from any main. I'm just simulating first to see how you get where your going. I'm trying to understand.......Lost

Last Edited: Tue. Dec 2, 2008 - 04:18 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
I don't know. It's your code.

?????
That's not my code.
Your subroutine SampleKnob ends with RET.
If there is nowhere in your code (main?) where you use
rcall   sampleKnob

you WILL end up in RESET over and over.
Also I was asking for a RJMP to where ever you want to go when you've read the encoder. The code you posted end with

sts   KnobState, R16

If there is no instruction to tell mcu where to go next it will crash within milliseconds and end up in RESET.

Finally to get to the expected table of encoder states you need to multiply also ZH by two, else you will be reading flash memory from a different location than where you're table's at.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Lennart wrote:
Quote:
I don't know. It's your code.

?????
That's not my code.
Your subroutine SampleKnob ends with RET.
If there is nowhere in your code (main?) where you use
rcall   sampleKnob

you WILL end up in RESET over and over.
Also I was asking for a RJMP to where ever you want to go when you've read the encoder. The code you posted end with

sts   KnobState, R16

If there is no instruction to tell mcu where to go next it will crash within milliseconds and end up in RESET.

Finally to get to the expected table of encoder states you need to multiply also ZH by two, else you will be reading flash memory from a different location than where you're table's at.


No, it wasn't Lennert's code, it was mine. But since this seems to be the most comprehensive list of misimpressions, I'll answer them here.

First, I only said this was an excerpt from my code, intended to show how an encoder could be serviced by a table-based state machine. Something has to call "sampleKnob" periodically. In the application I snagged this from, sampleKnob was part of a timer-overflow interrupt service routine (ISR); I just tossed a "ret" at the end out of expediency.

As to the organization of the file, it demonstrates a penchant of mine for keeping the initializing functions for services inside the library files that define those services. For the Atmel assembler, where you have to concatenate all your source code into one giant input stream via ".include" statements, you can't use externally-linked libraries in an application. But what you CAN do is write things like UART receive/transmit utilities, LED multiplexing, and rotary encoder monitoring, as separate, ".include-able" files, and, well, .include them. Notice that the top comment proclaims the file to be an ".inc", rather than a ".src" or ".asm" one. So, an application that "used" it would be built like so:

.include "m32def.inc"
.include "KnobService.inc"

    ldi r16, 0xff
    out ddra, r16
    
begin:
    ldi r25, 2

pollIt:
    rcall sampleKnob
    sbiw r24, 1
    brne pollIt

    lds r16, KnobState
    out PORTA, r16

    rjmp begin

The idea is that .include files are brought in at the top of an application, where the assembler's instruction stream flows from reset. Each .include file contains an RJMP instruction that either bypasses any code content entirely, or branches to the end of the .include file, where whatever setup/initialization work that's needed by the module's functions are done. After that point, the execution falls through to whatever comes next - either another library .include module, or to the "main" application code. A virtue (to my way of thinking, anyway) of this approach is that all the initialization a library function needs (and ONLY the initialization it needs) is packaged into the library itself. Most other folks use a practice in which each initializer might be a subroutine ending in RET. Besides wasting code, that approach requires a library user to do TWO things in order to use the library: .include it, and insert a call on its initializer function in some appropriate spot. My way is more "one-stop-shopping".

Finally, as to the "mistake" of not multiplying the upper byte of the lookup table's address by two... Just what do you think adding it to itself (adc ZH, ZH) does?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

"No, it wasn't Lennert's code, it was mine. But since this seems to be the most comprehensive list of misimpressions, I'll answer them here."

I'm all confoosed here. I got my L names mixed up. It's been one of those multiple fire putting out days at work. Thats what I get for trying to do code with a firehose in my hand. I'll sort this all out when I get home. In the mean time thanks for all of everyone's input.

Last Edited: Tue. Dec 2, 2008 - 04:20 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Levenkay,

I wasn't complaining about your code.
Actually I did not read your post, but tried to help OP who in last post stated that the program ended up in reset. So I gave some suggestions for causes, if in fact OP's posted code was meant to be complete.

Totally missed that you added ZH to itself to get the flash adress. Sorry...
But why that extra instruction?
Using carry to detect something?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Lennart wrote:
Levenkay,

Totally missed that you added ZH to itself to get the flash adress. Sorry...
But why that extra instruction?
Using carry to detect something?


To access a lookup table in FLASH storage, you need to construct the exact address of the desired byte in the Z register, and that will be the base byte address of the table plus the "offset" of the table member desired. The base byte address, as you've said, is two times the word address of the base of the table that the Atmel assembler will assign. And the addition must be made with 16-bit precision, in case the table happens to span over a 256 byte boundary in FLASH space. By using the "add with carry" form of the add instruction to generate the high byte of the desired combined address, the carry generated (if any) during the computation of the low byte (ZL) of the table member address is properly preserved. The effect is similar to the more common
    ldi ZH, low(2 * tableBase)
    adc ZH, ZEROREG

, except that it doesn't depend on having a processor register pre-cleared to zero.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
... except that it doesn't depend on having a processor register pre-cleared to zero.

Nifty trick, thanks!

Andreas

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Okay after trying to understand Levenkay's code I am still lost. Instead of trying to reinvent the wheel, I still would like to know A: why my code generates false pulses in the opposite direction of travel, B: I'm just trying to generate pulses 10ms in length when the encoder is turned in a specific direction.
2 clicks to the left = 1 10mS pulse out of B0.
2 clicks to the right = 1 10mS pulse out of B1.
2 clicks = 1 pulse because of the 0-1 action or is it 1-0 action ?
Left or Right will trigger an interrupt 0 or 1.
Thanks but I don't need to count pulses, sense how far something has travelled across the floor etc.
I'm just making pulses as mentioned above.
If anyone can examine my code and offer suggestions to it. Thanks

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hmmm--is it allowed to miss a pulse? Let's say that you keep a minimum 10ms between pulses--or whatever time you spec--1ms? Then a fast turn of the dial (I'm assuming this is a dial) will give input clicks faster than the pulse train.

If that is OK, then you could ignore clicks during the pulsing.

If it is not OK, then I'd create a FIFO queue of "commands" to a "pulse engine". I have that very thing in controlling a Sony Walkman that needs even wider pulses, and a long train for "volume up 14 levels" when each pulse is 100ms on and off.

First, I'd get the encoder counting exactly right. You could have a port of LEDs shoowing the count, and can even be +/- if you are good at hex in your head. ;) Once you think you've got it right, do the pendulum test. Suspend a long pendulum from the shaft end-weighted. Note the position when still and steady; perhaps 0 your count. Ideally, make that rest position repeatable, and not close to an encoder edge. Give the pendulum a good swing, and come back 5-10 minutes later when it comes back to rest. Repeat a few times. Once you have a series of runs that all come back to the same value, you are probably there.

The reason that this is a good test is the nature of the pendulum decay means that over the test, or the series of tests, the reverses in direction will occur at all (well, that is infinite but close enough) postions within a "click".

Lee

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch wrote:
Hmmm--is it allowed to miss a pulse? Let's say that you keep a minimum 10ms between pulses--or whatever time you spec--1ms? Then a fast turn of the dial (I'm assuming this is a dial) will give input clicks faster than the pulse train.

If that is OK, then you could ignore clicks during the pulsing.

Yes it's okay to miss pulses. Just not okay to add pulses in the opposite direction. If I'm turning to the right, there are pulses that false to the left output occasionally. That's what I'm trying to eliminate. No accuracy per se in the intended travel direction, no counting, just pulses.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The best way to deal with an encoder is this:

IN the two bit (channels).
together with last times IN values you make a 4 bit lookup table.
that table will hold values -1($FF), 0 and 1. and you just add that number
to the encoder value. (perhaps it count double speed that you need !)
old new table
00 00 0
00 01 -1
00 10 1
00 11 0 this is an error.
.
.
.
Jens

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Folks:

I know this is an OLD thread but I thought it prudent to update it with some working assembly code. 

 

I recently have started to work with the rotary encoders. Not being a "C" guy, I was looking for some working AVR assembly code and came across this thread. I used the code in post #6 above but had difficulty in doing so as it did not appear to function correctly. After reading this blog post, I decided to look into the table values and found they didn't seem to match the encoder states. I did not spend any time trying to figure out why. Instead, I recreated the table from scratch so that it would work with the (slightly modified) code from post #6 above with the exception that the rotary encoder is not polled but rather drives two of the pin-change interrupts. This way, the AVR can be put into low-power SLEEP mode and/or service the rotary encoder only when required. Also, the pointer to the data table does not calculate it's address properly for all addresses, so that code has been modified as well.

 

For the code below, it is complete and tested on ATmega328P with ENC_A on PORTC2 and ENC_B on PORTC3. It should run without modification under AVR STUDIO.

 

Enjoy!

 

Peace and blessings.

 

; Knobservice.inc - Example code for tracking the movements of a rotary knob
; ref: https://www.avrfreaks.net/comment/431425#comment-431425
;
; In this example, the rotary encoder is connected as follows:
;		A = PORTC2, B=PORTC3
#define		ENC_A	PORTC2
#define		ENC_B	PORTC3
;
.nolist
.include "m328Pdef.inc"		;32K FLASH, 2KB DATA, 1KB E2P
.list
;
; Here are two RAM variables to support polling a quadrature-encoded
; rotary knob.  One holds the "state" of the knob-reading machine;
;               the other holds a model of the knob's angular position
;
	.dseg
KnobState:	.BYTE 1
KnobPosition:	.BYTE 1
;
;************************************************************************
;Start of code space here...
;************************************************************************
	.cseg
	.org	0x0000

;set up interupt vectors
	jmp	RESET				; Reset vector

	.org 	0x0008
	jmp	PCI1_int			; Pin Change Interrupt Request 1
;
;*********************************
RESET:
        clr     r0                              ;clear a register
	ldi	ZL,(	(0<<SM2) | \
			(0<<SM1) | \
			(0<<SM0) | \
			(1<<SE) )		;sleep enable in idle mode
	out	SMCR,ZL				;init sleep mode

	rcall	KnobInit			;init the state machine
	sei					;enable interrupts
Main:
	sleep
	nop
	rjmp	Main				;loop
;
;===========================================
; Knob scanning state machine lookup table |
;===========================================
; The table format is:
;  bits 5,4 :  Contains "previous state"
;  bits 3,2 :  Contains "current state"
;  bits 1,0 :  Contains 2-bit "delta" increment+1
;				to be added to the counter (-1,0,1)
;	Clockwise = add +1, counter-clockwise = add -1
;	Invalid states = add 0
;
;	Valid table is as follows:
;		BA BA dT
;		00 00  0	[0]
;		00 01 +1	[1]
;		00 10 -1	[2]
;		00 11  0	[3] invalid state
;		01 00 -1	[4]
;		01 01  0	[5] invalid state
;		01 10  0	[6] invalid state
;		01 11 +1	[7]
;		10 00 +1	[8]
;		10 01  0	[9] invalid state
;		10 10  0	[A] invalid state
;		10 11 -1	[B]
;		11 00  0	[C] invalid state
;		11 01 -1	[D]
;		11 10 +1	[E]
;		11 11  0	[F]

KnobTable:
;			  BABAdT    BABAdT
		.db	0b000001, 0b000110
		.db	0b001000, 0b001101
		.db	0b010000, 0b010101
		.db	0b011001, 0b011110
		.db	0b100010, 0b100101
		.db	0b101001, 0b101100
		.db	0b110001, 0b110100
		.db	0b111010, 0b111101
;
;****************************************************************************
; PCI1_int:	Pin Change Interrupt Request 1 service routine.
; 			Scan the knob and update the variables
;****************************************************************************
PCI1_int:
        lds 	ZH, KnobState			; Read "last table value"
        andi 	ZH, 0x30			; Keep only the "old knob bits"
        in 	ZL, PINC			; Read the bits from the rotary encoder
        andi 	ZL,(1<<ENC_B | 1<<ENC_A); Discard all but the two bits that we want
        or 	ZH, ZL				; Combine "new" bits with old to form an
						;	table address index (x4)

	lsr	ZH				; shift right into bits 3:0 for index
	lsr	ZH

        ; Now form a pointer into the correct byte of our lookup table:
        ;
	ldi 	ZL, low(2 * KnobTable)
	add 	ZL, ZH
	ldi 	ZH, high(2 * KnobTable)
	adc 	ZH, r0

        ; And then read the "next state" and "counts" from table
        ;
	lpm	ZL, Z

        ; At this point, bits 0 and 1 of ZL contain a value that is one greater
        ; than the number of clicks that should be added to the knob's current
        ; "position", and bits 2 and 3 contain a copy of the values
        ; just read from the knob.
        ;
	sts 	KnobState, ZL			; Update the current knob position and status
	andi 	ZL, 3					; Keep only the delta bits
	dec 	ZL						; Shift delta bits into  range from -1 to 1

	lds 	ZH, KnobPosition		; Load the current KnobPosition
	add 	ZH, ZL					; And add detected movement to the modeled knob
	sts 	KnobPosition, ZH		; And store it

	reti

        ; ----------------
        ; INITIALIZATION =
        ; ----------------
KnobInit:
;enable pullups for Rotary Encoder A/B inputs
	ldi	ZL,((1<<ENC_B) | \
			(1<<ENC_A))
	out	PORTC,ZL

;enable Rotary Encoder A/B pin interrupts
	ldi	ZL,((1<<PCINT10) |\
			(1<<PCINT11))
	sts	PCMSK1,ZL
	ldi	ZL,(1<<PCIE1)
	sts	PCICR,ZL

;clear any pending interrupts
	lds	ZL, PCIFR
	sbr	ZL, (1<<PCIF1)
	sts	PCIFR, ZL

; Initialize our state machine's "current state" bits:
	in 	ZL, PINC		; What state is the knob in now?
	andi 	ZL, (1<<ENC_B | 1<<ENC_A); We're only interested in bits 2 and 3
	lsl 	ZL			; But we want to store them in bits 4 and 5
	lsl 	ZL			;  of our official "state" variable:
	sts 	KnobState, ZL
	clr	ZL			;zero the knob position variable
	sts 	KnobPosition, ZL
	ret			;return to caller
;

 

 

Last Edited: Fri. Jan 25, 2019 - 05:21 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The encoder code have been up many times, and have be convinced that there are simpler ways to do it, the one I like the most is :

 

if ( (newA ^ oldA) == (newA ^ newB) )

  inc

else

 dec

 

old =new

 

where A and B the two channels, and its easier if they are on same port, so old and new is a in from port masked off