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
).
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:
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):
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.
The best way to explain closures is to show how they work. Consider this function:
def add(x, y): return x + y
Then we can evaluate things like:
>>> 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.
If a lambda is created inside the add
function, the code in it has access to x
and y
:
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:
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:
>>> 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:
>>> 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.
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.
pin5 = Debouncer(make_pin_reader(board.D5)) pin6 = Debouncer(make_pin_reader(board.D6)) ...
Text editor powered by tinymce.