Switch debouncing on Arduino

This is one of a series of posts about various aspects of my DIY weather station project YAWS. An overview of the project and links to all related posts are here. This post focusses on debouncing, particularly in the context of using a cup anemometer. 

It feels as though I have spent the last three days thinking about one line of code, and it wasn’t even my code. The line that I had come across on the web was something like:

if ((millis() - lastDebounce) > DEBOUNCE_TIME) { 

It was a line in an Arduino sketch, part of an Interrupt Service Routine (ISR) to handle a pin state change connected to a reed switch. If the condition was met the rest of the routine registered to the state change and set the lastDebounce variable to the current millis() value.

The initial thing that struck me was that this seemed to be remarkably simple debounce logic. The more I thought about it the less I was convinced that I understood how it could be working. My confusion was probably compounded by lack of experience with Arduino interrupt processing and too little real knowledge of the characteristics of bouncing in circuits. This led me to a couple of days research, experimentation and thinking.

Firstly, what is bouncing and debouncing? Bounces are transient oscillations in a signal as a switch changes state, due to either mechanical or electrical causes. Bouncing can occur on any state change, so from HIGH to LOW or LOW to HIGH. The problem is that reading the signal before it stabilises could return the wrong state. 

For example, consider this idealised representation of the signal from a switch:

signal1.png

The signal starts LOW (t0), then climbs when the switch is closed reaching a HIGH state (t1). It then bounces back down to LOW (t2) before settling at HIGH (t3). When the switch is opened (t4) a similar sequence happens, it goes LOW (t5), bounces HIGH (t6) and settles at LOW (t7). If the state of the switch was read at each of these points it would appear that the switch had been closed and opened three times instead of once.

Clearly this could be a significant problem. How serious would depend on the application. In my case I am specifically thinking about reading from a cup anemometer which uses a reed switch and magnet to signal each revolution of the head. Assuming that the reed switch could bounce something like this a simple count of the number of revolutions could be out by 100% or more.

Debouncing is the process of eliminating or ignoring these spurious bounce readings during state transitions. In other words, successful debouncing would interpret the signal above as a single close/open not two or three. Bounce can be reduced or eliminated by the physical design of the switch circuit, or dealt with in software.

Debouncing code typically reads the signal multiple times until the signal has been stable (i.e returning the same value) for some minimum period (the debounce time). Only then is the state change registered. The sort of debounce logic I am familiar with looks like this:

void ORS_Button::poll()
{
  byte reading = digitalRead(_pin);
  unsigned long milliNow = millis();

  if (reading != _lastReading) {
    // state has changed so start the debounce timer
    _debounceStart = milliNow;
  }

  if ((milliNow - _debounceStart) >= _debounceTime) {
    // reading constant within the debounce time
    if (reading != _state) {
      _changed = true;
      _state = reading;
      _changedMillis = _debounceStart;
    } else {
      _changed = false;
    }
  }
  _lastReading = reading;
}

This is a method from a class I wrote to handle debouncing switches. This method would be called in the loop() function of a sketch before interrogating the state of the switch.

Note that this code differentiates between the state of the switch (the last state that passed the debounce logic) and the last reading (the last state read from the switch). So in the example below the state of the switch would be LOW until sometime after t3 (actually t2 + the debounce time) when the HIGH signal would have passed the debounce logic and been declared the new state. During this period the readings will have gone through LOW, HIGH, LOW and HIGH periods.

signal2.png

One drawback of this design is that it relies on the method being called regularly to avoid missing some change in the state of the switch. This is easy to do in a sketch where the execution time of loop() is bounded and short. It demands a bit more thought if there is the possibility of loop() going off to do some long duration task.

The obvious way to avoid having to schedule the regular checks of the switch would be to use an Interrupt Service Routine (ISR) and have the Arduino's interrupt system call the ISR when the state changes. This is the approach taken by the code I was looking at on the web. Counting the number of 'clicks' of the switch over a fixed period of time was handled by an ISR. 

So what was worrying me about the debounce logic in the ISR? Glossing over initialisation issues, the first time the ISR is called it will register a click and start the debounce timer (by resetting the last debounce time). Any subsequent calls to the ISR within the debounce time will be ignored. Once the debounce period has expired the next ISR call will register the click and restart the timer. In other words, all this logic does is ignores all calls that occur within the debounce time after a call that is registered. The question is, is this enough for effective debouncing?

A key piece of information in answering this is when will the ISR be called? An ISR is set-up using the attachInterrupt() function, for example:

attachInterrupt(digitalPinToInterrupt(PIN_X), isr_name, RISING);

The Arduino reference for attachInterrupt() states that the RISING mode is used "to trigger when the pin goes from low to high". Looking at our model signal again there are three periods where the signal is rising and three points at which it goes from LOW (under 3v) to HIGH (above 3v).

signal3.png

It would be reasonable to assume that the ISR would be called three times for this signal (at t0, t1 and t2). To be effective the debounce logic would have to ignore two of these. Whether this design does that will depend on the debounce time. If the debounce time is longer than (t2 - t0) it will register t0 and ignore t1 and t2. If the debounce time were shorter it would register t0, ignore t1 and register t2.

In other words the debounce time would have to be greater than the expected duration of the pulse (from starting to rise to the end of the fall - Note that this is not what is measured by pulseIn(), apologies for any confusion). To me this seems like an unacceptable solution. It reduces the debounce process to one hardcoded assumption, effectively saying "Any interrupts in the next N milliseconds can be ignored because I know that they belong to this pulse".

Perhaps this would be OK if the expected duration was well understood, did not vary significantly between pulses, and if the variations were significantly smaller than the expected duration between pulses. If these were not true the risk would be that an unusually long pulse could register as two (registering a bounce on the falling side of the pulse), or that two unusually short pulses close together could be treated as a single pulse.

This discussion is all starting to feel a bit detached from reality. Lots of nice assumptions, but when are interrupts really fired, how long are the pulses, how long does a reed switch bounce for anyway?

This is where I wish I had an oscilloscope or perhaps a logic analyser (and believe me I did spend some time contemplating whether I could justify such purchases). In the absence of such luxuries I decided to see what data I could get from a bit of Arduino introspection. I wired up an Uno to a spare anemometer (Yes, "spare". Sad but true) and a manual switch. I then wrote a sketch to capture the timing of switch pulses (using pulseIn() and micros()) and the timing of RISING interrupts (using micros() in an ISR). I ran the data through a Python script to help visualise the results.

First off, this is the plot from pulses from the anemometer in light-ish wind:

anemOnly.png

The red marks show the interrupts and the blue marks show the pulses. This all looks pretty clean - one interrupt per pulse, at the start of the pulse. Typically the interrupts are occurring 1 to 2 milliseconds before the start of the pulse. The duration of the pulses varies between about 210 and 345 milliseconds, with an average gap of about 135 milliseconds between pulses. While this looks like a nice manageable dataset it has enough variability to defeat the ISR debouncing approach. The debounce time would need to be greater than 345 milliseconds, which could incorrectly handle a 210 millisecond pulse followed by a 135 millisecond gap before the next pulse.

To confuse things, here is another sample from the same set-up, but lighter winds:

thu01.png

This is a very different pattern. Here the red marks below the blue represent ISR calls during the pulse. So this shows very short pulses (about 30 microseconds) each with two interrupts, one immediately before the pulse and the second about 12 microseconds before the end of the pulse.

I find this utterly baffling. The anemometer is turning more slowly, as shown by the larger gaps between the pulses, but the pulses are shorter and seemingly more noisy (on the assumption that the second interrupt is to do with bounce). I was expecting to see pulse length inversely proportional to rotation speed (i.e. higher speed, shorter pulses as the magnet would pass by more quickly). An order of magnitude difference in pulse length with a halving of rotation speed doesn't indicate the sort of nice linear relationship I would have expected. Something drastic seems to have changed between the two datasets.