HOW TO WRITE SUPER LOOPS IN FIRMWARE

How To Write Super Loops In Firmware

Article by:
Date Published:
Last Modified:

1. 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.

2. Basic Top-Level Code

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:

int main(void) {
    init();
    while(1) { // You might see for(;;) used here also
        imu_loop();
        motor_loop();
        serial_loop();
    }
}

3. 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.

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.

3.1. 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 this[1]).

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:

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.

#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:

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

References


Authors

Twitter

Related Content:

Tags:

comments powered by Disqus