Common Mistakes in Embedded C Development: Using delay() or busy loops for timing

Ilya KatlinskiIlya Katlinski
4 min read

📘 Introduction

In embedded systems, timing isn’t just important - it’s everything. From handling sensor data to controlling actuators or maintaining communication protocols, precise timing and safe concurrent execution are critical to ensuring system reliability. Yet, concurrency and timing bugs are among the most elusive and destructive in Embedded C development. They rarely show up during initial tests, but they surface in the worst moments, causing random glitches, missed interrupts, or system hangs.

This article is part of our ongoing series on common mistakes in embedded development. While we focus here on concurrency and timing pitfalls, you may also be interested in the related articles on memory management challenges:

This article kicks off a larger series on common concurrency and timing mistakes in Embedded C development. The issues are subtle, widespread, and critical, so we’re breaking them into five focused parts:

  • Using delay() or Busy Loops for Timing (this article)

  • Race Conditions on Shared Variables

  • Broken Producer-Consumer Buffer Logic (coming soon…)

  • Doing Too Much Work Inside ISRs (coming soon…)

  • Assuming Atomicity of Multi-byte Data (coming soon…)

Each article presents a real-world-inspired code example, highlights what goes wrong, and offers a practical fix, all aimed at helping you write more robust and predictable embedded software.

When starting out in embedded development, using a simple delay() or a busy loop feels like an easy way to control timing - blink an LED, wait between sensor reads, or debounce a button. It’s intuitive, immediate, and appears to work. But under the hood, this approach quickly becomes problematic.

🧠 Using delay() or busy loops for timing

🐞 The Problem

One of the most common mistakes in embedded development is using software-based delays, such as delay() functions or busy for loops, to wait for a specific period. While this might appear to work during prototyping, it leads to poor timing accuracy, wasted CPU cycles, and unpredictable behaviour as the system grows in complexity or runs on different clock speeds.

Busy-waiting delays are typically not synchronised with the system clock, are sensitive to compiler optimisation, and completely block the processor from performing any other tasks — a critical issue in real-time or interrupt-driven systems.

Here’s a typical pattern seen in early-stage embedded development:

#include <stdint.h>

void led_on() {
    digitalWrite(2, HIGH); // Built-in LED on most ESP32 boards
}

void led_off() {
    digitalWrite(2, LOW);
}

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

void delay() {
    for (volatile uint32_t i = 0; i < 1000000; ++i) {
        // Busy-wait
    }
}

void loop() {
    led_on();
    delay();       // Wait ~1s (depending on clock)
    led_off();
    delay();       // Wait ~1s again
}

This code blinks an LED with a delay between toggles, but:

  • The actual delay depends on CPU frequency and optimisation flags.

  • The CPU is locked in the for loop and cannot respond to other events (e.g., button presses, interrupts).

  • The timing is inconsistent across platforms and even within builds.

Solution: Use a Timer-based Delay

Many embedded platforms provide a timer or a hardware abstraction delay function. For example, the Arduino framework for ESP32 provides the delay() function, which is internally implemented using the FreeRTOS tick. Unlike busy-wait loops, this version yields control to the scheduler, allowing other tasks and system functions to run while the wait is in progress.

void led_on() {
    digitalWrite(2, HIGH); // Built-in LED on most ESP32 boards
}

void led_off() {
    digitalWrite(2, LOW);
}

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

void loop() {
    led_on();
    delay(1000);   // Wait 1000 ms using system tick (non-blocking for RTOS)
    led_off();
    delay(1000);   // Wait 1000 ms again
}

💡 Takeaway

Using software delays or busy loops can be tempting for quick results, but they break down quickly in real-world applications. Hardware timers and system delay functions provide precise, efficient, and scalable timing solutions, which are critical for responsive and reliable embedded systems.

🚀
Call to action: Avoid blocking the CPU with idle loops. Let the hardware do the timing work, and keep your application reactive and efficient.

At Itransition, we build IoT solutions with all these challenges in mind, ensuring our clients receive reliable, scalable systems with minimal maintenance overhead. Learn more about our approach at https://www.itransition.com/iot.

0
Subscribe to my newsletter

Read articles from Ilya Katlinski directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ilya Katlinski
Ilya Katlinski