Introduction: A Menu in Arduino, and How to Use Buttons

In my Arduino 101 tutorial, you'll be taught how to setup your environment in Tinkercad. I use Tinkercad because it is a pretty powerful online platform that allows me to demonstrate a range of skills to students for building circuits. Feel free to build all my tutorials using the Arduino IDE and a real Arduino!

In this tutorial, we're going to learn about buttons! We need to know:

  • How to wire them up
  • Reading their value
  • Debounce, and why it's important
  • A practical application (creating a menu)

Most people think the most practical thing to do with a button is turn a light on and off. We'll, not here! We're going to use ours to create a menu and set some options on the Arduino.

Ready? Let's get started!

Step 1: Setup the Board

The first step is to put an Arduino and Breadboard Small onto the prototyping area. Check the images above to see how to wire up the power rails.

A Breadboard Mini has two power rails top and bottom. We wire these up to the Arduino so we can provide power to more components. Later in this tutorial we will use 3 buttons so we'll need more power. The thing to note is that on a breadboard small, the power rails run across the board, horizontally. This is different to the columns in the main prototyping area in the middle; these run vertically. You can use any of the power pins to provide power to any column in the main area in the middle.

When you add power, use black and red wires to the negative and positive respectively. Add wires at the end that run power to the other side of the board. We won't use that side, but it's good practise.

Step 2: Add the Button and Resistor

Add a small pushbutton from the components tray. It should look like the one in the image. Make sure it isn't a switch! Add a resistor, too. Click it, and set its value to 10kΩ. That is enough to pull the pin low when it is not connected, which is very important later in the code.

Place the component across the middle of the breadboard. The way a button works is:

  • Corner to corner, the button is not connected. Pushing the button closes the contacts and connects the corners.
  • The sides of the button are connected. If you connected a wire to the top left and bottom left, the circuit would be closed.

This is why we put the component across the space in the middle. It makes sure the corners are not connected under the pins in the board.

The next step provides a couple of images that illustrates these points.

Place the resistor from the bottom right pin across columns, so it sits horizontally.

Step 3: Button Connections

The images above make it fairly clear how the buttons connect. It was always a point of confusion when you think something is all good and it doesn't work!

Now, let's add the wires.

  • Place a red lead from a positive power pin to the same column as the bottom right pin on the button
  • Place a black lead from a negative power pin to the same column as the resistor.
  • Place a coloured wire (not red/black) from the top left pin to Digital Pin 2 on the Arduino

Check the images above to make sure your wiring is correct.

Step 4: The Code...

Let's have a look at the code for a basic button.

Open the code editor and change from Blocks to Text. Clear the warning that comes up. We're happy with text!

You know the basic setup, so let's define the button and do a basic read. We'll print the output to Serial.

I put a few extra comments into the code below so it's easier to read than the image.

// Define constants
#define button 2

void setup() {
	pinMode(button, INPUT);
	Serial.begin(9600);
}

void loop() {

	// Read the digital pin to check status of button
	int pressed = digitalRead(button);

	// Button returns HIGH if pressed, LOW if not
	if(pressed == HIGH){
		Serial.println("Pressed!");
	}
}

Ok, well that works!

Essentially, all we're doing is checking the status of the digital pin each time the code loops. If you click Start Simulation and press the button, you'll see the Serial Monitor (click the button below the code) display "Pressed!" repeatedly.

One feature you'll see in the code above is the if() condition evaluation taking place. All the code is doing is asking a question and evaluating if it is true, in this case. We use the is equal (double equal signs, like this: == ) to check if the value of the variable is equal to a certain value. A digitalRead() returns either HIGH or LOW.

Using the if()else if / else we can check many conditions or all conditions, and if you go back to the Arduino Basics, you'll see some of the comparisons you can make.

Now... Our code might look complete... But we have a problem.

See, that works really well when in the simulator. But real electricity has noise, especially DC electronics. So our button might return a false reading sometimes. And that's a problem, because your project might not respond the right way for the user.

Let's fix it!

Step 5: A Little Debounce

We use a procedure called debounce to overcome our button problem. This essentially waits a specified amount of time between when the button was pushed and actually responding to the push. It still feels natural to the user (unless you make the time too long). You can also use it for checking length of press, so you can respond differently each time. You don't need to change any of the wiring!

Let's look at the code:

#define button 2<br>#define debounceTimeout 100

The first change is on the global scope. You'll remember that's where we define variables lots of our functions might use or those that can't be reset each time the loop fires. So, we added debounceTimeout to the defined constants. We made this 100 (which will later translate to 100ms), but it could be shorter. Any longer and it'll feel unnatural.

long int lastDebounceTime;

This variable is declared below the constants. This is a long int type, which basically allows us to store long numbers in memory. We called it lastDebounceTime.

We don't need to change anything in the void setup() function. Let's leave that one.

void loop() {<br>	// Read the digital pin to check status of button
	int pressed = digitalRead(button);
	long int currentTime = millis();
	
	// Button code
}

The first change we make in the loop() function is under the call to read the button. We need to keep track of the current time. The millis() function returns the current time of the clock since the Arduino booted up in milliseconds. We need to store this in a long int type variable.

Now, we need to make sure we are aware of the time since the button was pressed, so we reset the timer when it isn't pressed. Take a look:

void loop() {<br>	// Read the digital pin to check status of button
	int pressed = digitalRead(button);
	long int currentTime = millis();
	
	if(pressed == LOW){
		// Reset the count time while button is not pressed
		lastDebounceTime = currentTime;
	}
	
	// Button code
}

The if(pressed == LOW) algorithm checks if the button isn't pressed. If it isn't, then the code stores the current time since last debounce. That way, each time the button is pressed, we have a point in time from which we can check when the button was pressed. We can then do a quick mathematical calculation to see how long the button was pressed for, and respond correctly. Let's look at the rest of the code:

void loop() {<br>	// Read the digital pin to check status of button
	int pressed = digitalRead(button);
	long int currentTime = millis();
	
	if(pressed == LOW){
		// Reset the count time while button is not pressed
		lastDebounceTime = currentTime;
	}
	// Button has been pressed for a given time
	if(((currentTime - lastDebounceTime) > debounceTimeout)){
		// If the timeout is reached, button pressed!
		Serial.println("Pressed!");
	}
}

The last block of code takes the current time, subtracts the last debounce time and compares it to the timeout we set. If it is greater, the code assumes the button has been pressed for that time and responds. Neat!

Run your code and check it works. If you have errors, check your code!

Now, let's look at a practical example.

Step 6: The Making of a Menu

Buttons are interesting, because there are so many possibilities with them! In this example, we're going to make a menu. Let's say you've created this really great device, and need users to be able to change options to turn certain things on or off, or set a particular value for a setting. This three button design can do that!

So, for this project we need:

  • Three buttons
  • Three resistors set to 10kΩ

We already have one of these, we just need the other two. So add those to the board. Wiring up is a little more complex, but only because I wanted to keep it really compact. You could follow the same pattern for the first button, or follow the image above.

The three buttons are a menu open/next option, a change option (as in, alter the setting), and a save/close menu button.

Wire it up, let's look at the code!

Step 7: Code Breakdown - Global

Ok, this is going to be a long step, but I am going to go through each section of code.

First, let's look at the global variables needed.

// Define constants<br>#define menuButton 2
#define menuSelect 3<br>#define menuSave 4
#define debounceTimeout 50
<br>// Define variables
int menuButtonPreviousState = LOW;
int menuSelectPreviousState = LOW;
int menuSavePreviousState = LOW;
long int lastDebounceTime;
<br>// Menu options
char * menuOptions[] = {"Check Temp", "Check Light"};
bool featureSetting[] = {false,false};
bool menuMode = false;
bool menuNeedsPrint = false;
int optionSelected = 0;

These three blocks are fairly similar to what we have seen before. In the first, I have defined the three buttons and the timeout. For this part of the project, I have set it to 50ms so it takes a deliberate press to make it work.

The second block is all the variables. We need to keep track of the buttonPreviousState, and we need to keep track of the lastDebounceTime. These are all int type variables, but the last is a long type because I am assuming we need the space in memory.

The menu options block has a few new features. First, the char * (yes, that is a deliberate asterisk), which is a character/string literal variable. It is a pointer to a static storage in memory. You can't change it (like you can in Python, for example). This char *menuOptions[ ] line creates an array of string literals. You could add as many menu items as you like.

The bool featureSetting variable is just the array of values that represents each menu item. Yes, you could store anything you like, just change the variable type (they all have to be the same type). Now, there might be better ways to manage this, like dictionaries or tuples, but this is simple for this application. I would probably create one of the latter in a deployed application.

I have kept track of the menuMode, so if I wanted other things on my display I could do that. Also, if I had sensor logic I might pause that during menu operation, just in case something conflicts. I have a menuNeedsPrint variable because I want to print the menu at specific times, not just all the time. Finally, I have an optionSelected variable, so I can keep track of the option selected as I access it in a number of places.

Let's look at the next set of functions.

Step 8: Code Breakdown - Setup and Custom Functions

The setup() function is easy enough, just three input declarations:

void setup() {<br>	pinMode(menuSelect, INPUT);
	pinMode(menuSave, INPUT);
	pinMode(menuSelect, INPUT);
	Serial.begin(9600);
}

Next are the three custom functions. Let's look at the first two, then the last one separately.

We need two functions that return some information. The reason is, we want to make sure this is sort of human readable. It will also help with debugging the code if we have an issue. Code:

// Function to return the current selected option<br>char *ReturnOptionSelected(){
	char *menuOption = menuOptions[optionSelected];
	// Return optionSelected
	return menuOption;
}
// Function to return status of current selected option
char *ReturnOptionStatus(){
	bool optionSetting = featureSetting[optionSelected];
	char *optionSettingVal;
	if (optionSetting == false){
		optionSettingVal = "False";
	}else{
		optionSettingVal = "True";
	}
	// Return optionSetting
	return optionSettingVal;
}

The char *ReturnOptionSelected() function checks the option selected (if you see above, we set a variable to keep track of that), and pulls the string literal from the array we created earlier. It then returns it as a char type. We know this because the function indicates the return type.

The second function, char *ReturnOptionStatus() reads the status of the option saved in the array and returns a string literal that represents the value. For example, if the setting we have stored is false, I would return "False". This is because we show the user this variable and it is better to keep all this logic together. I could do it later, but it makes more sense to do it here.

// Function to toggle current option<br>bool ToggleOptionSelected(){
	featureSetting[optionSelected] = !featureSetting[optionSelected];
	return true;
}

The function bool ToggleOptionSelected() is a convenience function to change the value of the setting we have selected in the menu. It just flips the value. If you had a more complex set of options, this might be quite different. I return true in this function, because my callback (the call later in the code that fires this function) expects a true/false reply. I am 100% sure this will work, so I didn't account for it not working, but I would in a deployed application (just in case).

Step 9: The Loop...

The loop() function is fairly long, so we'll do it in parts. You can assume everything below nests within this function:

void loop() {
	// Do work in here <-----
}

Ok, we saw this stuff before:

// Read the buttons<br>	int menuButtonPressed = digitalRead(menuButton);
  	int menuSelectPressed = digitalRead(menuSelect);
  	int menuSavePressed = digitalRead(menuSave);
	

	// Get the current time
	long int currentTime = millis();
	
	if(menuButtonPressed == LOW && menuSelectPressed == LOW && menuSavePressed == LOW){
		//Reset the count time while button is not pressed
		lastDebounceTime = currentTime;
		menuButtonPreviousState = LOW;
		menuSelectPreviousState = LOW;
		menuSavePreviousState = LOW;
	}

All I had to do here was add in the three digitalRead() calls, and make sure I accounted for the fact that if all buttons were low, we should reset the timer (lastDebounceTime = currentTime) and set all previous states to low. I also store millis() in currentTime.

The next section nests inside the line

if(((currentTime - lastDebounceTime) > debounceTimeout)){
	//Do work in here <----
}

There are three sections. Yes, I could have moved them into their own functions, but for sake of simplicity I kept the three main button algorithms in here.

if((menuButtonPressed == HIGH) && (menuButtonPreviousState == LOW)){<br>  
	if(menuMode == false){
		menuMode = true;
		// Let the user know
		Serial.println("Menu is active");
	}else if (menuMode == true && optionSelected < 1){			
		// Change option if menu is active
		optionSelected = optionSelected + 1;
	}else if (menuMode == true && optionSelected >= 1){
		// Reset option
		optionSelected = 0;
	}
  
	// Print the menu
	menuNeedsPrint = true;
	// Toggle the button prev. state to only display menu 
	// if the button is released and pressed again
	menuButtonPreviousState = menuButtonPressed; // Would be HIGH
}

This first one handles when menuButtonPressed is HIGH, or when the menu button is pressed. It also checks to make sure the previous state was LOW, so that the button had to be released before it was pressed again, which stops the program from constantly firing the same event over and over again.

It then checks that if the menu is not active, it activates it. It will print the first option selected (which is the first item in the menuOptions array by default. If you press the button a second or third (etc) time, you'll get the next option in the list. Something I could fix is that when it gets to the end, it cycles back to the beginning. This could read the length of the array and make cycling back easier if you changed the number of options, but this was simple for now.

The last little section (//Prints the menu) obviously prints the menu, but it also sets the previous state to HIGH so the same function won't loop (see my note above about checking if the button was previously LOW).

// menuSelect is pressed, provide logic<br>if((menuSelectPressed == HIGH) && (menuSelectPreviousState == LOW)){
	if(menuMode){
		// Change the selected option
		// At the moment, this is just true/false 
		// but could be anything
		bool toggle = ToggleOptionSelected();
		if(toggle){
			menuNeedsPrint = true;
		}else{
			Serial.println("Something went wrong. Please try again");
		}
	}
	// Toggle state to only toggle if released and pressed again
	menuSelectPreviousState = menuSelectPressed;
}

This bit of code handles the menuSelectPressed button in the same way, except this time we just fire the ToggleOptionSelected() function. As I said before, you could change this function so it does more, but that's all I need it to do.

The main thing to note is the toggle variable, which tracks success of the callback and prints the menu if true. If it returns nothing or false, it will print the error message. This is where you can use your callback to do other things.

if((menuSavePressed == HIGH) && (menuSavePreviousState == LOW)){<br>	// Exit the menu
	// Here you could do any tidying up
	// or save to EEPROM
	menuMode = false;
	Serial.println("Menu exited");
  
	// Toggle state so menu only exits once
	menuSavePreviousState = menuSavePressed;
	}
}

This function handles the menuSave button, which just exits the menu. This is where you could have a cancel or save option, maybe do some cleaning up or save to the EEPROM. I just print "Menu exited" and set the button state to HIGH so it doesn't loop.

if(menuMode && menuNeedsPrint){<br>	// We have printed the menu, so unless something 
	// happens, no need to print it again
	menuNeedsPrint = false;
	char *optionActive = ReturnOptionSelected();
	char *optionStatus = ReturnOptionStatus();
	Serial.print("Selected: ");	
	Serial.print(optionActive);
	Serial.print(": ");
	Serial.print(optionStatus);	
	Serial.println();
}

This is the menuPrint algorithm, which only fires when the menu is active and when the menuNeedsPrint variable is set to true.

This could definitely be moved to its own function, but for the sake of simplicity..!

Well, that's it! See the next step for the whole code block.

Step 10: Final Code Block

// Define constants
#define menuButton 2
#define menuSelect 3
#define menuSave 4
#define debounceTimeout 50

int menuButtonPreviousState = LOW;
int menuSelectPreviousState = LOW;
int menuSavePreviousState = LOW;

// Define variables
long int lastDebounceTime;
bool lightSensor = true;
bool tempSensor = true;

// Menu options
char * menuOptions[] = {"Check Temp", "Check Light"};
bool featureSetting[] = {false,false};
bool menuMode = false;
bool menuNeedsPrint = false;
int optionSelected = 0;

// Setup function
<p>void setup() {<br>	pinMode(menuSelect, INPUT);
	pinMode(menuSave, INPUT);
	pinMode(menuSelect, INPUT);
	Serial.begin(9600);
}</p>
// Function to return the current selected option
char *ReturnOptionSelected(){
	char *menuOption = menuOptions[optionSelected];
	// Return optionSelected
	return menuOption;
}

// Function to return status of current selected option
char *ReturnOptionStatus(){
	bool optionSetting = featureSetting[optionSelected];
	char *optionSettingVal;
	if (optionSetting == false){
		optionSettingVal = "False";
	}else{
		optionSettingVal = "True";
	}
	// Return optionSetting
	return optionSettingVal;
}

// Function to toggle current option
bool ToggleOptionSelected(){
	featureSetting[optionSelected] = !featureSetting[optionSelected];
	return true;
}

// The main loop
<p>void loop()<br>{
  	// Read the buttons
	int menuButtonPressed = digitalRead(menuButton);
  	int menuSelectPressed = digitalRead(menuSelect);
  	int menuSavePressed = digitalRead(menuSave);
	// Get the current time
	long int currentTime = millis();
	
	if(menuButtonPressed == LOW && menuSelectPressed == LOW && menuSavePressed == LOW){
		//Reset the count time while button is not pressed
		lastDebounceTime = currentTime;
		menuButtonPreviousState = LOW;
		menuSelectPreviousState = LOW;
		menuSavePreviousState = LOW;
	}
	if(((currentTime - lastDebounceTime) > debounceTimeout)){
		// If the timeout is reached, button pressed!</p><p>		// menuButton is pressed, provide logic
		// Only fires when the button has previously been released
		if((menuButtonPressed == HIGH) && (menuButtonPreviousState == LOW)){
  
			if(menuMode == false){
				menuMode = true;
				// Let the user know
				Serial.println("Menu is active");
			}else if (menuMode == true && optionSelected < 1){
				// Change option if menu is active
				optionSelected = optionSelected + 1;
			}else if (menuMode == true && optionSelected >= 1){
				// Reset option
				optionSelected = 0;
			}
  
			// Print the menu
			menuNeedsPrint = true;
			// Toggle the button prev. state to only display menu 
			// if the button is released and pressed again
			menuButtonPreviousState = menuButtonPressed; // Would be HIGH
		}
		// menuSelect is pressed, provide logic
		if((menuSelectPressed == HIGH) && (menuSelectPreviousState == LOW)){
			if(menuMode){
				// Change the selected option
				// At the moment, this is just true/false 
				// but could be anything
				bool toggle = ToggleOptionSelected();
		    		if(toggle){
				menuNeedsPrint = true;
				}else{
					Serial.print("Something went wrong. Please try again");
				}
			}
			// Toggle state to only toggle if released and pressed again
			menuSelectPreviousState = menuSelectPressed;
		}
		if((menuSavePressed == HIGH) && (menuSavePreviousState == LOW)){
			// Exit the menu
			// Here you could do any tidying up
			// or save to EEPROM
			menuMode = false;
			Serial.println("Menu exited");
  
			// Toggle state so menu only exits once
			menuSavePreviousState = menuSavePressed;
			}
		}
		// Print the current menu option active, but only print it once
		if(menuMode && menuNeedsPrint){
			// We have printed the menu, so unless something 
			// happens, no need to print it again
			menuNeedsPrint = false;
			char *optionActive = ReturnOptionSelected();
			char *optionStatus = ReturnOptionStatus();
			Serial.print("Selected: ");	
			Serial.print(optionActive);
			Serial.print(": ");
			Serial.print(optionStatus);	
			Serial.println();
		}
	}
}

The circuit is available on the Tinkercad site. I have embedded the circuit below for you to see, too!

As always, if you have questions or issues, please let me know!

Epilog X Contest

Participated in the
Epilog X Contest