Tuesday 5 May 2015

Raspberry Pi and KY040 Rotary Encoder

For a Raspberry Pi powered vintage radio build I'm doing I wanted to replace the original tuning dial with a turning switch which I could use to skip tracks backwards and forwards.

Being notoriously tight I opted for the VERY cheap KY040 (Amazon UK, US) rotary encoder module (less than £1 from the right stores). What a didn't realise when I brought it was that it doesn't use the typical 'gray code' for its encoding - this led to more than a few hours of frustration, so here is some setup and code to save you from the same!


The KY040 has 5 pins:
  • GND - ground
  • + - 3.3v
  • SW - switch
  • DT - data
  • CLK - clock
The SW pin is the switch pin and goes high when the rotary encoder is pushed.

The CLK (clock) and DT (data) pins are how you read the direction the encoder has been turned. The CLK pin goes low when the encoder has been turned and the DT pin shows which was it has been turned, low for clockwise, high for anti clockwise.

The SW, CLK and DT pins should be connected to GPIO pins on the and Pi, the + and GND are pretty self explanatory and should be connected to a 3.3v and ground pin.


I connected CLK to GPIO 5, DT to GPIO 6 and SW to GPIO 13.

The code uses RPi.GPIO's edge detection functions to trigger a callback when the CLK pin goes low and then reads the value from the data pin to work out if it was turned clockwise or anti-clockwise.

The code repository is on github https://github.com/martinohanlon/KY040.

There is a class, KY040, which expects the GPIO pins to be passed and 2 callback functions which are called when the dial is turned or the switch is pressed.

#KY040 Python Class
#Martin O'Hanlon
#stuffaboutcode.com

import RPi.GPIO as GPIO
from time import sleep

class KY040:

    CLOCKWISE = 0
    ANTICLOCKWISE = 1
    
    def __init__(self, clockPin, dataPin, switchPin,
                 rotaryCallback, switchCallback):
        #persist values
        self.clockPin = clockPin
        self.dataPin = dataPin
        self.switchPin = switchPin
        self.rotaryCallback = rotaryCallback
        self.switchCallback = switchCallback

        #setup pins
        GPIO.setup(clockPin, GPIO.IN)
        GPIO.setup(dataPin, GPIO.IN)
        GPIO.setup(switchPin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    def start(self):
        GPIO.add_event_detect(self.clockPin,
                              GPIO.FALLING,
                              callback=self._clockCallback,
                              bouncetime=250)
        GPIO.add_event_detect(self.switchPin,
                              GPIO.FALLING,
                              callback=self._switchCallback,
                              bouncetime=300)

    def stop(self):
        GPIO.remove_event_detect(self.clockPin)
        GPIO.remove_event_detect(self.switchPin)
    
    def _clockCallback(self, pin):
        if GPIO.input(self.clockPin) == 0:
            data = GPIO.input(self.dataPin)
            if data == 1:
                self.rotaryCallback(self.ANTICLOCKWISE)
            else:
                self.rotaryCallback(self.CLOCKWISE)
    
    def _switchCallback(self, pin):
        if GPIO.input(self.switchPin) == 0:
            self.switchCallback()

#test
if __name__ == "__main__":
    
    CLOCKPIN = 5
    DATAPIN = 6
    SWITCHPIN = 13

    def rotaryChange(direction):
        print "turned - " + str(direction)
    def switchPressed():
        print "button pressed"

    GPIO.setmode(GPIO.BCM)
    
    ky040 = KY040(CLOCKPIN, DATAPIN, SWITCHPIN,
                  rotaryChange, switchPressed)

    ky040.start()

    try:
        while True:
            sleep(0.1)
    finally:
        ky040.stop()
        GPIO.cleanup()

You can buy the same rotary encoder without the module but you will have to add pull ups to the CLK and DT pins.

16 comments:

  1. Where can you get the module from?

    ReplyDelete
    Replies
    1. ebay, amazon - just search for "KY040"
      99p was the cheapest I saw.

      Delete
  2. Hey Martin!

    I needed a rotary encoder (for a radio-like project as well), so I ordered some KY-040 and tried your code! However, I did get a lot of faulty results (clockwise instead of counterclockwise). Did you experience that as well?

    Anyway, I managed to lower the number of faulty results by adding a check for a low clockPin in the _clockCallback method, similar to the _switchCallback method. If there is a reason why this check shouldn't be there, please enlighten me, perhaps I did something stupid ;-)

    I submitted a pull request for the change, mainly because I've never done this before and wanted to try it out :-) Anyway, thanks again for the code, and good luck with your project!

    ReplyDelete
    Replies
    1. I didnt receive the errors, in theory you shouldn't because you should only get a callback when the voltage falls but its not unusual for the edge detection to be tripped falsely so its not a bad idea to check the value. I suspect mine might have just been slightly better manufactured.

      I have merged your pull request thank you.

      Delete
  3. Super duper example! My KY040 switch is giving off some bad readings though so to stabalize that I had to require two results in the same direction to flush out bad readings. After that all is stable even when I reduce the bounce time to 100.

    def _clockCallback(self, pin):
    if GPIO.input(self.clockPin) == 0:
    data = GPIO.input(self.dataPin)
    if data != self.lastDirection:
    # Skip the first signal when we change direction.
    # This also removes false signals from my KY040 since it
    # requires two values in the same direction before rotaryCallback is called.
    self.lastDirection = data

    elif data == 1:
    self.rotaryCallback(self.ANTICLOCKWISE)
    ...
    ...

    self.lastDirection is initialized to -1 in __init__

    Anyways, thanks for the great example!

    ReplyDelete
  4. Martin, thanks for this code example. I have just received my encoder from China and you have saved me a great amount of time.

    Can you help me understand why your callback functions include the argument 'pin'?
    def _clockCallback(self, pin):

    I don't see it used anywhere in the code. My best guess is that GPIO.add_event_detect returns the 'pin' that was 'active' during the event detected and in this application that information is already known so it is safely ignored.

    ReplyDelete
    Replies
    1. Your right, RPi.GPIO returns the pin (aka channel) in the callback. If I hadn't specified a variable in the function def it would have errored. see documentation here - http://sourceforge.net/p/raspberry-gpio-python/wiki/Inputs/

      Delete
  5. I'm use another microcontroller. This sensor documentation says that it works with 5V, I don't understand why here you use 3.3V, it works the same way with no problems?

    ReplyDelete
  6. Please...
    You know if is possible build power on/off controller to Raspberry using push button of the KY-040?

    ReplyDelete
    Replies
    1. Shutting down would be pretty easy, just called sudo halt, but there is no way to turn the power off unless you disconnect the power. Im sure there is a solution but you are going to be looking at a 'fairly' complicated software and hardware solution.

      Delete
  7. Hi,
    only every second rotation-step is recognised with the library... do you have any idea why?

    ReplyDelete
    Replies
    1. Not sure, maybe the bounce times are too long so you are missing the next turn. Try reducing them and see how it goes.

      Delete
  8. Hi, is there a way to read discrete 1-click rotations, disregarding direction? I'm having a heck of a time figuring it out myself.

    ReplyDelete
    Replies
    1. Im not sure I understand your problem. Wouldnt yo ujust use the code above and ignore the direction!?

      Delete
  9. Thank you for your code. I have reduced the bounce time to 100, and my encoder is still missing steps. Would you have an idea how to catch all the steps ?

    With your code, do you catch every single steps ?

    ReplyDelete
  10. Thank you for your code. I have reduced the bounce time to 100, and my encoder is still missing steps. Would you have an idea how to catch all the steps ?

    With your code, do you catch every single steps ?

    ReplyDelete

Note: only a member of this blog may post a comment.