]> Repositories - Adafruit_Blinka-hackapet.git/blob - src/adafruit_blinka/microcontroller/bcm283x/rotaryio.py
Merge pull request #981 from makermelissa/main
[Adafruit_Blinka-hackapet.git] / src / adafruit_blinka / microcontroller / bcm283x / rotaryio.py
1 # SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4 # SPDX-License-Identifier: BSD-3-Clause
5 """
6 `rotaryio` - Support for reading rotation sensors
7 ===========================================================
8 See `CircuitPython:rotaryio` in CircuitPython for more details.
9
10 Raspberry Pi PIO implementation
11
12 * Author(s): Melissa LeBlanc-Williams
13 """
14
15 from __future__ import annotations
16 import array
17 import microcontroller
18
19 try:
20     import adafruit_pioasm
21     from adafruit_rp1pio import StateMachine
22 except ImportError as exc:
23     raise ImportError(
24         "adafruit_pioasm and adafruit_rp1pio are required for this module"
25     ) from exc
26
27 _n_read = 17
28 _program = adafruit_pioasm.Program(
29     """
30 ;
31 ; Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
32 ;
33 ; SPDX-License-Identifier: BSD-3-Clause
34 ;
35 .pio_version 0 // only requires PIO version 0
36
37 .program quadrature_encoder
38
39 ; the code must be loaded at address 0, because it uses computed jumps
40 .origin 0
41
42
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)
47
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
50 ; the steps sampled
51
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)
57
58 ; 00 state
59     jmp update    ; read 00
60     jmp decrement ; read 01
61     jmp increment ; read 10
62     jmp update    ; read 11
63
64 ; 01 state
65     jmp increment ; read 00
66     jmp update    ; read 01
67     jmp update    ; read 10
68     jmp decrement ; read 11
69
70 ; 10 state
71     jmp decrement ; read 00
72     jmp update    ; read 01
73     jmp update    ; read 10
74     jmp increment ; read 11
75
76 ; to reduce code size, the last 2 states are implemented in place and become the
77 ; target for the other jumps
78
79 ; 11 state
80     jmp update    ; read 00
81     jmp increment ; read 01
82 decrement:
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
88
89     ; this is where the main loop starts
90 .wrap_target
91 update:
92     mov isr, y      ; read 11
93     push noblock
94
95 sample_pins:
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
100     out isr, 2
101     in pins, 2
102
103     ; save the state in the OSR, so that we can use ISR for other purposes
104     mov osr, isr
105     ; jump to the correct state machine action
106     mov pc, isr
107
108     ; the PIO does not have a increment instruction, so to do that we do a
109     ; negate, decrement, negate sequence
110 increment:
111     mov y, ~y
112     jmp y--, increment_cont
113 increment_cont:
114     mov y, ~y
115 .wrap    ; the .wrap here avoids one jump instruction and saves a cycle too
116 """
117 )
118
119 _zero_y = adafruit_pioasm.assemble("set y 0")
120
121
122 class IncrementalEncoder:
123     """
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.
127
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.
131     """
132
133     def __init__(
134         self, pin_a: microcontroller.Pin, pin_b: microcontroller.Pin, divisor: int = 4
135     ):
136         """Create an incremental encoder on pin_a and the next higher pin
137
138         Always operates in "x4" mode (one count per quadrature edge)
139
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")
143
144         try:
145             self._sm = StateMachine(
146                 _program.assembled,
147                 frequency=0,
148                 init=_zero_y,
149                 first_in_pin=pin_a,
150                 in_pin_count=2,
151                 pull_in_pin_up=0x3,
152                 auto_push=True,
153                 push_threshold=32,
154                 in_shift_right=False,
155                 **_program.pio_kwargs,
156             )
157         except RuntimeError as e:
158             if "(error -13)" in e.args[0]:
159                 raise RuntimeError(
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"
163                 ) from e
164             raise
165         self._buffer = array.array("i", [0] * _n_read)
166         self.divisor = divisor
167         self._position = 0
168
169     def deinit(self):
170         """Deinitializes the IncrementalEncoder and releases any hardware resources for reuse."""
171         self._sm.deinit()
172
173     def __enter__(self) -> IncrementalEncoder:
174         """No-op used by Context Managers."""
175         return self
176
177     def __exit__(self, _type, _value, _traceback):
178         """
179         Automatically deinitializes when exiting a context. See
180         :ref:`lifetime-and-contextmanagers` for more info.
181         """
182         self.deinit()
183
184     @property
185     def position(self):
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