Introduction: Arduino Button Tutorial
What's simpler and dumber than a button, you ask...
I say: behind a button, unexpected things can hide. And within a program that does various things, handling a button can be tricky. The nice thing is that interesting functions can be built with a simple dumb button.
This tutorial will address these aspects:
The programming approach is based on polling, encouraged by the Arduino loop() principle, and which is perfectly acceptable for simple to moderately complex Arduino projects. We'll require the duration of each loop() execution to be "fairly" the same each time.
More advanced implementations, not covered here, may include the usage of interrupts and timers. They are more accurate, but also harder to understand.
This tutorial is intended to people with a basic first Arduino experience (i.e. with knowledge of the IDE, and of the compilation, flashing and running of sketches).
In the following ZIP file, the four sketches used in this tutorial can be found.
I say: behind a button, unexpected things can hide. And within a program that does various things, handling a button can be tricky. The nice thing is that interesting functions can be built with a simple dumb button.
This tutorial will address these aspects:
- wiring and configuring pins, using pull-up/pull-down resistor,
- deglitching,
- detecting states versus events,
- detecting long presses,
- and some object-oriented programming.
The programming approach is based on polling, encouraged by the Arduino loop() principle, and which is perfectly acceptable for simple to moderately complex Arduino projects. We'll require the duration of each loop() execution to be "fairly" the same each time.
More advanced implementations, not covered here, may include the usage of interrupts and timers. They are more accurate, but also harder to understand.
This tutorial is intended to people with a basic first Arduino experience (i.e. with knowledge of the IDE, and of the compilation, flashing and running of sketches).
In the following ZIP file, the four sketches used in this tutorial can be found.
Attachments
Step 1: Connecting the Button
The Button
This is a momentary switch, with one stable position (open) when no force is exerted, and conducting (closed) when pressed. It is one of the simplest electro-mechanical sensing device.
Connect the button like illustrated in the photo of this step.
(Don't be impressed by the size of my breadboard. A small one will be actually handier.)
The I/O Pin
The AVR (aka ATmega, i.e. the Atmel chip powering the Arduino board) has several I/O pins. I/O means that they can be freely configured by software as input or output.
Pin 2 will be a good choice for our example. It will be used to measure the state of the button: pressed or released.
Pull-up Resistor
The pin has to be connected to somewhere via the button. The question is: where.
A first idea would be to go to VCC. VCC is the usual denomination for the supply voltage, here 5V.
So when the button is pressed, the pin would be connected to VCC, and the software would read HIGH. But when the button is released, the pin is connected to nowhere, aka "floating", and will be exposed to noise, and the software will read HIGH and LOW in an erratic way.
So the solution is to use a so-called pull-up or pull-down resistor. Such a resistor ensures that the pin is always connected to GND or VCC, directly or via the resistor, depending on the button position.
Fortunately, the AVR chip has, internally, a 20 kOhm pull-up resistor that can be connected to the pin (internally). The pin must be configured as input, and its value, in this situation, tells whether the pull-up is connected (otherwise the value defines, when the pin is configured as output, its output state).
With this pull-up, we'll connect the pin to GND through the button, and have these situations when the button is released, respectively pressed:
Consumption
When the button is pressed, the resistor gets a voltage difference equal to VCC, and a current I is flowing:
Polarity
Had we a pull-down at our disposal, we would have connected the pin to VCC instead of GND, and read HIGH upon press, which is more logical. But since we have a pull-up only, we'll have to reverse the polarity by software, at pin sampling.
For more about I/O pins, follow http://www.arduino.cc/en/Tutorial/DigitalPins.
Programming
The configuration of the AVR pin (as input and with pull-up enabled) is described in the code below.
Code
––––––––––8<––––––––––
This is a momentary switch, with one stable position (open) when no force is exerted, and conducting (closed) when pressed. It is one of the simplest electro-mechanical sensing device.
Connect the button like illustrated in the photo of this step.
(Don't be impressed by the size of my breadboard. A small one will be actually handier.)
The I/O Pin
The AVR (aka ATmega, i.e. the Atmel chip powering the Arduino board) has several I/O pins. I/O means that they can be freely configured by software as input or output.
Pin 2 will be a good choice for our example. It will be used to measure the state of the button: pressed or released.
Pull-up Resistor
The pin has to be connected to somewhere via the button. The question is: where.
A first idea would be to go to VCC. VCC is the usual denomination for the supply voltage, here 5V.
So when the button is pressed, the pin would be connected to VCC, and the software would read HIGH. But when the button is released, the pin is connected to nowhere, aka "floating", and will be exposed to noise, and the software will read HIGH and LOW in an erratic way.
So the solution is to use a so-called pull-up or pull-down resistor. Such a resistor ensures that the pin is always connected to GND or VCC, directly or via the resistor, depending on the button position.
Fortunately, the AVR chip has, internally, a 20 kOhm pull-up resistor that can be connected to the pin (internally). The pin must be configured as input, and its value, in this situation, tells whether the pull-up is connected (otherwise the value defines, when the pin is configured as output, its output state).
With this pull-up, we'll connect the pin to GND through the button, and have these situations when the button is released, respectively pressed:
Button not pressed: VCC | 20K | | internal | | pull-up |_| | | _____ input ––––*––––––o–––––––––o o––––– GND pin released button
Input is isolated from GND, so only connected to VCC via the resistor. No current flows.
Without the pull-up resistor, the input would be "floating".
Button pressed: VCC | : 20K | | : internal | | : pull-up |_| : some current flows | `- - - - - - - - - -> | input ––––*––––––o–––––––––o–––––o––––– GND pin pushed button
Input is now directly connected to GND. Some current flows through the resistor.
In both cases, we now have a clearly defined situation.Consumption
When the button is pressed, the resistor gets a voltage difference equal to VCC, and a current I is flowing:
I = VCC / R
= 5 / 20,000 = 0.25 mA
Corresponding to consuming the power P:
P = VCC2 / R
= 52 / 20,000 = 1.25 mW
Polarity
Had we a pull-down at our disposal, we would have connected the pin to VCC instead of GND, and read HIGH upon press, which is more logical. But since we have a pull-up only, we'll have to reverse the polarity by software, at pin sampling.
For more about I/O pins, follow http://www.arduino.cc/en/Tutorial/DigitalPins.
Programming
The configuration of the AVR pin (as input and with pull-up enabled) is described in the code below.
Code
––––––––––8<––––––––––
#define BUTTON_PIN 2 void setup() { ... pinMode(BUTTON_PIN, INPUT); digitalWrite(BUTTON_PIN, HIGH); // connect internal pull-up ... } void loop() { ... }––––––––––>8––––––––––
Step 2: Sampling, Deglitching, and Reading States
Glitches
Buttons are like many things: imperfect. Even when they give a firm mechanical sensation, they generate a couple of oscillations when the button position changes.
These oscillations are called glitches or bounces. They can be eliminated by adding a capacitor (introducing some delay), or by software.
Filtering by Sampling
Often, your Arduino application is loop()-based, i.e. does something, then sleeps awhile, repeatedly. The button state sampling code can look like:
The first picture of this step shows the glitches and the periodic sampling (indicated in blue).
Reading the State
The code below shows how the periodic sampling is done. When the button is seen as not pressed, a dot is sent to the serial line; when the button is seen as pressed, a caret is sent.
The second picture of this step shows the serial output, for one long press followed by two short presses.
Programming
In the code below, note that I have chosen a variable name (button_pressed) representing a high level of abstraction (the button state), and not the electrical state (button, conducting or not, nor electrical value read on the pin), which would be confusing because of the fact that, due to the pull-up resistor, the pin reads HIGH when the button is released.
Pro/Cons
Very simple implementation. Suited for continuous control (e.g. break pedal).
Unsuited for incremental control (e.g. sound incremental volume control) -- which will be explained in the next step.
Code
––––––––––8<––––––––––
Buttons are like many things: imperfect. Even when they give a firm mechanical sensation, they generate a couple of oscillations when the button position changes.
These oscillations are called glitches or bounces. They can be eliminated by adding a capacitor (introducing some delay), or by software.
Filtering by Sampling
Often, your Arduino application is loop()-based, i.e. does something, then sleeps awhile, repeatedly. The button state sampling code can look like:
void loop() { // handle button boolean button_pressed = read_button(); // do other things do_stuff(button_pressed); // sleep a moment before next iteration delay(DELAY); }This means that at least DELAY milliseconds elapse between successive button samplings. Depending on DELAY, this will make us insensitive to the glitches. The responsiveness is not totally accurate, but largely sufficient: there will be some jitter in the magnitude of DELAY. It must be insured that the things we do during each loop take a "small" and comparable time for each iteration.
The first picture of this step shows the glitches and the periodic sampling (indicated in blue).
Reading the State
The code below shows how the periodic sampling is done. When the button is seen as not pressed, a dot is sent to the serial line; when the button is seen as pressed, a caret is sent.
The second picture of this step shows the serial output, for one long press followed by two short presses.
Programming
In the code below, note that I have chosen a variable name (button_pressed) representing a high level of abstraction (the button state), and not the electrical state (button, conducting or not, nor electrical value read on the pin), which would be confusing because of the fact that, due to the pull-up resistor, the pin reads HIGH when the button is released.
Pro/Cons
Very simple implementation. Suited for continuous control (e.g. break pedal).
Unsuited for incremental control (e.g. sound incremental volume control) -- which will be explained in the next step.
Code
––––––––––8<––––––––––
#define BUTTON_PIN 2 // Button #define DELAY 20 // Delay per loop in ms void setup() { pinMode(BUTTON_PIN, INPUT); digitalWrite(BUTTON_PIN, HIGH); // pull-up Serial.begin(9600); } boolean handle_button() { int button_pressed = !digitalRead(BUTTON_PIN); // pin low -> pressed return button_pressed; } void loop() { // handle button boolean button_pressed = handle_button(); // do other things Serial.print(button_pressed ? "^" : "."); // add newline sometimes static int counter = 0; if ((++counter & 0x3f) == 0) Serial.println(); delay(DELAY); }––––––––––>8––––––––––
Step 3: Detecting Edges
Edges
When we want to catch edges or transitions, we need to slightly enhance the program of the previous step.
We introduce a global variable (button_was_pressed) that remembers the last read state, so that we can detect a state change.
In this example, we detect the transitions from not pressed to pressed, and will signal them by an event, as shown on the first picture of this step.
Programming
At each iteration, if we have an event, we'll send a caret to the serial line, otherwise a dot. See the second picture of this step. Again, for one long press and two short presses. Notice that the long press has generated only one event.
In the code below, also note that I have chosen variable names (button_now_pressed) representing a high level of abstraction (the button state), and not the electrical state (button, conducting or not, nor electrical value read on the pin).
Pro/Cons
Still simple implementation. The introduction of global variables starts to put the scalability of the program at risk. For one button, still quite okay though.
Code
––––––––––8<––––––––––
When we want to catch edges or transitions, we need to slightly enhance the program of the previous step.
We introduce a global variable (button_was_pressed) that remembers the last read state, so that we can detect a state change.
In this example, we detect the transitions from not pressed to pressed, and will signal them by an event, as shown on the first picture of this step.
Programming
At each iteration, if we have an event, we'll send a caret to the serial line, otherwise a dot. See the second picture of this step. Again, for one long press and two short presses. Notice that the long press has generated only one event.
In the code below, also note that I have chosen variable names (button_now_pressed) representing a high level of abstraction (the button state), and not the electrical state (button, conducting or not, nor electrical value read on the pin).
Pro/Cons
Still simple implementation. The introduction of global variables starts to put the scalability of the program at risk. For one button, still quite okay though.
Code
––––––––––8<––––––––––
#define BUTTON_PIN 2 // Button #define DELAY 20 // Delay per loop in ms boolean button_was_pressed; // previous state void setup() { pinMode(BUTTON_PIN, INPUT); digitalWrite(BUTTON_PIN, HIGH); // pull-up Serial.begin(9600); button_was_pressed = false; } boolean handle_button() { boolean event; int button_now_pressed = !digitalRead(BUTTON_PIN); // pin low -> pressed event = button_now_pressed && !button_was_pressed; button_was_pressed = button_now_pressed; return event; } void loop() { // handle button boolean raising_edge = handle_button(); // do other things Serial.print(raising_edge ? "^" : "."); // add newline sometimes static int counter = 0; if ((++counter & 0x3f) == 0) Serial.println(); delay(DELAY); }––––––––––>8––––––––––
Step 4: Distinguishing Short From Long Presses
Pulse Length
Many devices with few controls (e.g. digital watches) pack multiple functions per button.
It is very useful, saving precious volume, but must be used wisely, otherwise the device may become unintuitive to use.
Distinguishing short from long presses is about measuring the pulse length.
The event is no longer emitted upon pressing the button, but upon releasing it. This can affect the feeling of responsiveness. But until non-causal devices can be purchased, we cannot predict the duration and must hence proceed so.
This is a combined event and state handling: we detect the state change (an event), and the duration that the resulting state is lasting.
Programming
So we have to introduce another global variable (button_pressed_counter). Note also that the event has no longer a boolean value (event present or not, like in the previous step), but three states. They are defined by an enum. (I had troubles defining an enum type; Arduino-specific issue? So I used enum to define constants).
The first picture of this step show a long press and a short press timing, with a long press threshold of 3 sampling periods (which is way too short, but suited to the drawing).
The second picture shows the serial output for one long press followed by two short presses.
Pro/Cons
Nicely enhances the button functionality (if used wisely, to insure good usability). But the code starts to grow, and get polluted by global variables... The next step shows the OO variant, made for scaling.
Code
––––––––––8<––––––––––
Many devices with few controls (e.g. digital watches) pack multiple functions per button.
It is very useful, saving precious volume, but must be used wisely, otherwise the device may become unintuitive to use.
Distinguishing short from long presses is about measuring the pulse length.
The event is no longer emitted upon pressing the button, but upon releasing it. This can affect the feeling of responsiveness. But until non-causal devices can be purchased, we cannot predict the duration and must hence proceed so.
This is a combined event and state handling: we detect the state change (an event), and the duration that the resulting state is lasting.
Programming
So we have to introduce another global variable (button_pressed_counter). Note also that the event has no longer a boolean value (event present or not, like in the previous step), but three states. They are defined by an enum. (I had troubles defining an enum type; Arduino-specific issue? So I used enum to define constants).
The first picture of this step show a long press and a short press timing, with a long press threshold of 3 sampling periods (which is way too short, but suited to the drawing).
The second picture shows the serial output for one long press followed by two short presses.
Pro/Cons
Nicely enhances the button functionality (if used wisely, to insure good usability). But the code starts to grow, and get polluted by global variables... The next step shows the OO variant, made for scaling.
Code
––––––––––8<––––––––––
#define BUTTON_PIN 2 // Button #define LONGPRESS_LEN 25 // Min nr of loops for a long press #define DELAY 20 // Delay per loop in ms enum { EV_NONE=0, EV_SHORTPRESS, EV_LONGPRESS }; boolean button_was_pressed; // previous state int button_pressed_counter; // press running duration void setup() { pinMode(BUTTON_PIN, INPUT); digitalWrite(BUTTON_PIN, HIGH); // pull-up Serial.begin(9600); button_was_pressed = false; button_pressed_counter = 0; } int handle_button() { int event; int button_now_pressed = !digitalRead(BUTTON_PIN); // pin low -> pressed if (!button_now_pressed && button_was_pressed) { if (button_pressed_counter < LONGPRESS_LEN) event = EV_SHORTPRESS; else event = EV_LONGPRESS; } else event = EV_NONE; if (button_now_pressed) ++button_pressed_counter; else button_pressed_counter = 0; button_was_pressed = button_now_pressed; return event; } void loop() { // handle button boolean event = handle_button(); // do other things switch (event) { case EV_NONE: Serial.print("."); break; case EV_SHORTPRESS: Serial.print("S"); break; case EV_LONGPRESS: Serial.print("L"); break; } // add newline sometimes static int counter = 0; if ((++counter & 0x3f) == 0) Serial.println(); delay(DELAY); }––––––––––>8––––––––––
Step 5: Adding Buttons, and Going OO
Issues Scaling Up
As we saw in the previous step, it is becoming hard to scale up. There are global state variables, which are referred to by some functions. Imagine adding a 2nd button: the nightmare will begin; not speaking about a 3rd button...
Programming: Going OO (Object-Oriented)
Arduino uses the Processing language built on top of C++, so why not use some OO features?
We need to pack these global variables into structures that we can create at will. Such structures, with associated handling functions are called classes and methods in the OO world. A class is just the description of the object, whereas the effective memory allocated for each object is called the instance.
The first picture of this step shows that we have added a 2nd button. The second picture shows the now traditional long/short presses, working independently very nicely. The 2nd button has, by the way, a longer long-press threshold.
Pro/Cons
This code is getting more complex, which is normal because, when scaling, the complexity raises by one significant step. The good news is that it can then support any number of additional buttons at no complexity increase.
Processing sets some limitations (e.g. there is no dynamic creation: the new and delete operators are not available). Which is not bad, because we also don't want to get lost into some of the particularly gory sides of C++, that are (arguably) unnecessary in small embedded systems like Arduino.
Sure the same could be done in pure C, so if you need/prefer a C implementation, just ask me.
Code
––––––––––8<––––––––––
As we saw in the previous step, it is becoming hard to scale up. There are global state variables, which are referred to by some functions. Imagine adding a 2nd button: the nightmare will begin; not speaking about a 3rd button...
Programming: Going OO (Object-Oriented)
Arduino uses the Processing language built on top of C++, so why not use some OO features?
We need to pack these global variables into structures that we can create at will. Such structures, with associated handling functions are called classes and methods in the OO world. A class is just the description of the object, whereas the effective memory allocated for each object is called the instance.
The first picture of this step shows that we have added a 2nd button. The second picture shows the now traditional long/short presses, working independently very nicely. The 2nd button has, by the way, a longer long-press threshold.
Pro/Cons
This code is getting more complex, which is normal because, when scaling, the complexity raises by one significant step. The good news is that it can then support any number of additional buttons at no complexity increase.
Processing sets some limitations (e.g. there is no dynamic creation: the new and delete operators are not available). Which is not bad, because we also don't want to get lost into some of the particularly gory sides of C++, that are (arguably) unnecessary in small embedded systems like Arduino.
Sure the same could be done in pure C, so if you need/prefer a C implementation, just ask me.
Code
––––––––––8<––––––––––
#define BUTTON1_PIN 2 // Button 1 #define BUTTON2_PIN 3 // Button 2 #define DEFAULT_LONGPRESS_LEN 25 // Min nr of loops for a long press #define DELAY 20 // Delay per loop in ms ////////////////////////////////////////////////////////////////////////////// enum { EV_NONE=0, EV_SHORTPRESS, EV_LONGPRESS }; ////////////////////////////////////////////////////////////////////////////// // Class definition class ButtonHandler { public: // Constructor ButtonHandler(int pin, int longpress_len=DEFAULT_LONGPRESS_LEN); // Initialization done after construction, to permit static instances void init(); // Handler, to be called in the loop() int handle(); protected: boolean was_pressed; // previous state int pressed_counter; // press running duration const int pin; // pin to which button is connected const int longpress_len; // longpress duration }; ButtonHandler::ButtonHandler(int p, int lp) : pin(p), longpress_len(lp) { } void ButtonHandler::init() { pinMode(pin, INPUT); digitalWrite(pin, HIGH); // pull-up was_pressed = false; pressed_counter = 0; } int ButtonHandler::handle() { int event; int now_pressed = !digitalRead(pin); if (!now_pressed && was_pressed) { // handle release event if (pressed_counter < longpress_len) event = EV_SHORTPRESS; else event = EV_LONGPRESS; } else event = EV_NONE; // update press running duration if (now_pressed) ++pressed_counter; else pressed_counter = 0; // remember state, and we're done was_pressed = now_pressed; return event; } ////////////////////////////////////////////////////////////////////////////// // Instantiate button objects ButtonHandler button1(BUTTON1_PIN); ButtonHandler button2(BUTTON2_PIN, DEFAULT_LONGPRESS_LEN*2); void setup() { Serial.begin(9600); // init buttons pins; I suppose it's best to do here button1.init(); button2.init(); } void print_event(const char* button_name, int event) { if (event) Serial.print(button_name); Serial.print(".SL"[event]); } void loop() { // handle button int event1 = button1.handle(); int event2 = button2.handle(); // do other things print_event("1", event1); print_event("2", event2); // add newline sometimes static int counter = 0; if ((++counter & 0x1f) == 0) Serial.println(); delay(DELAY); }––––––––––>8––––––––––
Step 6: Conclusion
If you're new to programming and have read this tutorial so far, kudos! don't hesitate to study the provided code, be critical, and ask questions. The full code archive can be found in the intro step, and here is the link again: arduino--button_tut.zip
I hope you have enjoyed this small tutorial. Now get some buttons, program something awesome, and show us what you made!
Other Links
I hope you have enjoyed this small tutorial. Now get some buttons, program something awesome, and show us what you made!
Other Links
-
A short and basic article about Arduino's programmable input/output pins:
http://www.arduino.cc/en/Tutorial/DigitalPins -
An in-depth article on input pins, pull-up/down resistors, glitches:
http://www.ladyada.net/learn/arduino/lesson5.html -
Getting a quick idea of OO and C++:
http://www.mactech.com/articles/mactech/Vol.09/09.10/CPPBasics/index.html