multiple LOW readings even with CHANGE interrupt (bouncy push button)

Hi,

I've wired up a momentary push button to pin2/interrupt0 on an Arduino Uno R3, and registered an interrupt function using attachInterrupt(0, ..., CHANGE). When I press the button I get multiple interrupts, which is fine since it bounces, however I get multiple interrupt calls while LOW and then while HIGH, which confuses me given that I've registered my interrupt routine with CHANGE.

I'm just starting out and I'm trying to understand why such a thing is happening. (I can pretty easily figure out how to debounce this to just the events that I care about.)

Here's the code:

/*
 *  Log state of digital pin 2, reporting how it changes.
 *
 *  CC0: Public Domain
 *  To the extent possible under law, Drew Folta has waived all copyright and
 *  related or neighboring rights to this work. 
 */


/**********************************************************************
 * LIBRARIES
 */
#include "Arduino.h"


/**********************************************************************
 * CONFIGURATION
 */

// how fast to report
const uint32_t  REPORT_BAUD = 115200;

// how long to gather interrupts (after first one)
const uint32_t  REPORT_WINDOW_MS = 1000;

// max number of interrupts to report
const size_t    REPORT_MAX_ENTRIES = 32;

// characters to use for showing state of the pin
const char      REPORT_IGNORE   = ' ';
const char      REPORT_LOW      = 'x';
const char      REPORT_FALLEN   = 'x';
const char      REPORT_HIGH     = '-';
const char      REPORT_RISEN    = '-';


/**********************************************************************
 * GLOBALS
 */
struct {
    uint32_t    when;   // when interrupt happened
    bool        pin2;   // state of digital pin 2
}                   report_entries[REPORT_MAX_ENTRIES];
volatile uint32_t   report_timeout      = 0;    // when we stop reporting
volatile size_t     report_next         = 0;    // index of next entry
volatile size_t     report_overflows    = 0;    // number of times we've gone
                                                // past REPORT_MAX_ENTRIES


/**********************************************************************
 * REPORTING
 */
void report_clear() {
    report_timeout      = 0;
    report_next         = 0;
    report_overflows    = 0;
}


// interrupts should be disabled when calling this so that the report_next
// doesn't change while we're filling out the structure
void report_entry(bool pin2) {
    uint32_t now = millis();
    if (! report_timeout) {
        report_timeout = now + REPORT_WINDOW_MS;
    }
    report_entries[report_next].when = now;
    report_entries[report_next].pin2 = pin2;
    report_next++;
    if (report_next >= REPORT_MAX_ENTRIES) {
        report_overflows++;
        // preserve the first log entry, since we diff aaginst it
        report_next = 1;
    }
}


void report_print(void) {
    char buffer[32];
    size_t last;

    Serial.print("======================================== REPORT");
    if (report_overflows) {
        Serial.print(" -- ");
        Serial.print(report_overflows);
        Serial.print(" overflows");
    }
    Serial.println("");

    for (size_t e = 0; e < report_next; e++) {
        last = e - 1;

        // index
        snprintf(buffer, 32, "%2u  ", e);
        Serial.print(buffer);

        // pin2
        snprintf(buffer, 32, " ");
        buffer[0] = report_entries[e].pin2 ? REPORT_HIGH : REPORT_LOW;
        if (e && (report_entries[e].pin2 != report_entries[last].pin2)) {
            buffer[0] = report_entries[e].pin2 ? REPORT_RISEN : REPORT_FALLEN;
        }
        buffer[1] = '\0';   // just in case;
        Serial.print(buffer);

        // when
        snprintf(buffer, 32, "  %6lu", report_entries[e].when);
        Serial.print(buffer);

        // diff against last
        if (e) {
            snprintf(buffer, 32, "  %5lu", report_entries[e].when - report_entries[last].when);
            Serial.print(buffer);
        }
        else {
            Serial.print("       ");
        }

        // diff against first
        snprintf(buffer, 32, "  %5lu", report_entries[e].when - report_entries[0].when);
        Serial.print(buffer);

        Serial.println("");
    }

    Serial.println("");
}


/**********************************************************************
 * INTERRUPTS
 */
void int_0(void) {
    report_entry(digitalRead(2));
}


void int_start(void) {
    attachInterrupt(0, int_0, CHANGE);
}


// we don't want to stop all interrupts, since Serial will need a clock to get
// data out
void int_stop(void) {
    detachInterrupt(0);
}


/**********************************************************************
 * EXECUTION
 */
void setup() {
    Serial.begin(REPORT_BAUD);
    report_clear();
    pinMode(2, INPUT_PULLUP);
    int_start();
    Serial.println("======================================== BOOTED");
    Serial.println("");
}


void loop() {
    if (report_timeout) {
        if (millis() >= report_timeout) {
            int_stop();
            report_print();
            report_clear();
            int_start();
        }
    }
}

(It's a little more complicated then necessary since it's a simplified version of attempting to log all digital pins via the pin change interrupts.)

Here is a sample run:

======================================== BOOTED

======================================== REPORT
 0  x     667             0
 1  x     724     57     57
 2  x     724      0     57
 3  -     869    145    202
 4  -     869      0    202

Any idea why I'm seeing multiple concurrent LOW readings even with attachInterrupt(..., CHANGE) ?

It is possible that the bounces are too short for you to get a separate interrupt for each one, bearing in mind that your ISR is quite slow. Also, the value you read with digitalRead is not necessarily the same as the state of the pin when the interrupt was triggered, because digitalRead is quite slow.

Push buttons and the like are better read using polling, except when you want to use them to wake up the mcu from a sleep mode.

Second that.

Mechanical contacts generating interrupts - very bad idea. Just not worth the trouble.

If you use an interrupt to wake, that interrupt should be disabled as part of the wake sequence, and polled thereafter (e.g. Red button on mobile phone).

OK, easy enough. Thanks everyone!