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

AllyCat

Senior Member
Hi,

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.

Code:
; 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
#IFDEF FASTER
    hsersetup B19200_4 , %10010            ; HSERIN NOT enabled (=16), HSEROUT Idle low (ie. Inverted=2)
#terminal 19200                                ; Can be higher or lower frequency as required
#ELSE
#terminal 4800                                ; For SERTXD with SETFREQ m4
#ENDIF

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
init:
    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
main:
do
    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)
#IFDEF FASTER
    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)
#ELSE
    on tempb goto nochange , up , both    ; Fall-through for down/decrement (both has unknown direction)
down:
    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)
up:
    inc counter
done:
    sertxd(#counter , " ")                    ; Transmit the numerical (denary) number
nochange:                                    ; Skips transmitting the counter byte
#ENDIF
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.
 

agoodevans

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 :)
 

popchops

Well-known member
Hello again Alan,

Seems I found this work of yours at exactly the right time for me. I certainly remember previous attempts to reduce the resource load of the optical decoder on Picaxe: best-way-to-interface-with-rotary-encoder.

I was able (with help :p) to get the PICAXE to decode quadrature inputs at an acceptable rate, problem is that I also have other functions that must be performed including IR input, which involves some wait interval (timeout). My problem is how to integrate the (relatively resource-hungry) quadrature decoder with IRIN and SPI output operations. Should I perform quadrature decoding on a separate PICAXE? That doesn't seem to solve the problem because I still have to post the result somehow to the main decision-making chip. One option maybe is to accumulate the decoded increments & decrements in a separate decoder device, and simply present an 8-bit value (via 8 pins) to the main chip. This can be polled after every IRIN iteration to pick up changes in the knob position(?).

I want to avoid:
- errors in quadrature decoding or missed changes
- large erroneous steps
- missed IR inputs

What do you think?
  • Is there a way - even with your efficient code - to perform high-performance quadrature decoding on the same PICAXE as IRIN?
  • If decoding is performed on a separate dedicated PICAXE, what's the best way to ensure the angular value is communicated correctly?
Thanks for sharing!

Pops
 

popchops

Well-known member
Instead of 'bit-banging' the counter value (accumulated increments and decrements) perhaps HSPI is better? I'm using 28X2 as my main chip, could easily use another for the optical encoder. Does HSPI output allow the main quadrature decoding function to go on uninterrupted in the foreground? I don't want to miss any edges.

If I am receiving via HSPI on the PICAXE running IRIN - will the HSPI interfere with the IRIN or vice versa?

Thanks, Pops
 
Last edited:

AllyCat

Senior Member
Hi,

I don't think that I can help much; this thread was mainly a theoretical design and I have never used any X2 PICaxes. AFAIK, the IRIN command is fully bit-banged so it blocks any other functions. I believe inglewoodpete devised some interrupt-driven IR receiver code for an X2, but it was for the NEC protocol and used most of the resources of an X2 at 64 MHz, so probably not compatible with a rotary encoder.

My program code above is "compatible" with interrupt operation but I'm not aware that full details of the behaviour of the PICaxe when using multiple interrupts are described anywhere. Alternatively, you may be able to Poll the (base PIC's) "Interrupt-on-Change" flags, to avoid missing any transitions.

I think the "H" (Hardware) Serial, I2C and SPI commands probably operate in a similar manner to each other, but with their Input and Output commands working rather differently: The "Input" commands appear to be fully interrupt driven so can probably co-exist with most foreground tasks (but not any that disable interrupts such as IRIN). However, I believe that the Hardware "Output" commands continue running (i.e. without allowing any other commands to execute) until the "final" byte of the data transfer has been loaded into the hardware buffer. Note that with SPI and I2C Data Buses the Host and Slave are not necessarily the same as the (Data) Transmitter and Receiver respectively, so I'm not clear in which direction your SPI data interface is (or would be) operating.

Overall, I think that you probably do need to use multiple PICaxe processors, but communication between them should be quite simple and reliable by using the HSEROUT and HSERIN commands. The X2s support "background receive" but even receiving into an M2 should be possible, provided that a protocol transmitting single bytes, with some type of handshake/verification, is adopted.

Cheers, Alan.
 

popchops

Well-known member
Thx Alan,

Instead of using another 28X2 - I found this:
https://lsicsi.com/products/ls7083n-s-ls7083n-ls7084n-s-ls7084n/

This would then present at input to the PICAXE either:
LS7083: INC and DEC pulses on 2 different pins
LS7084: Edge (state transition) pulses on one pin, and INC/DEC encoded on the other pin

Is it possible to use two PICAXE interrupts independently i.e. hint0 to receive and respond to INC pulses via 'gosub1' and hint1 to receive and respond to DEC pulses via 'gosub2'?

If not - easy to use LS7084 pulses to trigger a single interrupt and then observe the direction (INC or DEC) on the other pin: 'IF True THEN Inc ELSE Dec' . Just want to minimise the size of whatever goes into the interrupt subroutine. Two independent interrupts would be neater - then I only need 'inc counter' or 'dec counter' in the gosubs.

Thanks!!

Pops
 

hippy

Technical Support
Staff member
Is it possible to use two PICAXE interrupts independently
No, there can only be one 'interrupt' routine but should be able to handle multiple interrupts. Untested -

Code:
interrupt:
  Do
    If hint1flag = 1 Then
      hint1flag = 0
      counter = counter + 1
    End If
    If hint2flag = 1 Then
      hint2flag = 0
      counter = counter - 1
    End If
  Loop While hint1flag = 1 Or hint2flag = 1
  Return
 

popchops

Well-known member
Code:
interrupt:
  Do
    If hint1flag = 1 Then
      hint1flag = 0
      counter = counter + 1
    End If
    If hint2flag = 1 Then
      hint2flag = 0
      counter = counter - 1
    End If
  Loop While hint1flag = 1 Or hint2flag = 1
  Return
Thanks again Hippy. The interrupt flags might be very brief, so I don't want to poll them some time after the interrupt. This helps me to decide that, rather than encoding information and timing interrupts together, I need one fast interrupt and separate stable +/- status indicated by a second pin that only changes when direction is reversed. Then - even if this is polled 10ms after the interrupt it will be robust. Conversely, the interrupt will 'wait' 1ms only before returning to False.

Pops.
 

inglewoodpete

Senior Member
The interrupt flags might be very brief, so I don't want to poll them some time after the interrupt. This helps me to decide that, rather than encoding information and timing interrupts together, I need one fast interrupt and separate stable +/- status indicated by a second pin that only changes when direction is reversed. Then - even if this is polled 10ms after the interrupt it will be robust. Conversely, the interrupt will 'wait' 1ms only before returning to False.
From what you say above, I'm not sure that you understand how the hintflags work. You say "The interrupt flags might be very brief". That is the beauty of using the 'hint' capability - a brief input is captured and latched by the X2 chip, even if the interrupting state disappears. The hint flag will remain active until the user program resets it, usually in the interrupt routine.
 
Top