How to implement several time bases in Arduino’s sketches
(Lee este artículo en español aquí.)
In our programs it is common to have several tasks or peripherals executing in different time rates: an LED that flashes every 500ms, a display that refreshes every 5ms, a keyboard that must be scanned every 100ms, etc.
Arduino, along with its horrible super-loop, imposes some restrictions on us when it comes to setting different time periods:
- The hardware timer that makes the
delay()
andmillis()
functions work is tricky to access. In normal development environments we use one of the several microcontroller hardware timers for our time bases. In Arduino it is difficult to achieve it without intervening its source code (I already did it). - The
loop()
function is called continuously, so we must insert a mechanism in it that allows us to carry out time-based programming.
In this article I want to show you what I did to implement different time bases inside the super-loop (I hate it!). (As for hacking the hardware timer, I’ve already done that, and this solution evolved in the KleOS project, but that will be the subject of another article.)
Keep reading to see how you can implement two or more different time bases in your Arduino projects. It’s super easy!
System Tick
The first thing to understand is that the loop()
function is continuously called by the main()
function continuously inside an infinite loop:
// Arduino's main() function int main(void) { init(); initVariant(); #if defined(USBCON) USBDevice.attach(); #endif setup(); for (;;) { loop(); if (serialEventRun) serialEventRun(); } return 0; }
To begin with we need to put a mechanism that acts as the main time base inside the loop()
function. In embedded systems, we call this time base “the system tick” and it is the one that will drive the rest of the program.
This software tick can be implemented either with the delay()
function or with the millis()
function. The former uses relative time and will therefore introduce offsets, while the latter uses absolute time and will not introduce offsets. For this reason we will use millis()
:
#define SYSTEM_TICK 5 // in milliseconds #define MS_TO_TICKS( x ) ( (x) / ( SYSTEM_TICK ) ) void loop() { static unsigned long last_time = 0; unsigned long now = millis(); if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return; last_time = now; // code that needs to execute on every tick... }
The Arduino loop will continue to execute continuously, as expected, but until the next tick arrives, the function will terminate (i.e. it does nothing, as in normal systems in which their system tick is based on a hardware timer).
From this small but very important code you will notice several things:
- I have defined a constant
SYSTEM_TICK
to simplify things. In this example the system tick is set to 5 milliseconds. - I have defined a macro that is responsible for converting time in milliseconds to time in ticks:
- The tick could change period and the last thing we want is to search and modify in the whole program those places where we would have directly used ticks.
- It is easier for us to understand milliseconds than ticks.
- The
last_time
variable is the one that takes the absolute time of our tick and is marked asstatic
so that it saves its value between calls (personally I do NOT use global variables if they are not strictly necessary, and this is one of those cases).
For time bases to work you’ll have to avoid using blocking functions inside the loop()
function (the delay()
function is a blocking function: it hoards the CPU until it’s done). You will need to modify your code to use Event Driven Programming and this will involve the use of state machines.
Before implementing the different time bases I must mention that we can use this tick directly if we have one or more tasks whose period coincides with it. I chose 5ms because it is the perfect time to refresh a 4-digit, 7-segment display (here you can see the project), and with this value it is easy to obtain many others. (In many embedded systems it is common to set the tick period to 1ms, I do it very often, but this ocassion I will keep it as 5ms.)
For example, let’s say we want to use the tick to refresh the screen (omitting the details):
void loop() { static unsigned long last_time = 0; unsigned long now = millis(); if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return; last_time = now; display.update() // refreshes an LED display every 5ms // more code ... }
Writing the different time bases
Now that we’re equipped with the main tick we can write our other time bases. Let’s start with a 100ms one.
For each time base we are going to need a tick counter variable. Faithful to my principles I will set it as `static (I repeat: I don’t use global variables if static ones can do the job). I recommend using a name that evokes the period you will be counting, for example base_time_100ms (100ms_base_time sounds better, but we can’t have variables that start with numbers):
void loop() { static unsigned long last_time = 0; unsigned long now = millis(); if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return; last_time = now; static uint16_t base_time_100ms = MS_TO_TICKS( 100 ); --base_time_100ms; if( base_time_100ms == 0 ) { base_time_100ms = MS_TO_TICKS( 100 ); // code that must be executed every 100ms } // ... }
How does it work?
It is very simple. The base_time_100ms variable is first loaded with an initial value (20 for this example, which is obtained by dividing 100 by 5). At each tick (e.g., 5ms) this variable is decremented by one, and when it reaches 0 the corresponding code is executed. Every time the variable reaches 0 it must be reloaded to repeat the process. And that’s all! (almost, we still need to talk about the code that will be executed).
For other time bases it is enough to replicate the previous procedure. Let’s add another 500ms base time to complete the exercise:
static uint16_t base_time_100ms = MS_TO_TICKS( 100 ); --base_time_100ms; if( base_time_100ms == 0 ) { base_time_100ms = MS_TO_TICKS( 100 ); // code that must be executed every 100ms } static uint16_t base_time_500ms = MS_TO_TICKS( 500 ); --base_time_500ms; if( base_time_500ms == 0 ) { base_time_500ms = MS_TO_TICKS( 500 ); // code that must be executed every 500ms }
NOTE: When using standard size integers, like uint16_t
, don’t forget to include the header file <stdint.h>
, as shown in the complete example below.
Event Driven Programming and State Machines
And that’s it?
As for the time bases, yes. The next part, which has to do with the code to be executed, is more complicated. Did you notice that in a previous code I wrote the following statement:
display.update();
The code inside the .update()
method is a state machine. When you use time bases like the ones we’ve developed, your code can’t hoard the CPU (e.g., to wait for an event without doing anything, like calling the delay()
function or waiting indefinitely for a character from the serial port). The secret is to execute an action (read, ask, do, etc) and exit immediately.
Let’s see an example. An LED that will change its state indefinitely every 500ms, without using the delay()
function! I show you only the relevant code (the complete example is in its way):
static uint16_t base_time_500ms = MS_TO_TICKS( 500 ); --base_time_500ms; if( base_time_500ms == 0 ) { base_time_500ms = MS_TO_TICKS( 500 ); static bool led_state = false; if( led_state == false ) { led_state = true; digitalWrite( 13, LOW ); } else { led_state = false; digitalWrite( 13, HIGH ); } }
Lines 7 through 17 form a simple state machine. The machine may have more states and be more complex depending on what you are programming.
In any case, the 10 and 15 instructions are the ones that indicate the next state the machine will enter, and the 11 and 16 instructions are the actions carried out in each state.
NOTE: The above code to blink the LED connected to pin 13 could be expressed in a single line:
digitalWrite( 13, (led_state = !led_state) ? LOW : HIGH );
However, the update of the led_state
variable is not appreciated correctly, nor the action to be executed. Also, this code only works for two states, and normally we will have more than 2 of them in our machines (in this Automatic water reservoir system you can see a more complex state machine (Soon in english.)).
Now I want to show you the same code but using a switch
, since when we have many states a boolean variable no longer works for us, and it is also more convenient than using if’s and expresses better our intentions (even more, it is the most common way of writing a state machine):
static int led_state = 0; switch( led_state ) { case 0: led_state = 1; digitalWrite( 13, LOW ); break; case 1: led_state = 0; digitalWrite( 13, HIGH ); break; }
Note that the variable led_state, which is the one that controls the machine’s states, is now of type int
.
More examples
If you want to see more complex and complete examples of different timings and state machines in real code, I invite you to see the articles 4-digit 7-segment LED module and so mentioned Automatic water reservoir system project. Review them as soon as you can, meanwhile I’ll show you the part that is more interesting for us towards today’s topic (from the water reservoir project, and in which the system tick has been stablished as 100ms. You cand download the complete code here):
static unsigned long last_time = 0; auto now = millis(); if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return; last_time = now; // This code will be executed every 100ms: keypad.state_machine(); lcd_backlight.state_machine(); led13.state_machine(); buzzer.state_machine(); static bool error = false; static bool settings = false; if( not error and settings == false ) { uint8_t minutes, seconds; if( state == eStates::WAITING ) { minutes = downTimer_set.minutes; seconds = downTimer_set.seconds; } else // process is running: { timer.get( &minutes, &seconds ); } print_time( 0, 1, minutes, seconds ); } // This code will be executed every 1000ms: --seconds_tick_base; if( seconds_tick_base == 0 ) { seconds_tick_base = MS_TO_TICKS( 1000 ); timer.state_machine(); timer_after.state_machine(); }
Pay attention to all the state machines I’ve implemented; each event or peripheral that is time-triggered requires its own state machine. In the source code associated with that project you can see how the machines are implemented for each type of time-event.
In fact, it was from this project that the need to implement different time bases arose, and then I considered writing precisely on this topic.
Personally, I would have preferred to use a real-time operating system in that project (more on this below), but I wanted it to be available to as many electronics enthusiasts as possible, so I decided to do it with Arduino sketches.
Complete code
I’ll show next the complete code that I’ve used throughout the article. It’s too simple to keep it in a repository:
#include <stdint.h> #define SYSTEM_TICK 5 // in milliseconds #define MS_TO_TICKS( x ) ( (x) / ( SYSTEM_TICK ) ) void setup() { pinMode( 13, OUTPUT ); } void loop() { static unsigned long last_time = 0; unsigned long now = millis(); if( not ( now - last_time >= ( SYSTEM_TICK ) ) ) return; last_time = now; static uint16_t base_time_100ms = MS_TO_TICKS( 100 ); --base_time_100ms; if( base_time_100ms == 0 ) { base_time_100ms = MS_TO_TICKS( 100 ); // code that will be executed every 100ms } static uint16_t base_time_500ms = MS_TO_TICKS( 500 ); --base_time_500ms; if( base_time_500ms == 0 ) { base_time_500ms = MS_TO_TICKS( 500 ); static bool led_state = false; if( led_state == false ) { led_state = true; digitalWrite( 13, LOW ); } else { led_state = false; digitalWrite( 13, HIGH ); } } }
What’s next?
This solution works and scales well up to a certain degree; however, if your application requires many time bases, or complex timing, or a lot of code to be executed within each base, then it is best to migrate to more elaborate solutions, such as real-time operating systems. For this case I introduce you to my KleOS project and my free course Arduino in real time with FreeRTOS, chapter 10.
The real time operating system, FreeRTOS, allows you to create complex timings directly using tasks; however, it includes a software timer functionality that let’s us create the same complex timers more easily.
In any case, the topic that I’ve presented to you today might serve as an introduction to the Event-driven development and more complicated timings
Suscribe if you like this article!
Looking for professional KiCAD PCB designer?
I’m on Fiverr. Designs from 30USD!
Have you heard about my free course (in spanish by now) on Arduino in real time for the UNO and Due Arduino boards?
- Esta clase poco conocida de Arduino le dará vida a tus displays de texto y te olvidarás de tener que escribir funciones de conversión - septiembre 20, 2024
- Printable: The class you didn’t know existed in Arduino and that you won’t be able to stop using - septiembre 1, 2024
- Printable: La clase que no sabías que existía en Arduino y que no podrás dejar de usar - agosto 3, 2024
2 COMENTARIOS