/ examples / quadrature_encoder.py
quadrature_encoder.py
  1  import array
  2  import sys
  3  
  4  import adafruit_pioasm
  5  
  6  if sys.implementation.name == 'circuitpython':
  7      from rp2pio import StateMachine
  8      _n_read = 9
  9  else:
 10      from adafruit_rp1pio import StateMachine
 11      _n_read = 17
 12  
 13  _program = adafruit_pioasm.Program("""
 14  ;
 15  ; Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
 16  ;
 17  ; SPDX-License-Identifier: BSD-3-Clause
 18  ;
 19  .pio_version 0 // only requires PIO version 0
 20  
 21  .program quadrature_encoder
 22  
 23  ; the code must be loaded at address 0, because it uses computed jumps
 24  .origin 0
 25  
 26  
 27  ; the code works by running a loop that continuously shifts the 2 phase pins into
 28  ; ISR and looks at the lower 4 bits to do a computed jump to an instruction that
 29  ; does the proper "do nothing" | "increment" | "decrement" action for that pin
 30  ; state change (or no change)
 31  
 32  ; ISR holds the last state of the 2 pins during most of the code. The Y register
 33  ; keeps the current encoder count and is incremented / decremented according to
 34  ; the steps sampled
 35  
 36  ; the program keeps trying to write the current count to the RX FIFO without
 37  ; blocking. To read the current count, the user code must drain the FIFO first
 38  ; and wait for a fresh sample (takes ~4 SM cycles on average). The worst case
 39  ; sampling loop takes 10 cycles, so this program is able to read step rates up
 40  ; to sysclk / 10  (e.g., sysclk 125MHz, max step rate = 12.5 Msteps/sec)
 41  
 42  ; 00 state
 43      jmp update    ; read 00
 44      jmp decrement ; read 01
 45      jmp increment ; read 10
 46      jmp update    ; read 11
 47  
 48  ; 01 state
 49      jmp increment ; read 00
 50      jmp update    ; read 01
 51      jmp update    ; read 10
 52      jmp decrement ; read 11
 53  
 54  ; 10 state
 55      jmp decrement ; read 00
 56      jmp update    ; read 01
 57      jmp update    ; read 10
 58      jmp increment ; read 11
 59  
 60  ; to reduce code size, the last 2 states are implemented in place and become the
 61  ; target for the other jumps
 62  
 63  ; 11 state
 64      jmp update    ; read 00
 65      jmp increment ; read 01
 66  decrement:
 67      ; note: the target of this instruction must be the next address, so that
 68      ; the effect of the instruction does not depend on the value of Y. The
 69      ; same is true for the "jmp y--" below. Basically "jmp y--, <next addr>"
 70      ; is just a pure "decrement y" instruction, with no other side effects
 71      jmp y--, update ; read 10
 72  
 73      ; this is where the main loop starts
 74  .wrap_target
 75  update:
 76      mov isr, y      ; read 11
 77      push noblock
 78  
 79  sample_pins:
 80      ; we shift into ISR the last state of the 2 input pins (now in OSR) and
 81      ; the new state of the 2 pins, thus producing the 4 bit target for the
 82      ; computed jump into the correct action for this state. Both the PUSH
 83      ; above and the OUT below zero out the other bits in ISR
 84      out isr, 2
 85      in pins, 2
 86  
 87      ; save the state in the OSR, so that we can use ISR for other purposes
 88      mov osr, isr
 89      ; jump to the correct state machine action
 90      mov pc, isr
 91  
 92      ; the PIO does not have a increment instruction, so to do that we do a
 93      ; negate, decrement, negate sequence
 94  increment:
 95      mov y, ~y
 96      jmp y--, increment_cont
 97  increment_cont:
 98      mov y, ~y
 99  .wrap    ; the .wrap here avoids one jump instruction and saves a cycle too
100  """)
101  
102  _zero_y = adafruit_pioasm.assemble("set y 0")
103  
104  class IncrementalEncoder:
105      def __init__(self, pin_a, pin_b=None, divisor=4):
106          """Create an incremental encoder on pin_a and the next higher pin
107  
108          Always operates in "x4" mode (one count per quadrature edge)
109  
110          Assumes but does not check that pin_b is one above pin_a."""
111          self._sm = StateMachine(
112              _program.assembled,
113              frequency=0,
114              init=_zero_y,
115              first_in_pin=pin_a,
116              in_pin_count=2,
117              pull_in_pin_up=0x3,
118              auto_push=True,
119              push_threshold=32,
120              in_shift_right=False,
121              **_program.pio_kwargs
122          )
123          self._buffer = array.array('i',[0] * _n_read)
124          self.divisor = divisor
125          self._position = 0
126  
127      def deinit(self):
128          self._sm.deinit()
129  
130      @property
131      def position(self):
132          self._sm.readinto(self._buffer) # read N stale values + 1 fresh value
133          raw_position = self._buffer[-1]
134          delta = int((raw_position - self._position * self.divisor) / self.divisor)
135          self._position += delta
136          return self._position
137  
138  if __name__ == '__main__':
139      import board
140      # D17/D18 on header pins 11/12
141      # GND on header pin 6/9
142      # +5V on header pins 2/4
143      q = IncrementalEncoder(board.D17)
144      old_position = q.position
145      while True:
146          position = q.position
147          if position != old_position:
148              delta = position - old_position
149              print(f"{position:8d} {delta=}")
150          old_position = position