Arduino Button Tutorial

by laxap in Circuits > Arduino

281723 Views, 310 Favorites, 0 Comments

Arduino Button Tutorial

IMG_3679c.JPG
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:
  • 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.

Connecting the Button

IMG_3679b.JPG
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:
  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

This is not much, and is consumed only when the button is pressed. Often, pull-up and pull-down resistors have even greater values, consuming hence less power. If you don't have particular reasons, use this handy 20k internal pull-up.


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––––––––––
    

Sampling, Deglitching, and Reading States

timing1.png
Screenshot--dev-ttyUSB0-1.png
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:
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––––––––––
    

Detecting Edges

timing2.png
Screenshot--dev-ttyUSB0-2.png
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<––––––––––
#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––––––––––
    

Distinguishing Short From Long Presses

timing3.png
Screenshot--dev-ttyUSB0-3.png
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<––––––––––
#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––––––––––
    

Adding Buttons, and Going OO

IMG_3682.JPG
Screenshot--dev-ttyUSB0-4.png
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<––––––––––
#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––––––––––
    

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