Arduino digitalWrite() and digitalRead() Guide: Control LEDs and Read Buttons Reliably
Almost every Arduino project that switches an LED or a relay driver, or reads a button or a digital sensor, relies on two functions. The digitalWrite() function sets an output pin to HIGH or LOW, and the digitalRead() function reports whether an input pin is at a HIGH or a LOW level. This tutorial explains how both functions work together with pinMode(), which voltage and current limits apply, how INPUT_PULLUP removes the need for an external resistor and how to debounce a button.
Two working examples follow the explanations. The first blinks an LED on pin 13, and the second turns the same LED on and off with a button on pin 2. The figures in this guide describe the ATmega328P used on the Arduino UNO and Nano unless a section names another board.
This guide belongs to the Arduino Core Tutorials series. Readers who need to read analog voltages can continue with the ADC and analogRead() tutorial, and readers who want to dim an LED instead of switching it can use the PWM and analogWrite() tutorial.
Table of contents
Arduino pinMode() Function
The pinMode() function configures a pin as an output or as an input, and it has to run before the pin is used with digitalWrite() or digitalRead(). Every pin starts as an input after a reset, so the call is what turns a pin into an output.
pinMode() Syntax and Modes
The function takes the pin number and the mode, and it returns nothing.
pinMode(pin, mode);
The pin can be a number such as 13 or a name such as A0. The call normally sits in setup() and needs to run only once for each pin. The table lists the three modes that matter for digital I/O.
| Mode | Pin behavior | Typical use |
|---|---|---|
| OUTPUT | The pin is driven HIGH or LOW by digitalWrite(). | LEDs, transistor bases, relay drivers and buzzers. |
| INPUT | The pin has a high impedance and reads the level applied from outside. | Sensors with a push-pull output and buttons with an external resistor. |
| INPUT_PULLUP | The pin is an input with the internal pull-up resistor connected. | Buttons and switches that connect the pin to GND. |
The pin numbers of each board are listed in the Arduino UNO pinout guide and the Arduino Nano pinout guide.
Arduino digitalWrite() Function
The digitalWrite() function sets an output pin to HIGH or LOW. HIGH puts the pin close to the supply voltage, which is 5 V on the UNO and Nano and 3.3 V on 3.3 V boards, and LOW puts the pin at 0 V.
digitalWrite() Syntax and Parameters
The function takes the pin number and the value, and it returns nothing.
digitalWrite(pin, value);
The value is HIGH or LOW. The following sketch configures pin 13 as an output and drives it HIGH once in setup().
void setup() {
pinMode(13, OUTPUT); // configure pin 13 as an output
digitalWrite(13, HIGH); // drive pin 13 HIGH
}
void loop() {
// nothing to repeat
}
Output Voltage and Current Limits
The ATmega328P datasheet gives an absolute maximum of 40 mA for each I/O pin, and Arduino recommends staying at or below 20 mA. The current through all VCC and GND pins together must not exceed 200 mA. A HIGH output also drops below the supply voltage as the load current rises, so the pin voltage is lower than 5 V while an LED is lit.
Loads that need more than 20 mA, such as relays, motors, solenoids and larger buzzers, must not be connected to a pin directly. They need a driver such as a transistor, a MOSFET or a relay module with its own driver circuit, and inductive loads also need a flyback diode across the coil. The passive buzzer tutorial shows a sound module that is driven from a digital output.
Arduino LED Blink with digitalWrite()
The first example blinks an LED on pin 13, which is the simplest test of a working digital output.
LED Blink Wiring
Connect the anode of the LED, which is the longer leg, to pin 13 through a 330 Ω resistor, and connect the cathode to GND. With a 5 V output and a red LED, the resistor limits the current to about 9 mA, which is well below the recommended 20 mA per pin. Pin 13 also drives the onboard LED of the UNO and Nano, so the onboard LED blinks together with the external one.
LED Blink Code
const uint8_t LED_PIN = 13; // LED on pin 13 through a 330 ohm resistor
void setup() {
pinMode(LED_PIN, OUTPUT); // pin 13 as an output
}
void loop() {
digitalWrite(LED_PIN, HIGH); // LED on
delay(1000); // wait one second
digitalWrite(LED_PIN, LOW); // LED off
delay(1000); // wait one second
}LED Blink Output
The LED stays on for one second and off for one second, and changing the two delay values changes the blink rate. The delay() function stops the sketch while it waits, so the Arduino cannot do other work in that time. The delayMicroseconds() tutorial covers timing below one millisecond, and the debounce section of this guide shows a timing method that does not block.
Arduino digitalRead() Function
The digitalRead() function returns HIGH or LOW for the level on an input pin. It is used for buttons, switches and sensors that report their state as a digital signal.
digitalRead() Syntax and Return Value
The function takes the pin number and returns an int that is either HIGH or LOW, which equal 1 and 0.
int state = digitalRead(pin);
The following sketch configures pin 2 as an input and prints its state to the Serial Monitor every 100 milliseconds.
const uint8_t BUTTON_PIN = 2;
void setup() {
pinMode(BUTTON_PIN, INPUT); // plain input without a pull-up
Serial.begin(9600);
}
void loop() {
int buttonState = digitalRead(BUTTON_PIN); // HIGH or LOW
Serial.println(buttonState);
delay(100);
}With nothing connected to pin 2, this sketch prints a random mix of 0 and 1, because the pin floats and picks up noise. The next section explains why this happens and how to prevent it.
Input Voltage Levels
According to the ATmega328P datasheet, an input at a supply of 5 V counts as LOW below 0.3 × VCC, which is 1.5 V, and as HIGH above 0.6 × VCC, which is 3.0 V. A voltage between the two thresholds gives an undefined result. The pin voltage must stay between 0 V and the supply voltage, and the datasheet lists VCC + 0.5 V as the absolute maximum.
Arduino INPUT vs INPUT_PULLUP
An input pin that nothing drives is floating, and its reading depends on noise and on the fingers near the wires. A pull-up or a pull-down resistor gives the pin a defined level while the button is released, and INPUT_PULLUP connects such a pull-up resistor inside the chip.
| INPUT | INPUT_PULLUP | |
|---|---|---|
| Default pin state | Floating and unpredictable | HIGH through the internal pull-up |
| External resistor | Required, either a pull-down or a pull-up | Not required |
| Button wiring | Button between the pin and 5V with a 10 kΩ pull-down resistor to GND | Button between the pin and GND |
| Reading when pressed | HIGH | LOW |
| Risk of false reads | High without the resistor | Low |
The internal pull-up resistor has a value between 20 kΩ and 50 kΩ according to the datasheet, which is high enough to avoid wasting current and low enough to hold a released button at HIGH. The logic is inverted compared with a pull-down circuit, because the pin reads HIGH while the button is released and LOW while it is pressed.
pinMode(2, INPUT_PULLUP); // pin 2 reads HIGH until the button pulls it to GND
INPUT_PULLUP is the better choice for most buttons and switches, because it needs no extra component and the wiring is a single connection to GND.
Arduino Button Wiring with digitalRead()
The button example uses INPUT_PULLUP, so the button needs only a connection to pin 2 and a connection to GND. Connect one leg of the push button to pin 2 and the opposite leg to GND. A four-leg tactile button has two pairs of legs that are joined inside, so the connection must go to two legs on opposite sides, otherwise the contacts are always closed. Keep the LED and the 330 Ω resistor on pin 13 as in the blink example.
Arduino Button Controls LED Example
This example turns the LED on while the button is pressed and turns it off when the button is released.
Button Controls LED Code
const uint8_t BUTTON_PIN = 2; // button between pin 2 and GND
const uint8_t LED_PIN = 13; // LED on pin 13 through a 330 ohm resistor
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // pin reads HIGH until the button is pressed
pinMode(LED_PIN, OUTPUT);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) { // LOW means the button is pressed
digitalWrite(LED_PIN, HIGH); // LED on
} else {
digitalWrite(LED_PIN, LOW); // LED off
}
}The pull-up keeps pin 2 at HIGH while the button is released, and pressing the button connects the pin to GND, so digitalRead() returns LOW. The sketch therefore tests for LOW to detect a press. Because the loop reads the pin continuously, the LED follows the button with no noticeable delay.
Button Controls LED Output
The LED lights while the button is held and goes out as soon as the button is released. A sketch that must react to a button while it does other work can use Arduino external interrupts instead of reading the pin in loop().
Arduino Button Debounce
A mechanical button does not switch cleanly. The contacts bounce for a short time when they close or open, usually a few milliseconds and sometimes longer on a worn switch, and the Arduino reads each bounce as a separate press. The effect stays invisible in the previous example, because the LED simply follows the pin, but it causes wrong counts as soon as a sketch counts presses or toggles an output.
Delay Debounce
The simplest remedy is to confirm a press after a short wait. The sketch detects a LOW level, waits 20 milliseconds and checks the pin again before it accepts the press.
if (digitalRead(BUTTON_PIN) == LOW) { // possible press
delay(20); // wait for the contacts to settle
if (digitalRead(BUTTON_PIN) == LOW) { // still pressed, so the press is real
// handle the press here
}
}This method is short, but the delay() call blocks the sketch for 20 milliseconds, and a button that is held down triggers the code again on every pass through loop().
Non-Blocking Debounce with millis()
The second method compares timestamps instead of waiting, so the sketch keeps running. The sketch below toggles the LED once for every press and ignores the bounce.
const uint8_t BUTTON_PIN = 2; // button between pin 2 and GND
const uint8_t LED_PIN = 13; // LED on pin 13 through a 330 ohm resistor
const unsigned long DEBOUNCE_MS = 20; // time the input must stay stable
int lastReading = HIGH; // previous raw reading
int stableState = HIGH; // debounced button state
unsigned long lastChange = 0; // time of the last raw change
bool ledOn = false;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int reading = digitalRead(BUTTON_PIN);
if (reading != lastReading) { // the raw input changed
lastChange = millis(); // restart the stability timer
lastReading = reading;
}
if ((millis() - lastChange) > DEBOUNCE_MS && reading != stableState) {
stableState = reading; // the new state has been stable long enough
if (stableState == LOW) { // a real press: toggle the LED
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
}
}
}Programs that run under FreeRTOS can handle buttons with interrupts and semaphores, as described in the FreeRTOS interrupts tutorial.
Arduino Digital Pin Limits Table
The table collects the electrical limits that apply to the digital pins of the UNO and Nano, which use the ATmega328P at 5 V.
| Parameter | Value |
|---|---|
| Input LOW threshold (maximum) | 0.3 × VCC, which is 1.5 V |
| Input HIGH threshold (minimum) | 0.6 × VCC, which is 3.0 V |
| Recommended current per pin | 20 mA |
| Absolute maximum current per pin | 40 mA |
| Absolute maximum current through all VCC and GND pins | 200 mA |
| Internal pull-up resistor | 20 kΩ to 50 kΩ |
| Absolute maximum pin voltage | VCC + 0.5 V |
Special Digital Pins on the UNO and Nano
The pins D0 and D1 are shared with the serial interface and the USB connection, so they should stay free while the Serial Monitor is in use. Pin D13 drives the onboard LED. The pins D2 and D3 are the external interrupt pins. The analog pins A0 to A5 also work as digital pins 14 to 19 after pinMode() is called for them, and the Arduino UNO pinout guide shows where each of them sits on the board.
Mixing 3.3 V and 5 V Devices
A 3.3 V output is above the 3.0 V HIGH threshold of a 5 V UNO, so it normally reads as HIGH, although the margin is small. A 5 V output must never be connected directly to the input of a 3.3 V device, because it exceeds the absolute maximum rating of that pin. A level shifter or a resistor divider solves the problem. Digital sensor modules such as the PIR motion sensor and the IR sensor module report their state as HIGH or LOW, and their output level should be checked against the board before they are connected.
Conclusion
The digitalWrite() and digitalRead() functions cover most simple switching and sensing tasks, provided that pinMode() sets the mode first. A floating input gives random readings, INPUT_PULLUP gives a stable level with no extra resistor, and a series resistor keeps an LED inside the current limit of the pin.
Buttons also need debouncing before presses can be counted reliably, and the millis() method does this without blocking the sketch. A load above 20 mA belongs on a driver circuit and not on the pin.
The next steps in the series are the ADC and analogRead() tutorial for reading analog voltages, the PWM and analogWrite() tutorial for dimming an LED and the external interrupts tutorial for reacting to a button without polling.
Download Project Here
Download the CubeMX project files and HAL source code — all tested on real hardware.
If you find this useful, you can support us with a Donation as well.
Browse More Arduino Core Tutorials
Arduino delayMicroseconds() Tutorial: Precise Timing, Pulses & Alternatives
Arduino I2C Tutorial: Wire Library, Master/Slave, Scanner & Troubleshooting
Arduino External Interrupts: attachInterrupt(), ISR & Debounce
Arduino Nano Pinout – Complete Guide with Diagram
Arduino UNO Pinout – Complete Guide with Diagram
Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.
Recommended Tools
Essential dev tools
Categories
Browse by platform



Way better than the official documentation. Way better than any other youtube video out there. Simply superb.