Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

Event-Based Firmware (Part 2/2)

Posted on 2019-07-092022-09-04 by Lucky Resistor

In the first part of this series, we explored the general concept of event-based firmware. To read that article, follow this link.

The concepts we discussed were directly tailored to one specific firmware. Now, let’s develop those concepts further to build an event system that can be integrated into many different applications.

First, we’ll look at the last example:

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) {}
}

What makes this code static?

First, the variable gBlinkLedEvent is tied directly to this single event and cannot be reused by other events.

Second, the if (gBlinkLedEvent.isReady(currentTime)) { ... } code is repeated for all events. Repetitive code typically indicates a bad design. While there are very few cases where repetitive code is acceptable, in most cases it should be removed using structures, loops and tables.

Backtrack for a New Direction

Let’s first address the issue of the fixed function call. To solve it, we need to backtrack and set a new direction. The following example consists of the files blink9.ino, Event.hpp and Event.cpp:

#pragma once

#include <Arduino.h>

class Event
{
public:
  typedef void (*Function)();
public:
  Event();
  Event(Function call, uint32_t next);
  Event(const Event&) = default;
  Event& operator=(const Event&) = default;
public:
  bool isValid() const;
  bool isReady(uint32_t currentTime) const;
  Function getCall() const;
  void clear();
private:
  Function _call;
  uint32_t _next; 
};
#include "Event.hpp"

Event::Event()
  : _call(nullptr), _next(0)
{
}

Event::Event(Function call, uint32_t next)
  : _call(call), _next(next)
{   
}

bool Event::isValid() const
{
  return _call != nullptr;
}

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

Event::Function Event::getCall() const
{
  return _call;
}

void Event::clear()
{
  _call = nullptr;
}
#include "Event.hpp"

const uint8_t cOrangeLedPin = 13;

Event gEvent;

void ledOnEvent();
void ledOffEvent();

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

void ledOnEvent()
{
  digitalWrite(cOrangeLedPin, HIGH);
  gEvent = Event(&ledOffEvent, millis() + 800);
}

void ledOffEvent()
{
  digitalWrite(cOrangeLedPin, LOW);  
  gEvent = Event(&ledOnEvent, millis() + 600);
}

void loop() {
  const auto currentTime = millis();
  if (gEvent.isValid() && gEvent.isReady(currentTime)) {
    auto call = gEvent.getCall();
    gEvent.clear();
    call();
  }
  while (millis() == currentTime) {}
}

Here, I rewrote the Event class to fit our new requirements. In the first part of the class, I defined methods for copy and construction:

public:
  Event();
  Event(Function call, uint32_t next);
  Event(const Event&) = default;
  Event& operator=(const Event&) = default;

The default constructor Event() creates an invalid event. The second constructor Event(Function call, uint32_t next) will create a valid event that will call the given function at the specified time point next.

To declare the class assignable and copyable, I included the copy constructor Event(const Event&) and the assign operator Event& operator=(const Event&). The statement = default will instruct the compiler to generate the default implementation for these functions. Another way to do this would be to simply omit these two lines to obtain the same result. However, adding them will help everyone know that the class supports copy and assign. 

Next, I define the functions used to manage the event:

public:
  bool isValid() const;
  bool isReady(uint32_t currentTime) const;
  Function getCall() const;
  void clear();

The function isValid() tests whether the event is valid. In this case, the event has a function to call. To test whether the function is ready to be called, we can use the function isReady(), which we have used in the past.

A simple getter getCall() lets us access the function to call, and the method clear() will cause the event to be invalid by assigning nullptr to the _call variable.

A Short Introduction to Function Pointers

If you already have experience using function pointers, feel free to skip this section.

Function pointers provide a low-level method to create dynamic calls, and it is likely that you have indirectly used something similar. The interrupt vector functions as an array of function pointers, or for some MCUs, the start address of the firmware is written at a defined location into the flash ROM.

In our example, I define a new type for a function pointer using the code shown below:

typedef void (*Function)();
// ... or ...
using Function = void(*)();

This is a confusing, outdated syntax used to declare a function pointer type. If you decide to use function pointers for desktop applications in C++, you should instead use the header <functional> with the following syntax:

#include <functional>
// ...
using Function = std::function<void()>;

This declaration creates a new type Function, which defines a function pointer to functions with a specific syntax. In our case, it points to functions with no return value void and no parameter (). A function pointer using this type can only call functions with the given syntax.

using Function = void(*)();

Function gFunc = nullptr;

void a() {
}

void b() {
}

void main() {
  gFunc = &a;
  gFunc();
  gFunc = &b;
  gFunc();
}

The example shown above illustrates this principle. In the main() function, first the address of function a is assigned to the function pointer using the syntax &a. Therefore, the next statement gFunc() will call function a().

Next, the address of the function b is assigned to gFunc. Therefore, gFunc() located in the next line will call function b().

Note: There are much better patterns and concepts in C++ that can be used to implement dynamic calls for events, but they produce much more machine code, which is an issue for embedded systems.

Creating Dynamic Events

Let’s now analyze how we use the new Event class in blink9.ino. At the beginning of the code, there is one event variable gEvent and two forward declarations of the event functions:

Event gEvent;

void ledOnEvent();
void ledOffEvent();

The forward declarations are required in order to use the function names in the code before their definitions.

In the setup() function, the LED pin is configured as an output, and the first event ledOnEvent() is called manually.

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

Manually calling the first event function offers a smart method to set the output into a defined state and assign the first delayed event. Alternatively, I could also write the setup() function as follows:

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  digitalWrite(cOrangeLedPin, HIGH);
  gEvent = Event(&ledOffEvent, millis() + 800);
}

The two event functions will simply turn the output to HIGH or LOW and schedule a new delayed event for the opposite function.

void ledOnEvent()
{
  digitalWrite(cOrangeLedPin, HIGH);
  gEvent = Event(&ledOffEvent, millis() + 800);
}

void ledOffEvent()
{
  digitalWrite(cOrangeLedPin, LOW);  
  gEvent = Event(&ledOnEvent, millis() + 600);
}

It is interesting to note the new abstract loop() function:

void loop() {
  const auto currentTime = millis();
  if (gEvent.isValid() && gEvent.isReady(currentTime)) {
    auto call = gEvent.getCall();
    gEvent.clear();
    call();
  }
  while (millis() == currentTime) {}
}

As you can see, in this loop there is no mention of ledOnEvent() or ledOffEvent(). There is also no visible delay value in this loop. Essentially, you can use the same loop for any kind of event based code, which is a step in the right direction.

Issues in the Previous Example

The previous example adds some flexibility to our code by using function pointers to call the event function, but a number of issues remain:

  • There is a single event variable gEvent. Adding more events will bind the events to own event variables, which is certainly not optimal.
  • The code Event(&ledOnEvent, millis() + 600); uses the real time counter millis() directly instead of passing a single delay value. Not only is this code difficult to use, but you also have the risk of losing long-term precision of the event timing.

Encapsulate and Allowing Multiple Events

For multiple events, we need to use multiple event variables. To keep the assignment to these variables dynamic, we will use an array of events.

We start by creating a new compile unit with the files EventLoop.hpp and EventLoop.cpp. The interface of this compile unit contains all functions required to operate the new event system.

As shown in the previous examples, we do not use a class, but rather a collection of functions encapsulated in a namespace. There is one single event loop for the entire firmware, meaning a class would only add complexity to the implementation.

EventLoop.hpp

#pragma once

#include "Event.hpp"

namespace EventLoop {

typedef void (*Function)();

void initialize();
void addDelayedEvent(Function call, uint32_t delay);
void loop();
  
}

The function initialize() must be called from the setup() function to initialize the event system. Adding new events should remain as simple as possible by using the addDelayedEvent() function. Now, we simply need the function pointer and the delay, and we no longer require any calculation. Finally, the function loop() will process the events from the loop of the firmware. You now need to call EventLoop::loop() from the loop() function of the firmware, and we will demonstrate this shortly. 

The example below shows an ineffective implementation to illustrate the basic concept of our new event system.

EventLoop.cpp

#include "EventLoop.hpp"

#include "Event.hpp"

namespace EventLoop {

const uint8_t cEventStackSize = 8;

uint32_t gCurrentTime = 0;
Event gEvents[cEventStackSize];

void initialize()
{
  gCurrentTime = millis();
}

void addEvent(const Event &newEvent)
{
  for (auto &event : gEvents) {
    if (!event.isValid()) {
      event = newEvent;
      break;
    }
  }
}

void addDelayedEvent(Function call, uint32_t delay)
{
  addEvent(Event(call, gCurrentTime + delay));
}

void processEvents()
{
  for (auto &event : gEvents) {
    if (event.isValid() && event.isReady(gCurrentTime)) {
      const auto call = event.getCall();
      event.clear();
      call();      
    }
  }  
}

void loop()
{
  const auto currentTime = millis();
  if (gCurrentTime != currentTime) {
    gCurrentTime = currentTime;
    processEvents();  
  }
}

}

At the beginning of the file, we define constants and variables for the module:

const uint8_t cEventStackSize = 8;

uint32_t gCurrentTime = 0;
Event gEvents[cEventStackSize];

Here, the constant cEventStackSize defines the maximum number of “parallel” events for our system. The value of 8 means that there can be eight pending events waiting to be executed. Any additional events will be ignored and will likely result in undefined behaviour.

It is normally very easy to find the maximum number of pending events for a firmware. This differs from a desktop application, where every slight movement of a mouse will add 100+ events to the stack. Instead, each new event is added by a previous event or some other defined action.

The variable gCurrentTime will serve to store the real time counter value for the current event that is processing. It may be slightly tricky to understand why this variable is used for our simple example, but it helps to improve the timing quality of the system.

If at a given time multiple functions are called from events, and these calls last more than one millisecond, utilizing millis() + delay would cause a slight shift in the timing. The variable gCurrentTime always stores the point of time at the beginning when processing the events. This reduces (but does not completely solve) these problems.

Towards the end, we find the actual array or stack of events. The variables are automatically initialized with the default constructor of the Event class and, therefore, are all invalid events.

void initialize()
{
  gCurrentTime = millis();
}

The initialize() function simply sets the initial value of gCurrentTime for cases when the firmware is adding events with the setup() function.

void addEvent(const Event &newEvent)
{
  for (auto &event : gEvents) {
    if (!event.isValid()) {
      event = newEvent;
      break;
    }
  }
}

void addDelayedEvent(Function call, uint32_t delay)
{
  addEvent(Event(call, gCurrentTime + delay));
}

This example uses a very simple implementation to add an event to the array. In the function addEvent(), the for loop searches for the first free “slot” and stores the event. This is neither effective nor a smart practice, but it is easy to understand.

Adding a delayed event using addDelayedEvent constructs a new Event object and adds it to the array using addEvent(). The delay is calculated by adding gCurrentTime and the passed delay parameter.

void processEvents()
{
  for (auto &event : gEvents) {
    if (event.isValid() && event.isReady(gCurrentTime)) {
      const auto call = event.getCall();
      event.clear();
      call();      
    }
  }  
}

void loop()
{
  const auto currentTime = millis();
  if (gCurrentTime != currentTime) {
    gCurrentTime = currentTime;
    processEvents();  
  }
}

The key to the system is the loop() function. Here I will assume it is called indefinitely from the main loop() function. It first stores the current real time counter value from millis() , then compares it to the last stored time in gCurrentTime.

If there is indeed a difference, the new time is stored in gCurrentTime and all events are processed by calling the processEvents() function.

This implementation will only process events every millisecond, and it will always wait for the next tick of the clock (real time counter). If the loop is delayed for any reason, the events will be processed immediately if there was a change of the clock.

Processing the events in processEvents() is similar to what we demonstrated in the previous example, and in this case, we process every event in the array. But this implementation is ineffective, and the order of events is not sequential, but it is at least very simple and easy to understand.

Using the New Event System

Let’s now test the new event system EventLoop by attempting to blink two LEDs at different speeds:

blink10.ino

#include "EventLoop.hpp"

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

void orangeOnEvent();
void orangeOffEvent();
void greenToggleEvent();

void setup() {
  pinMode(cOrangeLedPin, OUTPUT);
  pinMode(cGreenLedPin, OUTPUT);
  EventLoop::initialize();
  orangeOnEvent();
  greenToggleEvent();
}

void orangeOnEvent() {
  digitalWrite(cOrangeLedPin, HIGH);
  EventLoop::addDelayedEvent(&orangeOffEvent, 800);
}

void orangeOffEvent() {
  digitalWrite(cOrangeLedPin, LOW);  
  EventLoop::addDelayedEvent(&orangeOnEvent, 600);
}

void greenToggleEvent() {
  static bool isHigh = false;
  digitalWrite(cGreenLedPin, (isHigh ? HIGH : LOW));
  isHigh = !isHigh;
  EventLoop::addDelayedEvent(&greenToggleEvent, 200);  
}

void loop() {
  EventLoop::loop();
}

The code begins with the constants and forward declarations. In the setup() function, I first configure the two outputs for the LEDs, then initialize the event system using the EventLoop::initialize() call. This must be done before any addDelayedEvent() function is called.

Next, we will manually call two of the event functions to set the state of the output and add the initial event.

Next, we look at the three event functions we have already seen in previous examples. The only difference here is that these functions call EventLoop::addDelayedEvent() to add a new event.

At the end of the code, you can see the nearly empty loop() function, which is simply calling EventLoop::loop().

Observations

  • This new example code is very clean and easy to read.
  • All implementation details of the event system are hidden behind the EventLoop interface.
  • The loop() function is a simple one-liner.

Remaining Issues to Fix

  • Currently, there is only one type of event, the delayed event. In most firmwares, users can have something like an interval or polling event as well.
  • Adding and processing of events is not very effective with the current implementation. The array should be managed as a stack, adding new events at the end and removing processed events from any position.
  • This will also solve the issue we encountered related to the order of processing. Events that are added at a later point in time should also be processed later. If multiple events are to be executed at the same time, they should always be processed in the order in which they were added.
  • Interrupts should be able to act as the source of events in a safe way.

Example Files

You can find all the examples in the following GitHub repository:

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

Conclusion

In the second part of this series, hopefully you were able to better understand the basic concepts of event-based firmware. Now, you can apply this knowledge to your next project.

All the examples we demonstrated here are as minimalistic and simple as possible, but for real applications you can write better interfaces and add more functionality to your code. As you continue to develop and understand the concept, this should come with ease.

If you have any questions, missing some information, or simply wish to provide feedback, feel free to comment below!

Learn More

Write Less Code using the "auto" Keyword

Write Less Code using the “auto” Keyword

The auto keyword was introduced with C++11. It reduces the amount of code you have to write, reduces repetitive code and the number of required changes. Sadly, many C++ developers are not aware of how ...
Read More
Bit Manipulation using Templates

Bit Manipulation using Templates

Did you read my article about C++ templates for embedded code? You learned how to use function templates. This post adds a practical example to this topic. Bit Manipulation You may be familiar with bit ...
Read More
Make your Code Safe and Readable with Flags

Make your Code Safe and Readable with Flags

Flags play an important role in embedded software development. Microcontrollers and chips are using registers where single bits or combinations of bits play a big role in the configuration. All the bits and their role ...
Read More
It's Time to Use #pragma once

It’s Time to Use #pragma once

In my opinion, preprocessor macros and the outdated #include mechanism are one of the worst parts of the C++ language. It is not just these things are causing a lot of problems, even more, it ...
Read More
Real Time Counter and Integer Overflow

Real Time Counter and Integer Overflow

After writing the article about event-based firmware, I realised there are some misunderstandings about how real-time counters are working and should be used. Especially there is a misconception about an imagined problem if such a ...
Read More
How and Why to Avoid Preprocessor Macros

How and Why to Avoid Preprocessor Macros

While most naming conflicts in C++ can be solved using namespaces, this is not true for preprocessor macros. Macros cannot be put into namespaces. If you try to declare a new class called Stream, but ...
Read More

4 thoughts on “Event-Based Firmware (Part 2/2)”

  1. Zaar Hai says:
    2019-09-16 at 12:02

    Thanks again for another excellent article. Can you recommend a well maintained event library for arduino? The best I found so far is Eventually [1], but there does not seem be any active development going on.

    [1] https://github.com/johnnyb/Eventually

    Reply
    1. Lucky Resistor says:
      2019-09-16 at 12:36

      Actually, I have no recommendation. I didn’t look into the `Eventually` library but from my experience, ready-made solutions will not fit your needs. Every author has his own goals for the event system, which may not be yours.

      I wrote this article, so you understand the basic concepts of event-based systems and can write a simple system which will match your goals and grow with your projects. You can get some inspiration from the event system in my HAL library: https://github.com/LuckyResistor/HAL-common The event loop code is in the subdirectory `event`.

      To make it short: For a minimalistic event handling, write your own simple system or derive one from existing code. If you require a complex event handling with priorities, scheduling, etc. and have a large project with many tasks, have a look at real-time operating systems.

      Reply
      1. Zaar Hai says:
        2019-09-16 at 15:41

        Thanks for the details. I’m coming from software-only world and used to have “standard” libraries like libevent in C, asyncio in Python, etc. which get you started quickly with periodic timers and reaction to IO.
        Your tutorial is very informative and I’ll take your advice to build my own event tools, particularly because I’m learning.
        Thanks again,
        Zaar

      2. Lucky Resistor says:
        2019-09-16 at 16:58

        Building your own library is no general advice, but only for this special use case. If your application just has to handle a couple of timing events, writing an own solution is simple and straightforward. If you need task scheduling and have plenty of flash memory, check e.g. FreeRTOS. My solution is for the small microcontroller, like the Atmel ATtiny series.

        If you use libraries for embedded systems, always check the code quality first. Often these libraries are a macro infested hell of obscure code, undocumented and with no error handling. You also often see that even it looks superficially like C++ code, the author didn’t actually know how to write proper C++ and just put a `class` around procedural C code.

Leave a Reply Cancel reply

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

Stay Updated

Join me on Mastodon!

Top Posts & Pages

  • Storage Boxes System for 3D Print
  • Event-based Firmware (Part 1/2)
  • Build a 3D Printer Enclosure
  • Yet Another Filament Filter
  • Circle Pattern Generator
  • Circle Pattern Generator
  • Real Time Counter and Integer Overflow
  • Projects
  • Logic Gates Puzzle 11
  • Units of Measurements for Safe C++ Variables

Latest Posts

  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29
  • The 3D Printed Modular Lantern2023-01-17
  • Rail Grid Alternatives and More Interesting Updates2022-12-09
  • Large Update to the Circle Pattern Generator2022-11-10

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.
 

Loading Comments...