How To Write Super Loops In Firmware
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:
- Read inputs.
- Process inputs/data.
- 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.
Basic Top-Level Code
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:
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.
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:
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.
Running the above code, we get the following output:
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).
Footnotes
-
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. ↩