M0AGX / LB9MG

Amateur radio and embedded systems

Debouncing buttons on EFM32 Happy Gecko

Button bounce is always a problem for microcontrollers. There are many ways to deal with the issue. The pins can be sampled at a low frequency so that the bounce will settle between consecutive samplings. They can be low-pass filtered in software. Some approaches require the pin to be stable for some amount of time to register a press.

Interrupts are usually avoided because the MCU could register almost every edge of the button bounce. With a small piece of code however interrupts can be used for one-shot operation and "rearmed" later from a timer interrupt (after the bounce period). This is the approach the driver uses. A major benefit is that the system reacts to the press immediately.

Interface

The driver has an init function that has to be called once at startup, a periodic task that reenables interrupts after the bounce stabilizes and function to get current press.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#pragma once
typedef enum {
    button_left  = 0,
    button_right = 1,
    button_enter  = 2,
    button_none  = 100,
} button_t;

void button_init(void);
void button_subtask_fromISR(void);
button_t button_press_get(void);

Implementation

The driver is written to run on an EFM32 Happy Gecko but should be easily portable to other MCUs in the family (and porting it to a totally different MCU should not be that difficult). First the pins are initialized as inputs with pullups (and filters), they are set to generate interrupts on the falling edge (I usually wire buttons from MCU pin to GND to save components by using the internal pullups).

When an interrupt is raised a flag is set indicating the button that was pressed. A predefined value (DEBOUNCE_PERIODS) is written to a per-button counter. This flag can be immediately (well... after the ISR) used by the application giving very nice, responsive operation to the user. The interrupt handler also disables the interrupt for that particular pin (to avoid receiving the bounces). Happy Gecko GPIOs have separate interrupt vectors for even and odd pin numbers so I used gcc alias attribute to point both vectors to the same function.

Second part happens in a periodic task. If a button that has been pressed a counter value is checked and decremented. When it reaches zero the button interrupt is reenabled.

The driver can be easily extended to use more buttons by configuring more pins and adding the masks to BUTTON_PIN_MASK array. button_t enum values can be adjusted to use meaningful names elsewhere in the application (the values should follow the order of pins in BUTTON_PIN_MASK).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "button.h"
#include 

#define PORT_BUTTON_S1 gpioPortA
#define PIN_BUTTON_S1  8

#define PORT_BUTTON_S2 gpioPortA
#define PIN_BUTTON_S2  9

#define PORT_BUTTON_S3 gpioPortA
#define PIN_BUTTON_S3  10

#define DEBOUNCE_PERIODS 20

static volatile button_t _current_button_press = button_none;

static volatile uint8_t _button_pressed[3]; //set by GPIO ISR, cleared by timer subtask
static const uint32_t BUTTON_PIN_MASK[3] = { (1 << PIN_BUTTON_S1), (1 << PIN_BUTTON_S2), (1 << PIN_BUTTON_S3) };

void button_init(void){
    GPIO_PinModeSet(PORT_BUTTON_S1, PIN_BUTTON_S1, gpioModeInputPullFilter, 1/*pullup*/);
    GPIO_PinModeSet(PORT_BUTTON_S2, PIN_BUTTON_S2, gpioModeInputPullFilter, 1/*pullup*/);
    GPIO_PinModeSet(PORT_BUTTON_S3, PIN_BUTTON_S3, gpioModeInputPullFilter, 1/*pullup*/);
    GPIO_IntConfig(PORT_BUTTON_S1, PIN_BUTTON_S1, false/*risingEdge*/, true/*fallingEdge*/, true/*enable*/);
    GPIO_IntConfig(PORT_BUTTON_S2, PIN_BUTTON_S2, false/*risingEdge*/, true/*fallingEdge*/, true/*enable*/);
    GPIO_IntConfig(PORT_BUTTON_S3, PIN_BUTTON_S3, false/*risingEdge*/, true/*fallingEdge*/, true/*enable*/);
    NVIC_EnableIRQ(GPIO_ODD_IRQn);
    NVIC_EnableIRQ(GPIO_EVEN_IRQn);
}

void button_subtask_fromISR(void){
    for (uint32_t i = 0; i < sizeof(_button_pressed)/sizeof(_button_pressed[0]); i++){
        if (_button_pressed[i]){
            _button_pressed[i]--;
            if (_button_pressed[i] == 0){
                GPIO_IntClear(BUTTON_PIN_MASK[i]);
                GPIO_IntEnable(BUTTON_PIN_MASK[i]);
            }
        }
    }
}

button_t button_press_get(void){
    button_t r = _current_button_press;
    _current_button_press = button_none;
    return r;
}

void button_irq_handler(void){
    uint32_t flags = GPIO_IntGet();

    for (uint32_t i = 0; i < sizeof(_button_pressed)/sizeof(_button_pressed[0]); i++){
        if (flags & BUTTON_PIN_MASK[i]){
            _button_pressed[i] = DEBOUNCE_PERIODS;
            GPIO_IntDisable(BUTTON_PIN_MASK[i]);
            GPIO_IntClear(BUTTON_PIN_MASK[i]);
            _current_button_press = i;
        }
    }
}
/* The GPIO interrupts are split into vectors for even and odd pins.
 * Make both vectors point to the same handler.
 */
void GPIO_EVEN_IRQHandler(void) __attribute__ ((alias ("button_irq_handler")));
void GPIO_ODD_IRQHandler(void)  __attribute__ ((alias ("button_irq_handler")));

How to use

The driver needs a periodic timer interrupt. I simply used the systick timer configured to fire at 100 Hz. The 100 Hz tick rate with DEBOUNCE_PERIODS of 20 means that the button is ignored for 20/100th of a second after a press (so only 5 pressed per second can be delivered - well enough for my application), this is a very safe value.

1
2
3
button_init();
SysTick_Config(SystemCoreClockGet() / 100); //100Hz tick rate
NVIC_EnableIRQ(SysTick_IRQn);

The timer interrupt handler:

1
2
3
void SysTick_Handler(void){
    button_subtask_fromISR();
}

Happy debouncing :-)

I release this code into public domain.