Best way to interface with rotary encoder

popchops

Well-known member
Thanks to inglewoodpete,

Latest code detects all edges CW and CCW including first edge after reversal.
CSS:
'Simple Rotary Encoder Demonstration Programme
'Written by inglewoodpete
'
#PICAXE 40X2
#Terminal 38400            'SerTxd at 32MHz
#No_Data                   'Comment this statement out if EEPROM data is to be initialised
'
'                                 Leg
'Symbol iEncoder0       = pinC.6 ' 25 Digital input - bit A of encoder
'Symbol iEncoder1       = pinC.7 ' 26 Digital input - bit B of encoder
'
' **** Variables b0 to b27 or w0 to w13 (Prefix b for byte; w for word)
'
Symbol bByteToShow      = b1     'w0 Used to display hexadecimal value (for debugging)
Symbol bCurrentState    = b2     'w1 Current copy of encoder output
Symbol bPreviousState   = b3     'w1 Previous copy of encoder output
Symbol bLatestTransition= b4     'w2 4-bit value, from Previous and Current states
Symbol bPrevTransition  = b5     'w2 Copy of previous transition, in upper 4-bits
Symbol bDirection       = b6     'w3 Validated indication of rotation direction
Symbol bTemp            = b7     'w3 Used within ShowHex routine
'
' ---------------------------------------------------------------------
Init: SetFreq M16
      ;PullUp %00011000
      Pause 1000         'Allows time for logging terminal to open after download
      ;b8 = 128
    SerTxd (CR, LF, "Rotary Encoder Demo", CR, LF)
      ;bPreviousState = PinsC And %00011000
    bPreviousState = PinsC And %11000000
    
      Do ' ****  M a i n    L o o p  ****
         bCurrentState = PinsC And %11000000
           low D.2
        low D.3
         If bCurrentState <> bPreviousState Then      'Has encoder been turned
            bByteToShow = bCurrentState
            GoSub ShowHex                             'Show byte value as hex
            SerTxd (" ")
            ' At this point, bPreviousState = XY000000 and bCurrentState = xy000000
            ' First, get transition value into lower 4-bits indicating direction (CW or ACW)
            ' (/ 4) Move previous XY bits right 2 bit-positions: 00XY0000
            '  (Or) merge current xy bits:                       xyXY0000                   
            ' (/16) Move all 4 bits right by 4 bit positions:    0000xyXY
            ' This results in previous, current encoder bits appearing in lower nibble
            ;bLatestTransition = bPreviousState / 4 Or bCurrentState / 16
        bLatestTransition = bCurrentState / 4 Or bPreviousState / 16
            '                                             
            bDirection = bPrevTransition Or bLatestTransition
            bByteToShow = bDirection
            GoSub ShowHex                             'Show byte value as hex
            SerTxd(" ")
            Select Case bDirection
              Case $7E, $E8, $81, $17, $D7, $41, $28, $BE: SerTxd("CW ")    high D.2
              Case $42, $2B, $BD, $D4, $7D, $EB, $82, $14: SerTxd("CCW ") high D.3
End Select
;debug
    ;bByteToShow = bLatestTransition
            bPreviousState = bCurrentState            'Save for next comparison
            bPrevTransition = bLatestTransition * 16  'Copy transition into upper 4-bits
            SerTxd(CR, LF)
         EndIf
    Loop
'
' **** Subroutines *****************************
'
' ---- ShowHex: Send Hexadecimal value to PE Terminal -----------------------------------------
'
'        Send a  byte  as hex to terminal on the programming lead (ShowHex:)
'        Send a nibble as hex to terminal on the programming lead (SHexNibl:)
'        Alternate "D" entries cause a leading $ sign to be logged
'
'        Entry:   bByteToShow (byte) data to be transmitted
'        Exit:    bByteToShow (byte) unchanged
'        Used:    bTemp       (byte)
'
ShowHexD:SerTxd("$")                      'Alternate entry
ShowHex: bTemp = bByteToShow / 16         'Get high nibble
         GoSub Sh4bits                    'Show it
         Goto SHexNibl                    'Then do low nibble
ShowNibD:SerTxd("$")                      'Alternate entry
SHexNibl:bTemp = bByteToShow And %00001111
Sh4bits: bTemp = bTemp + "0"              'Convert to ASCII
         If bTemp> "9" Then               'Separate out and
            bTemp = bTemp + 7             ' correct over-
         EndIf                            ' decimal part.
         SerTxd(bTemp)                    'Display it
         Return
I still have the LED output in the code, as it's very convenient for quick sanity check, and I still have no counter but I'll fix that now. Next step will be to see if I can shoehorn IR input, SPI output, channel selection and some memory functions into the same PICAXE. :eek:

Pops.
 

popchops

Well-known member
Counter added - works like a charm. Can increment 20 times and decrement 20 times all within 1 second! I pruned out all superfluous reporting, so not ideal for debugging. I also think there is still too much info being recorded. To decide on CW or CCW, INC or DEC, you need only 4 bits: current state and previous state. The code below is recording and manipulating 8 bits for the Select Case decision. Not sure what the overhead is here.

CSS:
;Simple Rotary Encoder Demonstration Programme
;Written by inglewoodpete

#PICAXE 40X2
#Terminal 38400            ;SerTxd at 32MHz
#No_Data                   ;Comment out if EEPROM data is to be initialised

;Symbol iEncoder0       = pinC.6 ;25 Digital input - bit A of encoder
;Symbol iEncoder1       = pinC.7 ;26 Digital input - bit B of encoder

Symbol bByteToShow      = b1    ;w0 Used to display hex for debugging
Symbol bCurrentState    = b2    ;w1 Current copy of encoder output
Symbol bPreviousState   = b3    'w1 Previous copy of encoder output
Symbol bLatestTransition= b4    'w2 4-bit value, from Previous and Current states
Symbol bPrevTransition  = b5    'w2 Copy of previous transition, in upper 4-bits
Symbol bDirection       = b6    'w3 Validated indication of rotation direction
Symbol bTemp            = b7    'w3 Used within ShowHex routine
Symbol bCounter        = b8    'w4 Resulting integral count
'
' ---------------------------------------------------------------------
Init: SetFreq M16
      Pause 1000         'Allows time for logging terminal to open after download
      bCounter = 128
    SerTxd (CR, LF, "Rotary Encoder Demo", CR, LF)
    bPreviousState = PinsC And %11000000
   
      Do ' ****  M a i n    L o o p  ****
        bCurrentState = PinsC And %11000000
        If bCurrentState <> bPreviousState Then    ;Has encoder moved?
        ;Assume bPreviousState = XY000000 and bCurrentState = xy000000
        ;(/ 4) Shift current XY bits right by 2 bits: 00XY0000
        ;(Or) Merge previous xy bits: xyXY0000                  
        ;(/16) Shift all 4 bits right by 4 bits:    0000xyXY
        ;This places the latest transition in the lower nibble

        bLatestTransition = bCurrentState / 4 Or bPreviousState / 16
            bDirection = bPrevTransition Or bLatestTransition
        ;Determine INC & DEC based on current and previous transition
            Select Case bDirection
            Case $7E, $E8, $81, $17, $D7, $41, $28, $BE: inc bCounter
            Case $42, $2B, $BD, $D4, $7D, $EB, $82, $14: dec bCounter
        End Select

        bByteToShow = bCounter
        GoSub ShowHex
            bPreviousState = bCurrentState            'Save for next comparison
            bPrevTransition = bLatestTransition * 16  'Shift into upper 4-bits
            SerTxd(CR, LF)
         EndIf
    Loop

; **** Subroutines *****************************
;
; ---- ShowHex: Send Hexadecimal value to PE Terminal ----
;
;        Send a  byte  as hex to terminal on the programming lead (ShowHex:)
;        Send a nibble as hex to terminal on the programming lead (SHexNibl:)
;        Alternate "D" entries cause a leading $ sign to be logged
;
;        Entry:   bByteToShow (byte) data to be transmitted
;        Exit:    bByteToShow (byte) unchanged
;        Used:    bTemp       (byte)
;
ShowHexD:SerTxd("$")                     ;Alternate entry
ShowHex: bTemp = bByteToShow / 16         ;Get high nibble
         GoSub Sh4bits                    ;Show it
         Goto SHexNibl                    ;Then do low nibble
ShowNibD:SerTxd("$")                      ;Alternate entry
SHexNibl:bTemp = bByteToShow And %00001111
Sh4bits: bTemp = bTemp + "0"              ;Convert to ASCII
         If bTemp> "9" Then               ;Separate out and
            bTemp = bTemp + 7             ; correct over-
         EndIf                            ; decimal part.
         SerTxd(bTemp)                    ;Display it
         Return
Best regards

Pops
 

inglewoodpete

Senior Member
Counter added - works like a charm. Can increment 20 times and decrement 20 times all within 1 second! I pruned out all superfluous reporting, so not ideal for debugging. I also think there is still too much info being recorded. To decide on CW or CCW, INC or DEC, you need only 4 bits: current state and previous state. The code below is recording and manipulating 8 bits for the Select Case decision. Not sure what the overhead is here.
I don't think there is much to be saved by only working with 4 bits. From my experience, the PICAXE spends a large part of its command execution time actually unpacking and interpreting its tokenised code. Experiment with the code and prove me wrong 😲.

I noticed that you are using my ShowHex routine to show the value of your counter. Hexadecimal is great for displaying bit patterns but not so suitable for just showing numbers or magnitudes. I suggest you use the PICAXE native SerTxd(#bCounter) or SerTxd(#bCounter, CR, LF) command.
 

popchops

Well-known member
I don't think there is much to be saved by only working with 4 bits. From my experience, the PICAXE spends a large part of its command execution time actually unpacking and interpreting its tokenised code. Experiment with the code and prove me wrong 😲.
Yep I suspect you're right, if it takes extra operations to access only the relevant nibble. You can see in the Case code above it's only the LS nibble that's relevant:
- E, 8, 1 or 7 indicate CW
- 2, B, D or 4 indicate CCW.

There's half the number of cases if you ignore previous transitions, and one OR can be removed. I'll try it later.

Pops.
 

popchops

Well-known member
OK - this is it:
Rich (BB code):
;Simple Rotary Encoder Demonstration Programme
;Written by inglewoodpete

#PICAXE 40X2
#Terminal 38400            ;SerTxd at 32MHz
#No_Data                   ;Comment out if EEPROM data is to be initialised

;Tested with Grayhill 62AG11 optical encoder
;Symbol iEncoder0       = pinC.6 ;25 Digital input - bit A of encoder
;Symbol iEncoder1       = pinC.7 ;26 Digital input - bit B of encoder

Symbol bCurrentState     = b1    ;Current copy of encoder output
Symbol bPreviousState    = b2    ;Previous copy of encoder output
Symbol bLatestTransition = b3    ;4-bit value, Previous and Current states
Symbol bCounter          = b4    ;Resulting integral count

Init: SetFreq M16
      Pause 1000             ;Allows time for terminal to open
      bCounter = 128
    SerTxd (CR, LF, "Rotary Encoder Demo", CR, LF)
    bPreviousState = PinsC And %11000000
Main:
      Do ; ****  M a i n    L o o p  ****
        bCurrentState = PinsC And %11000000
        If bCurrentState <> bPreviousState Then    ;Has encoder moved?
        ;Assume bPreviousState = XY000000 and bCurrentState = xy000000
        ;(/ 4) Shift current XY bits right by 2 bits: 00XY0000
        ;(Or) Merge previous xy bits: xyXY0000                 
        ;(/16) Shift all 4 bits right by 4 bits:    0000xyXY
        bLatestTransition = bCurrentState / 4 Or bPreviousState / 16
      
        ;Determine INC & DEC based on current and previous states
            Select Case bLatestTransition
            Case $0E, $08, $01, $07: inc bCounter
            Case $02, $0B, $0D, $04: dec bCounter
        End Select

        SerTxd(#bCounter, CR, LF)
            bPreviousState = bCurrentState    ;Save for next comparison
         EndIf
    Loop
Size is reduced from 219 to 127 bytes, and seems to be good for 60 edges per second. When it starts to drop edges it's quite well behaved; there is is no INC or DEC for a single edge missed (the main loop runs but with no matching Case), so it's quite benign. Then, it picks up the next edge and the count is off again.

Next step for me is to record the bCount in memory so it powers up at the last known value each time. Thanks for all the help guys!

Pops
 

inglewoodpete

Senior Member
OK - this is it:
<code removed>

Size is reduced from 219 to 127 bytes, and seems to be good for 60 edges per second. When it starts to drop edges it's quite well behaved; there is is no INC or DEC for a single edge missed (the main loop runs but with no matching Case), so it's quite benign. Then, it picks up the next edge and the count is off again.

Next step for me is to record the bCount in memory so it powers up at the last known value each time. Thanks for all the help guys!

Pops
Well done! That's quite an improvement (space and response time). You've come a long way in just a few days. When used efficiently and with careful programming practices, the PICAXE is a very capable processor.
 

popchops

Well-known member
Hi all,

Next question... if it's not too bold... is how to integrate infra-red receiver with the encoder as inputs to the same picaxe...(?)

I'm using IRIN which has an optional timeout... I always used values around 100 ms which works very well for INC/DEC demands for volume.

However, I obviously can't stick a 'wait for 100ms' IRIN command into my optical encoder loop. Is there a solution here, or must I simply use a different PICAXE for IRIN?

Thanks again, Pops.
 

AllyCat

Senior Member
Hi,

You're asking a lot because IRIN operates only at 4 MHz and obviously the PICaxe cannot receive signals from both sources at the same time. However, IR messages are normally repeated at least several times, so you might include a "test" for IR within the polling loop (I don't know if that could be an IRIN with a very short timeout, or separate "sniffer" code), and then bale out (to receive a complete packet) if IR were detected. Therefore, the speed of operation might be even more critical, so perhaps an "update" that I was just about to post is more relevant?
_______

Your SERTXD specifies 16 MHz, but the #TERMINAL is 32 MHz, which are 8 or 16 times faster than can be tolerated by the normal AXE134 OLED code (that you implied earlier in the thread). So you might need to improve either the AXE134 code, or the way that data is sent to it.

The program is indeed much faster than the code concluded in #13 earlier, but as the thread title is "Best Way....." I thought it amusing to see if it could be made even faster. Of course speed is not the only criterion for "best", but it has been the emphasis for the latter part of this thread. The following program should be somewhat faster and allows any two pins on a single port to be used.

The "divide - OR - divide" is an ingenious method to "shift, mask, combine bits and shift again" and is quite fast when written in a single line. But shifting right is slower than left, because it uses mathematical division; thus shifting left by one bit, combing two bits and then masking can be marginally faster, and permits any port pins to be used. I don't know if >> 4 , etc. would be faster when an X2 is available, but sometimes ** 4096 for example, can be faster for right shifting.

Internally, the SELECT .. CASE appears to use a LOOKDOWN which is moderately fast, but still involves a "search" through 4 bytes (to answer a previous question, an 8-byte lookdown takes around 60% longer), so a READ or READTABLE lookup is much faster. Only a single bit value is strictly required, but I've coded a "don't care" (or don't know) value to allow errors to be detected. Of course the "double-step" value (i.e both bits changing) cannot determine the direction of rotation alone, but it might be reasonable to assume that an overshoot has occurred in the same direction as the previous step(s). However, I haven't devised a neat way to code that (yet). For a simple binary choice an IF .... structure could be used, but I've used an ON ... GOTO which has comparable speed, but can be extended to trap an error or overshoot. I've measured this READ{TABLE} ... GOTO structure as almost 3 times faster than the SELECT ... CASE .

So here's my version, I've reverted to Pete's 08M2 pins as that's the chip I've used for testing, and direct variable names (compatible with the symbols) because the bits are mapped to specific bytes.
Code:
#picaxe 08m2
#terminal 4800
; Fast Rotary Encoder Demonstration
; Derived from code by inglewoodpete
; Symbol iEncoder0       = pinC.3     ; Digital input - bit A of encoder
; Symbol iEncoder1       = pinC.4     ; Digital input - bit B of encoder

Symbol bCurrentState     = b1        ; Current copy of encoder output
Symbol bPreviousState    = b2        ; Previous copy of encoder output
Symbol bLatestTransition = b3        ; 4-bit value, Previous and Current states (%0000PCPC)
Symbol bCounter          = b4        ; Resulting integer count
data 0,(2,0,1,2,1,2,2,0,0,2,2,1,2,1,0,2)  ; 0 = Up, 1 = Down , 2 = Error

Init:
    Pause 1000                                      ; Allow time for terminal to open
    bCounter = 128
    SerTxd (CR, LF, "Rotary Encoder Demo", CR, LF)
    bPreviousState = PinsC AND %00011000            ; Any 2 pins on a single port
Main:
    Do                                                         ; M a i n    L o o p
      bCurrentState = PinsC AND %00011000            ; Any two pins on one port
      If bCurrentState <> bPreviousState then      ; Encoder has moved
            b3 = b3 + b3 and 15                            ; Shift left one bit (use *4 for 2 bits) and mask
            bit24 = bit11                                    ; Or any other pin on the port
            bit26 = bit12                                    ; Or any other pin on the port
            read b3 , b2
            on b2 goto up    , down        ; Can fall into "down:" or trap invalid code(s)
            sertxd("error ")                ; Optional
down:
            dec bcounter
            goto done
up:        
            inc bcounter
done:
              SerTxd(#bCounter, CR, LF)
          bPreviousState = bCurrentState    ; Save for next comparison
      EndIf
    Loop
Cheers, Alan.

PS: Of course, without the Error trap, the lookup table could just contain +1 and -1 (i.e. 255) and the counter becomes simply : bcounter = bcounter + b2 . :)
 
Last edited:

popchops

Well-known member
Hi Alan,
So here's my version, I've reverted to Pete's 08M2 pins as that's the chip I've used for testing, and direct variable names (compatible with the symbols) because the bits are mapped to specific bytes.
I managed to make your version work with a bit of bashing around. It's very good - the best yet I think. Tested and proven to decode ~100 edges per second (without error) on my 40X2 at 16 MHz. That's approaching the limits of my encoder. Wow. Here's the working code:
Rich (BB code):
;Simple Rotary Encoder Demonstration Programme
;Adapted from code by inglewoodpete, AllyCat

#PICAXE 40X2
#Terminal 38400            ;SerTxd at 32MHz
#No_Data                   ;Comment out if EEPROM data is to be initialised

;Tested with Grayhill 62AG11 optical encoder
;Symbol iEncoder0       = pinC.6 ;25 Digital input - bit A of encoder
;Symbol iEncoder1       = pinC.7 ;26 Digital input - bit B of encoder

Symbol bCurrentState     = b1    ; Current copy of encoder output
Symbol bPreviousState    = b2    ; Previous copy of encoder output
Symbol bTransition      = b3    ; previous and current states %0000aAbB
Symbol bCounter         = b4    ; Resulting integral count
Symbol bLookup         = b5 ; 0,1 or 2 looked-up from data
table 0,(2,1,0,2,0,2,2,1,1,2,2,0,2,0,1,2)  ; 0 = Up, 1 = Down , 2 = Error

Init: SetFreq M16
      Pause 1000             ;Allows time for terminal to open
      bCounter = 128
    SerTxd (CR, LF, "Rotary Encoder Demo", CR, LF)
    bPreviousState = PinsC And %11000000
Main:
      Do ; ****  M a i n    L o o p  ****
        bCurrentState = PinsC And %11000000
        If bCurrentState <> bPreviousState Then    ; Encoder has moved
                bTransition = bTransition + bTransition and 15    ; Shift left
                bit24 = bit15 ; grab latest B code into b3 bit0
                bit26 = bit14 ; grab latest A code into b3 bit2
                readtable bTransition, bLookup ; read value at b3 into bLookup
                on bLookup goto up, down ; Can fall into "down:" or trap error
                sertxd("error ")        ; Optional
down:
                dec bCounter
                goto done
up:       
                inc bCounter
done:
                SerTxd(#bCounter, CR, LF)
                  bPreviousState = bCurrentState    ; Save for next comparison
          EndIf
    Loop
That's definitely going to help give me a phenomenal rate of increase of volume! :) 🙃

Pops.
 

inglewoodpete

Senior Member
There are three ways of receiving IR with a PICAXE. Several years ago, I built a Home Theatre Controller (commonly called a "receiver" nowadays). At the time I had a Sony TV, so could not use the native PICAXE IRIN command.
  1. Using IRIN command. As you've found, it is a blocking command even if you use a timeout. So things become a little unpredictable.
  2. I designed a circuit that used a separate 08M (now replaced with an 08M2) and a CD4091 schmitt trigger to separate the start pulse, from the 0 and 1 bits. The software in the PICAXE was be written to work with any NEC 32-bit code remote (which is most remotes from my experience). The sole task of the 08M2 is to wait for IR codes and, when a valid code is received, set a flag for the main processor (a 40X2 in my case) before sending a serial code to the master.
  3. Use "background" (software-only) IR detection as I documented here, in "Completed Projects". This also worked for a wide range of IR remote controls using the NEC protocol.
 

popchops

Well-known member
There are three ways of receiving IR with a PICAXE. Several years ago, I built a Home Theatre Controller (commonly called a "receiver" nowadays). At the time I had a Sony TV, so could not use the native PICAXE IRIN command.
That's very interesting... I'm only aiming for stereo volume and source selection, but with digital control because I'm fed up with crackly pots and switches.

At the moment, I'm thinking of hiding the IRIN in an ELSEIF clause in the main loop:

Rich (BB code):
If bCurrentState <> bPreviousState Then
Then, if the optical encoder is turned then the IRIN code and the delay will not be executed. I can't imagine wanting to use the knob and the remote at the same time.

If this is too clunky then I'm inclined to use two picaxe chips, with one simply reporting the knob count since power up, and the other combining the count with IR requests for INC or DEC, commanding the PGA2311, and communicating with OLED (slowly).

What's the best 'idiotproof' way to send an 8-bit count from one PICAXE to another, refreshing at ~30 Hz? (I'm tempted to use 8 parallel pins). Can then just read with 'pinsC' for example.

Pops.
 
Last edited:

AllyCat

Senior Member
Hi,
What's the best 'idiotproof' way to send an 8-bit count from one PICAXE to another, refreshing at ~30 Hz?
As usual, the first qualifier has to be "Which PICaxe(s) ?". But by far the easiest and most reliable method is probably to transmit a serial byte (from almost any pin, at a convenient baud rate and polarity) and receive it with the HSERIN hardware. Then you have only to poll the command or SFR at least once every 30ms (which shouldn't be difficult to interleave with the Rotary Encoder loop).

Generally, there are two pins able to receive HSERIN (including M2s) and there are some "issues" with the M2s firmware. But they're mainly concerned with receiving multiple bytes and I do consider them nearly all solved or "solveable". Your starting point should be an examination of the HSERIN command (valid pins are in PICaxe Manual 1 or the base PIC data sheet), or a forum search for more detail. ;)

As for the IR, I'd probably keep a record of how long ago the last Encoder pulse was detected (up to a second or two), "sniff" the IR within the polling loop and bale out (to an IRIN) if IR activity is detected and the Encoder was not "recently" active.

Cheers, Alan.
 
Top