Created by Franck Albinet with source available here. Also presented by NSRC at APRICOT 2018.
Button: Detecting Pysense board button pressure
Introduction
In this example, we will detect the pressure of the button on the Pysense board. Because the board does not have a keyboard (or mouse), we will use the USB connection between the board and the development PC to detect the pressure of the button.
The Pysense board has one button as shown at the bottom in the picture below.
Learning outcomes
You will learn how to: detect the pressure of the button; implement Polling and Interrupts techniques * tackle iteratively a coding "challenge" (from naive but functioning programs to implementations fostering reusability and modularity)
Required Components
For this example you will need:
- a LoPy module plugged into a Pysense board
- a microUSB cable
- a development PC
The source code is in the src/button
directory. > Make sure you press the button on the Pysense board (highlighted by a red rectangle in image above) and not the one on the LoPy module (reset button).
boot.py
from machine import UART import os uart = UART(0, 115200) os.dupterm(uart)
The boot.py file should always start with the above code, so we can run our python scripts over Serial or Telnet. Newer Pycom boards have this code already in the boot.py file.
Version 1: Polling button state
Even for simple program like this one, there are numerous possible implementations. We should aim for a straighforward solution first. Premature optimization or abstraction are common pitfalls.
The use case we want to address is the following: 1. when the button of the Pysense board is pressed, display a message "Button pressed" once in Atom's console; 2. when released, display a message "Button released" once.
main.py
from machine import Pin button = Pin("P14", mode=Pin.IN, pull=Pin.PULL_UP) is_pressed = False while True: if button() == 0 and not is_pressed: print("Button pressed") is_pressed = True elif button() == 1 and is_pressed: print("Button released") is_pressed = False else: pass
Let's go through the snippet code above:
# we import the Pin `class` from machine Pycom modules from machine import Pin # then we create a `pin` object button = Pin("P14", mode=Pin.IN, pull=Pin.PULL_UP)
You notice that to create a Pin
object we need to specify three arguments:
- pin's
id
"P14"
- see Pysense board manual "pysense-pinout-comp.pdf" in/labs/references
; - pin's mode
Pin.IN
specifying that this is an input - pull method
Pin.PULL_UP
specifying we want a pull-up resistor. Loosely speaking, a pull-up or pull-down resistor will ensure that the pin is in either a high or low state, while also using a low amount of current and as a result prevents unknown state of the input. You can consult this blog post for further information https://learn.sparkfun.com/tutorials/pull-up-resistors.
Now that we have a button object, let's read repeatedly the state of the input. In our case when not pressed the button's value button()
should be 1 and 0 when pressed (pull-up resistor).
# flag saving previous state of the button is_pressed = False # we keep reading the input value while True: # when pressed and was not previously pressed if button() == 0 and not is_pressed: print("Button pressed") # as it has been pressed we update button's status is_pressed = True # if no pressed and was pressed previously elif button() == 1 and is_pressed: print("Button released") is_pressed = False else: pass
To read the value/state of the button, simply call button()
.
The flag is_pressed
allows to display messages once otherwise the message would be printed as long as you press or release the button which is not the expected use case.
This is important to note that in the implementation above, we keep reading the state of the button potentially every 100μs given that the clock rate is 160MHz. This is most probably a waste of CPU time and energy (remember that low consumption is key in IoT world) given that button pressure might not be a so much frequent event and that we might not require a response time of micro seconds.
A first slight improvement could be to pause the execution for half a second for instance: time.sleep_ms(500)
(insert it somewhere in the while
loop into main.py
).
A better alternative might be however to use in that situation Interrupts.
Version 2: Interrupt Service Routine (ISR)
We do not want to waste time checking button's state every 100μs
(just an order of magnitude in our use case), instead we will use a technique called "Interrupt Service Routine (ISR)".
ISR, is a special block of code associated with a specific interrupt condition, here button status. When this condition is met, a handler
(a function) will be called to "do something" in response.
When an interrupt occurs, the current task is halted (preempts the current flow of control), execution context is saved, the interrupt handler "do something" (in our case simply print a message) and then previous execution is restored. For many reasons, it is highly desired that the interrupt handler executes as briefly as possible. Consider for instance that several interrupts might occur more or less simultaneously and the system need to manage priorities and concurrency.
In our case this is simple:
from machine import Pin is_pressed = False def handler(pin): global is_pressed value = pin.value() if not value and not is_pressed: print('Button pressed') is_pressed = True elif value and is_pressed: print('Button released') is_pressed = False else: pass btn = Pin("P14", mode=Pin.IN, pull=Pin.PULL_UP) btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, handler)
Let's unpack the code above:
from machine import Pin is_pressed = False
Nothing new here, just import the Pin class
from machine
module and intialize our button flag.
def handler(pin): global is_pressed value = pin.value() if not value and not is_pressed: print('Button pressed') is_pressed = True elif value and is_pressed: print('Button released') is_pressed = False else: pass
We define a function (the handler), that will we called when an interrupts occur. The body of the handler is the same as in previous implementation.
# we create the pin object btn = Pin("P14", mode=Pin.IN, pull=Pin.PULL_UP) btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, handler)
In the last line, we specify that our handler
will be called (callback) when the pin' state is either transitioning from 1 to 0 (IRQFALLING) or from 0 to 1 (IRQRISING).
Advanced code: a bit of abstraction
In the previous example, we had to use the global
keyword to save a state. There is no good reason to store such state globally; this should remain the button's business. Using global
is fine for snippet code of that size but it scales poorly and leads to non robust code (you will sooner or later loose track of which function and where you modified these global states).
In such situation, using a class
, we can encapsulate such state, add extra behaviours, create various instances of the Button classes.
from machine import Pin class Button: def __init__(self, id): self.pressed = False self.btn = Pin(id, mode=Pin.IN, pull=Pin.PULL_UP) def on(self): self.btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, self._handler) def off(self): self.btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, None) def _handler(self, pin): value = pin.value() if not value and not self.pressed: print('Button pushed') self.pressed = True elif value and self.pressed: print('Button released') self.pressed = False else: pass btn = Button('P14') btn.on()
In this implementation, button's state is encapsulated in the object itself and we can create various instances (objects) of Button simply by calling Button('button_id')
. That way we can re-use safely our code.
Let's unpack it a bit:
class Button: def __init__(self, id): self.pressed = False self.btn = Pin(id, mode=Pin.IN, pull=Pin.PULL_UP)
__init___
is a special method called at instance/object creation (when we call Button('button_id')
actually). We initialize here button's state and create it.
def on(self): self.btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, self._handler) def off(self): self.btn.callback(Pin.IRQ_FALLING | Pin.IRQ_RISING, None)
We create two methods to enable or disable our button. And finally,
def _handler(self, pin): value = pin.value() if not value and not self.pressed: print('Button pushed') self.pressed = True elif value and self.pressed: print('Button released') self.pressed = False else: pass
we define a method/handler as in previous implementation. The prefixed _
in _handler
is just a naming convention indicating that this method is supposed to be used internally by other class' methods and not exposed publicly as it is for on
and off
methods.
Exercises
Toggle the LED each time the button is pressed. If the LED is OFF, turn it ON by pressing the button. If the LED is ON, turn it OFF by pressing the button.
Increase a counter every time the button is pressed and visualize it.
Turn the red light on whenever the button is pressed for more than 3 seconds.