Advanced Debouncing

circuitpython_advanced_trampoline.jpg
From Wikipedia CC0 by Andre Forget - Andrew Scheer

We've gone over how to set up debouncers on input pins: you create and configure a digitalio.DigitalInOut instance and pass it to Debouncer().

But what if you want to debounce input signals to a Crickit? Or its touch inputs?

To do that you can use the more general way of making a debouncer: instead of passing in a DigitalInOut you pass in a predicate. Specifically, a zero-argument predicate. What's a predicate? Simply a function that returns a Boolean (i.e. True or False).

Download: file
from adafruit_crickit import crickit
from adafruit_debouncer import Debouncer

ss = crickit.seesaw
ss.pin_mode(crickit.SIGNAL1, ss.INPUT_PULLUP)

def read_signal():
    return ss.digital_read(crickit.SIGNAL1)

signal_1 = Debouncer(read_signal)

while True:
    signal_1.update()
    if signal_1.fell:
        print('Fell')

Remember when we talked about lambdas in the guide on CircuitPython functions? No? I encourage you to go read it now.

To reiterate (or if you didn't read that guide... yet), lambdas are simple, one line functions that return a value. The general form of a lambda is.

lambda arg1, ..., argn: value_to_return

As shown above, lambdas can take some number of arguments, including none. These are followed by a colon and the code that computes the return value. Note that the return keyword is implicit.

So how does this help? Well, notice in the above code, we defined the function read_signal solely to be passed to the Debouncer constructor. All it does is return a single value. That makes it a prime candidate to be a lambda: simple, returns a value, and only used once. Using a lambda we can rewrite the above code as:

Download: file
from adafruit_crickit import crickit
from adafruit_debouncer import Debouncer

ss = crickit.seesaw
ss.pin_mode(crickit.SIGNAL1, ss.INPUT_PULLUP)
signal_1 = Debouncer(lambda: ss.digital_read(crickit.SIGNAL1))

print('Starting')
while True:
    signal_1.update()
    if signal_1.fell:
        print('Fell')

Not a huge difference in this case: it let us get rid of four lines and a function. It does have some other impact though. If you define a function, that implies that what the function does is important enough to create a function around it. It implies it's an important enough concept in your code that it's worth giving a name to, and quite likely that it is something to be used more than once. In this case, none of that is true. It's simply to wrap the signal read in a zero-argument predicate. To be used once: passed into the Debouncer constructor. That's a perfect job for a lambda.

Function Factories

Let's look back to debouncing DigitalInOut objects (i.e. input pins on the microcontroller):

Download: file
pin = digitalio.DigitalInOut(board.D12)
pin.direction = digitalio.Direction.INPUT
pin.pull = digitalio.Pull.UP
switch = Debouncer(pin)

That's 3 lines to create and configure the DigitalInOut, and another to create the Debouncer for it. But what if you have a Grand Central M4 Express and want to debounce a couple dozen (or more) inputs? For 24 inputs that's 96 lines. And that's just configuration.. .no actual program logic. Worse, it's not really 96 lines: it's 4 lines repeated almost identically 24 times.

Now we'll see the real value of lambdas. Not only are they a short-hand way to make simple functions, but they are closures. That might take a bit of explaining.

Closures

The best way to explain closures is to show how they work. Consider this function:

Download: file
def add(x, y):
    return x + y

Then we can evaluate things like:

Download: file
>>> add(1, 2)
3

At the prompt, or anywhere outside the add function, code can see add, but not x or y. They're hidden inside the function as shown here.

circuitpython_add-function.png
By Dave Astels

If a lambda is created inside the add function, the code in it has access to x and y:

circuitpython_add-function-lambda.png
By Dave Astels

A lambda is an object like any other (incidentally, so is any function). Because it's an object it can be stored in a variable and passed into another function as we've already seen when constructing a Debouncer. Key to this discussion is the fact that it can also be returned from a function.

Now, recall that the lambda has access to the variables (and parameters) in the function (more generally, the lexical scope) where it was created. When a lambda is returned from a function it maintains access to those variables that are hidden inside the function:

circuitpython_add-function-returned-lambda.png
By Dave Astels

This allows us to do all manner of cool things. Here's a simple example. Lets riff on our add function to make an incrementer that adds 1 to whatever value is passed in, returning the result:

Download: file
>>> def inc(x):
...     return x + 1
... 
>>> inc(2)
3

That's fine, but all it does is add 1 to it's parameter. What if we needed functions to add arbitrary values instead of just 1? We could make a bunch of functions like inc_1, inc_2, inc_42, and so on. But what if we didn't know until runtime which ones we needed?  Here's where lambdas shine. Let's make a function that makes custom lambdas:

Download: file
>>> def make_adder(y):
...     return lambda x: x + y
... 
>>> inc_1 = make_adder(1)
>>> inc_1(2)
3
>>> inc_42 = make_adder(42)
>>> inc_42(5)
47

The lambda that is created in and returned from make_adder maintains a reference to make_adder's y parameter and, more importantly, it's value when the lambda was created.  So, when the resulting lambda is saved to a variable and later executed it can add that specific value of y to it's argument.

Use with the Debouncer

Let's go back to that issue of having a couple dozen input pins to debounce. We can create a function that takes a pin (from the board module), creates and configures a DigitalInOut object, and returns a lambda that reads it.

Download: file
def make_pin_reader(pin):
    io = digitalio.DigitalInOut(pin)
    io.direction = digitalio.Direction.INPUT
    io.pull = digitalio.Pull.UP
    return lambda: io.value

We can use this to make reader lambdas for however many debouncers we need.

Download: file
pin5 = Debouncer(make_pin_reader(board.D5))
pin6 = Debouncer(make_pin_reader(board.D6))
...
This guide was first published on Jan 08, 2019. It was last updated on Jan 08, 2019. This page (Advanced Debouncing) was last updated on Nov 20, 2019.