Interrupt Handlers in MicroPython
What is an Interrupt Handler?
An Interrupt Handler (also called an ISR for Interrupt Service Request) is a special Python function that is called when specific events occur such as a button being pressed. ISRs are the preferred way to detect external events, as opposed to polling methods that are inconsistent and inefficient. However, they are a bit tricky to setup and debug. So a good design should be as simple as possible and avoid using complex features unless you really know you need them.
Polling vs. Interrupt Handlers
So why are ISRs so important? Let's illustrate this is a story.
Imagine you have 10 friends each with a button at their home. In the polling method you would need to drive to each of their houses and ask them "Is the button get pressed"? You would have to do this frequently in case the button was pressed and released too quickly. This is a slow and painful process and takes a lot of CPU cycles.
An interrupt handler on the other hand has each friend tell you directly if their button has been pressed. The messages are quick and efficient. They don't use a lot of extra CPU power and the results get handled quickly.
However, there are specific rules about what we can and can't do within an ISR function. They need to be quick and efficient. We can't wonder off and do crazy things like printing debugging lines within a good ISR. Our job is typically update a global value and finish ASAP. A good ISR should be as efficient as possible.
Simple Button Press ISR Example
This is our first ISR example. It has several parts:
- import statements - (pretty standard but we have to add the import micropython)
- global variables - we will update these to communicate the result of our ISR
- the callback function - this is the function will be automatically called on a pin event
- the button defintion - this is where we indicate what pin and the PULL_DOWN value
- the irq handler - this is where we associate the event with the callback function
- the main loop - here we only print the button press count if it has changed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Now if you run this program, you will see that it prints to the Terminal each time the button is pressed and it also tells us how many times the button has been pressed.
example output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
But if you are careful, you will note something slightly unexpected might happen. I the example above, I actually only pressed the button about 10 times. But the button value is 21! What could be going on here? Could there be a bug in the code?
The answer is that buttons are not perfect on/off switches. They are essentially noisy on/off devices that may go through a transition of off/on/off/on each time we press the button.
As a switch goes from open to closed, it moves from a stable state, through an unstable transition state and then it finally arrives at a new stable state. This is illustrated in the drawing below.
We can reduce this "noise" with a small capacitor next to the button. The capacitor will quickly absorb the energy of the button transition and it will "smooth" out the spikes. This will give us a more consistent readout of the number of button presses and avoid accidental "double presses" that were not intended.
However, we can also get a clean signal by using software. The key is when we first detect that a transition may be happening we "stop listening" for a short period of time until we are confident that the unstable transition state is over. This is typically around 20 milliseconds, but there may be a few stray signals left. Since we may not have to detect changes more frequently than 5 presses per second, we can go to sleep in our ISR for up to 200 milliseconds. This will give us a nice stable reading from the button.
These are general rules but for our breadboard mounted momentary switches, the values are appropriate.
Debounced Version of a Button Press Detection
Now let's show you the code that does the hard work of debouncing a signal from a button or switch.
In this example, our ISR is called button_pressed_handler(pin)
. As soon as it is called, it checks the number of milliseconds since it was last called. If the time difference is under 200 milliseconds we are good to go and we update the button_presses global variable. If we are under the 200 millisecond window, we might be in that transition state and we don't do anything.
1 2 3 4 5 |
|
The net effect is that the presses variable will ONLY be incremented once, and not multiple times during the transition. Here is the full code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
ISR with Deactivation
Although there are benefits to the simplicity of the code above, some microcontrollers developers suggest that you simply deactivate the IRQ during the debounce sleep. This makes sense since there is two small calculation of the time differences (a subtraction and a compare operation) that do not need to be performed.
The key lines we add are a deactivate of the IRQ, a sleep for 200 milliseconds and a re-enable of the IRQ after the sleep. Both approaches have worked for me and I will let you decide the tradeoffs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Debounce Without Disabling the IRQ
References
- MicroPython Documentation on Interrupt Handlers
- A Guide to Debouncing by Jack G. Ganssle - this is an excellent reference if you want to know how long to set the debounce intervals for various types of switches. It has lots of transition plots and shows the incredible variation in transition states of switches. In the Summary of the final page, Jack suggest that most low-cost switch denouncers should use a debounce period of between 20 and 50 milliseconds if the users want a fast response.