Saturday, January 16, 2021

Read and debounce multiple buttons using interrupt on AVR microcontrollers

This library provides an easy way for reading and debouncing one or multiple buttons connected to a microcontroller. Apart from basic functionality, the library provides some extra functions such as reading a combination of buttons, and detecting a button long press or a double pressed button. These are especially useful in a low button count system.

Multiple buttons can also be read by a single ADC pin.


Button debouncing library for AVR microcontrollers

Contents

 

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

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

This code example demonstrates the use of all functions, although most of them are optional. For example not everyone might need to detect a long button press or double press or a button combination.

The uart library is not necessary but can be used to make sure the code works as expected. The library has support for UPDI microcontrollers, however in this example the test pin is used like with the classic microcontrollers such as ATmega328PB. When using UPDI microcontrollers, the DDR is not needed since the PORT register can be used to set the pin as output.

// See https://www.programming-electronics-diy.xyz/2024/01/defining-fcpu.html
#ifndef F_CPU
	#warning	"F_CPU not defined. Define it in project properties."
#elif F_CPU != 16000000
	#warning	"Wrong F_CPU frequency!"
#endif

/* Test Led */
#define TEST_LED_DDR			DDRD
#define TEST_LED_PORT			PORTD
#define TEST_LED_PIN			PD7

#include <avr/io.h>
#include "buttonReader.h"
#include "millis.h"
#include "uart.h"

int main(void){
    // Set a test led as output low
    TEST_LED_PORT &= ~(1 << TEST_LED_PIN);
    TEST_LED_DDR |= (1 << TEST_LED_PIN);
	
    // Define port and pin numbers for each button
    uint8_t BUTTON_LOCATION[] = {'C', 0, 4, 'D', 4};

    enum Buttons{
	BTN_1 		= 1,	// PC0
	BTN_2 		= 2,	// PC4
	BTN_3 		= 4,	// PD4
    };
	
    //buttons_t active_buttons = 0;
    buttons_t pressed_buttons = 0;
    buttons_t button_combination = 0; // optional
    buttons_t button_dbl_click = 0; // optional
    buttons_t long_pressed = 0; // optional
	
    // Setup millis
    millis_init(F_CPU);
    millis_interrupt_attach(btnReader);
	
    // Setup buttonReader
    btnReaderSetup(BUTTON_LOCATION, sizeof(BUTTON_LOCATION));
    btnReaderSetLongPressTime(2000); // set 2 seconds to detect button long press
	
    // Setup UART (optional) for debugging
    UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT);
	
    while(1){
		
        // Check if reading of buttons is complete
	if(btnReaderReady()){
	    // Holds each button that is currently pressed
	    //active_buttons = btnReaderActiveButtons(); // optional
			
	    // Hold a button that was pressed AND released
	    pressed_buttons = btnReaderPressedButtons();
			
	    // The button that was double pressed
	    button_dbl_click = btnReaderDoubleClicked(); // optional
			
	    // After the number of pressed buttons equals the function argument
	    // the function returns what buttons are pressed as a combination.
	    button_combination = btnReaderButtonCombination(2); // optional
			
	    // Returns the button number that was pressed for longer than the timeout
	    long_pressed = btnReaderLongPressButtons(); // optional

			
	    // Checks if BTN_1 is pressed while the BTN_2 is pressed then released.
	    // Could be used for example to use + and - buttons to set a variable
	    // but only if Setup is pressed down. When Setup is not pressed
	    // + and - could have other functions in cases where number of buttons
	    // available is minimal. Only works when btnReaderButtonCombination() is not used.
	    /*
	    if((active_buttons & BTN_1) && ((pressed_buttons & (BTN_2 | BTN_3)))){
		UART_sendString(&uart0, "Setup: ");
		if(pressed_buttons == BTN_2){
		    UART_sendString(&uart0, "+\n");
		}else{
		    UART_sendString(&uart0, "-\n");
		}
	    }
	    */
			
	    /* Button combination (optional) */
	    if(button_combination == (BTN_1 | BTN_2)){
	        // Some code...
		UART_sendString(&uart0, "B1 & B2 pressed\n");
	    }
			
	    /* Double clicked (optional) */
	    if(button_dbl_click == BTN_3){
		// Some code...
		UART_sendString(&uart0, "B3 double click\n");
				
		// Don't trigger a button pressed action
		pressed_buttons = 0;
	    }
			
			
	    /* Pressed and released or long pressed */
	    switch(pressed_buttons){
		case BTN_1:
		    if(long_pressed){
			// This button was long pressed
			TEST_LED_PORT &= ~(1 << TEST_LED_PIN); // set test led low
			UART_sendString(&uart0, "BT1 long pressed\n");
		    }else{
			// This button was pushed but not long pressed
			TEST_LED_PORT |= (1 << TEST_LED_PIN); // set test led high
			UART_sendString(&uart0, "BT1 pressed\n");
		    }
		break;
				
		case BTN_2:
		    if(long_pressed){
			// This button was long pressed
			TEST_LED_PORT &= ~(1 << TEST_LED_PIN);
			UART_sendString(&uart0, "BT2 long pressed\n");
		    }else{
			// This button was pushed but not long pressed
			TEST_LED_PORT |= (1 << TEST_LED_PIN);
			UART_sendString(&uart0, "BT2 pressed\n");
		    }
		break;
				
	        case BTN_3:
		    TEST_LED_PORT ^= (1 << TEST_LED_PIN); // toggle test led
		    UART_sendString(&uart0, "BT3 pressed\n");
		break;
	    } // end switch()
	} // end btnReaderReady()
    } // end while(1)
} // end main()
 

API

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		BTNS_LIMIT_8

Maximum number of buttons supported, expressed in bits. Can be BTNS_LIMIT_8, BTNS_LIMIT_16, BTNS_LIMIT_32.

MIN_PRESS_TIME			10

Debouncing time in milliseconds. Default value is 10ms.

TIMER_ISR_RESOLUTION		1

Time period in milliseconds of the timer interrupt. Default is 1ms since this is the default for the millis library.

typedef BUTTONS_SIZE_LIMIT buttons_t;

Data type that defines the number of maximum buttons supported.

Millis timing library

This library is used to generate an interrupt every 1 millisecond that is needed to calculate debouncing time and long press detection. Two functions are needed:

void millis_init(F_CPU)

Used to initialize the millis library. F_CPU is recommended to be defined by project settings. I have described how this can be done, here: https://www.programming-electronics-diy.xyz/2024/01/defining-fcpu.html.

void millis_interrupt_attach(btnReader)

This attaches the btnReader function to the millis timer interrupt.

millis & micros project page: https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html.

Setup

 
void btnReaderSetup(uint8_t buttonLocation[], uint8_t arrayLength)

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 resistors are 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

Size of the array can be obtained like this sizeof(BUTTON_LOCATION).

The enum

To be able to identify what each button does and also if a combination of buttons was pressed an enumerator can be used 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.This is because in the button reading functions return value, each bit represents a button and first bit (from the right) represents 1 in binary, next bit 2, then 4, then and 8 and so on. ORing button 1 and 2 results 3 in binary.

This way any combination of buttons can be checked by using the OR like this BTN_UP | BTN_DOWN. This will be true in an if statement or a switch case if both buttons are pressed.

If you want to experiment with binary numbers you could use this online tool: https://bitwisecmd.com/.

Setting long press timeout


void btnReaderSetLongPressTime(uint16_t monitorPressTime)

Sets the time in milliseconds after a button is considered long pressed. Argument with value 0 will disable the long press detection.

Check reading complete

 
uint8_t btnReaderReady(void)

Used to check if the button reading and debouncing is complete.

Return: 1 if button reading is complete, 0 otherwise.

Get pressed buttons

 
buttons_t btnReaderPressedButtons(void)

Used to check which buttons were pressed then released.

Return: a binary representation of the pressed buttons with each bit of 1 representing a button.

Get active buttons

 
buttons_t btnReaderActiveButtons(void)

Check which buttons are actively pressed. In most cases this is not needed, but there are some in which knowing if a button is pressed is useful. Here is an example:

// Checks if BTN_1 is pressed while the BTN_2 is pressed then released.
// Could be used for example to use + and - buttons to set a variable
// but only if Setup is pressed down. When Setup is not pressed
// + and - could have other functions in cases where number of buttons
// available is minimal. Only works when btnReaderButtonCombination() is not used.
if((active_buttons & BTN_1) && ((pressed_buttons & (BTN_2 | BTN_3)))){
    UART_sendString(&uart0, "Setup: ");
    if(pressed_buttons == BTN_2){
	UART_sendString(&uart0, "+\n");
    }else{
	UART_sendString(&uart0, "-\n");
    }
}

active_buttons is set by this function while pressed_buttons is set by btnReaderPressedButtons().

Return: a binary representation of the actively pressed buttons with each bit of 1 representing a button.

Check for long pressed buttons

 
buttons_t btnReaderLongPressButtons(void)

Returns buttons that are pressed more that the set timeout.

Return: a binary representation of the long pressed buttons with each bit of 1 representing a button.

Check for double pressed button

 
buttons_t btnReaderDoubleClicked(void)

Returns the button that was double pressed. The time between the two 'clicks' is defined by TIMEOUT_DOUBLE_CLICK and while testing, 250ms default value seems to work well. If the value is higher then a normal button press could be interpreted as a double press.

Return: a binary representation of the double pressed button with a bit of 1 representing the button.

Read a button combination

 
buttons_t btnReaderButtonCombination(uint8_t active_btns_nr)

Returns the combination of buttons that is actively pressed.

active_btns_nr

How many buttons need to be pressed at the same time, to be considered a valid combination.

Return: returns once, a binary representation of the button combination.


Download

v3.0 buttonReader buttonReader .h and .c
millis library millis.h
millis.c
millis timing library. millis home page.
uart library uart.h
uart.c
uart library that can be used for debugging. UART home page.
Changelog
v3.0 (7, March, 2024) - added support for newer AVR UPDI devices. Tested on ATtiny402.
- library name and functions were renamed.
- API was simplified with some added functions.
- improved algorithm for a more reliable double click and button combination.
- some bugs were kil.. moved in a forest.

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