Bird
Raised Fist0
Arduinoprogramming~15 mins

Timing-based state machines in Arduino - Deep Dive

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Overview - Timing-based state machines
What is it?
A timing-based state machine is a way to control what a program does by moving through different steps or states based on time. Instead of waiting or pausing the program, it checks how much time has passed and changes states when needed. This helps the program do many things smoothly without stopping. It is often used in devices like Arduino to manage tasks that happen after certain delays or intervals.
Why it matters
Without timing-based state machines, programs might stop and wait, making devices slow or unresponsive. This concept lets devices like robots or lights work on many tasks at once, making them feel smart and fast. It solves the problem of doing things in order and on time without freezing the whole system. This makes gadgets more useful and enjoyable to use.
Where it fits
Before learning this, you should understand basic programming concepts like variables, functions, and simple state machines. After this, you can learn about event-driven programming, interrupts, and multitasking on microcontrollers. This topic builds a bridge between simple step-by-step code and more complex time-managed control.
Mental Model
Core Idea
A timing-based state machine moves through steps by checking elapsed time instead of stopping to wait.
Think of it like...
It's like a traffic light that changes colors based on a timer, not by someone pressing a button or waiting around.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   State A     │──────▶│   State B     │──────▶│   State C     │
│ (Start Timer) │       │ (Check Timer) │       │ (Reset Timer) │
└───────┬───────┘       └───────┬───────┘       └───────┬───────┘
        │                       │                       │       
        │                       │                       │       
        └───────────────────────┴───────────────────────┘       
                 Time passes triggers state changes
Build-Up - 6 Steps
1
FoundationUnderstanding States and Transitions
🤔
Concept: Introduce the idea of states and how a program can move between them.
A state machine is like a set of boxes (states) where the program can be in only one box at a time. It moves from one box to another based on rules called transitions. For example, a light can be OFF or ON, and pressing a button moves it from OFF to ON or back.
Result
You know how to represent different modes or steps in a program and switch between them.
Understanding states and transitions is the foundation for controlling program flow in a clear and organized way.
2
FoundationUsing Arduino's millis() for Timing
🤔
Concept: Learn how to use Arduino's millis() function to track time without stopping the program.
Arduino's millis() returns how many milliseconds have passed since the board started. Instead of using delay(), which stops everything, you can check millis() to see if enough time has passed to do something. For example, if you want to blink an LED every second, you check if current millis() minus last blink time is more than 1000.
Result
You can measure time intervals without pausing the program.
Knowing how to track time with millis() lets you build programs that stay responsive and do many things at once.
3
IntermediateCombining States with Timing Checks
🤔Before reading on: Do you think a timing-based state machine pauses the program or keeps running while waiting? Commit to your answer.
Concept: Learn how to use timing checks inside states to decide when to move to the next state.
Instead of using delay(), each state records the start time using millis(). The program keeps running and checks if the required time has passed to move to the next state. For example, in a traffic light, the green state lasts 5 seconds, so after 5 seconds, it switches to yellow.
Result
The program moves through states based on elapsed time without stopping.
Understanding that the program keeps running while checking time prevents freezing and allows multitasking.
4
IntermediateImplementing a Simple Timing State Machine
🤔Before reading on: Can you predict what happens if you forget to update the timer when changing states? Commit to your answer.
Concept: Build a basic Arduino program that cycles through states with different time delays.
Example code: unsigned long previousMillis = 0; int state = 0; void loop() { unsigned long currentMillis = millis(); switch(state) { case 0: if (currentMillis - previousMillis >= 1000) { // 1 second previousMillis = currentMillis; state = 1; } break; case 1: if (currentMillis - previousMillis >= 2000) { // 2 seconds previousMillis = currentMillis; state = 0; } break; } } This cycles between two states with different delays.
Result
The program switches states on time intervals without delay() calls.
Knowing to reset the timer on each state change is key to accurate timing and smooth transitions.
5
AdvancedHandling Timer Overflow Safely
🤔Before reading on: Do you think millis() can cause timing errors after running for days? Commit to your answer.
Concept: Learn how to handle the fact that millis() resets after about 50 days and avoid bugs.
millis() returns an unsigned long that rolls over to zero after about 49.7 days. To handle this safely, always subtract times like this: if ((currentMillis - previousMillis) >= interval) { // safe even if millis() overflowed } This works because unsigned subtraction handles rollover correctly.
Result
Your timing checks remain correct even after long runtimes.
Understanding unsigned arithmetic prevents rare but serious bugs in long-running devices.
6
ExpertScaling Timing State Machines for Complex Systems
🤔Before reading on: Can a single timing-based state machine handle multiple independent timed tasks well? Commit to your answer.
Concept: Explore how to manage multiple timing-based state machines or tasks without conflicts or delays.
In complex systems, you might have many timed tasks (e.g., blinking LEDs, reading sensors, controlling motors). Each task can have its own state machine and timer variable. The main loop checks all tasks every cycle, updating states as needed. This approach avoids blocking and keeps the system responsive. Example pattern: struct Task { int state; unsigned long lastTime; unsigned long interval; }; Task ledTask = {0, 0, 500}; Task sensorTask = {0, 0, 1000}; void loop() { unsigned long now = millis(); // Update ledTask if (now - ledTask.lastTime >= ledTask.interval) { ledTask.lastTime = now; // change ledTask.state } // Update sensorTask if (now - sensorTask.lastTime >= sensorTask.interval) { sensorTask.lastTime = now; // change sensorTask.state } } This pattern scales well and keeps tasks independent.
Result
Multiple timed tasks run smoothly without blocking each other.
Knowing how to separate timers and states for each task is essential for building responsive, complex Arduino programs.
Under the Hood
Arduino's millis() function reads a hardware timer that counts milliseconds since startup. The timing-based state machine uses this count to check elapsed time by subtracting stored timestamps from the current millis() value. Because the program never stops, it continuously checks these values in the main loop, deciding when to switch states. The unsigned arithmetic ensures correct timing even when the timer rolls over to zero after about 50 days.
Why designed this way?
The design avoids using delay(), which blocks the CPU and stops all other tasks. Using millis() and state machines allows multitasking on simple microcontrollers without real operating systems. This approach was chosen to keep programs responsive and efficient with limited hardware resources.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│  Hardware     │       │  millis()     │       │  Program      │
│  Timer        │──────▶│  returns time │──────▶│  loop() checks│
└───────────────┘       └───────────────┘       └───────┬───────┘
                                                      │
                                                      ▼
                                             ┌─────────────────┐
                                             │ State machine   │
                                             │ checks elapsed  │
                                             │ time and changes│
                                             │ states          │
                                             └─────────────────┘
Myth Busters - 3 Common Misconceptions
Quick: Does using delay() inside a state machine keep the program responsive? Commit to yes or no.
Common Belief:Using delay() inside a state machine is fine because it just waits for the right time.
Tap to reveal reality
Reality:delay() stops the entire program, freezing all tasks and making the device unresponsive during the wait.
Why it matters:Using delay() causes missed inputs, slow reactions, and poor user experience in real devices.
Quick: Do you think millis() can return negative numbers? Commit to yes or no.
Common Belief:millis() can return negative values if the timer overflows or after long runtimes.
Tap to reveal reality
Reality:millis() returns an unsigned long, so it never returns negative numbers; it rolls over from max value back to zero safely.
Why it matters:Misunderstanding this can lead to incorrect timing checks and bugs in long-running programs.
Quick: Can one timing-based state machine easily handle multiple independent timed tasks? Commit to yes or no.
Common Belief:A single timing-based state machine can manage many tasks by switching states quickly.
Tap to reveal reality
Reality:Each independent timed task needs its own state and timer to avoid conflicts and ensure accurate timing.
Why it matters:Trying to handle multiple tasks in one state machine without separation causes timing errors and complex, hard-to-maintain code.
Expert Zone
1
Using unsigned arithmetic for time comparisons is subtle but critical to handle timer rollover without bugs.
2
Separating state and timer variables per task avoids hidden dependencies and race conditions in multitasking.
3
Combining timing-based state machines with interrupts can improve responsiveness but requires careful design to avoid conflicts.
When NOT to use
Timing-based state machines are not ideal for very high-precision timing or real-time control where hardware timers or RTOS features are better. For complex multitasking, using an RTOS or event-driven framework might be more suitable.
Production Patterns
In real Arduino projects, timing-based state machines manage LED patterns, sensor polling, motor control, and communication protocols. Developers often use structs or classes to encapsulate states and timers, enabling modular and reusable code. Combining these with event flags or interrupts creates responsive and efficient embedded systems.
Connections
Event-driven programming
Builds-on
Understanding timing-based state machines helps grasp event-driven systems where actions happen in response to time or events without blocking.
Real-time operating systems (RTOS)
More advanced alternative
Timing-based state machines are a lightweight way to manage tasks before moving to full RTOS multitasking, showing the evolution of embedded control.
Project management scheduling
Similar pattern
Both involve managing tasks over time, prioritizing, and switching between activities efficiently to meet deadlines.
Common Pitfalls
#1Using delay() inside the state machine blocks the program.
Wrong approach:void loop() { switch(state) { case 0: delay(1000); // blocks everything state = 1; break; case 1: delay(2000); state = 0; break; } }
Correct approach:void loop() { unsigned long currentMillis = millis(); switch(state) { case 0: if (currentMillis - previousMillis >= 1000) { previousMillis = currentMillis; state = 1; } break; case 1: if (currentMillis - previousMillis >= 2000) { previousMillis = currentMillis; state = 0; } break; } }
Root cause:Misunderstanding that delay() stops all code execution, preventing multitasking.
#2Not resetting the timer when changing states causes incorrect timing.
Wrong approach:void loop() { unsigned long currentMillis = millis(); if (state == 0 && currentMillis - previousMillis >= 1000) { state = 1; // forgot to update previousMillis } if (state == 1 && currentMillis - previousMillis >= 2000) { state = 0; } }
Correct approach:void loop() { unsigned long currentMillis = millis(); if (state == 0 && currentMillis - previousMillis >= 1000) { previousMillis = currentMillis; state = 1; } if (state == 1 && currentMillis - previousMillis >= 2000) { previousMillis = currentMillis; state = 0; } }
Root cause:Forgetting to update the timer variable leads to immediate or delayed state changes.
#3Trying to manage multiple timed tasks with a single timer variable.
Wrong approach:unsigned long previousMillis = 0; int state1 = 0; int state2 = 0; void loop() { unsigned long currentMillis = millis(); if (state1 == 0 && currentMillis - previousMillis >= 1000) { state1 = 1; previousMillis = currentMillis; } if (state2 == 0 && currentMillis - previousMillis >= 2000) { state2 = 1; previousMillis = currentMillis; } }
Correct approach:unsigned long previousMillis1 = 0; unsigned long previousMillis2 = 0; int state1 = 0; int state2 = 0; void loop() { unsigned long currentMillis = millis(); if (state1 == 0 && currentMillis - previousMillis1 >= 1000) { state1 = 1; previousMillis1 = currentMillis; } if (state2 == 0 && currentMillis - previousMillis2 >= 2000) { state2 = 1; previousMillis2 = currentMillis; } }
Root cause:Confusing multiple timers into one variable causes timing conflicts and incorrect behavior.
Key Takeaways
Timing-based state machines let programs move through steps based on elapsed time without stopping the whole system.
Using Arduino's millis() function with unsigned arithmetic ensures accurate and safe timing even after long runtimes.
Avoid delay() in state machines because it blocks the program and makes devices unresponsive.
Each timed task needs its own state and timer variables to work correctly and independently.
This approach enables responsive, multitasking behavior on simple microcontrollers without complex operating systems.

Practice

(1/5)
1.

What is the main purpose of using millis() in a timing-based state machine on Arduino?

easy
A. To pause the program for a fixed time
B. To reset the Arduino board
C. To track elapsed time without stopping the program
D. To read analog sensor values

Solution

  1. Step 1: Understand what millis() does

    millis() returns the number of milliseconds since the Arduino started running. It keeps counting without stopping the program.
  2. Step 2: Connect millis() to timing-based state machines

    Using millis() lets the program check how much time passed and change states without pausing or blocking other tasks.
  3. Final Answer:

    To track elapsed time without stopping the program -> Option C
  4. Quick Check:

    millis() tracks time without delay [OK]
Hint: Remember: millis() never stops your code [OK]
Common Mistakes:
  • Thinking millis() pauses the program
  • Confusing millis() with delay()
  • Using millis() to reset Arduino
2.

Which of the following is the correct way to check if 1000 milliseconds have passed using millis()?

unsigned long previousMillis = 0;
unsigned long interval = 1000;

void loop() {
  unsigned long currentMillis = millis();
  // What condition checks if interval passed?
  if (__________) {
    // do something
    previousMillis = currentMillis;
  }
}
easy
A. previousMillis + currentMillis <= interval
B. previousMillis - currentMillis >= interval
C. currentMillis + previousMillis >= interval
D. currentMillis - previousMillis >= interval

Solution

  1. Step 1: Understand elapsed time calculation

    Elapsed time is current time minus previous time: currentMillis - previousMillis.
  2. Step 2: Check if elapsed time reached interval

    We compare if elapsed time is greater or equal to the interval: currentMillis - previousMillis >= interval.
  3. Final Answer:

    currentMillis - previousMillis >= interval -> Option D
  4. Quick Check:

    Elapsed time = current - previous [OK]
Hint: Subtract previous from current time to get elapsed [OK]
Common Mistakes:
  • Reversing subtraction order
  • Adding times instead of subtracting
  • Using <= instead of >=
3.

What will be the output of this Arduino code snippet that uses a timing-based state machine?

unsigned long previousMillis = 0;
const long interval = 2000;
int ledState = LOW;

void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }
    digitalWrite(13, ledState);
    Serial.println(ledState);
  }
}
medium
A. Prints alternating 0 and 1 every 2 seconds
B. Prints 1 continuously every 2 seconds
C. Prints 0 continuously every 2 seconds
D. No output because of syntax error

Solution

  1. Step 1: Understand the timing and state toggle

    Every 2000 ms, the code toggles ledState between LOW (0) and HIGH (1).
  2. Step 2: Check output printed

    Each toggle prints the current ledState (0 or 1) to Serial, alternating every 2 seconds.
  3. Final Answer:

    Prints alternating 0 and 1 every 2 seconds -> Option A
  4. Quick Check:

    State toggles and prints 0,1 alternately [OK]
Hint: Toggle state and print inside timed if-block [OK]
Common Mistakes:
  • Assuming constant output without toggle
  • Confusing HIGH/LOW with 1/0
  • Missing update of previousMillis
4.

Identify the bug in this timing-based state machine code and choose the fix.

unsigned long previousMillis = 0;
const long interval = 1000;
int ledState = LOW;

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

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis > interval) {
    ledState = !ledState;
    digitalWrite(13, ledState);
  }
}
medium
A. Add previousMillis = currentMillis; inside the if-block
B. Change int ledState to bool ledState
C. Replace ! with ~ in toggle
D. Remove the if condition to toggle every loop

Solution

  1. Step 1: Check timing update logic

    The code never updates previousMillis, so the condition stays true forever after first pass.
  2. Step 2: Fix by updating previousMillis

    Adding previousMillis = currentMillis; inside the if-block resets the timer for the next interval.
  3. Final Answer:

    Add previousMillis = currentMillis; inside the if-block -> Option A
  4. Quick Check:

    Update previousMillis to reset timer [OK]
Hint: Always update previousMillis after interval check [OK]
Common Mistakes:
  • Forgetting to update previousMillis
  • Using bitwise NOT (~) instead of logical NOT (!)
  • Removing timing check causes fast toggling
5.

You want to create a state machine that cycles through three LED states: OFF, RED, GREEN. Each state lasts 3 seconds. Which code snippet correctly implements this using millis()?

unsigned long previousMillis = 0;
const long interval = 3000;
int state = 0;

void setup() {
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    state = (state + 1) % 3;
    switch(state) {
      case 0:
        digitalWrite(RED_PIN, LOW);
        digitalWrite(GREEN_PIN, LOW);
        break;
      case 1:
        digitalWrite(RED_PIN, HIGH);
        digitalWrite(GREEN_PIN, LOW);
        break;
      case 2:
        digitalWrite(RED_PIN, LOW);
        digitalWrite(GREEN_PIN, HIGH);
        break;
    }
  }
}
hard
A. Does not change states due to missing update
B. Correctly cycles OFF, RED, GREEN every 3 seconds
C. Cycles states every 1 second instead of 3
D. Cycles only RED and GREEN, skipping OFF

Solution

  1. Step 1: Check timing and state update

    The code uses millis() to check 3 seconds passed, then updates state cycling 0,1,2 with modulo 3.
  2. Step 2: Verify LED outputs per state

    State 0 turns both LEDs off, 1 turns RED on, 2 turns GREEN on. This matches the required cycle.
  3. Final Answer:

    Correctly cycles OFF, RED, GREEN every 3 seconds -> Option B
  4. Quick Check:

    State cycles with modulo and timing [OK]
Hint: Use modulo (%) to cycle states smoothly [OK]
Common Mistakes:
  • Forgetting to update previousMillis
  • Incorrect modulo causing wrong cycles
  • Not turning off LEDs in OFF state