The image above exemplifies why you need to debounce your pushbuttons.
When people push a button, they expect one reaction per push. Due to the springy nature that pushes back at you when pressing them, buttons tend to bounce around when pressed and released, which will mess up the signal from them.
For example, let’s say we have a button that we intend to output 0V (logic 0) when pressed, and 5V (logic 1) when unpressed. If we probed the signal coming from the button during the transition from pushing it down to letting go, we would expect an immediate and clean transition from logic 0 to logic 1. What we end up seeing instead is the capture above. Before the signal settles to a flat 5V, it bounces between the two logic states many times.
Imagine if this was the signal your TV received when you pressed a button on your remote. If the signal was taken as is, and a transition from 0 to 1 meant increment the channel, you would probably have an aneurysm trying to navigate to a specific channel. This is why we need to debounce our buttons!
Debouncing attempts to ignore any intermittent jumping between logic states during an actual intended transition from 0 to 1 or 1 to 0. This can be done in hardware, with RC circuits and Schmitt trigger inverters, or in software with just the microcontroller. Let’s focus on software debouncing for now.
In this post we will first consider how to read a pushbutton input and turn on some LEDs in response. Next we will use the same hexadecimal counter circuit used in the previous post, with a pushbutton used to increment the counter, first with an undebounced implementation and then with a software debounced implementation
While you may find some people have rule of thumb time values for how long to ignore a button while it is bouncing around between states, it really depends on the button you are using. That being said, I will take a moment to look at two buttons I have on hand.
The scope trace at the beginning of this post comes from viewing the transition of this button. If you look inside of this button you will find that the culprit is a huge spring. It is very springy, and the scope trace is evidence of this. These kind of buttons are absolutely useless when using with a microcontroller unless there is some form of debouncing.
For my circuit, as it was built last time, I will be using a common tactile pushbutton that is well behaved in comparison to the one above.
The following oscilloscope trace comes from the setup above, where the input has a 1k pullup, and the other end of the button is grounded. I set the scope trigger to 3v, pushed the button, making the output ground, then let go.
We can see that the signal bounced up to 5V for about 50µs , dropped down to 0v for about 75µs, and finally stabilized to 5V. This is one of many traces I observed, some not bouncing at all, but the bouncing did happen frequently enough and needs to be accounted for.
Why did I trigger the scope on 3v? Well it just so happens that the ATtiny85 has a minimum input voltage of 3v to interpret as logic HIGH, while 1.5 V is the maximum interpreted logic LOW voltage. So any transition from below 1.5 V to above 3 V will count as a positive edge.
I found that the bouncing tended to last for under 150 µs before settling down, so for my debouncing implementation I will be conservative and ignore any bouncing transitions for 200 µs, which will still leave the button very responsive on a human time scale.
Let’s first use an undebounced pushbutton with the ATtiny85 to turn on all of the LEDs in our circuit.
Above is the circuit diagram for our system and picture of an implementation on a tiny breadboard.
Here is the pinout for the ATtiny85, for reference.
In our main function, we first set the lower 4 bits of Data Direction Register B (DDRB) in order to set pins PB0-PB3 as outputs for our LEDs (0x0F = 0b00001111). Our pushbutton in the circuit needs a pullup resistor to set the logic level high, and set it low when the button is pressed, pulling the input to ground. To set up the internal pullup resistor on PB4 which is our pushbutton pin, we leave PB4 bit in DDRB as 0, and set the corresponding PORTB bit to 1. That saves us from needing another resistor in the circuit!
Inside the while loop, an if statement checks if the button is pressed, if so, turns on the 4 LEDs, else turns them off. The register PINB has the logic level states of each pin on PORTB. That is to say that if our button is not pressed, the PB4 bit on PINB will be 1 due to the internal pullup resistor, and if the button is pressed it will be 0 due to being connected to ground. The expression (PINB & (1 << PB4)) gives us a byte with only the PB4 logic state in it. For example, if the button wasn’t pressed, it would be 00010000 & 00010000 = 00010000, which is greater than 0 and equates to true for our if statement. If the pushbutton is pressed the state of PB4 on PINB will be 0, so the expression will be 00000000 & 00010000 = 00000000, which is 0 or false for our if statement. By using ! to complement the logic, the if statement will execute when the button is pressed, and the else statement will execute when it isn’t pressed. In either case, turning on or off the first 4 bits of PORTB, we need to keep PB4 set to 1 so our pushbutton’s pullup doesn’t get unset, which would lead to the button not working anymore.
The video above demonstrates the circuit behavior. Although the pushbutton here is not debounced, it doesn’t affect the ability of it to turn the LEDs on and off. This is because any bouncing logic transitions occur so fast that we don’t notice the flickering in the LEDs.
What if we instead wanted every button press to increment a variable, much like the example where our hypothetical remote’s button advances to the next channel with every press?
Next we will implement a hexadecimal pushbutton counter with the same circuit, that will count from 0 to 15, incrementing with each button press, and displaying the binary representation of variable i on the LEDs.
Here is the undebounced implementation:
Inside our main function we initialize our pins the same as last time, and now include two bytes, i for our counter variable, and buttonWasPressed, which will be 1 if the button was pressed, and 0 if it was unpressed during the last while loop iteration.
Inside the main while loop we first check if the button is currently pressed and wasn’t pressed during the last iteration. If so, that means the state of the button has transitioned, and we will increment the counter variable, set PORTB to it along with keeping PB4 set for the pullup, and then set the buttonWasPressed state to 1.
If the if condition is false, we then check if the button is currently not pressed and the previous state was pressed, in which case we reset the buttonWasPressed state to 0.
The purpose of using buttonWasPressed is to make sure that we only increment the counter variable i once for each positive transition of the button’s input logic. If we remove it everywhere, the LEDs will just cycle between 0 to 15 at a blazing speed when the button is pressed.
Let’s now see how an unbounced pushbutton input doesn’t work well for counting:
Essentially, because each button press may cause the logic state on the input to bounce around a bit before settling, these bounces will be interpreted as valid logic edges and will increment the counter variable i. One button press may behave like 2, or 3.
Let’s think about how we can ignore those bouncing logical states during a button state transition
We could poll the pushbutton state in the while loop, and upon a logic level change delay for say 200 µs, which we decided should be long enough for the button to settle down, then check the state again and if it is the same we can call that a legitimate button state transition.
The problem with this algorithm is that first of all we are polling in the main while loop, so checking the button pin state has the same priority as all other code in the loop. This would lead to extra latency for a pushbutton response. If you have been following the previous ATtiny85 posts, you know by now that I don’t like waiting around aimlessly. A delay of 200 µs is 200 wasted clock cycles at 1 Mhz, which we could be doing something else with. We could consider using a pin change interrupt, or even better, an INT0 interrupt which has programmable logic edge detection, which could fire the interrupt for either a change from 1 to 0 or 0 to 1. But these pin change and INT0 interrupts cannot be debounced in software.
The debounbing algorithm we will use instead has Timer/Counter 1 count up to 200 µs, upon which it will fire an interrupt whose ISR will check the state of the button pin. If the state is different than last time we checked, we check to see if the buttonWasPressed flag is set, and if not we increment i, and set the buttonWasPressed flag, else we reset the buttonWasPressed flag
We have previousReading which stores either 0 if the button pin was set previously (i.e. the button was pressed), or 0b00010000 if it wasn’t set. We also have the buttonWasPressed, which keeps track of the previous state of the button, so we only increment i once per button press.
We initialize timer/counter 1 to clear the counter on a compare match with the value of OCR1C. We set the prescaler bits to divide the system clock used by the timer/counter by 4, and set OCR1C to 50, such that a compare match occurs every ([1 / (1E6/4)] * 50 = 200 µs. When the compare match occurs, the timer compare match (TIMER1_COMPA) vector will be called, and the corresponding ISR will be executed. So every 200 µs we will execute the ISR which will take care of the button pin checking and incrementing business if needed.
The ISR checks if the button pin state has changed from what it was previously, then checks if the button wasn’t pressed before, and if not, increments i, sets the buttonWasPressed flag to 1, and sets PORTB to i and the pullup pin. If the button pin state changed, and the button was previously pressed, this means the button is no longer pressed, so we reset the buttonWasPressed flag. Finally, at the end of each ISR execution we set the current button pin reading to the previousReading variable.
Inside the main function we set the outputs for the LEDS, set the pullup for the button, and initialize the timer and interrupt. That is it! The while loop is empty once again, and is free to do something else, although the price we pay for a debounced input is it being periodically interrupted.
Above is a video demonstrating how reliable the debounced button now is.
So to wrap this lesson up, if you have pushbuttons attached to a microcontroller, you need to debounce them, or else your buttons will cause you or your users to go mad.