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 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.
Where can you get the module from?
ReplyDeleteebay, amazon - just search for "KY040"
Delete99p was the cheapest I saw.
Hey Martin!
ReplyDeleteI 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!
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.
DeleteI have merged your pull request thank you.
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.
ReplyDeletedef _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!
Martin, thanks for this code example. I have just received my encoder from China and you have saved me a great amount of time.
ReplyDeleteCan 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.
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/
DeleteI'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?
ReplyDeletePlease...
ReplyDeleteYou know if is possible build power on/off controller to Raspberry using push button of the KY-040?
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.
DeleteHi,
ReplyDeleteonly every second rotation-step is recognised with the library... do you have any idea why?
Not sure, maybe the bounce times are too long so you are missing the next turn. Try reducing them and see how it goes.
DeleteHi, is there a way to read discrete 1-click rotations, disregarding direction? I'm having a heck of a time figuring it out myself.
ReplyDeleteIm not sure I understand your problem. Wouldnt yo ujust use the code above and ignore the direction!?
DeleteThank 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 ?
ReplyDeleteWith your code, do you catch every single steps ?
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 ?
ReplyDeleteWith your code, do you catch every single steps ?