0
0
Arduinoprogramming~15 mins

Non-blocking code architecture in Arduino - Deep Dive

Choose your learning style9 modes available
Overview - Non-blocking code architecture
What is it?
Non-blocking code architecture is a way to write programs so they keep running smoothly without waiting for one task to finish before starting another. Instead of stopping everything to wait, the program checks if a task is ready and moves on if it isn't. This approach helps devices like Arduino handle many things at once, like reading sensors and controlling lights, without delays. It avoids the program freezing or getting stuck waiting.
Why it matters
Without non-blocking code, an Arduino program would pause and wait for each task to finish, making the device slow or unresponsive. For example, if it waits for a button press or a timer, it can't do anything else during that time. Non-blocking code lets the device multitask, improving user experience and making projects more reliable and efficient.
Where it fits
Before learning non-blocking code, you should understand basic Arduino programming, including loops and delays. After mastering it, you can explore advanced topics like interrupts, multitasking libraries, and real-time operating systems for embedded devices.
Mental Model
Core Idea
Non-blocking code lets a program keep working by checking tasks step-by-step instead of waiting for each to finish before moving on.
Think of it like...
It's like a chef preparing multiple dishes at once by checking on each pot regularly instead of standing by one pot until it's done.
┌───────────────┐
│ Start Loop    │
├───────────────┤
│ Check Task 1? │─No─┐
│ If ready, do │    │
│ else skip    │    │
├───────────────┤    │
│ Check Task 2? │─No─┤
│ If ready, do │    │
│ else skip    │    │
├───────────────┤    │
│ ...           │    │
├───────────────┤    │
│ Repeat Loop   │◄───┘
└───────────────┘
Build-Up - 6 Steps
1
FoundationUnderstanding Arduino Loop Basics
🤔
Concept: Learn how the Arduino loop() function runs repeatedly to perform tasks.
In Arduino, the loop() function runs over and over. Any code inside it runs again and again, letting the Arduino keep doing things. For example, turning an LED on and off repeatedly happens inside loop().
Result
The Arduino keeps running the code inside loop() forever, allowing continuous actions.
Knowing that loop() repeats endlessly is key to controlling timing and multitasking in Arduino programs.
2
FoundationProblems with delay() Function
🤔
Concept: Understand why delay() stops the program and causes blocking.
The delay() function pauses the whole program for a set time. For example, delay(1000) stops everything for 1 second. During this pause, the Arduino can't check buttons or sensors, making it unresponsive.
Result
Using delay() causes the Arduino to freeze temporarily, missing other important tasks.
Recognizing that delay() blocks all activity helps explain why non-blocking methods are needed.
3
IntermediateUsing millis() for Timing Without Blocking
🤔Before reading on: Do you think millis() pauses the program like delay()? Commit to your answer.
Concept: Learn to use millis() to track time without stopping the program.
millis() returns how many milliseconds have passed since the Arduino started. By saving the last time an action happened and comparing it to millis(), you can do things at intervals without stopping the program. For example, blink an LED every second by checking if enough time passed.
Result
The program can do other tasks while waiting for the right time to act, avoiding pauses.
Understanding millis() lets you build timers that don't freeze the program, enabling multitasking.
4
IntermediateChecking Multiple Tasks in Loop
🤔Before reading on: Can you predict what happens if you check two tasks with millis() in the same loop? Commit to your answer.
Concept: Learn to manage several timed tasks by checking each one separately inside loop().
Inside loop(), you can check if it's time to do Task A or Task B by comparing millis() to saved timestamps for each. If it's time, do the task and update the timestamp. This way, multiple things happen independently without blocking.
Result
Multiple tasks run smoothly, each on its own schedule, without stopping the program.
Knowing how to track multiple timers lets you build complex, responsive Arduino programs.
5
AdvancedState Machines for Complex Non-blocking Logic
🤔Before reading on: Do you think a state machine can replace delay() for multi-step tasks? Commit to your answer.
Concept: Use state machines to manage sequences of actions without blocking delays.
A state machine uses a variable to remember what step the program is in. Each loop checks the state and acts accordingly, moving to the next state when conditions are met. This replaces delay() pauses with step-by-step progress, allowing other tasks to run.
Result
Complex sequences run smoothly without freezing the program, improving responsiveness.
Understanding state machines unlocks powerful ways to handle multi-step processes non-blockingly.
6
ExpertCombining Non-blocking with Interrupts
🤔Before reading on: Can interrupts and non-blocking code work together without conflicts? Commit to your answer.
Concept: Learn how interrupts can complement non-blocking code for immediate responses.
Interrupts let the Arduino react instantly to events like button presses, pausing the main code briefly. Combining interrupts with non-blocking code means the program can handle urgent events immediately while still running multiple tasks smoothly. Careful design avoids conflicts and keeps code safe.
Result
Programs become highly responsive and efficient, handling both scheduled and urgent tasks.
Knowing how interrupts and non-blocking code interact is essential for advanced, real-world Arduino projects.
Under the Hood
Non-blocking code works by repeatedly running a fast loop that checks the status of tasks without waiting. Instead of pausing, it uses time stamps and state variables to decide when to act. The Arduino microcontroller executes instructions sequentially but quickly enough that checking many tasks feels simultaneous. Interrupts can temporarily pause this loop to handle urgent events, then return control.
Why designed this way?
Arduino's simple microcontroller lacks built-in multitasking, so blocking delays would freeze all activity. Non-blocking design was created to let programmers simulate multitasking by careful timing and state management. This approach avoids complex operating systems, keeping code simple and efficient for small devices.
┌───────────────┐
│ loop() start  │
├───────────────┤
│ Check Task 1  │
│ (compare time)│
├───────────────┤
│ Check Task 2  │
│ (compare time)│
├───────────────┤
│ Check States  │
│ (state machine)│
├───────────────┤
│ Return to loop│
│ start quickly │
└───────┬───────┘
        │
   ┌────▼─────┐
   │Interrupt │
   │Handler   │
   └──────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does using delay() sometimes cause problems in Arduino multitasking? Commit to yes or no.
Common Belief:Using delay() is fine because it just waits and then continues normally.
Tap to reveal reality
Reality:delay() stops all code execution, so the Arduino can't check sensors or respond to inputs during that time.
Why it matters:Using delay() can make your device unresponsive, missing button presses or sensor changes.
Quick: Can millis() replace delay() without any extra code? Commit to yes or no.
Common Belief:millis() automatically pauses the program like delay(), so no extra work is needed.
Tap to reveal reality
Reality:millis() only returns time; you must write code to check and act on it without pausing.
Why it matters:Without proper checks, millis() alone won't prevent blocking or improve responsiveness.
Quick: Is non-blocking code always simpler than blocking code? Commit to yes or no.
Common Belief:Non-blocking code is always easier to write and understand than blocking code.
Tap to reveal reality
Reality:Non-blocking code can be more complex because it requires managing states and timing carefully.
Why it matters:Underestimating complexity can lead to bugs and harder-to-maintain code.
Quick: Can interrupts replace all non-blocking code needs? Commit to yes or no.
Common Belief:Using interrupts means you don't need non-blocking code at all.
Tap to reveal reality
Reality:Interrupts handle immediate events but can't manage timed sequences or multitasking alone.
Why it matters:Relying only on interrupts can cause missed tasks or complicated code.
Expert Zone
1
Non-blocking code often requires careful variable scope management to avoid conflicts between tasks.
2
Combining state machines with event-driven programming can create highly efficient Arduino applications.
3
Interrupt service routines must be short and safe to avoid disrupting non-blocking logic.
When NOT to use
Non-blocking code is less suitable for very simple projects where blocking delays are acceptable or when using real-time operating systems that handle multitasking natively.
Production Patterns
In real-world Arduino projects, non-blocking code is used to handle sensor polling, user input, communication protocols, and LED animations simultaneously without freezing the device.
Connections
Event-driven programming
Non-blocking code builds on event-driven ideas by reacting to conditions without waiting.
Understanding event-driven programming helps grasp how non-blocking code responds to changes efficiently.
Operating system multitasking
Non-blocking code simulates multitasking on microcontrollers that lack full OS support.
Knowing OS multitasking concepts clarifies why non-blocking code is essential for embedded systems.
Human multitasking behavior
Both involve switching attention between tasks quickly without fully stopping any one task.
Recognizing how humans multitask helps appreciate the design and challenges of non-blocking code.
Common Pitfalls
#1Using delay() inside loop() causing program to freeze.
Wrong approach:void loop() { digitalWrite(LED_BUILTIN, HIGH); delay(1000); // blocks everything digitalWrite(LED_BUILTIN, LOW); delay(1000); // blocks everything }
Correct approach:unsigned long previousMillis = 0; const long interval = 1000; void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } }
Root cause:Misunderstanding that delay() stops all code, preventing multitasking.
#2Not updating timestamp after task runs, causing repeated immediate execution.
Wrong approach:unsigned long previousMillis = 0; const long interval = 1000; void loop() { if (millis() - previousMillis >= interval) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // missing previousMillis update } }
Correct approach:unsigned long previousMillis = 0; const long interval = 1000; void loop() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } }
Root cause:Forgetting to update the time reference causes the task to run repeatedly without delay.
#3Using long code inside interrupt routines causing missed interrupts.
Wrong approach:void ISR() { delay(100); // blocking inside interrupt // complex logic }
Correct approach:volatile bool flag = false; void ISR() { flag = true; // set flag quickly } void loop() { if (flag) { flag = false; // handle complex logic here } }
Root cause:Misunderstanding that interrupt routines must be short and fast to avoid blocking main code.
Key Takeaways
Non-blocking code lets Arduino programs run multiple tasks smoothly without freezing or waiting.
Using millis() instead of delay() enables timing without stopping the program.
State machines help manage complex sequences step-by-step without blocking.
Combining non-blocking code with interrupts creates responsive and efficient programs.
Understanding these concepts is essential for building real-world Arduino projects that feel alive and responsive.