Skip to content

Sketch structure and timing

All Arduino/ESP32 sketches follow the same basic pattern:

  • setup() runs once at the start.
  • loop() runs over and over again.

Understanding this, and how to deal with time, is essential for responsive projects.


1. Basic sketch structure

Minimal skeleton:

void setup() {
  // Runs once after reset or power-on
}

void loop() {
  // Runs repeatedly, as fast as possible (unless you add delays)
}

Example:

const int LED_PIN = 2;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(500);
  digitalWrite(LED_PIN, LOW);
  delay(500);
}

2. Why delay() can be a problem

delay(ms) blocks the whole loop() for the given time:

  • The ESP32 does nothing else in your sketch during a delay.
  • Multiple long delays can make your project feel “frozen” or sluggish.

Use delay() for:

  • Simple first examples (like Blink).
  • Very small delays (a few milliseconds) when needed.

For more complex projects, use non‑blocking timing with millis().


3. Non‑blocking timing with millis()

millis() returns the number of milliseconds since the board started. You can compare it to previous timestamps to decide when to do something, without blocking.

const int LED_PIN = 2;
const unsigned long INTERVAL = 500; // ms

unsigned long lastToggle = 0;
bool ledOn = false;

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

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

  if (now - lastToggle >= INTERVAL) {
    lastToggle = now;
    ledOn = !ledOn;
    if (ledOn) {
      digitalWrite(LED_PIN, HIGH);
    } else {
      digitalWrite(LED_PIN, LOW);
    }
  }

  // Here you can do other things without being blocked
}

This pattern lets you handle multiple tasks (reading sensors, updating a display, etc.) in a single loop().


4. Simple “state machine” pattern

As projects grow, it helps to think in states (e.g. IDLE, MEASURING, ALARM) instead of one big block of code.

enum State {
  IDLE,
  MEASURING,
  ALARM
};

State currentState = IDLE;

void loop() {
  switch (currentState) {
    case IDLE:
      // Wait for a button press or timeout
      // currentState = MEASURING; when ready
      break;

    case MEASURING:
      // Read sensors, process data
      // currentState = ALARM; when threshold exceeded
      break;

    case ALARM:
      // Blink LED/buzzer, send message
      // currentState = IDLE; when reset
      break;
  }
}

You can combine the millis() pattern with this state machine to build responsive, readable sketches.