Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Worldwide shipping via DHL
Skip to main content

Arduino Nano millis(): Blink two LEDs and read button

September 13, 2026 20 views

Arduino Nano millis(): Blink two LEDs and read button | ShillehTek
Project

Build an Arduino Nano millis() multitasking sketch that blinks two LEDs at different rates, reads a button instantly, and tunes speed with a pot from ShillehTek.

30 min Beginner6 parts

Project Overview

Arduino Nano + millis() multitasking: In this build, you will use an Arduino (Nano or Uno) and the built-in millis() timer to blink two LEDs at different rates, adjust one blink speed live with a potentiometer, and read a button instantly without using delay().

delay() is often the first function beginners learn and the first one that holds them back: while the Arduino is delaying, it cannot read a button, update a display, or blink a second LED. The fix is to replace “wait” with “check the clock.” This guide rebuilds the blink sketch around millis(), adds a second LED with its own rhythm, lets a potentiometer change the speed live, and reads a button with zero lag, all in one loop that never blocks.

  • Time: ~30 minutes
  • Skill level: Beginner
  • What you will build: A non-blocking sketch with independent timers, a live-adjustable blink rate, an instant button, and a reusable timer helper you will paste into every future project.
Arduino Nano on a breadboard with two LEDs and a potentiometer demonstrating non-blocking millis() timing
Stop waiting. Start checking the clock.

Parts List

From ShillehTek

External

  • None

Note: millis() returns the number of milliseconds since the board started, as an unsigned long. Always write timing as millis() - previous >= interval; that subtraction stays correct even when millis() rolls over after 49.7 days.

Step-by-Step Guide

Step 1 - Wire Two LEDs, a Pot, and a Button

Goal: Build the hardware setup with four things to juggle: two independent LED blink outputs, one analog input (pot), and one digital input (button).

What to do: Wire the following:

  • LED A: D5 → 220Ω → LED → GND
  • LED B: D6 → 220Ω → LED → GND
  • Potentiometer: ends to 5V and GND, wiper → A0
  • Button: D2 to GND (use the internal pull-up in code)
Arduino Nano wiring diagram showing two LEDs on D5 and D6 with resistors, a potentiometer to A0, and a button input on D2
The original layout, plus a button on D2 for the responsiveness test.

Expected result: Your circuit is ready for both the “bad” (delay-based) and the “good” (millis-based) sketch behavior.

Step 2 - Feel the Problem

Goal: Understand why delay() has to go for responsive input and multiple tasks.

What to do: Write the obvious version: toggle LED A, delay(1000), and in the same loop check the button and light LED B while it is held. Press the button.

It works only if you happen to press during the tiny gap between delays. Most presses are missed, and LED B can lag by up to a second.

Expected result: You see laggy or missed button input, which demonstrates the blocking problem.

Step 3 - The Sketch: Everything on the Clock

Goal: Replace blocking delays with independent timers so each task can run on its own schedule.

What to do: Upload this sketch, then turn the pot and tap/hold the button.

Code:

const int LED_A = 5, LED_B = 6, POT = A0, BTN = 2;

// a tiny reusable timer: "has `interval` ms passed since I last fired?"
struct Every {
  unsigned long interval, last = 0;
  Every(unsigned long ms) : interval(ms) {}
  bool ready() {
    unsigned long now = millis();
    if (now - last >= interval) { last = now; return true; }   // rollover-safe
    return false;
  }
};

Every blinkA(1000);      // LED A: fixed 1 s
Every blinkB(250);       // LED B: set by the pot
Every report(500);       // Serial status twice a second

void setup() {
  Serial.begin(9600);
  pinMode(LED_A, OUTPUT); pinMode(LED_B, OUTPUT); pinMode(BTN, INPUT_PULLUP);
}

void loop() {
  // task 1: LED A blinks at 1 Hz, no matter what else happens
  if (blinkA.ready()) digitalWrite(LED_A, !digitalRead(LED_A));

  // task 2: LED B blinks at a rate set live by the knob (50 ms ... 1000 ms)
  blinkB.interval = map(analogRead(POT), 0, 1023, 50, 1000);
  if (blinkB.ready()) digitalWrite(LED_B, !digitalRead(LED_B));

  // task 3: the button is checked thousands of times a second: instant response
  if (digitalRead(BTN) == LOW) { digitalWrite(LED_A, HIGH); digitalWrite(LED_B, HIGH); }

  // task 4: periodic status without slowing anything down
  if (report.ready()) {
    Serial.print("LED B interval: "); Serial.print(blinkB.interval); Serial.println(" ms");
  }
  // notice: no delay() anywhere
}

Expected result: LED A ticks steadily at one second, LED B speeds up and slows down as you turn the knob, and both LEDs light instantly whenever the button is pressed, while the Serial Monitor reports on schedule.

Step 4 - Read the Pattern, Then Reuse It

Goal: Internalize the non-blocking “task” pattern so you can scale it to larger projects.

What to do: Notice that every task follows the same shape: remember when it last ran, and if enough time has passed, run it again and update the timestamp. The Every helper packages that so each task becomes one if (x.ready()).

Add a fifth task (for example, a heartbeat print every 10 seconds) and notice it takes two lines and changes nothing else.

Expected result: Adding features no longer breaks the timing of existing features.

Step 5 - Where delay() Still Hides

Goal: Learn where blocking behavior can still show up, even after you remove explicit delay() calls.

What to do: Look for blocking calls you did not write directly:

  • pulseIn() on an ultrasonic sensor (use a timeout)
  • a DHT read (only every 2 s)
  • long Serial prints at 9600 baud (raise the baud rate)
  • an LCD full redraw (redraw only what changed)

Handle button debouncing with a timestamp too, instead of a delay. For state that spans time (for example, “beep for 200 ms then stop”), store an end time and check it, rather than delaying.

Expected result: Sketches that stay responsive as they grow.

Conclusion

Replacing delay() with millis() is one of the biggest upgrades in an Arduino programmer’s toolkit. With one small timer helper, tasks become independent, the button becomes instant, and adding the next feature stops being scary.

Reference credit: photos and images are credited to mdraber on Hackster.io, whose original guide served as the reference for this ShillehTek version.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

All 6 in stock
0 parts selected $0.00