Back to Home

Event-driven programming on Arduino: patterns and examples

Article demonstrates the implementation of event-driven architecture in Arduino projects through the EVA Core library. Principles of component composition, code examples for sensors and peripherals, limitations of the approach are considered. Material aimed at middle/senior developers.

Event model for Arduino: from theory to practice
Advertisement 728x90

Event-Driven Programming on Arduino: Enterprise Patterns for Embedded

Arduino is traditionally associated with linear sketches and the cyclic loop(). But as projects grow more complex, this approach leads to spaghetti code. We're introducing a method to bring event-driven architecture from enterprise applications into embedded development using the EVA Core library.

The Problem of Structure in Microcontrollers

In desktop and server applications, code is organized around event handlers: clicks, timers, network packets. Arduino, however, forces everything into a single loop(), where you have to manually track states using millis() and flags. This creates two critical limitations:

  • Inability to isolate component logic — timings get propagated through the entire object hierarchy
  • Lack of clear contracts between modules regarding timing requirements

Standard Arduino libraries address this only piecemeal: there are button handlers, separate timers, but no unified event system. Attempts to build an "all-in-one" solution run into tight coupling to a specific loop() implementation.

Google AdInline article slot

EVA Core: Three Components in a Unified System

The EVA Core library (Event-Driven Architecture) solves the problem through three interacting layers:

  • Callback mechanism for class methods — allows subscribing to events without global functions
  • Tickable interface — gives objects access to the time loop via the tick() method
  • Survival Kit — a set of ready-made components (buttons, timers) built on the first two principles

Key innovation — separating timing logic from business rules. The sensor manages its own timings, while the application reacts only to events. This enables:

  • Isolating filtering and debouncing algorithms in components
  • Guaranteeing update rhythm for nested objects
  • Eliminating the need to propagate calls through the hierarchy

Let's look at the implementation using a temperature sensor example:

Google AdInline article slot
#include <evaTickable.h>
#include <evaHandler.h>

using namespace eva;

class TempSensor : public Tickable {
private:
  IHandler* listener = nullptr;
  unsigned long lastRead = 0;

public:
  void subscribe(IHandler* handler) {
    listener = handler;
  }

private:
  void tick() override {
    if (millis() - lastRead > 1000) { // Interval 1 sek
      int value = readTemperature();
      if (listener && value != lastValue) {
        CallbackInfo info;
        info.eventType = TEMP_UPDATE;
        info.eventArg = value;
        listener->invoke(this, info);
      }
      lastRead = millis();
    }
  }
};

class ClimateControl : public IHandler {
private:
  TempSensor sensor;

  void onTempUpdate(int value) {
    if (value > 25) activateCooling();
  }

public:
  ClimateControl() {
    sensor.subscribe(this);
  }

  void invoke(void* sender, CallbackInfo info) override {
    if (info.eventType == TEMP_UPDATE) {
      onTempUpdate(info.eventArg);
    }
  }
};

void setup() {
  static ClimateControl system;
}

void loop() {
  eva::tac(); // Edinaya point update
}

Advantages of the approach:

  • TempSensor encapsulates polling and filtering logic
  • ClimateControl only knows about events, not timing details
  • Adding new sensors doesn't require changes to loop()
  • Testing business logic in isolation from timing

LEGO Architecture for Peripherals

Survival Kit implements the composition principle through compile-time templates. For example, multi-button handling on an analog input is built as a chain of transformers:

template <int PIN, int PIN_MODE, signed short... LEVELS>
using PinMultiButton = Button<QuantizeDecor<DebounceDecor<AnalogPinReader<PIN, PIN_MODE>>, LEVELS...>>;

Signal processing architecture:

Google AdInline article slot
  • Analog signal →
  • Debounce →
  • Quantization by levels →
  • Button logic →
  • Events

This pattern lets you assemble custom handlers from ready-made components. Example — a keypad from discrete buttons:

#include <evaSwitch.h>

class MyKeypad {
public:
  MyKeypad() {
    pinMode(2, INPUT_PULLUP);
    // ... initialization ostalnykh pinov
  }

  signed short getValue() {
    if (digitalRead(2) == LOW) return 'u';
    // ... processing drugikh knopok
    return 0;
  }
};

// Withborka chains processing
Switch<DebounceDecor<MyKeypad>> keypad;

// Podpiska on events
keypad.setListener(new Handler<App>(this, &App::onKeyPress), ON_PRESS);

Each layer handles its own responsibility. If needed, you can swap debounce for a more sophisticated algorithm or add press duration filtering — without altering the core application logic.

Limitations and Use Cases

EVA Core is positioned as a methodological tool, not a universal solution. Use it when:

  • Building projects with complex object hierarchies
  • Needing clear separation of timing and business logic
  • Teaching event-driven architecture principles on simple devices

Critical limitations:

  • Increased RAM usage due to virtual tables
  • Complexity for beginners (requires knowledge of templates and method pointers)
  • Overkill for simple projects (e.g., controlling a single LED)

Important to understand: the library's goal is demonstrating architectural patterns, not resource optimization. For production projects with tight constraints, you'll need to analyze the overhead.

Key Takeaways

  • Event model isolates timing dependencies in components
  • Tickable interface ensures regular updates for nested objects
  • Template composition offers flexibility without runtime overhead
  • No more propagating calls simplifies refactoring and testing
  • Arduino becomes a playground for enterprise patterns

This approach proves that even on resource-constrained microcontrollers, you can apply professional design methods. The key benefit — code becomes predictable and scalable, rather than just "getting the job done".

— Editorial Team

Advertisement 728x90

Read Next