Interrupts are an efficient way for a program to be able to respond immediately to a specific event. In the previous article I explained the basics of using interrupts in RPi.GPIO and gave an example of a simple “wait for an event” interrupt program.
In this second article I will introduce “threaded callback” which opens up a lot of new possibilities.
Threaded callback – what the heck is that?
I know it sounds complicated. And it probably is complicated in the C code it’s written in, but we’re Pythonites and we don’t have to go there. ;)
If you remember the previous example program was just a simple “wait for port 23 to be connected to GND when we press the button and then print a message and exit the program”.
So, while it was waiting, the program wasn’t doing anything else. The program only had one thread, which means only one thing was being done at once. Python is capable of running more than one thread at once. It’s called multi-threading. It means that you can go through more than one piece of code simultaneously. This is where we can reap the benefit of interrupts because we can do something else while we wait for our “event” to happen. (Just like your “postman detector” allowed you to get on with something else instead of being distracted by waiting for the mail.)
So that covers the threading part of threaded callback. What’s a callback?
When an event is detected in the second thread, it communicates this back to the main thread (calls back). What we now have in RPi.GPIO is the ability to start a new thread for an interrupt and specify a set of instructions (function) that will run when the interrupt occurs in the second thread. This is a threaded callback function.
This is like your “postman detector” giving you a list of reminders of things you wanted to do when your delivery arrives AND doing them for you, so you can carry on with what you want to be doing.
So What are we going to do now?
We’ll keep most of what we did before and add another button and an event detect threaded callback that runs when the new button is pressed, even though we are still waiting for the first button to be pressed.
But this time, the new button will connect GPIO port 24 to 3.3V (3V3) when pressed. This will allow us to demonstrate a rising edge detection. So we’ll be setting up port 24 with the built in pulldown resistor enabled.
Later on we’ll have to modify the code to cope with ‘button bounce’, but we won’t say any more about that just yet.
Do you need to update RPi.GPIO?
If you didn’t do it for the first example, you will quite likely need to update your RPi.GPIO package. You can check what version of RPi.GPIO you have in the command line with…
import RPi.GPIO as GPIO
This should show you what RPi.GPIO version you have. You need 0.5.2a or higher for this example.
You can exit the python environment with
Install RPi.GPIO version 0.5.2 for simple interrupts
If you need to, you can install 0.5.2 or later with
sudo apt-get update (This will update all your Raspbian packages and may take up to an hour)
sudo apt-get dist-upgrade
Update July 2014
The best way to get the latest RPi.GPIO (currently 0.5.5) is to flash a new SD card with the latest NOOBS or Raspbian. This will give you a clean start with the latest version of RPi.GPIO.
And now onto the code
I’ve put most of the explanation in the code, so that if you use it, you will still have it.
#!/usr/bin/env python2.7 # script by Alex Eames https://raspi.tv import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) # GPIO 23 & 24 set up as inputs. One pulled up, the other down. # 23 will go to GND when button pressed and 24 will go to 3V3 (3.3V) # this enables us to demonstrate both rising and falling edge detection GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # now we'll define the threaded callback function # this will run in another thread when our event is detected def my_callback(channel): print "Rising edge detected on port 24 - even though, in the main thread," print "we are still waiting for a falling edge - how cool?\n" print "Make sure you have a button connected so that when pressed" print "it will connect GPIO port 23 (pin 16) to GND (pin 6)\n" print "You will also need a second button connected so that when pressed" print "it will connect GPIO port 24 (pin 18) to 3V3 (pin 1)" raw_input("Press Enter when ready\n>") # The GPIO.add_event_detect() line below set things up so that # when a rising edge is detected on port 24, regardless of whatever # else is happening in the program, the function "my_callback" will be run # It will happen even while the program is waiting for # a falling edge on the other button. GPIO.add_event_detect(24, GPIO.RISING, callback=my_callback) try: print "Waiting for falling edge on port 23" GPIO.wait_for_edge(23, GPIO.FALLING) print "Falling edge detected. Here endeth the second lesson." except KeyboardInterrupt: GPIO.cleanup() # clean up GPIO on CTRL+C exit GPIO.cleanup() # clean up GPIO on normal exit
Two ways to get the above code on your Pi
If you are in the command line on your Pi, type…
Then click “copy to clipboard” (above) and paste into the nano window. Then
Alternatively, you can download this directly to your Pi using…
Then you can run it with…
sudo python interrupt2.py
What’s supposed to happen?
When you run the code it gives you a message “Waiting for falling edge on port 23”
If you press button 1, it will terminate the program as before and give you a message
“Falling edge detected.”
If, instead of button 1, you press button 2, you’ll get a message
“Rising edge detected on port 24”.
This will occur as many times as you press the button. The program is still waiting for the original falling edge on port 23, and it won’t terminate until it gets it. Because your second button press is detected in another thread, it doesn’t affect the main thread.
You may also notice, depending on how cleanly you press the second button, that sometimes you get more than one message for just one button press. This is called “switch bounce”.
Bouncy, bouncy, bouncy
When you press a button switch, the springy contacts may flex and rapidly make and break contact one or more times. This may cause more than one edge detection to trigger, so you may get more than one message for one button press. There is, of course, a way round it, in software.
Why didn’t this happen before?
I hear you ask. The answer is simple. Last time the program was simply waiting for a single button press. As soon as that button press was detected, it stopped waiting. So if the switch bounced, it was ignored. The program had already moved on. In our case, it had closed. But, when the event detection is running constantly in another thread, this is not the case and we actually need to slow things down a bit in what is called “software debouncing”.
How to do software debouncing
In order to debounce, we need to be able to measure time intervals. To do that, we use the time module. Near the top of the program we need to add a line…
import time. I add it immediately after the RPi.GPIO import
import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM)
Then, also quite near the top of the program we need to set an intial value for the variable time_stamp that we will use to measure time intervals
time_stamp = time.time() I put this after the GPIO.setup commands. This sets time_stamp equal to the time in seconds right now.
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) time_stamp = time.time()
Now we have to change our threaded callback function from
def my_callback(channel): print "Rising edge detected on port 24 - even though, in the main thread," print "we are still waiting for a falling edge - how cool?\n"
def my_callback(channel): global time_stamp # put in to debounce time_now = time.time() if (time_now - time_stamp) >= 0.3: print "Rising edge detected on port 24 - even though, in the main thread," print "we are still waiting for a falling edge - how cool?\n" time_stamp = time_now
And now, even if you deliberately press the button twice, as long as you do it within 0.3 seconds it will only register one button press. You have debounced the switch, by forcing the program to ignore a button press if it occurs less than 0.3 seconds after the previous one.
If you are IDLE (Python joke) you can get the amended code here…
How does this work?
time_now = time.time() stores the current time in seconds in the variable time_now.
Now look at the last line of the function.
time_stamp = time_now This stores the time in seconds when the function was started in a global variable called
time_stamp So next time this function is called, it will be able to check how much time has elapsed since the last time.
That’s what is happening in the if statement.
if (time_now - time_stamp) >= 0.3: In English this means “If more than 0.3 seconds has elapsed since the last button press, execute the code in the indented block”.
So if more than 0.3 seconds have elapsed it would print the message…
“Rising edge detected on port 24 – even though, in the main thread we are still waiting for a falling edge – how cool?”
If less than 0.3 seconds has elapsed, it will do nothing. Job done. Button switch is debounced.
Update – RPi.GPIO 0.5.2 onwards includes this debounce algorithm
Ben has included the above debounce algorithm in 0.5.2 onwards. This article was originally written for 0.5.1. So the above debounce code has been superseded by adding
bouncetime=xxx, where xxx is a time in milliseconds. e.g.
GPIO.add_event_detect(channel, GPIO.RISING, callback=my_callback, bouncetime=200)
I will update the next example to reflect this.
So, what’s next?
- We’ve learnt about simple “wait for” interrupts in the previous article
- We’ve covered threaded callback in this article.
- In the next article, we’ll go over the situation where you want to do more than one threaded callback interrupt at once.
If you can’t wait for the next article (coming soon to a blog near you) check out the documentation here and press on by yourself.
I hope you are enjoying this series. Click here for Part 3.