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