user16606: Buttons on separate interrupt pins work fine (triggered on FALLING), bouncing is handling by forcing a 80ms lockout period during which further presses are ignored. I like that solution for being lightweight.
My goal is to have 1 interrupt handle all events (buttons and other digital inputs). I don't want to depend on loop(), I wish to be free to use delay often rather than timer interrupts.
Most buttons execute an action when pressed, some, while pressed.
What I have now (just 2 buttons for now):


simulate this circuit – Schematic created using CircuitLab
void setup() {
  pinMode(17, INPUT_PULLUP);
  pinMode(16, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(3), wotHappnd, CHANGE);
}

boolean isDebouncing(){
  unsigned int now = millis();
  if(now - time_lastPressed > time_debounceLockout){
    time_lastPressed = now;
    return false;
  }
  return true;
}

void wotHappnd(){
  boolean interruptFalling = digitalRead(3) == LOW;
  if(interruptFalling && isDebouncing()) return;
  // digitalRead the pins; do stuff
}

Internal pullups on all used pins.
My problem is that often (~4%), on interrupt, the interrupt pin is LOW all button pins are HIGH ("not pressed" - likely bouncing just as I read it). How to fix that?
Can't use delay during an interrupt; delayMicroseconds to wait for stable readout still feels inappropriate; a cap-based delay on the interrupt pin induces a period of floating.
Prefer solutions in software or with no more than 1 extra component per input.
[edit] here's a claim that pin interrupts do some filtering already, which, if true, may make some solutions unreliable.

user30005: In the Isr, read the two pins and maintain their current status (UN debounced), and reset a counter.
In the main loop, increment that counter. If it is more than a certain number, it means that the current state of the key has been stable for a while - bounce no more.
Or Google Kuhn debounced. I wrote a blog page about that. If you want, I can post it later. The basic idea is the same.

user54011: As ever I am the voice of dissent: never write software to solve a hardware problem. 
For a double-throw switch the classic solution is an SR latch. 
Single throw is actually harder. On the face of it, debounce is a trivial matter done with a capacitor and a pull-down (or pull-up) resistor. This will certainly stop floating and false positives from induced current from spikes. However, microprocessors switch pretty fast and will recognise the edges of microsecond oscillations around the switching voltage as changes of switch state. To prevent this you need the hysteresis of a Schmitt Trigger such as an LM7414 (TTL) or LM7417 (suitable for 3V3). 
Here is the debounce circuit suggested in Understanding Schmitt Triggers. Unfortunately, it doesn't behave correctly under all conditions, for reasons explained on the Ganssle page referenced later.

You can fix the problem with a diode. For TTL, R1 = 82k&ohm;, R2 = 18k&ohm;, C = 1µF

Larger values of C will absorb slower bounces. One of my projects has a relay that bounces for so long I needed 470µF.
Here's what I regard as the last word on the subject — the Ganssle page. There's even a solution for multiple buttons on a single input. This is not quite the same as multiple buttons on a single interrupt but I think the approach may be applicable. 
Note that on the Ganssle page a software solution is touted as cheaper than hardware. That's irrelevant for a one-off, true for mid sized runs and potentially false for mass produced items. Debugging software has its own costs and so does product recall. Make your decision in context. Arduino projects are usually one-off.
One objection to a hardware solution is cost, in assembly time, component count and board size. For hobby work that's a personal choice, and for commercial work it's a trade-off between simplicity and reliability. Reliability of software hinges on deterministic behaviour and the management of complexity. The Atmel chip doesn't have a lot of DRAM or program space, and moving this problem out of the software will make it smaller, simpler and more predictable. 
That's not to say there aren't other ways to deal with the problem. 
If you really want to do it in software you need to measure the pulse width and compare it to a threshold. Since you need both falling and rising edges for this, bind to CHANGE and test for state. The primary difference between this and the Kuhn method mentioned by others is it uses the clock instead of a counter allowing you to specify millisecond duration rather than iterations.
In the excerpt below, the listen flag is a simple alternative to noInterrupt() or unbinding the handler. My loop code evaluates a state machine transition function for current state and inputs, and not all states want input. 
void isr_change()
{
    if (listen)
    {
        unsigned long now = millis();
        int d2 = digitalRead(2);
        if (d2 == LOW || fall_time == 0)
        { // FALLING
            fall_time = now;
        }
        if (d2 == HIGH)
        { // RISING
            command_pending = now - fall_time > MIN_PULSE_MS;
        }
    }
}


The state machine doesn't look at digital inputs, it looks at the command_pending flag, which is reset after the transition function finishes. 
This is one of those things that looks simple, isn't, but does have a stock solution. It beats me why there isn't a mass produced debouncer component the way you can get op-amps.
Thank you to kaay in comments for referring me to the MC14490 which is a DIP16 or SOIC16 with six diode protected debouncers. Kaay thinks it's expensive but consider the cost of the time to code and debug debounce of six inputs. What about the cost when the program doesn't quite fit and you have to rewrite it to shrink it? And then regression test everything. Would you pay someone $5 for an instant guaranteed result that doesn't complicate or bloat your software? The economics change with production scale of course, but in very small runs I think it's well worthwhile.
I found the MC14490 DIP16 for 5USD and the SOIC for 9AUD online. Local shops didn't carry it.

16606: <p>Buttons on separate interrupt pins work fine (triggered on <em>FALLING</em>), bouncing is handling by forcing a 80ms lockout period during which further presses are ignored. I like that solution for being lightweight.</p>

<p>My goal is to have 1 interrupt handle all events (buttons and other digital inputs). I don't want to depend on <em>loop()</em>, I wish to be free to use <em>delay</em> often rather than timer interrupts.<br>
Most buttons execute an action when pressed, some, while pressed.</p>

<p>What I have now (just 2 buttons for now):
</p>

<p><img src="https://i.stack.imgur.com/lW4oK.png" alt="schematic"></p>



<p><sup><a href="/plugins/schematics?image=http%3a%2f%2fi.stack.imgur.com%2flW4oK.png">simulate this circuit</a> &ndash; Schematic created using <a href="https://www.circuitlab.com/" rel="nofollow">CircuitLab</a></sup></p>

<pre class="lang-c++ prettyprint-override"><code>void setup() {
  pinMode(17, INPUT_PULLUP);
  pinMode(16, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(3), wotHappnd, CHANGE);
}

boolean isDebouncing(){
  unsigned int now = millis();
  if(now - time_lastPressed &gt; time_debounceLockout){
    time_lastPressed = now;
    return false;
  }
  return true;
}

void wotHappnd(){
  boolean interruptFalling = digitalRead(3) == LOW;
  if(interruptFalling &amp;&amp; isDebouncing()) return;
  // digitalRead the pins; do stuff
}
</code></pre>

<p>Internal pullups on all used pins.</p>

<p>My problem is that often (~4%), on interrupt, the interrupt pin is <em>LOW</em> all button pins are <em>HIGH</em> ("not pressed" - likely bouncing just as I read it). How to fix that?</p>

<p>Can't use <em>delay</em> during an interrupt; <em>delayMicroseconds</em> to wait for stable readout still feels inappropriate; a cap-based delay on the interrupt pin induces a period of floating.
Prefer solutions in software or with no more than 1 extra component per input.</p>

<p><strong>[edit]</strong> <a href="https://electronics.stackexchange.com/a/236692/65089">here</a>'s a claim that pin interrupts do some filtering already, which, if true, may make some solutions unreliable.</p>
 30005: <p>In the Isr, read the two pins and maintain their current status (UN debounced), and reset a counter.</p>

<p>In the main loop, increment that counter. If it is more than a certain number, it means that the current state of the key has been stable for a while - bounce no more.</p>

<p>Or Google Kuhn debounced. I wrote a blog page about that. If you want, I can post it later. The basic idea is the same.</p>
 54011: <p>As ever I am the voice of dissent: never write software to solve a hardware problem. </p>

<p>For a double-throw switch the classic solution is an SR latch. </p>

<p>Single throw is actually harder. On the face of it, debounce is a trivial matter done with a capacitor and a pull-down (or pull-up) resistor. This will certainly stop floating and false positives from induced current from spikes. However, microprocessors switch pretty fast and will recognise the edges of microsecond oscillations around the switching voltage as changes of switch state. To prevent this you need the hysteresis of a <a href="http://www.ti.com/lit/an/scea046/scea046.pdf" rel="nofollow noreferrer">Schmitt Trigger</a> such as an <a href="https://www.onsemi.com/pub/Collateral/MC74HC14A-D.PDF" rel="nofollow noreferrer">LM7414</a> (TTL) or <a href="http://www.ti.com/lit/ds/symlink/sn74aup1t17.pdf" rel="nofollow noreferrer">LM7417</a> (suitable for 3V3). </p>

<p>Here is the debounce circuit suggested in <a href="http://www.ti.com/lit/an/scea046/scea046.pdf" rel="nofollow noreferrer">Understanding Schmitt Triggers</a>. Unfortunately, it doesn't behave correctly under all conditions, for reasons explained on the Ganssle page referenced later.</p>

<p><a href="https://i.stack.imgur.com/pULTP.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/pULTP.png" alt="Almost, but misbehaves under out of spec conditions"></a></p>

<p>You can fix the problem with a diode. For TTL, R<sub>1</sub> = 82k&ohm;, R<sub>2</sub> = 18k&ohm;, C = 1&micro;F</p>

<p><a href="https://i.stack.imgur.com/Mdiua.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/Mdiua.png" alt="robust"></a></p>

<p>Larger values of C will absorb slower bounces. One of my projects has a relay that bounces for so long I needed 470&micro;F.</p>

<p><a href="http://www.ganssle.com/debouncing-pt2.htm" rel="nofollow noreferrer">Here's what I regard as the last word on the subject &mdash; the Ganssle page.</a> There's even a solution for multiple buttons on a single input. This is not quite the same as multiple buttons on a single interrupt but I think the approach may be applicable. </p>

<p>Note that on the Ganssle page a software solution is touted as cheaper than hardware. That's irrelevant for a one-off, true for mid sized runs and potentially false for mass produced items. Debugging software has its own costs and so does product recall. Make your decision in context. Arduino projects are usually one-off.</p>

<p>One objection to a hardware solution is cost, in assembly time, component count and board size. For hobby work that's a personal choice, and for commercial work it's a trade-off between simplicity and reliability. Reliability of <em>software</em> hinges on deterministic behaviour and the management of complexity. The Atmel chip doesn't have a <em>lot</em> of DRAM or program space, and moving this problem out of the software will make it smaller, simpler and more predictable. </p>

<p>That's not to say there aren't <em>other</em> ways to deal with the problem. </p>

<p>If you really want to do it in software you need to measure the pulse width and compare it to a threshold. Since you need both falling and rising edges for this, bind to CHANGE and test for state. The primary difference between this and the Kuhn method mentioned by others is it uses the clock instead of a counter allowing you to specify millisecond duration rather than iterations.</p>

<p>In the excerpt below, the <code>listen</code> flag is a simple alternative to noInterrupt() or unbinding the handler. My loop code evaluates a state machine transition function for current state and inputs, and not all states want input. </p>

<pre><code>void isr_change()
{
    if (listen)
    {
        unsigned long now = millis();
        int d2 = digitalRead(2);
        if (d2 == LOW || fall_time == 0)
        { // FALLING
            fall_time = now;
        }
        if (d2 == HIGH)
        { // RISING
            command_pending = now - fall_time &gt; MIN_PULSE_MS;
        }
    }
}

</code></pre>

<p>The state machine doesn't look at digital inputs, it looks at the <code>command_pending</code> flag, which is reset after the transition function finishes. </p>

<p>This is one of those things that looks simple, isn't, but does have a stock solution. It beats me why there isn't a mass produced debouncer component the way you can get op-amps.</p>

<p>Thank you to kaay in comments for referring me to the MC14490 which is a DIP16 or SOIC16 with six diode protected debouncers. Kaay thinks it's expensive but consider the cost of the time to code and debug debounce of six inputs. What about the cost when the program doesn't quite fit and you have to rewrite it to shrink it? And then regression test <em>everything</em>. Would you pay someone $5 for an instant guaranteed result that doesn't complicate or bloat your software? The economics change with production scale of course, but in very small runs I think it's well worthwhile.</p>

<p>I found the MC14490 DIP16 for 5USD and the SOIC for 9AUD online. Local shops didn't carry it.</p>
