Unit E - College Practice

28. Non-Blocking Timing with millis()

Run several timed tasks together without delay().

Estimated time 3 hours

Learning outcomes

  • Explain why delay blocks other work in loop
  • Apply the millis pattern: remember, compare, act, stamp
  • Run independent timers for two LEDs at different rates
  • Debounce a button with millis without calling delay
  • Use unsigned long for every millis timestamp

Parts and preparation

Uno, two LEDs with 330 ohm resistors, one push-button and jumpers.

Before power: inspect wiring, confirm supply voltage and ensure all connected circuits share GND.

Non-Blocking Timing with millis() instructional connection diagram

Why delay blocks

delay(ms) freezes the whole sketch until the wait ends. During that freeze, loop cannot read buttons, update a second LED, print Serial, or service sensors.

millis() returns milliseconds since the board started and keeps counting while your code runs. Instead of sleeping, you check whether enough time has passed since the last action for each task.

Comparison of delay freezing the sketch versus millis letting loop keep checking tasks
delay stops everything. millis lets many tasks share one loop.

The millis pattern

For each timed task: store the last time you acted, compare against the interval, act when due, then update the stamp.

Pseudocode: if (now - lastRun >= interval) { lastRun = now; doWork(); }

Three steps: remember lastRun, compare now minus lastRun to interval, then act and stamp
Remember, compare, act, stamp. Skip when the interval is not due yet.
unsigned long now = millis();
if (now - lastFast >= 200) {
  lastFast = now;
  fastState = !fastState;
  digitalWrite(ledFast, fastState);
}

Independent timers

Each task needs its own lastRun variable and its own interval. The worked sketch blinks D5 every 200 ms and D6 every 700 ms while still watching a button.

Tasks share loop but do not wait for each other. If one LED rate looks wrong, check that task's interval and stamp - not the other task.

Three cards for fast LED 200 ms, slow LED 700 ms, and button debounce 40 ms
Course build: fast LED, slow LED, and non-blocking debounce together.

Shared timeline

Imagine millis as a shared clock. Fast toggles happen often, slow toggles less often, and the button is checked on every loop pass. Empty gaps are not delay - loop is still spinning and checking.

Timeline showing frequent fast LED toggles, fewer slow LED toggles, and continuous button checking
Dots are actions. Between dots, loop is still running.

Debounce without delay

Mechanical buttons bounce. Lesson 08 used a millis debounce; here it sits beside the LED timers.

When the raw reading changes, restart lastDebounce. Only after about 40 ms of quiet, and only if the reading differs from the last stable state, accept the new edge. On a stable press to GND (LOW with INPUT_PULLUP), increment the press count.

Flow from raw reading change through 40 ms quiet window to accepting a stable press edge
Restart the quiet timer on every raw change. Never delay while waiting.

unsigned long and overflow

millis() returns unsigned long. Store every timestamp as unsigned long too. Using int will break as time grows.

After about 49 days millis rolls over to zero. The subtraction now - lastRun still works correctly with unsigned long arithmetic, so do not rewrite the pattern with signed types or absolute millis comparisons.

Comparison of int timestamps as wrong versus unsigned long as correct for millis
Match the type millis returns: unsigned long.
ItemCourse practice
Clockunsigned long now = millis();
Each task stampits own unsigned long lastX
Comparenow - lastX >= interval
UpdatelastX = now after the action

Wiring and safe build sequence

  1. D5 -> 330 ohm -> LED1 -> GND
  2. D6 -> 330 ohm -> LED2 -> GND
  3. Button from D2 to GND; use INPUT_PULLUP
Power rule: switch off before moving wires. Arduino I/O pins are control signals; high-current loads require a driver and suitable external supply.
const byte ledFast = 5;
const byte ledSlow = 6;
const byte buttonPin = 2;

unsigned long lastFast = 0;
unsigned long lastSlow = 0;
unsigned long lastDebounce = 0;
bool fastState = false;
bool slowState = false;
bool lastStable = HIGH;
bool lastReading = HIGH;
unsigned int pressCount = 0;

void setup() {
  pinMode(ledFast, OUTPUT);
  pinMode(ledSlow, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  unsigned long now = millis();

  if (now - lastFast >= 200) {
    lastFast = now;
    fastState = !fastState;
    digitalWrite(ledFast, fastState);
  }

  if (now - lastSlow >= 700) {
    lastSlow = now;
    slowState = !slowState;
    digitalWrite(ledSlow, slowState);
  }

  bool reading = digitalRead(buttonPin);
  if (reading != lastReading) {
    lastDebounce = now;
    lastReading = reading;
  }
  if ((now - lastDebounce) >= 40 && reading != lastStable) {
    lastStable = reading;
    if (lastStable == LOW) {
      pressCount++;
      Serial.print("Presses: ");
      Serial.println(pressCount);
    }
  }
}

How the code works

  1. Each LED has its own timer so the rates stay independent.
  2. The debounce window is 40 ms and does not call delay.
  3. Use unsigned long for every millis timestamp.
  4. INPUT_PULLUP: open reads HIGH, pressed to GND reads LOW.

Test and record evidence

Expected result: LED1 blinks faster than LED2 while button presses still print on Serial.

Practical evidence checklist

Common faults and checks
  • If both LEDs freeze when the button is held, a delay was introduced somewhere.
  • Missed presses usually mean the debounce time is too long or the button wiring is wrong.
  • Wrong blink rate: check that task's interval and that lastX is updated only when the action runs.
  • Confirm LED polarity and 330 ohm series resistors on D5 and D6.
  • Button: pinMode INPUT_PULLUP and switch to GND only.
Extension challenge: Add a third timed task that prints the uptime every two seconds without disturbing the blink rates.

Check your understanding

Q1. Why avoid long delay calls?

Show answer

They stop loop from servicing other tasks.

Q2. Why use unsigned long with millis?

Show answer

millis returns unsigned long; correct overflow math needs matching types.

Q3. What does each timed task need?

Show answer

Its own lastRun stamp and its own interval.

Q4. When do you update lastFast?

Show answer

After the fast LED action runs, set lastFast = now.