1 # SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   4 # SPDX-License-Identifier: BSD-3-Clause
 
   6 `rotaryio` - Support for reading rotation sensors
 
   7 ===========================================================
 
   8 See `CircuitPython:rotaryio` in CircuitPython for more details.
 
  10 Raspberry Pi PIO implementation
 
  12 * Author(s): Melissa LeBlanc-Williams
 
  15 from __future__ import annotations
 
  17 import microcontroller
 
  20     import adafruit_pioasm
 
  21     from adafruit_rp1pio import StateMachine
 
  22 except ImportError as exc:
 
  24         "adafruit_pioasm and adafruit_rp1pio are required for this module"
 
  28 _program = adafruit_pioasm.Program(
 
  31 ; Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
 
  33 ; SPDX-License-Identifier: BSD-3-Clause
 
  35 .pio_version 0 // only requires PIO version 0
 
  37 .program quadrature_encoder
 
  39 ; the code must be loaded at address 0, because it uses computed jumps
 
  43 ; the code works by running a loop that continuously shifts the 2 phase pins into
 
  44 ; ISR and looks at the lower 4 bits to do a computed jump to an instruction that
 
  45 ; does the proper "do nothing" | "increment" | "decrement" action for that pin
 
  46 ; state change (or no change)
 
  48 ; ISR holds the last state of the 2 pins during most of the code. The Y register
 
  49 ; keeps the current encoder count and is incremented / decremented according to
 
  52 ; the program keeps trying to write the current count to the RX FIFO without
 
  53 ; blocking. To read the current count, the user code must drain the FIFO first
 
  54 ; and wait for a fresh sample (takes ~4 SM cycles on average). The worst case
 
  55 ; sampling loop takes 10 cycles, so this program is able to read step rates up
 
  56 ; to sysclk / 10  (e.g., sysclk 125MHz, max step rate = 12.5 Msteps/sec)
 
  60     jmp decrement ; read 01
 
  61     jmp increment ; read 10
 
  65     jmp increment ; read 00
 
  68     jmp decrement ; read 11
 
  71     jmp decrement ; read 00
 
  74     jmp increment ; read 11
 
  76 ; to reduce code size, the last 2 states are implemented in place and become the
 
  77 ; target for the other jumps
 
  81     jmp increment ; read 01
 
  83     ; note: the target of this instruction must be the next address, so that
 
  84     ; the effect of the instruction does not depend on the value of Y. The
 
  85     ; same is true for the "jmp y--" below. Basically "jmp y--, <next addr>"
 
  86     ; is just a pure "decrement y" instruction, with no other side effects
 
  87     jmp y--, update ; read 10
 
  89     ; this is where the main loop starts
 
  96     ; we shift into ISR the last state of the 2 input pins (now in OSR) and
 
  97     ; the new state of the 2 pins, thus producing the 4 bit target for the
 
  98     ; computed jump into the correct action for this state. Both the PUSH
 
  99     ; above and the OUT below zero out the other bits in ISR
 
 103     ; save the state in the OSR, so that we can use ISR for other purposes
 
 105     ; jump to the correct state machine action
 
 108     ; the PIO does not have a increment instruction, so to do that we do a
 
 109     ; negate, decrement, negate sequence
 
 112     jmp y--, increment_cont
 
 115 .wrap    ; the .wrap here avoids one jump instruction and saves a cycle too
 
 119 _zero_y = adafruit_pioasm.assemble("set y 0")
 
 122 class IncrementalEncoder:
 
 124     IncrementalEncoder determines the relative rotational position based on two series of
 
 125     pulses. It assumes that the encoder’s common pin(s) are connected to ground,and enables
 
 126     pull-ups on pin_a and pin_b.
 
 128     Create an IncrementalEncoder object associated with the given pins. It tracks the
 
 129     positional state of an incremental rotary encoder (also known as a quadrature encoder.)
 
 130     Position is relative to the position when the object is constructed.
 
 134         self, pin_a: microcontroller.Pin, pin_b: microcontroller.Pin, divisor: int = 4
 
 136         """Create an incremental encoder on pin_a and the next higher pin
 
 138         Always operates in "x4" mode (one count per quadrature edge)
 
 140         Assumes but does not check that pin_b is one above pin_a."""
 
 141         if pin_b is not None and pin_b.id != pin_a.id + 1:
 
 142             raise ValueError("pin_b must be None or one higher than pin_a")
 
 145             self._sm = StateMachine(
 
 154                 in_shift_right=False,
 
 155                 **_program.pio_kwargs,
 
 157         except RuntimeError as e:
 
 158             if "(error -13)" in e.args[0]:
 
 160                     "This feature requires a rules file to allow access to PIO. See "
 
 161                     "https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/"
 
 162                     "using-neopixels-on-the-pi-5#updating-permissions-3189429"
 
 165         self._buffer = array.array("i", [0] * _n_read)
 
 166         self.divisor = divisor
 
 170         """Deinitializes the IncrementalEncoder and releases any hardware resources for reuse."""
 
 173     def __enter__(self) -> IncrementalEncoder:
 
 174         """No-op used by Context Managers."""
 
 177     def __exit__(self, _type, _value, _traceback):
 
 179         Automatically deinitializes when exiting a context. See
 
 180         :ref:`lifetime-and-contextmanagers` for more info.
 
 186         """The current position in terms of pulses. The number of pulses per rotation is defined
 
 187         by the specific hardware and by the divisor."""
 
 188         self._sm.readinto(self._buffer)  # read N stale values + 1 fresh value
 
 189         raw_position = self._buffer[-1]
 
 190         delta = int((raw_position - self._position * self.divisor) / self.divisor)
 
 191         self._position += delta
 
 192         return self._position