Friday, 18 January 2013

Rotary Encoders on RPi

I've been playing with an incremental rotary encoder on the Raspberry Pi - for no better reason than the mountaineers' lame excuse "because it's there"...


The incremental rotary encoder produces two quadrature square waves (as you can learn by reading any of a number of descriptions including this one). By detecting the relative phase of the square waves, you can figure the direction of rotation and (if you are really keen to encode angular velocity accurately) the speed of rotation too.

I settled for a clumsy attempt to decode direction alone, using this circuit...


Of course, you need some code too - I wrote the following simple demonstration in Python. It detects positive-going transitions on "PinA" and looks at the state of PinB during these transition events to figure the direction in which the encoder has been moved.


#!/usr/local/bin/python
# m0xpd
# shack.nasties 'at Gee Male dot com'
import RPi.GPIO as GPIO
# setup GPIO options...
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
# setup IO bits...
GPIO.setup(12, GPIO.IN) # PinA is on GPIO 12
GPIO.setup(16, GPIO.IN) # PinB is on GPIO 16
# check initial state...
oldPinA = GPIO.input(12)
Position = 0
inc = 5 # increment
REmax = 127 # Max value
REmin = 0 # Min value
# Main Operating Loop...
while True:
encoderPinA=GPIO.input(12)
encoderPinB=GPIO.input(16)
if (encoderPinA == True) and (oldPinA == False): # Detect "upward" transition on PinA
if (encoderPinB == False): # if PinB is low...
Position=Position+inc # we're going clokwise
if Position > REmax: # limit max value
Position = REmax
else: # otherwise...
Position=Position-inc # we're going anti-clockwise
if Position < REmin: # limit min value
Position = REmin
print Position
oldPinA=encoderPinA # save current value for next time

After the fun-and-games with the MIDI controller, I'm really excited about object-oriented software for the RPi, so here's an elaboration which includes a class for Rotary Encoders, connected to any two GPIO pins on the RPi. There's also facility to read a push-button (which some encoders - including mine - feature).


#!/usr/local/bin/python
# m0xpd
# shack.nasties 'at Gee Male dot com'
import RPi.GPIO as GPIO
# setup GPIO options...
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
class RotaryEnc:
'Base Class for Rotary Encoder on the RPi GPIO Pins'
def __init__(self,PinA,PinB,button,Position,REmin,REmax,inc):
self.PinA = PinA # GPIO Pin for encoder PinA
self.PinB = PinB # GPIO Pin for encoder PinB
self.button = button # GPIO Pin for encoder push button
self.Position = Position # encoder 'position'
self.min = REmin # Max value
self.max = REmax # Min value
self.inc = inc # increment
self.old_button = 1 # start value for previous button state
self.oldPinA = 1 # start value for previous PinA state
self.button_release = 0 # initialise outputs
self.button_down = 0 # initialise outputs
GPIO.setup(self.PinA, GPIO.IN) # setup IO bits...
GPIO.setup(self.PinB, GPIO.IN) #
GPIO.setup(self.button, GPIO.IN) #
#
def read(self): # Function to Read encoder...
encoderPinA=GPIO.input(self.PinA) # get inputs...
encoderPinB=GPIO.input(self.PinB) #
if encoderPinA and not self.oldPinA: # Transition on PinA?
if not encoderPinB: # Yes: is PinB High?
self.Position=self.Position+self.inc # No - so we're going clockwise
if self.Position > self.max: # limit maximum value
self.Position = self.max #
#
else: #
#
self.Position=self.Position-self.inc # Yes - so we're going anti-clockwise
if self.Position < self.min: # limit minimum value
self.Position = self.min #
self.oldPinA=encoderPinA # No: just save current PinA state for next time
#
def read_button(self): # Function to Read encoder button...
button=GPIO.input(self.button) # get input...
if button and not self.old_button: # 'Upward' transition on button?
self.button_release=1 # Yes - so set release output
else: #
self.button_release=0 # No - so clear it
if not button and self.old_button: # 'Downward' transition on button?
self.button_down = 1 # Yes - so set 'down' button
else: #
self.button_down = 0 # No - so clear it
self.old_button=button # Save current button state for next time
# -------------------------------------------------------------------------------------------------------------------
# Here's a simple example of how to use the RotaryEnc Class ...
#
# Set up Rotary Encoder...
# format is : RotaryEnc(PinA,PinB,button,Position,REmin,REmax,inc):
Position = 1 # Position is also used as a variable
RE1 = RotaryEnc(12,16,18,Position,0,10,1) # Instatiate the encoder
# Main Operating Loop...
while True:
RE1.read() # Read the encoder
if Position <> RE1.Position: # has the position changed?
print RE1.Position # Yes: so print new value
Position = RE1.Position # and update it
RE1.read_button() # Read the button
if RE1.button_down: # Has it been pressed?
print 'Button Pressed' # Yes: so print a message
if RE1.button_release: # Has it been released
print 'Button Released' # Yes: so print a message
Here's a screen shot showing me running the second program ...


The code works - but only just...

It is important to sample a rotary encoder at a sufficiently high sample rate to avoid aliasing which - in this case - results in missed "moves" or even moves of incorrect direction.

I cannot get the RPi to sample fast enough to guarantee that every movement is perfectly observed, whether in the "while" loop seen in the programs above, or in a tkinter ".after()" command with a delay argument of 1 millisecond. What's worse, the little graph of processor utilisation at the bottom right of the RPi desktop (like the PC's "Task Manager" or the Mac's "Activity Monitor") reveals that the processor is working hard to sample the encoder at this rate (as you can just see in the screenshot above). I don't know enough about RPi to know if an interrupt-driven approach, such as would be easy and obvious on a PIC, is possible or desirable.

Having said all that, the code above works well enough to let me add a "knob" to my simple RPi MIDI controller, making it much more user-friendly than the original mouse drag-able sliders.

Next, I hope to use the rotary encoder to control the frequency of the DDS generator module from eBay that's just arrived on my doormat - watch this space!

...-.- de m0xpd

5 comments:

  1. I'm looking for some help as a raw beginner. I'm not sure if I'm in the right place.
    I have a DC motor running with a small piece of Python code on the Pi. I'm using GPio pins to control the motor. The code is taken from one of the many sites that I have visited so I acknowledge the rights of the originator. The code is added below. I did make some modifications and it works.
    On the shaft of the motor is as wheel with 8 slots. These interrupt a Photo diode and a photo transistor. I need some help with the code in getting this input into my Pi. I would like to count the slots after a period of time running the motor. Most of the code that I have seen is for quadrature encoders.These are a bit more complex than what I have here.
    I appologise if I'm in the wrong area. Please point me in the right direction if I am.

    Many thanks
    George

    #!/usr/bin/python
    #http://www.raspberrypi.org/phpBB3/viewtopic.php?f=37&t=55288
    import RPi.GPIO as gpio
    import time

    loop_counter = 0

    try:
    gpio.setmode(gpio.BOARD)
    gpio.setup(7, gpio.OUT)
    gpio.setup(11, gpio.OUT)
    gpio.setup(13, gpio.OUT)
    gpio.setup(15, gpio.OUT)
    gpio.output(7, True)
    gpio.output(11, True)

    while loop_counter < 10:
    print "Start of motor loop"
    print "Motor forward for .25 second"
    gpio.output(13, True)
    gpio.output(15, False)
    time.sleep(.5)
    print "Motor reverse for 0.5 Second"
    gpio.output(13, False)
    gpio.output(15, True)
    time.sleep(.5)
    print "Motor forward for 0.15 Second"
    gpio.output(13, True)
    gpio.output(15, False)
    time.sleep(.25)
    print "Motor reverse for 0.5 Second"
    gpio.output(13, False)
    gpio.output(15, True)
    time.sleep (.5)
    #print "Motor Forward for 0.1 Second"
    loop_counter = loop_counter + 1;
    print "\n", loop_counter
    except KeyboardInterrupt:
    print "loop ending"

    finally:
    gpio.cleanup() # this ensures a clean exit
    print "Now the Ports are all sqeeky clean"


    ReplyDelete
  2. George. I really don't know enough to help you directly on this one in the context of the R Pi - I would do this on Arduino or a PIC and have no experience of this sort on the Pi.

    You'll want to count pulses from the optical sensor. This is (as you say) a rather different task to responding to a rotary encoder (not least because you're only talking about one input, whereas the rot. enc. uses two). HOWEVER, the "bones" of a rotary encoder interface routine (specifically an interrupt-driven, pulse-counting routine rather than the naive way in which I observed single steps) would teach you what you need to know. That's where I'd look if I were you.

    Good luck with your project.

    ReplyDelete
  3. Hello Sorry, you know the same application programming C?

    ReplyDelete
    Replies
    1. Fernando
      Not sure if I understand your question / comment...
      If you are asking about implementation of a simple rotary encoder read in "C", then the Arduino code for the "Occam's Microcontroller" rig includes essentially the same procedure as was expressed in this post in Python. You'll find the code on this page:
      https://sites.google.com/site/occamsmicrocontroller/home/software
      The Arduino code isn't pure C - but it is close enough to see what is happening.

      Delete
  4. Hi there,

    Thanks for your post, I was missing something quite important : POWER ! (Yeah I forgot to connect the 3V3 and I was asking myself : "But why does is this s**t not working?").

    You just saved my night and my hair, congratulations !

    Best

    ReplyDelete