Fundamentals

Digital Interfaces with Arduino

DIYODE Magazine

Issue 72, July 2023

In this edition of Fundamentals, we'll look at some commonly used circuit interfaces - pushbuttons, rotary encoders and keypads - and how we can process the digital circuits connected to them in code. We’ll also check out some problems that you might encounter when you use these components, and how to avoid them. - by Liam Davies

We spend a lot of time at DIYODE pushing the limits of our projects, with everything from massive 3D printed models to highly-refined analogue circuits. However, sometimes, we like to take a step back and get back to the basics. Not only does it help us refresh our own fundamental knowledge, but it also helps teach those who are just dipping their toes into electronics. This is one of those projects!

The purpose of this Fundamentals article is to introduce some of the most commonly-used interface components in a digital circuit. We’re mostly talking about the behaviour of these components in the realm of digital circuits, which means 0V represents OFF (or 0) and 5V represents ON (or 1).

The Pushbutton

The simplest electronic switch is the humble pushbutton. It’s fundamentally very simple, and yet, many of those getting started with Arduino seem to have endless problems with these little troublemakers!

SP0602 pushbutton from Jaycar

Physically connecting two wires together, pushbuttons are the easiest way to selectively join two parts of a circuit. Because it acts as a short-circuit when pushed, and open-circuit otherwise, it’s important that the discerning maker can predict what will happen in both of these states.

So, let’s make a circuit that connects to an Arduino, allowing it to read the state of the button with the ‘digitalRead’ function:

This should work, right? When our button is not pushed, there isn’t anything on Pin 6, so the Arduino reads the pin as LOW (OFF). When it is pressed, the 5V signal will now be present on Pin 6, and the Arduino reads the pin as HIGH (ON).

To test our theory, we can run the following code in our loop() function:

void loop() {
  int state = digitalRead(6);
  Serial.println(state);
}

This code will print out a ‘1’ if the Arduino sees a HIGH signal on Pin 6, and ‘0’ if it sees a LOW signal. After uploading it to the Arduino and pressing the button, the ‘1’ output works as expected. However, something interesting happens when the button isn’t pressed - it starts outputting random data. This is a mystifying issue whose cause is often not apparent to beginner makers!

When the button is not pressed, the voltage on the pin enters an ‘undefined’ state. Because the pin isn’t actually connected to anything, it suddenly acts as an antenna, and therefore, even touching the pin causes sudden state changes.

To fix this problem, we need to add a ‘pullup’ or ‘pulldown’ resistor. This essentially weakly ‘pulls’ a particular part of a circuit to a desired voltage when no other voltage is present. If we revisit our previous circuit, we can add a pulldown resistor as follows:

When our button is pushed, the 5V will be present on the pin, as before. However, when the button isn’t pressed, the 10kΩ resistor weakly ‘pulls’ this voltage to 0V, which sets the button in a known state.

A pushbutton with a pullup resistor and debouncing capacitor soldered onto it.

We can also use our circuit in a ‘pullup’ state, where the positions of the resistor and buttons are flipped. This means that the button will normally be ‘pulled up’ to 5V, but pressing it sets the voltage to 0V.

You can simplify this circuit further by using the inbuilt pullup resistors of the Arduino! That’s right, the Arduino Uno (and most other modern Arduino boards) have inbuilt pullup resistors that can be enabled with code.

In our setup() function, instead of writing;

pinMode(6, INPUT);

we can write:

pinMode(6, INPUT_PULLUP);

These inbuilt pullups are usually between 20kΩ and 50kΩ, so they aren’t ‘strong’ pullup resistors. In any case, you’ll just need to be mindful that the logic of pins with pullup resistors will be inverted! That is, when the button is pressed, the value read by the Arduino will be 0, and 1 when not pressed.

Rotary Encoders

Rotary Encoders are everywhere - you probably don’t realise how many appliances they’re used in! Washing machines, car dashboard interfaces and thermostats all frequently use rotary encoders. Precision machinery also uses them for a high degree of control and feedback. Rotary Encoders come in two distinct variations: Incremental and Absolute.

Incremental encoders are the most common components used with Arduino - they have a very similar form factor to a potentiometer, however, they operate solely by connecting two pins to a third pin in grey code order. What does that mean, and why would that be useful?

Rather than explain everything with only diagrams and theory, we took apart a rotary encoder to learn how it works.

It looks quite simple, but the operation of the encoder is actually extremely clever.

There are only two sensing pins here, which in turn, get connected to the centre pin via the alternating contacts. As the shaft rotates, these contacts rotate, causing a distinctive series of pulses to appear on the pins.

These pulses resemble ‘grey code’, which is similar to binary counting, with a critical exception - only ONE bit can flip when incrementing or decrementing a value. What’s the point? Why can’t we just output binary so our Arduino can see what position the encoder is in?

Grey

Pin 1

pin 2

pin 3

0

0

0

0

1

0

0

1

2

0

1

1

3

0

1

0

4

1

1

0

5

1

1

1

6

1

0

1

7

1

0

0

BINARY

Pin 1

pin 2

pin 3

0

0

0

0

1

0

0

1

2

0

1

0

3

0

1

1

4

1

0

0

5

1

0

1

6

1

1

0

7

1

1

1

While binary should work in theory, in reality, our two contacts will never engage at precisely the same time. During the process of the switch moving from one state to another, we may - for a very small amount of time - see a third state in between. Let’s demonstrate that.

For instance, let our rotary encoder count in binary. Our current state is 01 in binary or 1 in decimal - our right contact is engaged, and our left contact is not.

The user clicks the rotary encoder forward one step, which disengages the left contact, and engages the right contact. This makes our new value 10 in binary, or 2 in decimal, as we’d expect.

But wait, what happens if one of the contacts changes state before the other?

If our left contact changes state first, for a split second we’ll have the value 11 in binary! This is a decimal value of 3, which we definitely never set our encoder to.

Conversely, if our right contact changes state first, for a split second we’ll have 00 in binary, which we never had either.

Our Arduino will get confused by this - from its perspective, it will see the value 1, then 0 OR 3, then 2. It’ll be very challenging to write some code that handles this well.

It should be now obvious why we use grey code for rotary encoders! By only changing one bit at a time, we won’t see spurious states when the control shaft is moved.

Absolute encoders use the same grey code technique, but their position is represented by a large array of metal contacts, which represent the exact position they’re in. Some do use binary where the attached device isn’t fast enough or sensitive enough to be affected by transitional errors.

The internals of a grey code Absolute Encoder.

Keypads

We’ve written articles in DIYODE before about interfacing with Arduino-based keypads - our last one was in Issue 35, over three years ago now! Keypads are an awesome digital interface tool as they allow us to select options from menus or enter numeric information.

If you’re getting started in Arduino, you may be wondering how keypads like the one below can work despite having fewer pins than buttons!

Image Credit: 3x3 Keypad from Adafruit

The internal layout of these keypads is really quite simple - there are rows and columns, each connected to one button. By applying a voltage to one column at a time, and observing which buttons are connected to that voltage on each row, we can determine what buttons are pressed. Pretty easy, right?

The pseudocode for this behaviour might look like this:

for each keypad column:
  turn on this keypad column;
  turn off all other columns (high impedance);
  for each keypad row:
    if this row is ON:
      this button is pressed!
    else:
      this button is not pressed.
    end
  end
end

It’s worth noting that we’ve arbitrarily chosen columns as the active side (i.e. the side we power). It could just as easily be done by powering the rows and checking out what voltages appear on the columns, too.

To write Arduino code for a keypad, you can write your own based on our pseudocode above, or use premade libraries such as the official Keypad library. The Keypad library is fairly straightforward - just define your set key pins and everything else will be sorted out for you. However, it’s a good exercise to implement your own code. If you’d like to check out how we wrote our own Keypad routine, check out our Mini Desk Vending Machine from Issue 43.

Keypad Limitations

As with all ways of saving digital pins, there are limitations to our keypad matrix circuit! What happens when we start pressing multiple buttons at the same time?

With just one button pressed, our matrix operates as expected. You can see that powering the second column causes the second row to see a voltage on the button. We can register the ‘5’ key as pressed.

Note: We’ve shown this as a simplified diagram - Assuming pressing a button connects that row and column together.

What about with two keys? Let’s press the ‘6’ key at the same time.

Yep, this still seems to work. When powering the second and third columns, we see voltages on the second row.

Finally, let’s try pressing ‘5’, ‘6’ and ‘9’ at the same time. It may surprise you to learn that the Arduino measuring our keypad actually registers ‘5’, ‘6’, ‘9’ AND ‘8’! What’s going on here?

Pressing three keys in certain positions on this keypad causes a phenomenon called “ghosting”, where phantom keys are detected even when they aren’t pressed. Looking at the diagram above, we can see that the voltage applied to column two travels in a loop, going back through key ‘6’ and making its way onto key ‘9’. Since there is now a voltage on the third row while our second column is powered, the Arduino deduces that key ‘8’ is pressed.

If your user will only ever press one button, this isn’t an issue - you don’t really need to worry about it. However, registering a button when it was never pressed could be a serious issue for some projects!

To fix this issue, we can add diodes in series with each button. This prevents voltages feeding back into other columns, and allows every button to be detectable without interfering with others.

While Arduino-based keypads aren’t easily modifiable, with a bit of elbow grease, it’s possible. Check out Nick Gammon’s guide on his blog in the resources section.

Image Credit: Nick Gammon

This is where the term N-Key Rollover comes from in terms of computer keyboards, which ensures that the user can press as many keys as they like without ghosting occurring. Cheaper economy or office keyboards can sometimes only press up to three keys without Ghosting occurring, thanks to their lack of diodes in their matrix.

Image Credit: Owpkeenthy

Higher-end gaming keyboards usually support N-Key Rollover.

It’s worth noting that it is possible to detect when ghosting is occurring, so that the code responsible can turn off all output. It’s better that no output occurs as opposed to unpredictable key presses.

Debouncing

An issue that will affect all of the topics we’ve talked about is debouncing.

Unfortunately, a button or contact closing or opening is rarely absolutely clean. That is, it doesn’t switch from 0V one moment to 5V in another. When a contact in a switch closes, they will often make and break contact very rapidly before finally settling into a state.

This isn’t a problem for Arduino code that only relies on the current state of an input. For example, if you want to only illuminate an LED when a button is pressed. In this case, it doesn’t matter if the LED spends a few microseconds or milliseconds flickering on and off - the user doesn’t know and won’t care.

However, state-sensitive code can be seriously affected by bouncing states from digital inputs.

If you’re trying to design a button that will toggle a motor’s output, you may find that the button isn’t entirely reliable. After all, how will the Arduino know what the difference is between releasing a button and bouncing behaviour?

We can first help mitigate bouncing behaviour from a hardware perspective.

By mounting a capacitor across the button’s terminals, we can limit the changing voltage of the button. This dramatically reduces the amount of debouncing, which is why you’ll see ceramic capacitors soldered onto many of the buttons we use in our projects!

We can also reduce the problems with debouncing by using some clever code tricks.

If we monitor the state of a button, and how long it has been stable for, we can only trigger an action once it has been stable for a certain amount of time. Limor Fried of Adafruit wrote this code that shows a possible solution to this problem:

void loop() {
  // read the state of the switch into a 
  // local variable:
  int reading = digitalRead(buttonPin);
  // check to see if you just pressed the button
  // (i.e. the input went from LOW to HIGH), 
  // and you've waited long enough
  // since the last press to ignore any noise:
  // If the switch changed, due to noise or 
  // pressing:
  if (reading != lastButtonState) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there 
    // for longer than the debounce
    // delay, so take it as the actual current state:
    // if the button state has changed:
    if (reading != buttonState) {
      buttonState = reading;
      // only toggle the LED if the new button 
      // state is HIGH
      if (buttonState == HIGH) {
        ledState = !ledState;
      }
    }
  }
  // set the LED:
  digitalWrite(ledPin, ledState);
  // save the reading. Next time through the loop, 
  // it'll be the lastButtonState:
  lastButtonState = reading;
}

If reading code isn’t your thing, its operation can be visualised like this:

You can see how the code waits until the button has settled into a state for at least the time specified by “debounceDelay”. When it does, it finally does the action specified by the button. While it may sound as though waiting for the button to settle will reduce the time it takes to activate, keep in mind that debouncing is usually on the order of a few milliseconds - it’s not perceptible to the user.

There are also other solutions to avoiding problems caused by debouncing, which can be as simple as waiting a few milliseconds before continuing whatever the code was previously doing:

if(digitalRead(6)) {
  // Button was pressed.
  delay(10);
  // Wait 10ms for the button to calm down.
  // … continue what we were doing …
  // (more code)
}

This is a fairly simple but effective solution for debouncing, if you don’t mind your code being ever slightly slower.

In any case, the main point of this topic is to be aware that switching isn’t a clean process. If your code has weird behaviour, but only when a button is pressed, debouncing may be your solution.

Final Thoughts

We frequently get emails from readers asking about a seemingly simple topic - digital circuits with Arduino. However, the considerations that digital circuits require may be more in-depth than many beginners realise. Even though our circuits have digital logic with ON and OFF signals, the same electrical and physical constraints that analogue circuits struggle with still apply!

Digital circuits and switch-based components are simple in theory, and with the tips and tricks we’ve shown today, they should be simple to work with in practice too! Be sure to clue us in with any concepts you want us to write more educational content on at @diyodemag.