Basic Concepts Of Event Loop Driven Programming

I recently came across the concept of Event Loop Programming, but I’ve been struggling to find a clear, beginner-friendly explanation. I realized that part of the problem is that I’m missing an understanding of the basic, foundational concepts that support event loop programming.

What are the essential, fundamental concepts I should understand before trying to implement event loop programming in my own projects?

Do you have a link?

Also why do you think this method would be appropriate for Arduino, i.e. what problem are you trying to solve?

I think the fundamental concept is how to turn a single-threaded CPU into a system that can "multi-task", i.e. give the effect of handling more than task at once. The key here is to avoid "busy waiting", that is, do not block execution when waiting for an event to happen.

Instead, turn the wait into a detectable event, then only handle the event when it occurs. This is the basis for all multitasking code, including OS/RTOS.

1 Like

These are two of the tutorials that I have come across, but have had trouble understanding:

The two biggest problems that I am trying to solve are:

  1. I feel like I am over reliant on nested loops and usually end up having an unnecessarily large Void() loop.

  2. There comes a point in my coding where I struggle to follow my own code.

Without seeing your code we can only guess, but it sounds like you need to identify functions in your code and move that code into separate modules.

not really sure what you mean by Event Loop

in general, various pieces of code can poll for various events within loop(). This is typical of a microcontroller without an operating system.

however, one type of event is a "timer" where the code uses millis() to get the current # of millisec since the processor was started and compares the difference between the current and some previous timestamp

for example

const byte PinLed = LED_BUILTIN;
const byte PinBut = A1;

byte butState;

enum { Off = HIGH, On = LOW };

const unsigned long MsecPeriod = 1000;
      unsigned long msec0;
bool tmr;

// -----------------------------------------------------------------------------
void loop ()
{
    // check for timer expiration event
    unsigned long msec = millis ();
    if (tmr && msec - msec0 >= MsecPeriod)  {
        tmr = false;
        digitalWrite (PinLed, Off);
    }

    // check for received serial data
    if (Serial.available ()) {
        char buf [90];
        int n = Serial.readBytesUntil ('\n', buf, sizeof(buf)-1);
        buf [n] = '\0';                 // terminate c-string with NUL

        Serial.println (buf);
    }

    // check for button press
    byte but = digitalRead (PinBut);
    if (butState != but)  {             // state change
        butState  = but;
        delay (20);                     // debounce
        if (LOW == but)  {              // pressed
            tmr   = true;
            msec0 = msec;
            digitalWrite (PinLed, On);
            Serial.println ("button press");
        }
    }
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin    (9600);
    Serial.println ("ready");

    pinMode (PinBut, INPUT_PULLUP);
    butState = digitalRead (PinBut);

    pinMode (PinLed, OUTPUT);
    digitalWrite (PinLed, Off);
}

Ok, I see. There isn't always a clear path to understanding, sometimes eventually it just clicks.

In a sense, all code is following a path between different states. You might not realize what the states are, it is important to realize that the position in the code is a state in itself. e.g. if you are inside a loop, that is a different state to being outside a loop.

So to achieve multi-tasking, you need to identify what the states are, and which states are "idle" or "waiting" states. (It is possible some tasks do not have idle states, e.g. calculating PI to an infinite number of places).

Imagine that you are a supervisor with a number of workmen, but only one shovel. If you see a worker standing there idle, because he is waiting for the next barrow of dirt, you can take the shovel and give it to a different worker who has a mound of dirt to shovel.

To make it easier, you ask the workers to raise a flag when the need the shovel. "Raising a flag" is the equivalent of an event. It is something for the supervisor to take notice of.

In the event loop, the "supervisor" code is looking for events to handle. When it sees the event it gives control (the shovel) to a worker task.

To be fair, I think we have all been there!

The IPO model for process control fits well with event loop programming.

Read about it in the abstract here:

The IPO Model.

Using FSMs (finite state machines) also is a good match.

To keep the loop running freely, every pass is

  • evaluate inputs to the process

  • use the current state of affairs and the inputs to alter the state and compute any outputs

  • do any actual outputs.

This also allows thinking about each of I, P and O separately, for development as well as fixing, modifying or enhancements.

Divide and conquer.

I am obliged to give a shout-out to forum member @paulpaulson who gave me the name for something I'd been doing all along.

a7

1 Like

i thought the IPO model more often describes things like payroll systems that can often be done in batches.

i wouldn't have considered something with constantly changin non-deterministic input and changes in state, such as various modes of behavior of an aircraft (idle, take-off, cruising, landing) as IPO.

on my last project, a femtocell, each task was driven primarily by events such as messages from other tasks,

Hello drunyan0824

The essential and basic concept is the use of an array of function pointers, also known as tasks. An index of the array addresses the task to be executed. The task should return a number, which in turn is used as an index. A Task Control Block, aka TCB, can be designed around this business. All tasks must be programmed straight forward to prevent task locks.

Well not only. But thanks on the event loop thing, it is a different animal altogether. Something to know more about for sure.

In the context of Arduino programming I think of IPO as similar to read-eval-print loops. The wikipedia article allows an interpretation that works well with some projects.

a7

I didn't post any code because I struggling so much with finding a good explanation/tutorial that I figured it might be easier to figure out the fundamental concepts of event loop programming. Like if I am going to start doing event loop programming I need to understand what flags, how to use them and when to use them. That sort of thing

I may be using the wrong term, I have seen it called Event Loop Programming or Event Driven Programming, but my understanding is that the Void loop would contain nothing but function calls and all the complicated programing would be do in the individual functions called in the void loop.

Thank you, I will start looking into function pointes and arrays of function pointers.

1 Like
  • Describe what you are wanting to do.

Hello drunyan0824

Take a look at this small and simple code example for your studies.

// Make task names
enum TaskName {TaskOne, TaskTwo};
// Make array of tasks
uint16_t  (*tasks[])(uint32_t currentTime) {
  taskOne, taskTwo
};
// Make taskOne
uint16_t taskOne(uint32_t currentTime)
{
  Serial.print(__func__), Serial.print(" @ "), Serial.println(currentTime);
  return TaskTwo; // call TaskTwo
}
// Make tasktwo
uint16_t taskTwo(uint32_t currentTime)
{
  Serial.print(__func__), Serial.print(" @ "), Serial.println(currentTime);
  return TaskOne; // call TaskOne
}

void setup()
{
  Serial.begin(115200);
}

void loop()
{
  static uint16_t index = TaskOne; // start with TaskOne
  uint32_t currentTime = millis();
  index = tasks[index](currentTime); // call task and save return for next call
  delay(1000); // for testing only !!
}

This example from your second link seems like the "core" of "event loop programming":

void loop() {
    if (event_foo_happened())
        handle_event_foo();
    if (event_bar_happened())
        handle_event_bar();
    if (event_baz_happened())
        handle_event_baz();
    // etc...
}

(and additional info about event types in that reply is also good.)

There's not a lot of "concept" TO that. I guess the main thing to remember is that each of test and handle_event_xxx() functions needs to be "fast" compared to the time frames of the events that you are dealing with - no delay() calls or big loops. I might have written it more like:

void loop() {
    handle_foo();
    handle_bar();
    handle_baz();
    // etc...
}
void handle_foo() {
    if (event_foo_idle()) {
      return();   // return quickly if nothing to do
    }
    // code to actually do something, if there is something to do.
    //   (still "quickly"!)
    //   :
}
//etc

If a particular event would be long, you need to further divide it into sub-events, which is where concepts like "state machines" come in. You might do:

void loop() {
   // separate "event" states for serial input in the main event loop.
   handleSerial();  // input serial data (if any) until there is a full line
   handleLines();  // if a full line was received, process it.
//  :

Or you might do:

void loops() {
   handleInput();    // read and process Serial data
 :
}
void handleInput() {
    // separate states in the input event
  int c = Serial.read();
  if (c < 0)
    return;  // no data.
  if (c == '\r') {
    // process line
  } else {
    inputLine += (char) c;
  }
}

How about this: you may have heard that Node.js is single-threaded with an event loop. In the basic web server case, it opens up a port. The OS will notify when a connection occurs, thus creating an event. Parse enough of the request, and it might ask for a file. The process will ask the OS for that file, which takes some non-zero amount of time. Now the process is free to handle the next event. That event could be the OS saying, here's that file; or the start of another request, or something else. And if the file is large enough, you wouldn't send it all at once, but a piece (or chunk) at a time.

The basic idea is there's an event queue of things to do. Maybe it's empty: nothing to do. Something comes in, and do that thing, as long as that thing doesn't involve some indeterminate wait, or is likely/known to take "too long", since that will block everything else.

Because a task like downloading a big file can be interleaved with other tasks, the system is effectively multi-tasking. But as a single thread, there are no issues with concurrency and contention, locks and memory models, etc. There are other potential issues :slight_smile:

Several Node helper functions take other functions

  • setInterval: periodically add this function to the event queue; repeat unless cancelled
  • setTimeout: wait a while, then add this function to the event queue
  • setImmediate: add this function to the end of the event queue. If there are other things waiting, do those first, but get to this new function as soon as possible.

On Arduino, there's not much "built-in" that will trigger an event; you have to go "get it" yourself. To do something similar, here's a WorkItem class; with an array of them acting as a ring buffer, it can impersonate a simple event queue

typedef void(*work_t)(void);

constexpr size_t wqSize = 10;
size_t wqHead, wqLen;

class WorkItem {
  unsigned long previousMillis;
  unsigned long interval;
  bool repeat;
  work_t work;

  void requeue();
  bool doWork() {
    if (repeat) {
      if (millis() - previousMillis < interval) {
        requeue();
        return false;
      }
    }
    work();
    if (repeat) {
      unsigned long now = millis();
      do {
        previousMillis += interval;               // try to maintain cadence
      } while (now - previousMillis > interval);  // even when falling behind

      requeue();  // after work(), which may enqueue something else
    }
    return true;
  }

public:
  static bool enqueue(work_t work, unsigned long interval = 1000, bool repeat = true);
  static bool doNext();
} workQueue[wqSize];  // ring buffer

void WorkItem::requeue() {
  size_t tail = (wqHead + wqLen++) % wqSize;
  WorkItem *item = workQueue + tail;
  item->previousMillis = previousMillis;
  item->interval = interval;
  item->repeat = repeat;
  item->work = work;
}

bool WorkItem::enqueue(work_t work, unsigned long interval, bool repeat) {
  if (wqLen == wqSize) {
    return false;  // queue is full!
  }
  size_t tail = (wqHead + wqLen++) % wqSize;
  WorkItem *item = workQueue + tail;
  item->previousMillis = millis();
  item->interval = interval;
  item->repeat = repeat;
  item->work = work;
  return true;
}

bool WorkItem::doNext() {
  const size_t tail = wqHead + wqLen;
  size_t head = wqHead;  // index may go past wqSize
  while (head < tail) {
    WorkItem *item = workQueue + wqHead;
    wqHead = ++head % wqSize;  // always [0, wqSize)
    --wqLen;
    if (item->doWork()) {
      return true;
    }
  }
  return false;
}

The rest of the sketch has two functions that represent work-to-do. One of them uses a static variable, which is one way of having per-function persistent data; but it could easily reference a global variable.

void blink() {
  static int ledState = LOW;

  if (ledState == LOW) {
    ledState = HIGH;
  } else {
    ledState = LOW;
  }

  digitalWrite(LED_BUILTIN, ledState);
}

void tick() {
  Serial.println(millis());
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);

  WorkItem::enqueue(tick);
  WorkItem::enqueue(blink, 750);
}

void loop() {
  if (!WorkItem::doNext()) {
    // If there was "nothing to do", waiting a little bit until checking again
    // is "nicer" and may allow other tasks or an idle task, or use less power
    delay(1);
  } 
}

setup enqueues two repeating items. loop then scans the queue. The work functions can enqueue other items. Each repeating item adds itself back; so the queue has more "traffic" than a "pure" queue of "stuff to do". Another simplification: the queue size is fixed, more than big enough.

don't know why that approach would have such a name. I have a model RR signaling system composed of 14 files, 1800+ lines

void
loop ()
{
    msec = millis ();
    ledStatus ();

#if 1
    wifiMonitor ();
    sigCheck ();
#endif
    pcRead ();

    tglTest ();
}

seems that all programs can be describes as either IPO or simply PO.

IMO, this whole discussion is much ado about nothing. All techniques being discussed here are just variations on the same theme used over and over in Arduino (and most embedded) programming … check an “input”, compute a response, send the response, repeat. The “input” can be user data, a time interval, position of a switch, sensor data, state of a state machine, etc. Whether the above tasks are coded inline in the loop() function or in functions called by loop() is just an implementation detail.

1 Like