Thinking about using a rotary encoder to focus a Raspberry Pi lens led to a testbed:

There’s not much to it, because the RPi can enable pullup resistors on its digital inputs, whereupon the encoder switches its code bits to common. The third oscilloscope probe to the rear syncs on a trigger output from my knob driver.
I started with the Encoder library from PyPi, but the setup code doesn’t enable the pullup resistors and the interrupt (well, it’s a callback) handler discards the previous encoder state before using it, so the thing can’t work. I kept the overall structure, gutted the code, and rebuilt it around a state table. The code appears at the bottom, but you won’t need it.
Here’s the problem, all in one image:

The top two traces are the A and B encoder bits. The bottom trace is the trigger output from the interrupt handler, which goes high at the start of the handler and low at the end, with a negative blip in the middle when it detects a “no motion” situation: the encoder output hasn’t changed from the last time it was invoked.
Over on the left, where the knob is turning relatively slowly, the first two edges have an interrupt apiece. A detailed view shows them in action (the bottom half enlarge the non-shaded part of the top half):

Notice that each interrupt occurs about 5 ms after the edge causing it!
When the edges occur less than 5 ms apart, the driver can’t keep up. The next four edges produce only three interrupts:

A closer look at the three interrupts shows all of them produced the “no motion” pulse, because they all sampled the same (incorrect) input bits:

In fact, no matter how many edges occur, you only get three interrupts:

The groups of interrupts never occur less than 5 ms apart, no matter how many edges they’ve missed. Casual searching suggests the Linux Completely Fair Scheduler has a minimum timeslice / thread runtime around 5 ms, so the encoder may be running at the fastest possible response for a non-real-time Raspberry Pi kernel, at least with a Python handler.
If. I. Turn. The. Knob. Slowly. Then. It. Works. Fine. But. That. Is. Not. Practical. For. My. Purposes.
Nor anybody else’s purposes, really, which leads me to think very few people have ever tried lashing a rotary encoder to a Raspberry Pi.
So, OK, I’ll go with Nearer
and Farther
focusing buttons.
The same casual searching suggested tweaking the Python thread’s priority / niceness could lock it to a different CPU core and, obviously, writing the knob handler in C / C++ / any other language would improve the situation, but IMO the result doesn’t justify the effort.
It’s worth noting that writing “portable code” involves more than just getting it to run on a different system with different hardware. Rotary encoder handlers are trivial on an Arduino or, as in this case, even an ARM-based Teensy, but “the same logic” doesn’t deliver the same results on an RPi.
My attempt at a Python encoder driver + simple test program as a GitHub Gist:
# Rotary encoder test driver | |
# Ed Nisley - KE4ZNU | |
# Adapted from https://github.com/mivallion/Encoder | |
# State table from https://github.com/PaulStoffregen/Encoder | |
import RPi.GPIO as GPIO | |
class Encoder(object): | |
def __init__(self, A, B, T=None, Delay=None): | |
GPIO.setmode(GPIO.BCM) | |
self.T = T | |
if T is not None: | |
GPIO.setup(T, GPIO.OUT) | |
GPIO.output(T,0) | |
GPIO.setup(A, GPIO.IN, pull_up_down=GPIO.PUD_UP) | |
GPIO.setup(B, GPIO.IN, pull_up_down=GPIO.PUD_UP) | |
self.delay = Delay | |
self.A = A | |
self.B = B | |
self.pos = 0 | |
self.state = (GPIO.input(B) << 1) | GPIO.input(A) | |
self.edges = (0,1,-1,2,-1,0,-2,1,1,-2,0,-1,2,-1,1,0) | |
if self.delay is not None: | |
GPIO.add_event_detect(A, GPIO.BOTH, callback=self.__update, | |
bouncetime=self.delay) | |
GPIO.add_event_detect(B, GPIO.BOTH, callback=self.__update, | |
bouncetime=self.delay) | |
else: | |
GPIO.add_event_detect(A, GPIO.BOTH, callback=self.__update) | |
GPIO.add_event_detect(B, GPIO.BOTH, callback=self.__update) | |
def __update(self, channel): | |
if self.T is not None: | |
GPIO.output(self.T,1) # flag entry | |
state = (self.state & 0b0011) \ | |
| (GPIO.input(self.B) << 3) \ | |
| (GPIO.input(self.A) << 2) | |
gflag = '' if self.edges[state] else ' - glitch' | |
if (self.T is not None) and not self.edges[state]: # flag no-motion glitch | |
GPIO.output(self.T,0) | |
GPIO.output(self.T,1) | |
self.pos += self.edges[state] | |
self.state = state >> 2 | |
# print(' {} - state: {:04b} pos: {}{}'.format(channel,state,self.pos,gflag)) | |
if self.T is not None: | |
GPIO.output(self.T,0) # flag exit | |
def read(self): | |
return self.pos | |
def read_reset(self): | |
rv = self.pos | |
self.pos = 0 | |
return rv | |
def write(self,pos): | |
self.pos = pos | |
if __name__ == "__main__": | |
import encoder | |
import time | |
from gpiozero import Button | |
btn = Button(26) | |
enc = encoder.Encoder(20, 21,T=16) | |
prev = enc.read() | |
while not btn.is_held : | |
now = enc.read() | |
if now != prev: | |
print('{:+4d}'.format(now)) | |
prev = now |
Hmm – There’s a encoder DT overlay (rotary-encoder.dtbo). I wonder if that would solve the delay issue?
Wholly Smoke! Sounds like exactly what’s needed!
Having used
evdev
events with a remote keypad, there’s a good chance I can (figure out how to) make encoder events pop up in a program.Bonus: I also found the
gpio-key
overlay to convert the encoder knob switch into a useful keycode event.Many, many thanks!
You’re more than welcome – I’ve gotten LOTS of great ideas from this blog! (Not to mention many new rabbit holes to explore)
I wonder if a i2c or spi chip with a quadrature decoder and up/down counter would be a useful thingy.
Some preliminary screwing around shows
rotary-encoder.dtbo
elegantly solves the entire problem: no need for more hardware. Hooray!I think many of the ARM processors have an FIQ . . .