|
| 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 |
0 commit comments