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).
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:
- Three wires, for both the NO/NC outputs and the common, instead of two wires for SPST switches.
- Two pull-up/pull-down resistors. The MCU pull-up resistors can’t be used, since they must be on the latch inputs.
- Either two NAND or two NOR gates per switch; alternatively an SR latch IC.
- A bunch of soldering on protoboard.
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:
- CD4001/4001: quad NOR gates
- CD4011/4011: quad NAND gates
- 74HC00/SN74HC00: quad NAND gates
- 74HC02/SN74HC02: quad NOR gates
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.
- CD4043*/4043: quad NOR SR latch
- CD4044*/4044: quad NAND SR latch
- MC14043: quad NOR SR latch
- MC14044: quad NAND SR latch
- 74HC279: quad NAND SR latch? Careful, avoid e.g. 74LS279 and similar unless they suit your application.
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.
The simplest solution (diagram 1) requires:
- Two wires, but ok, it’s a switch.
- One pull-up resistor (R1). The MCU pull-up resistor can’t be used, as the pull-up must be before the RC network. (Note: some versions have the pull-up resistor after the RC network. Discussed here. Avoid unless you’re sure.)
- One resistor (R2) and one capacitor (R2) for the RC network.
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.
Anyway, the deluxe version requires:
- Two wires, but ok, it’s a switch.
- One pull-up resistor (R1).
- One resistor (R2) and one capacitor (R2) for the RC network.
- An inverting Schmitt trigger IC. Hex buffers are available, look for 74HC14 / SN74HC14.
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:
- While
digitalRead
consistently returns low/0,state
converges on0x0000
- While
digitalRead
consistently returns high/1,state
converges on0xffff
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:
- The falling edge/deactivation of a switch or button that is active high
- The rising edge/activation of a switch or button that is active low
(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?
}