Home Learning Non-Blocking Code Techniques for Arduino With Examples

Non-Blocking Code Techniques for Arduino With Examples

by shedboy71
[lebox id="1"]

Writing blocking code is one of the most common mistakes people make when programming Arduino. This type of code stops the microcontroller from doing anything else while it waits.

This usually happens when you use delay() or loops that run for a long time.

Blocking code works for simple demos, but it doesn’t work for long in real projects where you need to

:

  • Read sensors continuously
  • Respond to buttons instantly
  • Control multiple outputs at once
  • Maintain timing accuracy
  • Avoid freezes and missed events

By using time checks, state machines, and events instead of delays, non-blocking code lets your Arduino do more than one thing “at the same time.”

This tutorial explains:

  • What blocking vs non-blocking code really means
  • Why delay() is dangerous in real projects
  • Core non-blocking techniques
  • Many practical code examples
  • Common mistakes and best practices

What Is Blocking Code?

Blocking Code Defined

Blocking code halts program execution until a condition is met or time passes.

Classic Blocking Example

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

While delay(1000) runs:

  • No buttons are read
  • No sensors are updated
  • No communication is handled

The CPU is idle but unavailable.

Why Blocking Code Is a Problem

Blocking code causes:

  • Missed button presses
  • Slow or unresponsive interfaces
  • Inaccurate timing
  • Impossible multitasking
  • Unscalable code

In embedded systems, waiting is a bug, not a feature.

The Arduino loop() Is Already a Scheduler

This is a key mindset shift.

void loop() {
  // runs thousands of times per second
}

Arduino already gives you:

  • A continuously running main loop
  • Deterministic execution order

Non-blocking code simply uses this loop correctly.

Core Principle of Non-Blocking Code

Instead of:

Do task → wait → do next task

Think:

Check if task should run → run briefly → return

Each task:

  • Runs fast
  • Never waits
  • Checks time or conditions

Technique 1: Using millis() Instead of delay()

Basic millis() Timing Pattern

unsigned long lastTime = 0;
const unsigned long interval = 1000;

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

  if (now - lastTime >= interval) {
    lastTime = now;
    digitalWrite(13, !digitalRead(13));
  }
}

Why This Is Non-Blocking

  • No waiting
  • Code continues executing
  • Other logic can run freely

millis() Overflow Safety

millis() overflows roughly every 49 days. This pattern is safe:

if (now - lastTime >= interval)

Never compare absolute values:

// unsafe
if (now > lastTime + interval)

Technique 2: Multiple Timed Tasks

unsigned long ledTimer = 0;
const unsigned long ledInterval = 500;

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

  if (now - ledTimer >= ledInterval) {
    ledTimer = now;
    digitalWrite(13, !digitalRead(13));
  }

  if (digitalRead(2) == LOW) {
    Serial.println("Button pressed");
  }
}

Each task:

  • Has its own timer
  • Runs independently
  • Never blocks others

Technique 3: State Machines (Non-Blocking by Design)

State machines and non-blocking code go hand-in-hand.

Blocking Version (Bad)

digitalWrite(led, HIGH);
delay(500);
digitalWrite(led, LOW);
delay(500);

Non-Blocking State Machine Version

enum BlinkState { ON, OFF };
BlinkState state = OFF;
unsigned long lastChange = 0;

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

  switch (state) {
    case OFF:
      if (now - lastChange >= 500) {
        digitalWrite(13, HIGH);
        state = ON;
        lastChange = now;
      }
      break;

    case ON:
      if (now - lastChange >= 500) {
        digitalWrite(13, LOW);
        state = OFF;
        lastChange = now;
      }
      break;
  }
}

Technique 4: Event-Driven Logic

Example: Button Event Without Delay

bool lastButton = HIGH;

void loop() {
  bool currentButton = digitalRead(2);

  if (lastButton == HIGH && currentButton == LOW) {
    Serial.println("Button pressed event");
  }

  lastButton = currentButton;
}

The code:

  • Detects transitions
  • Reacts instantly
  • Never waits

Technique 5: Non-Blocking Sensor Sampling

Blocking Sensor Read (Bad)

delay(1000);
int value = analogRead(A0);

Non-Blocking Sensor Sampling

unsigned long sensorTimer = 0;
const unsigned long sensorInterval = 1000;

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

  if (now - sensorTimer >= sensorInterval) {
    sensorTimer = now;
    int value = analogRead(A0);
    Serial.println(value);
  }
}

Technique 6: Cooperative Multitasking Pattern

Structure

void loop() {
  taskBlink();
  taskReadButton();
  taskReadSensor();
}

Each task:

  • Returns immediately
  • Manages its own timing

Example Tasks

void taskBlink() {
  static unsigned long t = 0;
  if (millis() - t >= 500) {
    t = millis();
    digitalWrite(13, !digitalRead(13));
  }
}

void taskReadButton() {
  if (digitalRead(2) == LOW) {
    Serial.println("Button");
  }
}

Technique 7: Avoiding Blocking Loops

Blocking While Loop (Bad)

while (digitalRead(2) == HIGH) {
  // stuck here
}

Non-Blocking Alternative

if (digitalRead(2) == HIGH) {
  // condition met, continue normally
}

Or move logic into a state machine.

Technique 8: Timeouts Without delay()

Example: Wait for Input With Timeout

unsigned long start = millis();
const unsigned long timeout = 5000;

void loop() {
  if (digitalRead(2) == LOW) {
    Serial.println("Input received");
  } else if (millis() - start >= timeout) {
    Serial.println("Timeout");
  }
}

Technique 9: Non-Blocking Serial Handling

Blocking Serial Read (Bad)

while (!Serial.available()) {}

Non-Blocking Serial Read

if (Serial.available()) {
  char c = Serial.read();
}

Common Non-Blocking Mistakes

Mistake 1: Mixing delay() With millis()

This breaks timing assumptions and responsiveness.

Mistake 2: Long Computations in loop()

Heavy math or string processing can still block execution.

Mistake 3: One Timer for Everything

Each task should have its own timing control.

When delay() Is Acceptable

Rare cases:

  • Simple demos
  • One-time startup sequences
  • Debug-only sketches

Even then, it should be temporary.

Non-Blocking Design Checklist

  • No delay() in loop()
  • No while loops waiting for conditions
  • All tasks return quickly
  • Timing uses millis()
  • States represent behavior
  • Inputs checked continuously

Putting It All Together: Complete Example

unsigned long ledTimer = 0;
unsigned long sensorTimer = 0;

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

  if (now - ledTimer >= 500) {
    ledTimer = now;
    digitalWrite(13, !digitalRead(13));
  }

  if (now - sensorTimer >= 1000) {
    sensorTimer = now;
    Serial.println(analogRead(A0));
  }

  if (digitalRead(2) == LOW) {
    Serial.println("Button pressed");
  }
}

This sketch:

  • Blinks LED
  • Reads sensor
  • Detects button All without blocking.

Final Thoughts

Non-blocking code is the single most important skill for writing reliable Arduino projects.

Once you abandon delay() and embrace:

  • millis()
  • State machines
  • Event-driven logic

Your code becomes:

  • Faster
  • More responsive
  • Easier to debug
  • Much more scalable

 

Share
[lebox id="2"]

You may also like