Buttons#

Let’s implement a “tare” function: a button that, when pressed, zeroes the output. Very useful when you bake a cake to “null” the weight of the container you put the contents in.

Reading buttons is quite easy, just connect one terminal of the button to ground and the other to an input of the microcontroller.

Whenever you press the button, the microcontroller input is zero. But what happens when you let the button go? The input is disconnected and it’s value is undefined!

That’s easily fixed by adding a resistor between the microcontroller input and the supply, 3.3V. Now the input is at 3.3V, pulled up by the resistor. Pressing the button will pull it down to 0V. Since the current flowing into the input is very small, you can use a large resistor (e.g. 100kOhm) to minimize power dissipation.

The need for such “pull-up resistors” arises frequently. Because of this, many microcontrollers have resistors built-in that can be enabled with software.

The code below assumes a button connected to IO15 (button A of the OLED wing). The code for configuring the button with the pull-up enabled is below:

%connect balance

from machine import Pin
import time

button = Pin(15, mode=Pin.IN, pull=Pin.PULL_UP)

start = time.ticks_ms()
last_state = button.value()

while True:
    state = button.value()
    if state != last_state:
        if state == 0:
            print("pressed")
        else:
            print("released")
        last_state = state
Connected to balance @ serial:///dev/ttyUSB0
Interrupted

Click the little square at the top of the notebook display to stop the program.

The “mechanics” of buttons frequently results in a phenomenon called bouncing: rather than simply making electrical contact when pressed, the button contacts bounce off each other a few times before making solid contact.

This happens within milliseconds, not discernible to humans but well within the speed of microcontrollers. Because of this, tasks such as counting the number of button presses may report incorrect results.

The simplest remedy is to use a timer and ignore button “presses” for, say 100ms, after one has occurred. The button class, below, does just that. It uses MicroPython function ticks_diff to calculate time differences. Click on the link for an explanation.

In addition it uses interrupts to detect changes, rather than polling in a loop like the code above.

from machine import Pin
import time

class Button:
    
    def __init__(self, pin, debounce_ms=100):
        self._debounce_ms = debounce_ms
        self._last_press = 0
        self._count = 0
        button = Pin(pin, mode=Pin.IN, pull=Pin.PULL_UP)
        button.irq(handler=self._irq, trigger=Pin.IRQ_FALLING)
        
    def _irq(self, button):
        now = time.ticks_ms()
        if time.ticks_diff(now, self._last_press) < self._debounce_ms:
            # ignore "button presses" within debounce interval
            print("debounce")
            return
        self._last_press = now
        self._count += 1
        print("{} pressed {} times".format(button, self._count))
        
button = Button(15)

# report (print) button presses for a few seconds
# this is just for testing
time.sleep(5)
Pin(15) pressed 1 times
Pin(15) pressed 2 times

Depending on your switch, you may or may not see “debounce” in action.

Now let’s use this function to implement tare. For that we modify the Button class, taking a handler function that is called each time the button is pressed.

from scale import Scale
from ssd1306 import SSD1306_I2C
from machine import Pin, ADC, I2C
import time

class Button:
    
    def __init__(self, pin, handler, debounce_ms=100):
        self._handler = handler
        self._debounce_ms = debounce_ms
        self._last_press = 0
        button = Pin(pin, mode=Pin.IN, pull=Pin.PULL_UP)
        button.irq(handler=self._irq, trigger=Pin.IRQ_FALLING)
        
    def _irq(self, button):
        now = time.ticks_ms()
        if time.ticks_diff(now, self._last_press) < self._debounce_ms:
            return
        self._last_press = now
        self._handler(button)
                

# configure the display
i2c = I2C(0, scl=Pin(22), sda=Pin(23))
oled = SSD1306_I2C(128, 32, i2c)


# initialize the scale and button
# the button, when pressed, calls the tare function of Scale
scale = Scale()
tare_button = Button(15, scale.tare)

last_weight = 100
while True:
    weight = scale.measure()
    # print only significant changes
    if abs(weight-last_weight) > 3:
        oled.fill(0)
        oled.text("{:8.0f} gram".format(weight), 0, 12)
        oled.show()
        last_weight = weight
        time.sleep(0.1)
tare
tare

Interrupted

Play with the “tare” button. Is it working correctly?

Use one of the other buttons to switch between weight in grams and ounces.

%%writefile code/lib/button.py

from machine import Pin
import time

class Button:
    
    def __init__(self, pin, handler, debounce_ms=100):
        self._handler = handler
        self._debounce_ms = debounce_ms
        self._last_press = 0
        button = Pin(pin, mode=Pin.IN, pull=Pin.PULL_UP)
        button.irq(handler=self._irq, trigger=Pin.IRQ_FALLING)
        
    def _irq(self, button):
        now = time.ticks_ms()
        if time.ticks_diff(now, self._last_press) < self._debounce_ms:
            return
        self._last_press = now
        self._handler(button)
Overwriting code/lib/button.py
%rsync
UPDATE  /lib/button.py