How many times have you had to implement a button press/release routine (with de-bounce) for a project? If you write software for embedded projects, I’ll bet it is more than once. Because I was tired of writing this code over and over (well let’s be honest, I was copying it), I decided to write a button class that would be self-contained. I work with the ARM Mbed-OS a lot so I utilized some of the O/S services—primarily the EventQueue. I suppose with a little effort, it could be ported to most any other OS.
I use typically name files with the ‘*.hpp’ extension when they are a self-contained class, so I’ve contained all the button class in a named ‘button.hpp’ as follows:
/** * copyright (c) 2018, James Flynn * SPDX-License-Identifier: MIT */ /** * @file button.hpp * @brief A small BUTTON class for detecting & debouncing button presses * * @author James Flynn * * @date 24-Aug-2018 */ #include "mbed.h" #define BUTTON_DEBOUNCE 20 //specify the number of msec to debounce class Button { protected: InterruptIn user_button; void (*br_cb)(int); //button release callback void (*bp_cb)(void); //button press callback Thread button_thread; void button_monitor_task(void); EventQueue button_queue; uint64_t bp_time, bp_duration; //button press start time and button press duration int button_pressed; //counts the number of times the button has been pressed void button_press_handler(void) { if( (rtos::Kernel::get_ms_count() - bp_time) < BUTTON_DEBOUNCE) return; bp_time = rtos::Kernel::get_ms_count(); if( bp_cb ) bp_cb(); } void button_release_handler(void) { uint64_t tmp = rtos::Kernel::get_ms_count() - bp_time; if( tmp > BUTTON_DEBOUNCE ) { bp_duration = tmp; button_pressed++; if( br_cb ) br_cb(bp_duration); } } public: enum State { ActiveHigh = 0, ActiveLow }; Button(PinName p, State s, void (*cb)(int)=NULL) : user_button(p), br_cb(cb), bp_cb(NULL), bp_time(0), bp_duration(0), button_pressed(0) { // The user button is setup for the edge to generate an interrupt. // The release is caught an event queue callback button_thread.start(callback(&button_queue, &EventQueue::dispatch_forever)); if( s == ActiveHigh ) { user_button.rise( Callback<void()>(this, &Button::button_press_handler) ); user_button.fall( button_queue.event( Callback<void()>(this, &Button::button_release_handler))); } else{ user_button.fall( Callback<void()>(this, &Button::button_press_handler) ); user_button.rise(button_queue.event(Callback<void()>(this, &Button::button_release_handler))); } } // will return the number of times the button has been pressed (if it was pressed > 1 time before checked) // and returns the duration of the last button press in duration int chkButton_press(int *duration) { int bp = button_pressed; if( button_pressed ) { *duration = bp_duration; bp_duration = 0; button_pressed = 0; } return bp; } //allows the user to set a callback for a button press in void setButton_press_cb( void (*buttonpresscb)(void) ) { bp_cb = buttonpresscb; } };
The way it works is easiest to understand by using the following small program that implements as a test driver:
#include "mbed.h" #include "Button.hpp" void br_callback(int dur) { printf("\r\n!! button RELEASE called, press time: %d msec\r\n", dur); } void bp_callback(void) { printf("\r\n!! button PRESS called\r\n"); //normally you wouldn't call printf in an interrupt context } int main() { int k, dur; Button* button_ptr; printf("Testing button class\r\n"); printf("Test Standard polling type implementation\r\n"); button_ptr = new Button(USER_BUTTON, Button::ActiveHigh); while( (k=button_ptr->chkButton_press(&dur)) == 0 ) /* wait */; printf(">Button pressed %d times, last was %d msec\r\n",k,dur); delete button_ptr; printf("\nTest with Release callback\r\n"); button_ptr = new Button(USER_BUTTON, Button::ActiveHigh, br_callback); while( (k=button_ptr->chkButton_press(&dur)) == 0 ) /* wait */; printf(">Button pressed %d times, last was %d msec\r\n",k,dur); delete button_ptr; printf("\nTest with Press & Release callback\r\n"); button_ptr = new Button(USER_BUTTON, Button::ActiveHigh, br_callback); button_ptr->setButton_press_cb(bp_callback); while( (k=button_ptr->chkButton_press(&dur)) == 0 ) /* wait */; printf(">Button pressed %d times, last was %d msec\r\n",k,dur); while (1) { wait(.5); } }
When the object is created, the user specifies the USER_BUTTON which is the binary I/O that the button is attached to, then the user specifies if the button is active in the HIGH logic level (ActiveHigh) or the LOW logic level (ActiveLow). I dynamically the button in this test so I could delete it then recreate it using different methods for instantiation.
The first case simply monitors the button by calling ‘chkButton_press’ this function takes an argument that will be assigned the amount of time (msec) that the button was pressed. The functioin ‘chkButton_press’ actually returns the number of times the button has been pressed and the duration (‘dur’ in the example) is set to the length of time of the last button press.
In the second test case, when the object is instantiated, I assign a callback for the button release. This allows you to perform some action on the button release (like maybe turning off an LED). When the button is released. When the callback is invoked it is passed the duration of the button press.
In the third test, the button is instantiated with a callback function and then a button press function is set via the 'setButton_press_cb' function. This allows a function to be called when the button is pressed—like maybe lighting an LED. The important thing to remember is that the button press function will be running in an interrupt context so you want to avoid functions that will take long times to complete--unlike I did in the above example.
Hopefully, this class will help others easily obtain a clean and efficient way to handle button presses when using the Mbed-OS. If you want to import this into your on-line compiler, you can get it at https://os.mbed.com/users/JMF/code/button_class_and_test/