Saturday, January 16, 2021

Read and debounce multiple buttons using interrupt | AVR microcontrollers

This library provides an easy way for reading and debouncing one or many buttons connected to a microcontroller. It can also read a button combination, button long press and button double press.

Button debouncing library for AVR microcontrollers

What is button debouncing

When a button is pressed or released it takes a certain amount of time before the two contacts reach a stable state. During that time the contacts are known to be bouncing sending multiple on off signals to the microcontroller before they settle to either on or off.

The bouncing time period depends on the speed the button is pressed, the force, the button quality and the age of the button. With time the contacts will oxidize especially with low quality buttons.

For one or two buttons the debouncing can be made in hardware by connecting a capacitor in parallel with the button that will keep the voltage across the button stable during press or release. But when having many buttons it is more practical to make the debouncing in software where it is also easier to modify the debouncing time compared to a capacitor.

Button debouncing in software using the Binary Button Debounce (BBM) method

Button debouncing using Binary Button Debounce (BBD)

Every time the debouncing function runs, the states of all buttons are saved in a binary representation in one or multiple bytes. In a byte can fit up to 8 buttons and each bit represents a button - 0 the button is not pressed, 1 the button is pressed. I call this a snapshot. Each snapshot is saved in an array and at the end every bit must be 1 for a particular button to be considered pressed.

Connecting push buttons to a microcontroller
Connecting push buttons to a microcontroller

Usage example

#define F_CPU		16000000 // 16MHz CPU

#include <avr/io.h>
#include "buttonDebouncer.h"
#include "millis.h"


int main(void){
    // Set a test led as output low
    DDRB |= _BV(PB5);
    PORTB &= ~_BV(PB5);
	
    // Define port and pin numbers for each button
    uint8_t BUTTON_LOCATION[] = {'B', 0, 1, 2, 'C', '5'};

    enum Buttons{
       	BTN_1 		= 1,	// PB0
    	BTN_2 		= 2,	// PB1
	BTN_3 		= 4,	// PB2 
        BTN_4 		= 8,	// PC5
    };
	
    buttons_t pressed_buttons = 0;
    buttons_t pushed_buttons = 0;
    buttons_t button_combination = 0; // optional
	
    // Setup millis
    millis_init();
	
    // Setup buttonDebouncer
    debouncerSetup(BUTTON_LOCATION, sizeof(BUTTON_LOCATION));
    btnSetLongPressTime(2000); // minimum 2 seconds to detect button long press
	
    // Enable global interrupts
    sei();
	
    while(1){
		
	// Check if debouncing is complete
	if(debouncerComplete()){
	    pressed_buttons = debouncerPressedButtons();
	    pushed_buttons = debouncerPushedButtons();
	    button_combination = debouncerButtonCombination(); // optional
			
            // Check if a button was long pressed (optional)
	    if(BUTTON_LONG_PRESS){
		pushed_buttons = pressed_buttons;
	    }
			
	    // Check for button combination(s) (optional)
	    if(button_combination == (BTN_1 | BTN_2)){
		// Some code...
		// NOTE: this will also trigger a button pushed action below
		// for this buttons. Either don't include this buttons in the switch below
		// or use a flag variable to only consider them pushed when it is not set here as 1.
	    }
			
	    // Check if a button was double clicked (optional)
	    if(BUTTON_DOUBLE_CLICK && debouncerDoubleClicked() == BTN_3){
		// Some code...
		// NOTE: this will also trigger a button pushed action below
		// for this button. Either don't include this button in the switch below
		// or use a flag variable to only consider it pushed when it is not set here as 1.
	    }
			
	    // Check what button was pressed
	    switch(pushed_buttons){
		case BTN_1:
		    if(BUTTON_LONG_PRESS){
			// This button was long pressed
			PORTB &= ~_BV(PB5); // set test led low
		    }else{
			// This button was pushed but not long pressed
			PORTB |= _BV(PB5); // set test led high
		    }
		break;
				
		case BTN_2:
		    if(BUTTON_LONG_PRESS){
			// This button was long pressed
			PORTB &= ~_BV(PB5);
		    }else{
			// This button was pushed but not long pressed
			PORTB |= _BV(PB5);
		    }
		break;
			
		case BTN_3:
	            PORTB ^= _BV(PB5); // toggle test led
		break;
				
		case BTN_4:
		    PORTB ^= _BV(PB5); // toggle test led
		break;
	    }
	}
    }
}


// Millis timer
// This Interrupt Service Routine is located at the end of millis.h file.
// Either copy this code into the millis file or comment out this code
// that is inside the millis file otherwise the compiler will trigger an error.
ISR(ISR_VECT){
    ++milliseconds;
	 
    // Default: debouncing every 10ms, 1ms timer resolution
    buttonDebouncerTimer(); // 2.4us @ 8MHz
}

 

Using the library

It is not necessary to modify the settings inside the library header file but there are a few settings that can be modified if the default values are not desired.

BUTTONS_SIZE_LIMIT - can be BTNS_LIMIT_8 that can hold up to 8 buttons or BTNS_LIMIT_16 for up to 16 buttons.

MIN_PRESS_TIME - debouncing time in milliseconds. 10 or 20 m. Default value is 10ms.

TIMER_ISR_RESOLUTION - by default is 1ms because the millis interrupt triggers every 1ms.

Setup functions

debouncerSetup(BUTTON_LOCATION, BUTTON_LOCATION_SIZE)

This function tells the library on what port and pin each button is located and also sets the pins as inputs with internal pull-up resistor activated so no external resistor is needed.

BUTTON_LOCATION: an array that indicates on what port and pin each button is located.

Example:

uint8_t BUTTON_LOCATION[] = {'B', 2, 0, 5, 4, 'C', 5, 'D', 7};

Here we have some buttons on port B on pins 2, 0, 5 and 4, one button on port C pin 5 and one on port D pin 7.

BUTTON_LOCATION_SIZE: the size of the array can be obtained like so sizeof(BUTTON_LOCATION).

The buttons enum

To be able to identify what each button does and also if a combination of buttons was pressed an enum fits perfect for this purpose.

enum Buttons{
	BTN_UP 			= 1,	// PB2
	BTN_DOWN		= 2,	// PB0
	BTN_LEFT		= 4,	// PB5
	BTN_RIGHT		= 8,	// PB4
	BTN_MENU_OK		= 16,	// PC5
	BTN_START_STOP	        = 32	// PD7
}

The name of the buttons can be anything and more buttons can be added or removed from the list. The important thing is the order that they are inside the enum. Notice that the order of the enum is similar to that of the array. Also important is the number of each button in the enum - they must start at 1 and continue as follows 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 and so on.

Reasons for this can be seen later in an example but the cool thing is that any combination of buttons can be checked like this BTN_UP | BTN_DOWN. This will be true in an if statement or a switch case if both buttons are pressed.

Setting the long press time

btnSetLongPressTime(monitorPressTime)
Set the time in milliseconds after a button is considered long pressed.

Reading the buttons

There are two functions for reading the buttons. One returns the currently pressed buttons and the other the buttons that were pressed and released.

For example in many projects like a clock or a flashlight there is no room for many buttons and 1 or 2 buttons must accomplish many functions. Entering setup mode can be made as a secondary function by a long press of 2 seconds while a short press can accomplish a main action like switching the lights. Or some times a single button press must do something only once regardless if the user holds the button down and other times it is desired to repeat that button's function - when changing a numerical value for example.

debouncerPushedButtons()

Pushed buttons: returns an 8 or 16 bit value where each bit represents the state of the button at that bit position. Thanks to the Buttons enum there is no need for binary shifting to check which button was pressed. See the below examples on how easy is to check the button state.

Even if the user holds the button pressed continuously the function will return the on states only once. For example holding the key "a" will only return "a" whereas the following function will repeat the action.

debouncerPressedButtons()

Pressed buttons: same as the function above except that here the function returns the on states of the buttons as long as they are pressed. For example holding the key "a" will return "aaaaaaaaaaaaa" if the user doesn't implement a delay in code to account for this.

debouncerDoubleClicked()

Double clicked buttons: returns the button that was pressed twice very quickly. This is similar to a mouse doubleclick.

debouncerButtonCombination()

Button combination: returns the button combination that was pressed during a certain time interval. Default: 200ms.

debouncerComplete()

Used to check if the button reading and debouncing is complete. Returns 1 if button reading is complete, 0 otherwise.

Debouncing function

buttonDebouncerTimer()

Must be placed in a timer interrupt (ideally 1ms resolution). After a certain time it will set the NEXT_DEBOUNCE_READY flag that indicates that the debouncing function can run.

If you don't have a timer interrupt you can use the millis library from https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html


Download

Change log and license can be found at the beginning of the file
v2.3 buttonDebouncer.h

11 comments:

  1. Thanks for this library. I am trying it using the example code above (same as in the buttonDebouncer.h file) and I can't get it to recognize a long press.

    ReplyDelete
  2. Sorry to hear you have issues with it. It's hard to say what the issue is without seeing the code. First make sure buttonDebouncerTimer() is triggered every 1ms. In this case for debugging I use a logic analyzer and toggle a pin to see the timings and also to check if a code inside a an if statement is executed or not. Then check if the code inside if(BUTTON_LONG_PRESS){ // code in here } is executed. If all this works then check the switch case making sure the buttons inside the enum have the same order as the buttons in the array where they are defined.

    ReplyDelete
  3. It's strange because it's almost identical to your example above. Everything except the "if (BUTTON_LONG_PRESS)" bit is working. Since the example isn't totally complete, I wondered if I was misunderstanding how it's supposed to be used. https://gist.github.com/rahji/546f358d4c3cb7d9d077d4a49ce35fb4

    ReplyDelete
    Replies
    1. I think I might messed something up in the last update. I was trying to simplify the user code a bit and probably I forgot to check again the long press function.
      Here is a link to the version 2.0 that I have used in the digital clock with night lamp project where it was working:
      buttonDebouncer.h
      https://drive.google.com/file/d/12SITai2TdwmXVf0hbel_eDVTtq9qWsh_/view?usp=sharing
      The usage code is a bit different so i have modified a copy of your code from github here:
      main.c
      https://drive.google.com/file/d/1h_24Zv0Dajh7BT7C69bIqNAAK7inpaVs/view?usp=sharing

      I wish I could test it but I burned my development board. As soon as I make a new one I will solve the issue.

      Delete
    2. wow, thanks! I will give this a try

      Delete
    3. Ok, let me know if it does. Right now I'm soldering the components on a new dev board and hopefully I will be able to fix the library in a few days.

      Delete
    4. I can try it here as well. I did notice that when you updated you changed the license to not allow commercial use?

      Delete
    5. Unfortunately, it's worse. :/ It used to work, other than the long-press, but now it gets stuck after the first short press and won't recognize anything after that. Let me know if you get a chance to try the updated version and find that it's broken. Thanks again.

      Delete
    6. No need to use the old version 2.0. I managed to fix the issue in v2.3. The license is the same as in 2.1 and 2.2 - GNU GPL v3. There is also a new example code. I tested all functionalities on 8MHz and 16MHz and works.
      The way you defined F_CPU didn't compile well in Microchip. Consider defining like this:

      #ifdef F_CPU
      #undef F_CPU
      #endif
      #define F_CPU 8000000

      With #endif below #define F_CPU the delay.h library said the F_CPU is not defined.

      Delete
    7. If you can, use the updated code example without adding extra code, just to test the library. I have included a test led in the example.
      Also, I have noticed that your CPU is clocked at 8MHz. Make sure you set the fuse to disable the division by 8 of the CPU clock otherwise you are running at 1MHz but the library thinks is at 8MHz. It's an easy thing to forget.

      Delete
  4. The issue that triggered a button press after the button was long pressed has now been fixed.

    ReplyDelete