A Debouncer Module

circuitpython_Bouncy_Switch.png
In public domain by CC 0 by Wikipedia user Super Rad!

Let's do something more involved now that we know the general layout. A debouncer is a good next step. Here's a CircuitPython implementation that's been used in a few of the author's guides.

Download: file
import time
import digitalio

class Debouncer(object):
    """Debounce an input pin"""

    DEBOUNCED_STATE = 0x01
    UNSTABLE_STATE = 0x02
    CHANGED_STATE = 0x04

    def __init__(self, pin, interval=0.010):
        """Make am instance.
           :param int pin: the pin (from board) to debounce
           :param int mode: digitalio.Pull.UP or .DOWN (default is no pull up/down)
           :param int interval: bounce threshold in seconds (default is 0.010, i.e. 10 milliseconds)
        """
        self.state = 0x00
        self.pin = digitalio.DigitalInOut(pin)
        self.pin.direction = digitalio.Direction.INPUT
        self.pin.pull = digitalio.Pull.UP
        if self.pin.value:
            self.__set_state(Debouncer.DEBOUNCED_STATE | Debouncer.UNSTABLE_STATE)
        self.previous_time = 0
        if interval is None:
            self.interval = 0.010
        else:
            self.interval = interval

    def __set_state(self, bits):
        self.state |= bits

    def __unset_state(self, bits):
        self.state &= ~bits

    def __toggle_state(self, bits):
        self.state ^= bits

    def __get_state(self, bits):
        return (self.state & bits) != 0

    def update(self):
        """Update the debouncer state. Must be called before using any of the properties below"""
        self.__unset_state(Debouncer.CHANGED_STATE)
        current_state = self.pin.value
        if current_state != self.__get_state(Debouncer.UNSTABLE_STATE):
            self.previous_time = time.monotonic()
            self.__toggle_state(Debouncer.UNSTABLE_STATE)
        else:
            if time.monotonic() - self.previous_time >= self.interval:
                if current_state != self.__get_state(Debouncer.DEBOUNCED_STATE):
                    self.previous_time = time.monotonic()
                    self.__toggle_state(Debouncer.DEBOUNCED_STATE)
                    self.__set_state(Debouncer.CHANGED_STATE)

    @property
    def value(self):
        """Return the current debounced value of the input."""
        return self.__get_state(Debouncer.DEBOUNCED_STATE)

    @property
    def rose(self):
        """Return whether the debounced input went from low to high at the most recent update."""
        return self.__get_state(self.DEBOUNCED_STATE) and self.__get_state(self.CHANGED_STATE)

    @property
    def fell(self):
        """Return whether the debounced input went from high to low at the most recent update."""
        return (not self.__get_state(self.DEBOUNCED_STATE)) and self.__get_state(self.CHANGED_STATE)

Implementation

We'll start with the implementation in shared-module/debounce. As before we need to add an __init__.c file that contains module level functions (i.e. there's nothing in it) . Then we need the class implementation files. First the header that defines the data side of the Python class: Debouncer.h

Download: file
#ifndef MICROPY_INCLUDED_DEBOUNCE_DEBOUNCER_H
#define MICROPY_INCLUDED_DEBOUNCE_DEBOUNCER_H

#include "shared-bindings/digitalio/DigitalInOut.h"
#include "py/obj.h"

typedef struct {
  mp_obj_base_t base;
  uint8_t state;
  digitalio_digitalinout_obj_t pin;
  uint64_t previous_time;
  uint64_t interval;
} debounce_debouncer_obj_t;


#endif // MICROPY_INCLUDED_DEBOUNCE_DEBOUNCER_H

We'll go through the C file (Debouncer.c) a section at a time.  First, the includes, constants, and external declarations. The latter is the way we can refer to a function or variable that is defined elsewhere, but which we want to use in this file. We don't have to identify where it is, the linker will figure that out during the build. In this case, we want the function that implements Python's time.monotonic function.

Download: file
#include "common-hal/microcontroller/Pin.h"
#include "shared-bindings/digitalio/Pull.h"
#include "shared-bindings/digitalio/DigitalInOut.h"
#include "py/runtime.h"
#include "supervisor/shared/translate.h"
#include "Debouncer.h"


#define DEBOUNCED_STATE (0x01)
#define UNSTABLE_STATE (0x02)
#define CHANGED_STATE (0x04)


extern uint64_t common_hal_time_monotonic(void);

Next, we have the equivalent of the private methods in the Python version (prefixed by __). In C, we simply have to define them here and not make them available up to the runtime.

Download: file
void set_state(debounce_debouncer_obj_t* self, uint8_t bits)
{
  self->state |= bits;
}

void unset_state(debounce_debouncer_obj_t* self, uint8_t bits)
{
  self->state &= ~bits;
}

void toggle_state(debounce_debouncer_obj_t* self, uint8_t bits)
{
  self->state ^= bits;
}

uint8_t get_state(debounce_debouncer_obj_t* self, uint8_t bits)
{
  return (self->state & bits) != 0;
}

Next, let's turn to the constructor and lifecycle functions. This time the constructor takes two parameters: the pin to debounce, and the debounce interval (how long to let the input settle before accepting it's value).

Download: file
void shared_module_debounce_debouncer_construct(debounce_debouncer_obj_t* self,
                                             mcu_pin_obj_t* pin, mp_int_t interval) {
  digitalinout_result_t result = common_hal_digitalio_digitalinout_construct(&self->pin, pin);
  if (result != DIGITALINOUT_OK) {
    return;
  }
  common_hal_digitalio_digitalinout_switch_to_input(&self->pin, PULL_UP);

  self->state = 0x00;
  if (common_hal_digitalio_digitalinout_get_value(&self->pin)) {
    set_state(self, DEBOUNCED_STATE | UNSTABLE_STATE);
  }
  self->interval = interval;
}

bool shared_module_debounce_debouncer_deinited(debounce_debouncer_obj_t* self) {
  return common_hal_digitalio_digitalinout_deinited(&self->pin);
}

void shared_module_debounce_debouncer_deinit(debounce_debouncer_obj_t* self) {
  if (shared_module_debounce_debouncer_deinited(self)) {
    return;
  }
  common_hal_digitalio_digitalinout_deinit(&self->pin);
}

Notice that we have something related to the implementation to tell when the instance is valid: the pin. The deinited function checks for a valid pin, and the deinit function releases the pin and invalidates it.

The update function does the work in this code.

When it's called, the function first grabs the current time and clears the changed state. Then it checks to see if the the pin is different from last time. If so, it restarts the settling timer and updates the state (to be checked against next time). If the pin hasn't changed since last time, it checks to see if the settling timeout has expired. If so, it's done unless the pin is now different than the most recent debounced state. If the pin is different, the time and debounced state are updated, and the change is noted. This change flag is used in the rose and fell properties which key off a change in the pin's debounced value.

Download: file
void shared_module_debounce_debouncer_update(debounce_debouncer_obj_t* self) {
  if (shared_module_debounce_debouncer_deinited(self)) {
    return;
  }
  uint64_t now = common_hal_time_monotonic();
  unset_state(self, CHANGED_STATE);
  bool current_state = common_hal_digitalio_digitalinout_get_value(&self->pin);
  if (current_state != get_state(self, UNSTABLE_STATE)) {
    self->previous_time = now;
    toggle_state(self, UNSTABLE_STATE);
  } else {
    if (now - self->previous_time >= self->interval) {
      if (current_state != get_state(self, DEBOUNCED_STATE)) {
        self->previous_time = now;
        toggle_state(self, DEBOUNCED_STATE);
        set_state(self, CHANGED_STATE);
      }
    }
  }
}

Finally, we have the property getter functions:
value
 - the most recent debounced state
rose
 - whether the debounced state is high and got there during the most recent call to update
fell
 - whether the debounced state is low and got there during the most recent call to update

Download: file
bool shared_module_debounce_debouncer_get_value(debounce_debouncer_obj_t* self) {
  return get_state(self, DEBOUNCED_STATE);
}

bool shared_module_debounce_debouncer_get_rose(debounce_debouncer_obj_t* self) {
  return get_state(self, DEBOUNCED_STATE) && get_state(self, CHANGED_STATE);
}

bool shared_module_debounce_debouncer_get_fell(debounce_debouncer_obj_t* self) {
  return !get_state(self, DEBOUNCED_STATE) && get_state(self, CHANGED_STATE);
}

Interface

Again, there are no exposed method level functions, so shared-bindings/debounce/__init__.h is simply:

Download: file
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE___INIT___H
#define MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE___INIT___H

#include "py/obj.h"

// Nothing now.

#endif  // MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE___INIT___H

The file shared-bindings/debounce/__init__.c ties in the Debouncer class:

Download: file
#include <stdint.h>

#include "py/obj.h"
#include "py/runtime.h"

#include "shared-bindings/debounce/__init__.h"
#include "shared-bindings/debounce/Debouncer.h"

STATIC const mp_rom_map_elem_t debounce_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_debounce) },
    { MP_ROM_QSTR(MP_QSTR_Debouncer), MP_ROM_PTR(&debounce_debouncer_type) },
};

STATIC MP_DEFINE_CONST_DICT(debounce_module_globals, debounce_module_globals_table);

const mp_obj_module_t debounce_module = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t*)&debounce_module_globals,
};

shared-bindings/debounce/Debouncer.h declares the exposed functions in the implementation (above) as extern. Remember this just tells the compiler that these functions will be available; it's up to the linker to find them and make the connections.

Download: file
#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE_DEBOUNCER_H
#define MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE_DEBOUNCER_H

#include "shared-module/debounce/Debouncer.h"

extern const mp_obj_type_t debounce_debouncer_type;

extern void shared_module_debounce_debouncer_construct(debounce_debouncer_obj_t* self, mcu_pin_obj_t* pin, mp_int_t interval);
extern void shared_module_debounce_debouncer_deinit(debounce_debouncer_obj_t* self);
extern bool shared_module_debounce_debouncer_deinited(debounce_debouncer_obj_t* self);
extern void shared_module_debounce_debouncer_update(debounce_debouncer_obj_t* self);
extern bool shared_module_debounce_debouncer_get_fell(debounce_debouncer_obj_t* self);
extern bool shared_module_debounce_debouncer_get_rose(debounce_debouncer_obj_t* self);
extern bool shared_module_debounce_debouncer_get_value(debounce_debouncer_obj_t* self);

#endif // MICROPY_INCLUDED_SHARED_BINDINGS_DEBOUNCE_DEBOUNCER_H

As before (but more complex this time) the interface source file (shared-bindings/debounce/Debouncer.c) does the job of plumbing the implementation code into the CircuitPython runtime. This is still fairly straightforward as only the constructor has parameters.

Download: file
#include <stdint.h>

#include "lib/utils/context_manager_helpers.h"
#include "py/objproperty.h"
#include "py/runtime.h"
#include "py/runtime0.h"
#include "shared-bindings/microcontroller/Pin.h"
#include "shared-bindings/debounce/Debouncer.h"
#include "shared-bindings/util.h"

//| .. currentmodule:: debounce
//|
//| :class:`Debouncer` -- Debounce an input pin
//| ====================================================================================
//|
//| Debouncer cleans up an input pin and provides value/rise/fall properties.
//|
//| .. class:: Debouncer(pin, mode, interval)
//|
//|   Create a Debouncer object associated with the given pin. It tracks the value of the pin over time,
//|   allowing it to settle before acknowledging transitions.
//|
//|   :param ~microcontroller.Pin pin: Pin to debounce
//|   :param ~int interval: debounce interval in milliseconds
//|
//|   For example::
//|
//|     import debounce
//|     import board
//|
//|     deb = bebounce.Debounce(board.D10, 10)
//|     while True:
//|         deb.update()
//|         if deb.fell
//|             print("Pressed")
//|
STATIC mp_obj_t debounce_debouncer_make_new(const mp_obj_type_t *type, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
    enum { ARG_pin, ARG_interval };
    static const mp_arg_t allowed_args[] = {
        { MP_QSTR_pin_, MP_ARG_REQUIRED | MP_ARG_OBJ },
        { MP_QSTR_interval, MP_ARG_INT },
    };
    mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
    mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

    assert_pin(args[ARG_pin].u_obj, false);
    mcu_pin_obj_t* pin = MP_OBJ_TO_PTR(args[ARG_pin].u_obj);

    mp_int_t interval = 10;
    if (n_args == 2) {
      interval = args[ARG_interval].u_int;
    }

    debounce_debouncer_obj_t *self = m_new_obj(debounce_debouncer_obj_t);
    self->base.type = &debounce_debouncer_type;

    shared_module_debounce_debouncer_construct(self, pin, interval);

    return MP_OBJ_FROM_PTR(self);
}


//|   .. method:: deinit()
//|
//|      Deinitializes the debouncer and releases any hardware resources for reuse.
//|
STATIC mp_obj_t debounce_debouncer_deinit(mp_obj_t self_in) {
    debounce_debouncer_obj_t *self = MP_OBJ_TO_PTR(self_in);
    shared_module_debounce_debouncer_deinit(self);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(debounce_debouncer_deinit_obj, debounce_debouncer_deinit);

//|   .. method:: update()
//|
//|      Do an update cycle it it's time to.
//|
STATIC mp_obj_t debounce_debouncer_obj_update(size_t n_args, const mp_obj_t *args) {
    (void)n_args;
    shared_module_debounce_debouncer_update(args[0]);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(debounce_debouncer_update_obj, 1, 1, debounce_debouncer_obj_update);


//|   .. method:: __exit__()
//|
//|      Automatically deinitializes the hardware when exiting a context. See
//|      :ref:`lifetime-and-contextmanagers` for more info.
//|
STATIC mp_obj_t debounce_debouncer_obj___exit__(size_t n_args, const mp_obj_t *args) {
    (void)n_args;
    shared_module_debounce_debouncer_deinit(args[0]);
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(debounce_debouncer___exit___obj, 4, 4, debounce_debouncer_obj___exit__);


//|   .. attribute:: value
//|
//|     The current debounced value
//|
STATIC mp_obj_t debounce_debouncer_obj_get_value(mp_obj_t self_in) {
    debounce_debouncer_obj_t *self = MP_OBJ_TO_PTR(self_in);
    raise_error_if_deinited(shared_module_debounce_debouncer_deinited(self));

    return mp_obj_new_bool(shared_module_debounce_debouncer_get_value(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(debounce_debouncer_get_value_obj, debounce_debouncer_obj_get_value);


//|   .. attribute:: rose
//|
//|     Whether the input transitioned low to high since last update
//|
STATIC mp_obj_t debounce_debouncer_obj_get_rose(mp_obj_t self_in) {
    debounce_debouncer_obj_t *self = MP_OBJ_TO_PTR(self_in);
    raise_error_if_deinited(shared_module_debounce_debouncer_deinited(self));

    return mp_obj_new_bool(shared_module_debounce_debouncer_get_rose(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(debounce_debouncer_get_rose_obj, debounce_debouncer_obj_get_rose);


//|   .. attribute:: value
//|
//|     Whether the input transitioned high to low since last update
//|
STATIC mp_obj_t debounce_debouncer_obj_get_fell(mp_obj_t self_in) {
    debounce_debouncer_obj_t *self = MP_OBJ_TO_PTR(self_in);
    raise_error_if_deinited(shared_module_debounce_debouncer_deinited(self));

    return mp_obj_new_bool(shared_module_debounce_debouncer_get_fell(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(debounce_debouncer_get_fell_obj, debounce_debouncer_obj_get_fell);


const mp_obj_property_t debounce_debouncer_value_obj = {
    .base.type = &mp_type_property,
    .proxy = {(mp_obj_t)&debounce_debouncer_get_value_obj,
              (mp_obj_t)&mp_const_none_obj},
};

const mp_obj_property_t debounce_debouncer_rose_obj = {
    .base.type = &mp_type_property,
    .proxy = {(mp_obj_t)&debounce_debouncer_get_rose_obj,
              (mp_obj_t)&mp_const_none_obj},
};

const mp_obj_property_t debounce_debouncer_fell_obj = {
    .base.type = &mp_type_property,
    .proxy = {(mp_obj_t)&debounce_debouncer_get_fell_obj,
              (mp_obj_t)&mp_const_none_obj},
};

STATIC const mp_rom_map_elem_t debounce_debouncer_locals_dict_table[] = {
    // Methods
    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&debounce_debouncer_deinit_obj) },
    { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&debounce_debouncer___exit___obj) },
    { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&debounce_debouncer_update_obj) },
    { MP_ROM_QSTR(MP_QSTR_value), MP_ROM_PTR(&debounce_debouncer_value_obj) },
    { MP_ROM_QSTR(MP_QSTR_rose), MP_ROM_PTR(&debounce_debouncer_rose_obj) },
    { MP_ROM_QSTR(MP_QSTR_fell), MP_ROM_PTR(&debounce_debouncer_fell_obj) },
};
STATIC MP_DEFINE_CONST_DICT(debounce_debouncer_locals_dict, debounce_debouncer_locals_dict_table);

const mp_obj_type_t debounce_debouncer_type = {
    { &mp_type_type },
    .name = MP_QSTR_Debouncer,
    .make_new = debounce_debouncer_make_new,
    .locals_dict = (mp_obj_dict_t*)&debounce_debouncer_locals_dict,
};

Notice how each function has a form similar to the following:

Download: file
STATIC mp_obj_t debounce_debouncer_obj_get_rose(mp_obj_t self_in) {
    debounce_debouncer_obj_t *self = MP_OBJ_TO_PTR(self_in);
    raise_error_if_deinited(shared_module_debounce_debouncer_deinited(self));

    return mp_obj_new_bool(shared_module_debounce_debouncer_get_rose(self));
}
MP_DEFINE_CONST_FUN_OBJ_1(debounce_debouncer_get_rose_obj, debounce_debouncer_obj_get_rose);

Particularly, notice the final line:  MP_DEFINE_CONST_FUN_OBJ_1is what does the plumbing.

The final part of the file defines what is known about the Python class (notice we use plain C to define the Python classes):

Download: file
STATIC const mp_rom_map_elem_t debounce_debouncer_locals_dict_table[] = {
    // Methods
    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&debounce_debouncer_deinit_obj) },
    { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) },
    { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&debounce_debouncer___exit___obj) },
    { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&debounce_debouncer_update_obj) },
    { MP_ROM_QSTR(MP_QSTR_value), MP_ROM_PTR(&debounce_debouncer_value_obj) },
    { MP_ROM_QSTR(MP_QSTR_rose), MP_ROM_PTR(&debounce_debouncer_rose_obj) },
    { MP_ROM_QSTR(MP_QSTR_fell), MP_ROM_PTR(&debounce_debouncer_fell_obj) },
};
STATIC MP_DEFINE_CONST_DICT(debounce_debouncer_locals_dict, debounce_debouncer_locals_dict_table);

const mp_obj_type_t debounce_debouncer_type = {
    { &mp_type_type },
    .name = MP_QSTR_Debouncer,
    .make_new = debounce_debouncer_make_new,
    .locals_dict = (mp_obj_dict_t*)&debounce_debouncer_locals_dict,
};

Changes need to be made to Makefile and mpconfigport.h similar to those on the previous page, except that they reference the debounce module.

This guide was first published on Nov 15, 2018. It was last updated on Nov 15, 2018.
This page (A Debouncer Module) was last updated on Jun 03, 2020.