Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Created by Franck Albinet with source available here. Also presented by NSRC at APRICOT 2018.

Button: Detecting Pysense board button pressure

...

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.

...

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.

...

Code Block
languagepy
titlemain.py
linenumberstrue
from machine import Pin
import time

button = Pin("P14", mode=Pin.IN, pull=Pin.PULL_UP)

is_pressed = False

while True:
    if button() == 1 and not is_pressed:
        time.sleep(1)
    elif 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

...

Code Block
languagepy
linenumberstrue
# flag saving previous state of the button
is_pressed = False

# we keep reading the input value until the program is terminated

while True:
	# we do nothing until the #input whenvalue pressedis andchanged wasfor notthe previouslyfirst pressedtime
    if button() == 01 and not is_pressed:
        print("Button pressed")
  time.sleep(1)
	# when pressed and was not previously pressed
    elif # as it has been pressed we update button's statusbutton() == 0 and not is_pressed:
        print("Button pressed")
        is_pressed = True
    	# if nonot pressed and was pressed previously
    elif button() == 1 and is_pressed:
        print("Button released")
        is_pressed = False
    else:
        pass

...

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:

Code Block
languagepy
linenumberstrue
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:

Code Block
languagepy
linenumberstrue
from machine import Pin

is_pressed = False

Nothing new here, just import the Pin class from machine module and intialize our button flag.

Code Block
languagepy
linenumberstrue
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.

...

py

...

# 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.

Code Block
languagepy
linenumberstrue
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:

Code Block
languagepy
linenumberstrue
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.

Code Block
languagepy
linenumberstrue
    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,

Code Block
languagepy
linenumberstrue
    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

  1. 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.

  2. Increase a counter every time the button is pressed and visualize it.

  3. Turn the red light on whenever the button is pressed for more than 3 seconds.