Minimal (Fast) Quadrature Detector code for (optical) Rotary Encoder


Senior Member

This code was inspired by a recent thread to devise a "fast" detector for an optical rotary encoder. The requirement there, was to detect at least "30 edges/second", but this code can detect to at least 1 kHz (using a 32 MHz SETFREQ), although a more normal application would be to use the PICaxe default clock frequency (of 4 or 8 MHz) and/or to include the code in a much longer (i.e. slower) polling loop. This version has no provision to eliminate "contact bounce" of mechanical switches, so it may be better suited to optical or magnetic (Hall) sensors. However, contact bounce could be filtered with a small smoothing capacitor (to ground) on each input pin, or by customising the software, for example by reading each pin three times and performing a "majority decision" for each bit.

The program can use any two input pins (even on different ports), but needs one bit-addressable byte variable (i.e. b0 - b3) and two more variables. The two pins/bits are read consecutively so changes are not exactly synchronised, but this is not an issue because the encoding is a simple "Gray" code, which ensures that no two bits change at the same time. The code maintains a Byte or Word up/down counter which it outputs as a serial byte, or multiple serial ASCII bytes, at up to 76.8k Baud, either Repeatedly or only "On-Change", depending on the program configuration. The basic program has two operational configurations (determined by specifying "FASTER", or not) but several options are bundled together. The fastest mode continuously "updates" the counter (whether it has changed or not) and transmits its value using HSEROUT, whilst the alternative "Branching" structure uses SERTXD (or SEROUT) and allows additional sections of code to run (or be skipped), depending on the encoder status.

The program operates as a simple "state machine" which records the "Present" and "Previous" conditions of the two input pins (i.e. 16 permutations) and selects one of four possible "Outcomes", i.e. Count Up, Count Down, No Change or "Error" (both inputs changed, so the direction of movement cannot be determined). Generally this Error would be ignored (i.e. no change to the count) but a double-count could be implemented on the assumption that the double-change represents a faster movement in the same direction as the previous step. The Outcome is determined by a simple byte-lookup facility (i.e. an EEPROM READ{TABLE} or a LOOKUP command) coded with: Zero (no change), 1 (increment / clockwise), 2 (Undefined / no action) and -1 or 255 (decrement / anticlockwise). The lookup value can be applied directly to the (byte) counter (replacing the 2 in the table with zero if no action is desired), or to steer an ON ... GOTO command to facilitate additional actions. This command is significantly faster than other structures for multiple Branching operations (with any omitted labels executed as a fall-through) and is even marginally faster than an IF var = 0 THEN {GOTO} if only one label is included.

; Fast Quadrature Detector for (optical) Rotary Encoder. AllyCat September 2020
#picaxe 08m2                                ; Or any other
;  #no_data                                    ; Comment out when DATA/EEPROM needs to be (re-)written
; #define FASTER                            ; Update the counter and transmit its value on every pass
    hsersetup B19200_4 , %10010            ; HSERIN NOT enabled (=16), HSEROUT Idle low (ie. Inverted=2)
#terminal 19200                                ; Can be higher or lower frequency as required
#terminal 4800                                ; For SERTXD with SETFREQ m4

symbol Enc0 = pinC.3                     ; Any Digital input pin from encoder
symbol Enc1 = pinC.4                     ; Any other Digital input from encoder
symbol flags = b0                            ; Must be bit-addressable
symbol tempb = b1                            ; Temporary/Local byte (any)
symbol counter = b4                        ; Can be only a single byte in FASTER mode
symbol TBASE = 240 AND 0             ; Base address of table.

data TBASE,(0,1,255,0,255,2,2,1)      ; Branch / Data Table
data    (1,2,2,255,0,255,1,0)              ; 0 = No Change, 1 = Up, 2 = Both (up 2 or 0), 255 = Down
    counter = 80                             ; "O", or any desired starting value
    flags = TBASE                            ; Lower 4 flags = Encoder inputs: Last , Now , Last , Now
    bit0 = Enc0                                ; Bits 0 , 2 = Encoder inputs
    bit2 = Enc1
    flags = flags + flags AND 15         ; Left Shift "Now" to "Last" flags and delete previous Last flags
;    flags = flags + TBASE                ; Only required if TBASE <> 0 (or can add "+ TBASE" to previous line)
    bit0 = Enc0                                ; Update the "Now" flags
    bit2 = Enc1
    read flags , tempb                        ; Lookup the step size/direction in (Table) EEPROM
; lookup flags , (0,1,255,0,255,2,2,1_
;        ,1,2,2,255,0,255,1,0) , tempb    ; Alternative Lookup, without using DATA/EEPROM Table (slower)
    counter = counter + tempb            ; Update the counter ("both" could be coded with a lookup value of 0)
    hserout 0 , (counter)                       ; Always send a byte (0 gives no "break" signal)
    on tempb goto nochange , up , both    ; Fall-through for down/decrement (both has unknown direction)
    dec counter                                ; Can be a Word variable
    goto done
both:                                            ; Both flags changed, direction unknown so step 2 or 0 
    inc counter                                ; Fall through to double-step (Or DEC, or jump direct to skip count)
    inc counter
    sertxd(#counter , " ")                    ; Transmit the numerical (denary) number
nochange:                                    ; Skips transmitting the counter byte
loop                                            ; Or proceed with other polled functions first
Perhaps later, I will post an alternative version which, in common with nearly all of my programs, reserves a bit-addressable "temporary" (byte) variable to use throughout the program, as required. Thus, it does not need dedicated use of a bit-addressable variable, can use a lookup table not based on a zero address, and can handle the double-step (overrun) condition. "Remind" me if that seems useful. ;)

Cheers, Alan.


New Member
Thanks for your effort. I have played with Picaxe's educationally for many years. No longer teaching and semi retired, playing with a Nidec printer drum driver that comes with a 4 phase encoder. Can vary speed, break and measure RPM. Just started to play with the 4 phase capabilities and came across your code. Thanks as I'm now looking at how accurately I can position the rotation of the motor. I found I need to run the o08m at setfreq m8 to accurately read the encoder at full speed. Will post after I understand your code fully. Cheers :)