Event-based Firmware (Part 1/2)

In this article, I explain the event-based approach for writing an embedded firmware. While I use the term event-based, it is similar to events systems used for desktop application but much more straightforward. It is nothing new or innovative; the shown approach is just good practice to keep your firmware modular and extensible.

I will guide you in small iterations, using practical examples, through this complex topic. You can stop at any point and start improving your code to that level of complexity you fully understand and can handle.

What are Event-Driven Applications?

For desktop applications, an event-driven approach is chosen because the software always reacts to events generated by the user. Each movement of the mouse, key and mouse button press and release will generate an event. These events are pushed into a queue and processed in an event loop.

The second important source of events is timers. Either fixed timers, which will e.g. generate an event every 100ms – or delayed timers which will create an event after a defined delay.

For embedded code, we mostly use timers as an event source. If implemented correctly, you can easily generate events from button presses, interrupts or other asynchronous sources as well.

In this article, I will focus on events generated by timers. These will solve most of the problems in firmware. Asynchronous input sources can be implemented using polling to integrate them into the event-based approach.

The Blink Example – Without Events

Let us start with a simple example application, you can find for any platform. For this article, I will explain everything based on examples for the Arduino Uno platform, using the Arduino IDE. I do this because anyone can easily reproduce the examples, and it will remove the additional complexity of the actual hardware access layer from the code.

Nevertheless, the shown concepts are meant to be used on any platform, especially for real-world applications. I try to explain the core concepts to you, so you will be able to apply them in any situation.

The Simple Blink Example

blink1.ino

const uint8_t cOrangeLedPin = 13;

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

void loop() {
  digitalWrite(cOrangeLedPin, HIGH);
  delay(1000);
  digitalWrite(cOrangeLedPin, LOW);
  delay(1000);
}

The Example Explained

If you are not familiar with the Arduino libraries: There are two predefined functions, setup() and loop(). The setup() function is called once after the start to set up things and the loop() function is called endlessly afterwards. This structure is already prepared for the event-based approach.

In the blink example above, I set the pin where a red or orange LED is attached as output, using pinMode() and in the loop, I toggle the output from low to high and back with a delay of one second.

While this example works perfectly, it is not very extensible or modular. If you like to e.g. blink a second LED on another pin, test the states of several sensors and buttons, you will get into trouble.

Introducing a Real-Time Counter to use Time Points

Instead of just blocking the CPU until an amount of time has passed (using the delay function), a better approach is to introduce a real-time counter. Often this is also called real-time clock, which can easily get confused with the special chips which hold the current date and time. This is just a counter which starts at zero at the start of the firmware and is increased every millisecond or another time unit.

If you write a firmware from scratch, you use a hardware timer and interrupts to create a timer like this:

// pseudocode:

uint32_t gTimer = 0;

setup {
    // setup the hardware timer to create an interrupt
    // every millisecond.
}

interrupt {
    ++gTimer;
}

In the Arduino environment, this is already implemented, and there is the function millis() to get the current value of the counter. The counter will flip over at the highest value and restart at zero.

Blink using Time Points

If we have a real-time counter, we can rewrite the previous blink code like this:

blink2.ino

const uint8_t cOrangeLedPin = 13;

uint32_t gNextEvent;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  gNextEvent = millis() + 1000;
}

void loop() {
  while (millis() != gNextEvent) {}
  digitalWrite(cOrangeLedPin, HIGH);
  gNextEvent += 1000;
  while (millis() != gNextEvent) {}
  digitalWrite(cOrangeLedPin, LOW);
  gNextEvent += 1000;
}

I introduce a new variable gNextEvent, which stores the time point for the next event. This time point should always be somewhere in the future, therefore I initialize the variable in the setup function with millis() + 1000.

millis() will return the time point at the current moment when the code is execued. By adding 1000, I will create a time point which lays one second in the future and store it in gNextEvent.

In the loop() function, I replaced the delay with a while loop:

while (millis() != gNextEvent) {}

This loop will wait until the real time counter (millis()) reached the next expected point in time. As soon this point is reached, the code continues, toggling the output with the digitalWrite function and by setting a new time point in the future.

Here we do not use millis() as time base, but the last time point instead. Adding the delay in milliseconds to this variable will set a new time point in the future.

Before we analyse this further, let use remove some obvious redundancy by introducing a state:

blink3.ino

const uint8_t cOrangeLedPin = 13;

uint32_t gNextEvent;
bool gBlinkState = false;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  gNextEvent = millis() + 1000;
}

void loop() {
  while (millis() != gNextEvent) {}
  digitalWrite(cOrangeLedPin, (gBlinkState ? HIGH : LOW));
  gBlinkState = !gBlinkState;
  gNextEvent += 1000;
}

Adding gBlinkState simplifies the code in the loop() function. Especially because each event shall be called in the same interval of 1000 milliseconds.

Why is this Better than Using delay?

Actually, in this simple form, there is almost no improvement over the delay based blink code. The only small improvement is using the real time counter is an increased precision on the long term.

The execution of the code using delay for timing looks like this:

The illustration exaggerates the duration of the state change code, but over a longer time period, there will be a shift and therefore the LED actually blinks slower than once per second.

Compare the timing with the code using time points:

Because this code waits for a given time point, it will be always as precise as the real-time counter is.

Let me be clear at this point, the event-based approach is not about precise timing but about flexibility and extensibility. If you need precise time based-events, you should work with an interrupt based on a hardware timer.

Using code like this for a single event makes no sense, let us blink two LEDs at different intervals.

Add a Second Event

If you like to test the examples, just connect a LED to pin 12 of the Arduino Uno board as shown in the illustration below.

The code to use two events increases the code complexity a little bit. See the complete example below:

blink4.ino

const uint8_t cOrangeLedPin = 13;
const uint8_t cGreenLedPin = 12;

uint32_t gNextOrangeLedEvent;
uint32_t gNextGreenLedEvent;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  pinMode(cGreenLedPin, OUTPUT);
  gNextOrangeLedEvent = millis() + 100;
  gNextGreenLedEvent = millis() + 100;
}

void orangeLedEvent()
{
  static bool state = false;
  digitalWrite(cOrangeLedPin, (state ? HIGH : LOW));
  state = !state;
  gNextOrangeLedEvent += 810;
}

void greenLedEvent()
{
  static bool state = false;
  digitalWrite(cGreenLedPin, (state ? HIGH : LOW));
  state = !state;
  gNextGreenLedEvent += 1240;
}

void loop() {
  const auto currentTime = millis();
  if (currentTime == gNextOrangeLedEvent) {
    orangeLedEvent();
  }
  if (currentTime == gNextGreenLedEvent) {
    greenLedEvent();
  }
  while (millis() == currentTime) {}
}

The example starts with additional variables for the LED pins and two variables for the time points of the events. Next I moved the actual events into separate functions orangeLedEvent() and greenLedEvent().

I also move the state into these functions for better encapsulation. Each function will toggle the LED state and at the end, set a new time point for the own event.

In the loop, I first capture the current time (real-time counter) in a variable currentTime. Next, I compare the time with the gNextOrangeLedEvent and gNextGreenLedEvent variables to see if one or both of these event functions should be called.

At the end of the loop() function, I use the statement while (millis() == currentTime) {} to wait for the next tick of the counter.

What did we Improve? What is missing?

What did we improve?

  • †Moving the events into their own functions is the first step of modularisation.
  • The new loop() allows any number of additional events added to the system.

What is missing?

  • In the case, an event would take longer than one tick.
    In the worst case, an event could be skipped and the counter could have to wrap one time around until it is called again. Using a 1ms counter with a 32bit variable, it would take ~49 days.
  • Each event has to handle its own gNext... variable.
    There is a risk the programmer forget to handle it, which would cause the same problems as a skip from the point before.
  • All events are implemented with an own time point variable and using a separate if clause, which is not very modular.

Now let us solve these issues step by step.

How to Prevent Event Skips

An issue with the current comparison of the time point is the risk of event skips. See the illustration below:

For the illustration we have two events: Event A is scheduled at time point 100 and event B is schedules at time point 200.

If the code for event A takes longer than expected, the real-time counter has already exceeded the time point 200. A comparison like if (currentTime == gNextEventB) { ... will not match, until the counter wraps over after a very long time.

Check for the Past

Instead of checking for the exact point in time, it is better to check the time point is in the past. Simple subtraction will calculate the delta between the current time and the time point.

const auto delta = _next - currentTime;
(delta & static_cast<uint32_t>(0x80000000ul)) != 0

Because we are working with unsigned values if the result is negative and currentTime is therefore in the past, the highest bit of the result will be set. Alternatively, you could cast the values into signed integers first:

const auto delta = static_cast<int32_t>(_next)
                   - static_cast<int32_t>(currentTime);
delta < 0

Encapsulate it in a Class

To keep the code simple, we encapsulate the time point in a separate class. It can be used to add functions for all checks and manipulations. These will make the code using the time point value readable and short.

Event.hpp

#pragma once

#include <Arduino.h>

class Event
{
public:
  Event();
public:
  void start(uint32_t delay);
  bool isReady(uint32_t currentTime);
  void scheduleNext(uint32_t delay);
private:
  uint32_t _next; 
};

Event.cpp

#include "Event.hpp"

#include <Arduino.h>

Event::Event()
  : _next(0)
{
}

void Event::start(uint32_t delay)
{
  _next = millis() + delay;
}

bool Event::isReady(uint32_t currentTime)
{
  if (_next == currentTime) {
    return true;
  }
  const auto delta = _next - currentTime;
  if ((delta & static_cast<uint32_t>(0x80000000ul)) != 0) {
    return true;
  }
  return false;
}

void Event::scheduleNext(uint32_t delay)
{
  _next += delay;
}

blink5.ino

#include "Event.hpp"

const uint8_t cOrangeLedPin = 13;
const uint8_t cGreenLedPin = 12;

Event gNextOrangeLedEvent;
Event gNextGreenLedEvent;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  pinMode(cGreenLedPin, OUTPUT);
  gNextOrangeLedEvent.start(100);
  gNextGreenLedEvent.start(100);
}

void orangeLedEvent()
{
  static bool state = false;
  digitalWrite(cOrangeLedPin, (state ? HIGH : LOW));
  state = !state;
  gNextOrangeLedEvent.scheduleNext(810);
}

void greenLedEvent()
{
  static bool state = false;
  digitalWrite(cGreenLedPin, (state ? HIGH : LOW));
  state = !state;
  gNextGreenLedEvent.scheduleNext(1240);
}

void loop() {
  const auto currentTime = millis();
  if (gNextOrangeLedEvent.isReady(currentTime)) {
    orangeLedEvent();
  }
  if (gNextGreenLedEvent.isReady(currentTime)) {
    greenLedEvent();
  }
  while (millis() == currentTime) {}
}

Now, if an event overlaps with a scheduled time point, the other event is started as well – just with a delay.

With the new Event class, the methods start(), isReady() and scheduleNext() add clarity to the code.

Prevent Missing Re-Schedule

In our simple implementation, all events occur repeatedly forever. As long the event is dead simple, it is hard to forget the scheduleNext() method call. If the code gets more complicated, this is a weakness of our implementation.

Look at the following code, using the Event class from the previous example. It uses a simple state machine to implement a complex LED blink pattern.

blink6.ino

#include "Event.hpp"

const uint8_t cOrangeLedPin = 13;

Event gBlinkLedEvent;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  gBlinkLedEvent.start(10);
}

void blinkLedEvent()
{
  const uint16_t fadeCount = 64;
  enum class Phase {
    FadeIn,
    On,
    FadeOut,
    Off
  };
  static auto phase = Phase::FadeIn;
  static uint16_t phaseCount = 0;
  static bool state = false;
  switch (phase) {
  case Phase::FadeIn:
    state = !state;
    if (state) {
       gBlinkLedEvent.scheduleNext(phaseCount*2);
    } else {
       gBlinkLedEvent.scheduleNext((fadeCount-phaseCount+1)*2);      
    }
    if (phaseCount == fadeCount) {
      phaseCount = 0;
      phase = Phase::On;
    }
    break;
  case Phase::On:
    state = true;
    phaseCount = 0;
    phase = Phase::FadeOut;
    gBlinkLedEvent.scheduleNext(1000);          
    break;
  case Phase::FadeOut:
    state = !state;
    if (state) {
       gBlinkLedEvent.scheduleNext((fadeCount-phaseCount+1)*2);      
    } else {
       gBlinkLedEvent.scheduleNext(phaseCount*2);
    }
    if (phaseCount == fadeCount) {
      phaseCount = 0;
      phase = Phase::Off;
    }
    break;
  case Phase::Off:
    state = false;
    phaseCount = 0;
    phase = Phase::FadeIn;
    gBlinkLedEvent.scheduleNext(1000);          
    break;
  }
  phaseCount += 1;
  digitalWrite(cOrangeLedPin, (state ? HIGH : LOW));
}

void loop() {
  const auto currentTime = millis();
  if (gBlinkLedEvent.isReady(currentTime)) {
    blinkLedEvent();
  }
  while (millis() == currentTime) {}
}

For simplicity, there is only one event in this example. Other than the examples before, the timing varies for each call. Scheduling the next event is critical. If there is a single case where the scheduleNext() call is missing, it takes a long time for the next event.

If you just work with repeated delayed events like this, you may want to rewrite the event code to pass the delay to the next event as a return value. This will enforce a call of scheduleNext(), because this method will be called outside of the actual event code.

blink7.ino

#include "Event.hpp"

const uint8_t cOrangeLedPin = 13;

Event gBlinkLedEvent;

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  gBlinkLedEvent.start(10);
}

uint32_t blinkLedEvent()
{
  const uint16_t fadeCount = 64;
  enum class Phase {
    FadeIn,
    On,
    FadeOut,
    Off
  };
  static auto phase = Phase::FadeIn;
  static uint16_t phaseCount = 0;
  static bool state = false;
  uint32_t scheduledDelay;
  if (phase==Phase::FadeIn||phase==Phase::FadeOut) {
    state = !state;
    if (state ^ (phase == Phase::FadeOut)) {
       scheduledDelay = phaseCount*2;
    } else {
       scheduledDelay = (fadeCount-phaseCount+1)*2;      
    }
    if (phaseCount == fadeCount) {
      if (phase == Phase::FadeIn) {
        phase = Phase::On;
      } else {
        phase = Phase::Off;
      }
    }
    phaseCount += 1;
  } else {
    if (phase == Phase::On) {
      state = true;
      phase = Phase::FadeOut;
    } else {
      state = false;
      phase = Phase::FadeIn;
    }
    phaseCount = 0;
    scheduledDelay = 1000;          
  }
  digitalWrite(cOrangeLedPin, (state ? HIGH : LOW));
  return scheduledDelay;
}

void loop() {
  const auto currentTime = millis();
  if (gBlinkLedEvent.isReady(currentTime)) {
    const auto scheduledDelay = blinkLedEvent();
    gBlinkLedEvent.scheduleNext(scheduledDelay);
  }
  while (millis() == currentTime) {}
}

If you change the interface of a function, this often leads to a different implementation. Because the scheduled delay has now to be passed via return value, the calculation of the delay is the last operation, or as in this example, it has to be passed to the end of the function using an additional variable.

Why is the Last Example Better?

Without looking at the implementation changes in the blinkLedEvent() function, the last example has one important improvement: It is more modular.

All code which is responsible for the event handling, including the scheduleNext() call is outside of the event function. This is a first step into the direction of a universal usable event system.

Move the Event Code into a Module.

A module, or compiling unit, is one of the simpler methods to modularise or encapsulate functionality. First, there is the accessibility layer, where you hide all internal variables and functions from the user of your module. Second, there is the namespace, where you group functions, variables or classes under a logical name.

For this simple event, using a class would be a misuse of the concept. There is just a single instance of the event code; therefore all the required pointer mechanics using a class instance would be wasted.

As a first step, we move the code for the event into a separate compile unit with a header and implementation file.

BlinkLed.hpp

#include <Arduino.h>

namespace BlinkLed {

void initialize();
uint32_t event();
  
}

BlinkLed.cpp

#include "BlinkLed.hpp"

namespace BlinkLed {

enum class Phase {
  FadeIn,
  On,
  FadeOut,
  Off
};

const uint16_t fadeCount = 64;
const uint8_t cOrangeLedPin = 13;

auto phase = Phase::FadeIn;
uint16_t phaseCount = 0;
bool state = false;

void initialize()
{
  pinMode(cOrangeLedPin, OUTPUT);
}

uint32_t event()
{
  uint32_t scheduledDelay;
  if (phase == Phase::FadeIn || phase == Phase::FadeOut) {
    state = !state;
    if (state ^ (phase == Phase::FadeOut)) {
       scheduledDelay = phaseCount;
    } else {
       scheduledDelay = (fadeCount-phaseCount+1);      
    }
    if (phaseCount == fadeCount) {
      phase = (phase == Phase::FadeIn ? Phase::On : Phase::Off);
    }
    phaseCount += 1;
  } else {
    if (phase == Phase::On) {
      state = true;
      phase = Phase::FadeOut;
    } else {
      state = false;
      phase = Phase::FadeIn;
    }
    phaseCount = 0;
    scheduledDelay = 1000;          
  }
  digitalWrite(cOrangeLedPin, (state ? HIGH : LOW));
  return scheduledDelay;
}
  
}

The interface of the module is two functions in the namespace BlinkLed. The function initialize() is called once from the setup() function to initialise everything for the module. The function event() is the function which is called by the event system.

In the implementation, the constants and state variables are moved into the module where they persist over function calls as before. As module variables, they are accessible by all functions in the module. So you can, e.g. add a reset() function to reset the state or setLedPin() function to configure the pin for the LED.

blink8.ino

#include "BlinkLed.hpp"
#include "Event.hpp"

Event gBlinkLedEvent;

void setup() {
  BlinkLed::initialize();
  gBlinkLedEvent.start(10);
}

void loop() {
  const auto currentTime = millis();
  if (gBlinkLedEvent.isReady(currentTime)) {
    const auto scheduledDelay = BlinkLed::event();
    gBlinkLedEvent.scheduleNext(scheduledDelay);
  }
  while (millis() == currentTime) {}
}

The main code looks much simpler now. Everything related to the blink led event ist now clearly visible by the namespace BlinkLed.

Imagine adding more event modules like this. Each event has its own separate module with its own variables and data. Even complex projects with many events stay readable and clear.

Source Code

You can find all code examples in the GitHub repository below:

https://github.com/LuckyResistor/event-based-firmware

Learn More

In the Next Article…

In the part two of this article, I will develop a more versatile event system, with an event stack to handle event functions dynamically.

Event-Based Firmware – Part 2

One thought on “Event-based Firmware (Part 1/2)”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.