How To Write Super Loops In Firmware

Article by:
Date Published:
Last Modified:

Overview

A super loop (a.k.a. mega loop, main loop or infinite loop) is a popular way to architect firmware code for simple firmware applications. It involves looping through your code which loosely follows the following three steps:

  1. Read inputs.
  2. Process inputs/data.
  3. Set outputs.

A super loop does not run on “operating system” (e.g. RTOS), and therefore there is no scheduling or asynchronous code execution, everything is just run in a simple synchronous loop. The idea is to keep the average loop time short (in the microseconds to 10’s of milliseconds range) so that the system remains responsive to changes in inputs.

The advantages of a super loop:

  • Simple to understand.
  • Don’t have to worry about thread safety (memory contention, access to resources, dead-locks e.t.c).

The disadvantages to a super loop include:

  • Not as modular as an application running on an OS, and sometimes you run into complexity issues when using a super loop for large firmware projects.
  • No ability to prioritize certain tasks (except for using interrupts). This can lead to responsivity issues for large or complex projects.
TIP

The basic idea of a super loop could still be used for a single thread running on an RTOS, so it that sense, it could run on an operating system.

Basic Top-Level Code

1
2
3
4
5
6
7
8
int main(void) {
    init();
    while(1) { // You might see for(;;) used here also
        read_inputs();
        do_calculations();
        set_outputs();
    }
}

For larger firmware projects using the super loop architecture, you will want to break the code into modular .c/.h file combinations which have a specific purpose, for example one module for handling an IMU sensor, one module for controlling a motor and one module for printing serial debug data:

1
2
3
4
5
6
7
8
int main(void) {
    init();
    while(1) { // You might see for(;;) used here also
        imu_loop();
        motor_loop();
        serial_loop();
    }
}

The System Tick

The idea is to call a function to get the system tick (which represents the current time, usually measured as an integer number of microseconds or milliseconds since the microcontroller started) once per iteration of your super loop.

1
uint32_t curr_tick_us = get_system_tick_us();

It is important to note that you should only read the system tick once per iteration of the loop. This value should be used for all calculations involving the current time for the rest of the loop, even if the current time has changed. This prevents a whole range of tricky-to-debug issues when things start happening in a different order than what was expected.

Dealing With Tick Roll-over (Overflow)

It is important to make your firmware immune to the effect of the system tick overflowing (rolling over) and going back to 0 (just ask the Boeing 787 Dreamliner about this1).

The great news is, if you are using an unsigned integer data type to store this number, thanks to the maths, much of the code you write that is based on the difference between the current time and the last time an event happened will automatically run correctly even when the tick overflows!

Let’s pretend we need to print something every 100ms, and we’re using the ridiculously small storage of a uint8_t (to exaggerate the problem) to store the current time and the last time we printed something. Our code to detect when we need to print is:

1
2
3
4
if ((uint8_t)(cur_tick_ms - last_print_time_ms) >= 100) {
    printf("Print time! curr_tick_ms = %i\n", cur_tick_ms);
    last_print_time_ms = cur_tick_ms;
}

last_print_time_ms and cur_tick_ms start at 0. cur_tick_ms starts increasing everytime when iterate around our super loop. When cur_time_ms gets to 100, the if statement becomes true and we print. last_print_time_ms is updated to 100. The same thing happens when cur_tick_ms gets to 200. However, since cur_tick_ms is a uint8_t, it overflows after 255 and wraps back around to 0. What’s great is the math still works out. Because the difference cur_tick_ms - last_print_time_ms is also a uint8_t, The duration 0 - 200 is not -200 but 56. cur_tick_ms continues to increment until is reaches 44, in where 44-200 gives us 100, and we print again! And the cycle continues.

Full working example showing this phenomenon is below. Run it online at https://replit.com/@gbmhunter/sys-tick-overflow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdint.h>

uint8_t get_system_tick_ms() {
  static uint8_t cur_tick_ms = 0;
  cur_tick_ms += 10;
  return cur_tick_ms;
}

int main(void) {
  printf("Firmware starting...\n");

  uint8_t last_print_time_ms = 0;
  const uint8_t PRINT_PERIOD_MS = 100;
  uint32_t loop_count = 0;

  while(1) {
    uint8_t cur_tick_ms = get_system_tick_ms();

    if ((uint8_t)(cur_tick_ms - last_print_time_ms) >= PRINT_PERIOD_MS) {
      printf("Print time! curr_tick_ms = %i\n", cur_tick_ms);
      last_print_time_ms = cur_tick_ms;
    }

    // This bit wouldn't be in a firmware application, just to bail the example
    // code after it's done a few loops
    loop_count += 1;
    if (loop_count == 100) {
      break;
    }
  }
  return 0;
}

Running the above code, we get the following output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Firmware starting...
Print time! curr_tick_ms = 100
Print time! curr_tick_ms = 200
Print time! curr_tick_ms = 44
Print time! curr_tick_ms = 144
Print time! curr_tick_ms = 244
Print time! curr_tick_ms = 88
Print time! curr_tick_ms = 188
Print time! curr_tick_ms = 32
Print time! curr_tick_ms = 132
Print time! curr_tick_ms = 232

Keeping Track Of Loop Time

A really useful metric to keep track of in a super loop architecture is the loop time. The loop time is the time it takes to perform one complete iteration of the super loop. The loop time will always vary (technically, this is called jitter), due to different things occurring on every loop. The shortest loop times will be when no new events occur, the longest will be when many events occur on the same loop which trigger long-running actions. It is generally a good idea to keep the maximum loop time short, typically in the microsecond to 10’s of milliseconds range, such that the embedded system remains responsive to the outside world and doesn’t appear to “hang”.

It’s easy to compute the loop time from the “tick” from the last iteration of the loop, and the “tick” from the current iteration. Check that this does not exceed a reasonable number (reasonable will entirely depend on your exact application, printing out the loop time might clue you into a suitable limit), and handle the “error” as appropriate (in the example below, a warning is printed to the default debug port).

1
2
3
4
5
6
uint32_t prev_tick_ms = cur_tick_ms;
uint32_t cur_tick_ms = get_system_tick_ms();
uint32_t loop_time_ms = cur_tick_ms - prev_tick_ms;
if (loop_time_ms > 20) {
  printf("WARNING: Loop time greater than 20ms!");
}

References


  1. E. Alvarez (2015, May 1). To keep a Boeing Dreamliner flying, reboot once every 248 days. Engadget. Retrieved 2021-12-06, from https://www.engadget.com/2015-05-01-boeing-787-dreamliner-software-bug.html↩︎


Authors

Geoffrey Hunter

Dude making stuff.

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License .

Tags

comments powered by Disqus