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.

Contents
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 withdelay(). 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 likeled.on()andonButtonPress([]{ ... })are far more expressive thandigitalWrite(2, HIGH)anddelay(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.









