Debouncing switches

29 Apr '23

So you/I want to debounce a switch? How hard could it be?

Note: I’m not very good with electronics.

Switch basics

A reminder of the most basic switch circuit: A pull-up resistor is needed (or pull-down).

Three circuit diagrams of switch configurations. In all diagrams, the switch is connected to the negative/ground rail, and the output is considered to be on the positive side. In the first, there is a direct connection between the switch and the positive rail. In the second, there is no connection between the switch and the positive rail. In the third, there is a pull-up resistor between the switch and the positive rail.

Consider diagram 1, where the switch is directly connected to the positive rail. When the switch is open, the output is directly connected to the positive rail. When the switch is closed, it shorts the positive rail to the ground rail. Oops!

Consider diagram 2, where the switch is not connected to the positive rail. When the switch is closed, the output is directly connected to the negative/ground rail. When the switch is open, the output is floating. Oops!

Consider diagram 3, where the switch is connected to the positive rail with a pull-up resistor. When the switch is open, the output is pulled up to the positive rail. When the switch is closed, the output is directly connected to the ground rail. Only in this case is the input well-behaved (assuming the switch is ideal).

Microcontroller (MCU) inputs often have internal, configurable pull-up resistors (e.g. Arduino). This takes the place of the explicit pull-up resistor in the diagram. (Otherwise, 10k is a good value for the resistor.)

Note that this also the reason for the switch to pull the output to ground. This seems weird, since in the open position the signal will be high, and in the closed position the signal will be low. It’s easy to imagine the alternative scenario where the positive and negative rails are inverted, so that in the open position the signal will be low, and in the closed position the signal will be high. However, while MCU inputs often have pull-up resistors, it’s rarer for them to have pull-down resistors (e.g. Arduino doesn’t).

In reality, switch contacts “bounce”. That is when they make mechanical connection, the contacts physically bounce against each other. The result is an imperfect connection for a few milliseconds. This can be a problem when reading the switch state, as it might be read in the middle of a bounce. This can be okay. For example, if the switch state is simply begin reported, it might just be wrong for a few milliseconds. Usually though, switches are used to trigger actions or decisions, either when the switch goes from open to closed or from closed to open. This needs a clean transition, otherwise an action could be repeated very quickly multiple times. Achieving this is called “debouncing”.

This problem is as old as switches and digital logic. There have been many articles written about this problem, most famously Jack Ganssle’s “A Guide to Debouncing”. A Guide to Debouncing Part 1 “describes the problem of debouncing and gives emperical [sic] data”, A Guide to Debouncing Part 2 “shows, first, hardware solutions and then software debouncing code”.

However, I’ve found Max Maxfield’s Ultimate Guide to Switch Debounce to be excellent. Specifically part 2 for SPDT switches and part 3 for SPST switches.

Let’s discuss hardware debouncing first.

Hardware debouncing

SPDT switches

Some switches are single pole, double throw (SPDT) switches. This means the switch has both normally open (NO) and normally closed (NC) outputs. One example might be microswitches used in arcade joysticks or buttons. In this case, a “simple” hardware debounce option is a set-reset latch, explained in the articles above. A set-reset latch is also called SR latch or R-S latch, aka. bistable latch.

Why does this work? For two reasons.

First, SPDT switches - unless they are very weird - mechanically don’t connect both the NO and NC outputs to the common at the same time. Bouncing means that both outputs may be not connected, either connected to common, but never both connected to common. Assuming switching to low/ground with pull-ups, both outputs may be pulled to high, or either low, but never both low. Assuming switching to high with pull-downs, both outputs may be pulled to low/ground, or either high, but never both high.

Second, the latch. NAND SR latches are active low. They have no change when both inputs are high, and set/reset when either input is low. Both inputs low is invalid. NOR SR latches are active high. They have no change when both inputs are low, and set/reset when either input his high. Both inputs high is invalid.

Both NOR and NAND SR latches are handled in detail in the excellent video Latches and Flip-Flops 1 - The SR Latch by Computer Science.

Per switch, this requires:

Especially the three wires/two signal wires per switch can be surprisingly tedious. The upside is a fast and flawless debounce.

Luckily, quad NAND/NOR gate logic ICs are very commonly available, and largely suited to the task.

There are two common families for logic ICs, 4000-series ICs and 7400-series ICs. For 3.3V or 5V circuits, the HC 7400-series ICs are well-suited. Compared to 7400-series ICs, 4000-series ICs support higher input voltages but have slower propagation delays. For switch debouncing, either is likely fine. When looking for 7400-series ICs, “HC” or high-speed CMOS is generally suitable, so 74HC* part numbers. Look for these ICs:

SN74HC00 indicates Texas Instruments, MC74HC00 indicating Onsemi or M74HC00 indicating ST Microelectronics as the manufacturer. NXP/Nexperia seems to use just 74HC00. Careful, these part numbers are also used by clones. Sometimes, for ICs in a DIP package the “N” suffix is used; 74HC00N, SN74HC00N, etc.

There exist also SR latch ICs. At the time of writing, Digikey lists 48 SR latches. Most only have Q outputs, which is fine. By changing which IC inputs (S/R) the switch contacts (NO/NC) are connected to, the IC output (Q) can be chosen as desired. Basically no 7400-series HC SR latch ICs are available for reputable sources nowadays.

For a high-quality arcade stick, fight stick, or controller, where the part cost is already high and signal integrity is desired, this can be a great solution. Unfortunately, many Sanwa-style arcade joysticks using a PCB don’t break out both the NC and NO terminals…

SPST switches

For single pole, single throw switches, the above solution isn’t possible.

Three circuit diagrams of debounce configurations. In all diagrams, the switch is connected to the negative/ground rail, and to the positive rail via a pull-up resistor. A resistor and capacitor form a resistor-capacitor (RC) network on the switch output. This is the first diagram. In the second diagram, a diode is placed in parallel with the RC resistor. In the third diagram, a Schmitt trigger is added after the RC network.

The simplest solution (diagram 1) requires:

The values for the resistors and capacitor need to be chosen somewhat carefully, as described in the articles above. The capacitor is always discharged via R2. A diode can be added (diagram 2) to ensure the capacitor is only charged via R1, instead of R1 + R2.

The final improvement in diagram 3 is adding a Schmitt trigger buffer. This ensures that the output is always high or low, and not in an indeterminate state while the capacitor is charging/discharging.

Note that usually, an inverting Schmitt trigger is used. In this application where the logic level is high when the switch is open and low when the switch is closed (before the buffer), an inverting Schmitt trigger is quite nice, since the output will be inverted. Non-inverting Schmitt trigger ICs are also a lot less commonly available. Thankfully, Max Maxfield actually tells us why inverting Schmitt triggers are the usual choice:

In reality, it would be more common to use an inverting Schmitt trigger buffer, because inverting functions are faster than their non-inverting counterparts.

Max Maxfield, “Ultimate Guide to Switch Debounce” part 3

Anyway, the deluxe version requires:

The hex IC is nice, supporting 6 switches per IC. Consult the data sheet for how to terminate unused inputs and outputs. TI’s SN74HC14 data sheet states that “unused inputs must be terminated to either VCC or ground”, “unused outputs can be left floating”, and “do not connect outputs directly to VCC or ground”. They also recommend a 0.1uF decoupling cap between the ground/Vcc rails. The NXP/Nexperia data sheet is less helpful.

All of this seems like a lot of work though. And this is why hardware debouncing is largely not done for MCU applications. Because the input can also be debounced in software.

Software debouncing

Timer-based wait stabilisation

The timer-based solution is described in Adafruit’s make it switch debouncing article. The CircuitPython example is pretty obvious how this works:

if switch.value:
    time.sleep(0.05)  // wait 50 ms see if switch is still on
    if switch.value:
        # ok, it's really pressed

50 milliseconds seems like long enough to wait. This also means there is a 50ms delay until a press is registered.

Timer-based shift register

Single edge detection

Spread all over the internet, and in Jack Ganssle’s article is code like this:

bool debounce_pin() {
    static uint16_t state = 0;
    state = (state << 1) | digitalRead(PIN) | 0xfe00;
    return state == 0xff00;
}

I will explain the constants 0xfe00 and 0xff00 shortly. Let’s drop them for now; the second constant becomes 0x8000:

bool debounce_pin() {
    static uint16_t state = 0;
    state = (state << 1) | digitalRead(PIN);
    return state == 0x8000;
}

The read value is slowly shifted into state. Let’s examine the code:

For the condition of state == 0x8000 to be true, digitalRead must have returned high/1, followed by 15 low/0.

So, the code detects either:

(Invert digitalRead if the opposite behaviour is required.)

To be effective, this code must also be called periodically, e.g. every 5ms.

It’s now easy to see that the constant 0xfe00 or similar values (e.g. 0xe000) basically limits the shift register’s length. It can be generically calculated as:

// For example, debounce_pin is called every 5ms, and
// the total debounce time is desired to be 50ms.
// WARNING: bits must be <= 16
const uint16_t DEBOUNCE_DESIRED_BITS = 10;
const uint16_t DEBOUNCE_MASK = 0xffff << DEBOUNCE_DESIRED_BITS;
const uint16_t DEBOUNCE_COND = 0xffff << (DEBOUNCE_DESIRED_BITS - 1);

bool debounce_pin() {
    static uint16_t state = 0;
    state = (state << 1) | digitalRead(PIN) | DEBOUNCE_MASK;
    return state == DEBOUNCE_COND;
}

The combination of 0xe000 and 0xf000 gives an effective shift register length of 13 bits, the combination of 0xfe00 and 0xff00 a length of 9 bits.

A different condition could be chosen. Or even a different mask:

// For example, debounce_pin is called every 5ms, and
// the total debounce time is desired to be 45ms.
// WARNING: bits must be <= 16
const uint16_t DEBOUNCE_DESIRED_BITS = 9;
const uint16_t DEBOUNCE_MASK = (1 << DEBOUNCE_DESIRED_BITS) - 1;
const uint16_t DEBOUNCE_COND = (1 << (DEBOUNCE_DESIRED_BITS - 1));

bool debounce_pin() {
    static uint16_t state = 0;
    state = ((state << 1) | digitalRead(PIN)) & DEBOUNCE_MASK;
    return state == DEBOUNCE_COND;
}

The condition is now obvious. For 9 bits in length, the 9th bit is set (8 shift because zero-based).

This can be simplified further. For example, if the function can be called every 5ms, and a debounce time of 40ms is sufficient (usually is):

bool debounce_pin() {
    static uint8_t state = 0;
    state = (state << 1) | digitalRead(PIN);
    return state == (1 << 7);
}

This can be a good solution for detecting one edge, but not both.

Debouncing both edge transitions

Maybe something like this:

typedef struct _PinState {
    uint8_t pin;
    uint8_t state;
    uint8_t output;
} PinState;

void debounce_pin(PinState* pin)
{
    pin->state = pin->state << 1 | digitalRead(pin->pin);
    if (pin->state == 0xff)
        pin->output = HIGH;
    else if (pin->state == 0x00)
        pin->output = LOW;
}

int main()
{
    PinState pin = {
        .pin = MY_PIN,
        .state = 0,
        .output = LOW,
    };

    // call `debounce_pin(&pin);` in timer interrupt every 5ms
    // use `pin.output` in the code, e.g. for reporting via USB?
}
Newer Older