2026 Stop Using Delay ! with Powerful Event Driven Techniques | ESP32-S3 C++ Tutorial


Event Driven architecture is no longer optional — it has become a core engineering skill for embedded developers in 2026. In an era of abundant ESP32-S3 performance, making code run is easy. But building systems that are non-blocking, high-performance, and maintainable — that’s the real challenge.

When we first learned embedded programming, we were trained to think linearly:

Turn LED on → wait 1 second → turn LED off → wait again.

This delay(1000) mindset feels intuitive, but it severely limits the potential of the ESP32-S3 — a dual-core 240 MHz processor. Once your system must monitor sensors, handle network traffic, and update displays simultaneously, traditional blocking code quickly collapses into chaos.

If you still write delay() in your code, this article is for you.

We’ll use C++ encapsulation combined with Event Driven design and modern C++ tools like std::function to eliminate blocking logic. Instead of issuing rigid commands, your hardware control will evolve into a flexible Event Driven system — the foundation of modern embedded architecture.

Event Driven

Why Use Encapsulated Event Driven Control?

In embedded projects, as features multiply, directly manipulating GPIOs, filling code with delay(), and mixing hardware details quickly leads to messy and unmaintainable software.

By combining C++ encapsulation with Event Driven design, we transform hardware resources into objects with clear responsibilities and autonomous behavior. This creates a clean program structure while completely eliminating blocking.

Key Advantages

  • Eliminate Blocking, Free the CPU
    No more wasting cycles with delay(). Hardware timers (esp_timer) and interrupts handle timing, freeing the CPU to manage Wi-Fi, Bluetooth, sensors, and other concurrent tasks.
  • Reduce Coupling
    Hardware pins, timing control, and event callbacks are encapsulated within classes. External code subscribes to events without needing to know low-level implementation details.
  • Improve Readability and Responsiveness
    Calls like led.on() and onButtonPress([]{ ... }) are far more expressive than digitalWrite(2, HIGH) and delay(100). The system can also respond to hardware changes in real time.
  • Ease Maintenance and Extension
    Pin changes, timing adjustments, or new sensors only affect the class implementation. The application logic remains untouched.
  • Align with Modern Embedded C++ Best Practices
    ESP-IDF supports FreeRTOS and event loops. Managing hardware through C++ objects in an Event Driven architecture is a mature, widely adopted professional pattern.

With this approach, even as your project grows, the code remains clean, structured, non-blocking, and maintainable.

What is ESP32-S3?

ESP32-S3 is a modern MCU from Espressif with features including:

  • Dual-core Xtensa LX7 CPU
  • SIMD vector instruction support (for AI/ML acceleration)
  • Built-in Wi-Fi and Bluetooth LE
  • Suitable for Edge AIoT applications
  • Support for ESP-DL, ESP-SR, TensorFlow Lite Micro, and other AI frameworks

Development Environment

Before starting your programming, make sure to complete the following preparations:

  • Install ESP-IDF (version 5.x or higher): ESP-IDF is the official development framework for programming the ESP32, and it supports multiple operating systems such as Windows, macOS, and Linux.
  • ESP32-S3 Development Board: An ESP32-S3 board is required.

Project Structure

Assume we are creating a led_abstraction project. A typical Arduino/ESP32 PlatformIO structure would look like:

led_abstraction/
├── src/
│   ├── main.cpp
│   ├── LedController.h
│   └── LedController.cpp
└── platformio.ini

In a real project, such hardware encapsulation often belongs in a HAL (Hardware Abstraction Layer), separate from the application logic for a clean, layered architecture.

From “Controlling Pins” to “Controlling Objects”

A common beginner approach in Arduino/ESP32 looks like this:

// Traditional blocking code (Bad Smell)
void loop() {
    digitalWrite(LED_PIN, HIGH);
    delay(1000); // CPU halts, can't handle network requests
    digitalWrite(LED_PIN, LOW);
    delay(1000);
}

Problems:

  • Blocks the CPU, preventing concurrent task handling.
  • Hardware operations are scattered throughout loop(), creating messy code.

The ESP32-S3 C++ Mindset: Treat hardware as objects with state and behavior.

Encapsulating a Non-Blocking LED Controller

Let’s create a simple LedController class. It will handle all low-level LED details.

Here, we define the interface. Using constexpr and enum class is good modern C++ practice.

LedController.h

#pragma once
#include <Arduino.h>

class AsyncLed {
public:
    AsyncLed(uint8_t pin, bool activeHigh = true);
    void begin();
    
    void on();
    void off();
    void toggle();
    void blink(uint32_t interval); // Non-blocking blink interface
    void update();                 // Core: drives event updates

    bool isOn() const { return _state; }

private:
    uint8_t _pin;
    bool _activeHigh;
    bool _state;
    bool _isBlinking;
    uint32_t _interval;
    uint32_t _lastTick;
};

Here, we implement the logic. Note how we hide hardware detail (like digitalWrite logic).

LedController.cpp

#include "LedController.h"

AsyncLed::AsyncLed(uint8_t pin, bool activeHigh) 
    : _pin(pin), _activeHigh(activeHigh), _state(false), _isBlinking(false), _interval(0), _lastTick(0) {}

void AsyncLed::begin() {
    pinMode(_pin, OUTPUT);
    off();
}

void AsyncLed::on() {
    _isBlinking = false;
    _state = true;
    digitalWrite(_pin, _activeHigh ? HIGH : LOW);
}

void AsyncLed::off() {
    _isBlinking = false;
    _state = false;
    digitalWrite(_pin, _activeHigh ? LOW : HIGH);
}

void AsyncLed::blink(uint32_t interval) {
    _interval = interval;
    _isBlinking = true;
}

void AsyncLed::update() {
    if (!_isBlinking) return;
    
    if (millis() - _lastTick >= _interval) {
        _lastTick = millis();
        _state = !_state;
        digitalWrite(_pin, _activeHigh ? (_state ? HIGH : LOW) : (_state ? LOW : HIGH));
    }
}

An Elegant Main Loop

Now, observe how powerful our main.cpp becomes. We are no longer manipulating “pins” but orchestrating “behaviors.”

#include <Arduino.h>
#include "LedController.h"

AsyncLed statusLed(1); // Status LED
AsyncLed alertLed(2);  // Alert LED

void setup() {
    Serial.begin(115200);
    statusLed.begin();
    alertLed.begin();

    statusLed.blink(1000); // Blink every second
    alertLed.blink(100);   // Fast blink
}

void loop() {
    // Absolutely no delay() here!
    // All hardware objects handle their own timing events via update()
    statusLed.update();
    alertLed.update();

    // The CPU now has ample time for high-priority tasks like Wi-Fi or AI inference
}

No delay(). Each object manages its own timing events.

Conclusion

In 2026, hardware performance is no longer the bottleneck. Maintainability and Event Driven, non-blocking design define professional embedded engineering.

By adopting C++ class-encapsulated Event Driven control on the ESP32-S3:

  • Isolate Change — Blink logic changes stay inside the class
  • Unlock Potential — Eliminate delay() and unleash parallel processing
  • Evolve Elegantly — Move from command control to Event Driven subscription

When you start designing systems around objects, responsibilities, and Event Driven flows, you’ve crossed the line from Maker to embedded software architect.