Tutorial 8 : DELAY_SEC Macros Demystified - sudeshmoreyos/Morey_os-demo-1.0 GitHub Wiki
In this tutorial, we will explore the DELAY_SEC() and DELAY_SEC_PRECISE() macros in more detail.
1. Morey_os Pre-Built Macros
Macros in the C language are preprocessor directives that are expanded into code before compilation. By convention, all pre-built Morey_os macros are written in uppercase letters. Macros help make Morey_os code more readable and hide complex code blocks from the user. Some commonly used pre-built Morey_os macros are:
- TASK_CREATE
- TASK_AUTOSTART
- TASK_RUN
- BEGIN
- END
- DELAY_SEC
- DELAY_SEC_PRECISE
The TASK_CREATE macro is used to create a task.
TASK_AUTOSTART automatically starts a task at boot time.
TASK_RUN defines the runtime operation of a task.
BEGIN initializes the code inside a task after variable declarations, while
END marks the end of a task.
The DELAY_SEC macro provides a delay in seconds with a resolution of 0.001 second.
DELAY_SEC_PRECISE is similar to DELAY_SEC, with the only difference being that the delay generated is more precise and synchronized.
2. Task switching in Morey_os
Morey_os is based on cooperative threads. A thread here means a task in Morey_os that runs independently of other tasks. By creating multiple tasks, we can implement multi-tasking in our code. However, in practice, most embedded controllers have only a single CPU core, so they cannot execute multiple tasks truly in parallel. Instead, they switch between different tasks so quickly that it creates the illusion of multi-tasking.
This task switching can be implemented using either cooperative threads or pre-emptive threads. In a cooperative thread model, a task must voluntarily return control back to the OS. The OS itself cannot take control away from a running task. In contrast, a pre-emptive thread model allows the OS to take control back from a task without asking. Compared to cooperative threads, pre-emptive threads offer several advantages. However, they are more complex to implement and require a larger memory footprint. Therefore, Morey_os currently implements cooperative threads to keep the memory footprint as small as possible.
This multi-tasking functionality is provided by the Morey_os Kernel, which is based on a modified and trimmed-down version of the popular Contiki Operating System. This allows Morey_os to run even on controllers with as little as 8 KB Flash and 1 KB RAM. Let us now understand how task switching is implemented in Morey_os. Assume we have two independent tasks implementing simple LED blinking.
Task-1 :
TASK_RUN(led1)
{
BEGIN();
while(1)
{
Digital.write(pin13,HIGH);
DELAY_SEC(1);
Digital.write(pin13,LOW);
DELAY_SEC(1);
}
END();
}
Task-2 :
TASK_RUN(led2)
{
BEGIN();
while(1)
{
Digital.write(pin12,HIGH);
DELAY_SEC(0.1);
Digital.write(pin12,LOW);
DELAY_SEC(0.1);
}
END();
}
Assuming LEDs are connected to both pin12 and pin13 of an Arduino Uno, Task-1 blinks the LED connected to pin13 with a delay of 1 second, while Task-2 blinks the LED connected to pin12 with a delay of 0.1 second.
Since both LEDs blink simultaneously, it appears as though both tasks are running in parallel. In reality, Morey_os implements multi-tasking using the following sequence of operations:
- Controller/Board powers up.
- The setup() function runs.
- The OS initializes itself.
- Task-1 is initialized until the BEGIN() macro (typically variables are initialized).
- The BEGIN() macro returns control back to the OS.
- Task-2 is initialized until the BEGIN() macro (typically variables are initialized).
- The BEGIN() macro returns control back to the OS.
- Task-1 enters the while(1) loop and sets pin13 HIGH.
- The DELAY_SEC(1) macro schedules a delay of 1 second in the OS scheduler and returns control back to the OS.
- Task-2 enters the while(1) loop and sets pin12 HIGH.
- The DELAY_SEC(0.1) macro schedules a delay of 0.1 second in the OS scheduler and returns control back to the OS.
- Assuming all the above steps complete in negligible time, the next task for the OS is to wake up Task-2 after 0.1 second.
- Since the OS has no work to do until then, it goes to sleep.
- After 0.1 second, the OS wakes itself and then wakes up Task-2.
- Task-2 sets pin12 LOW.
- The DELAY_SEC(0.1) macro schedules another 0.1 second delay and returns control back to the OS.
- Since the OS has no work to do until the next scheduled event, it goes back to sleep.
- After 0.1 second, the OS wakes itself and then wakes up Task-2.
- Task-2 completes one iteration of the while(1) loop, returns to the beginning of the loop, and sets pin12 HIGH again.
- The DELAY_SEC(0.1) macro schedules another delay and returns control back to the OS.
- This process repeats until 1 second has elapsed.
- After 1 second, the OS must wake up both Task-1 and Task-2.
- Since Task-1 was created first, it has higher priority than Task-2, so Task-1 is executed first.
- Task-1 sets pin13 LOW.
- The DELAY_SEC(1) macro schedules another 1 second delay and returns control back to the OS.
- The OS wakes up Task-2.
- Task-2 sets pin12 HIGH.
- The DELAY_SEC(0.1) macro schedules another delay and returns control back to the OS.
- The process continues until 2 seconds have elapsed.
- After 2 seconds, the OS wakes up Task-1 again.
- Task-1 completes one iteration of the while(1) loop, returns to the beginning of the loop, and sets pin13 HIGH again.
- The DELAY_SEC(1) macro schedules another 1 second delay and returns control back to the OS.
- Similarly, the process continues indefinitely.
All of the above steps can be visualized graphically in the diagram shown below:
3. Importance of DELAY_SEC Macros
So, as explained above, the DELAY_SEC macro has two critical responsibilities:
- Schedule a delay for the desired time in seconds with a resolution of 0.001 second (1 millisecond).
- Return control back to the OS so that it can service other tasks as well.
Therefore, if we accidentally forget to add a delay inside an infinite loop, that task will get stuck in the loop and will never return control back to the OS. Effectively, the code will hang.
To avoid this situation, Morey_os uses a Watchdog Timer. Depending on the controller, the watchdog timer is typically configured for 1 or 2 seconds. This means that if the code remains stuck for more than 1 or 2 seconds, the watchdog timer will reset the controller and restart the application. So, if your Morey_os application keeps restarting repeatedly, it is usually an indication that you have forgotten to add a DELAY_SEC() or DELAY_SEC_PRECISE() macro inside an infinite loop.
Please note that DELAY_SEC macros can only be used inside TASK_RUN() and nowhere else. They cannot be used inside normal functions or sub-functions. It is also very important to note that when adding a DELAY_SEC() or DELAY_SEC_PRECISE() macro inside an infinite loop, it must execute at least once during every loop iteration. Consider the example below:
TASK_RUN(test)
{
static x = 0;
BEGIN();
while(1)
{
if(x == 1)
{
// Do Something
DELAY_SEC(1);
}
}
END();
}
As you can see, the DELAY_SEC() macro is placed inside an if statement. The execution of the if block depends on the condition x == 1. If this condition is not satisfied, the code will never enter the if block, and therefore DELAY_SEC() will not execute during that loop iteration. As a result, the task may get stuck inside the infinite loop, and eventually the watchdog timer will reset the controller. A simple solution is shown below:
TASK_RUN(test)
{
static x = 0;
BEGIN();
while(1)
{
if(x == 1)
{
// Do Something
DELAY_SEC(1);
}
DELAY_SEC(0.05);
}
END();
}
As shown above, we added DELAY_SEC(0.05), which provides a delay of 50 milliseconds during every loop iteration. Since this delay is guaranteed to execute every time the loop runs, the task will always return control back to the OS and the code will never get stuck inside the infinite loop. The DELAY_SEC macros support a minimum delay of 1 millisecond. However, the exact delay is not guaranteed every time. Therefore, it is recommended not to use delays smaller than 50 milliseconds when using DELAY_SEC() or DELAY_SEC_PRECISE() macros.
4. DELAY_SEC vs DELAY_SEC_PRECISE
In this section, we will explore the difference between the DELAY_SEC() and DELAY_SEC_PRECISE() macros.
Let us take the example of Task-1 discussed in the previous section. In this example, we are blinking an LED connected to pin13 with a delay of one second. The code uses the DELAY_SEC() macro. Let us now see what happens if we replace it with DELAY_SEC_PRECISE(), as illustrated in the diagram below:
As shown above, there are two code blocks. The left-side code block uses the DELAY_SEC() macro, while the right-side code block uses DELAY_SEC_PRECISE(). For the sake of explanation, let us assume that the Digital.write() function takes 1 millisecond to set an output HIGH or LOW. (In practice, it takes much less time.) The timestamps shown in the diagram indicate the time after each line of code has been executed. Let us first understand the timestamps for Task-1 using the DELAY_SEC() macro:
- Task begins at TIME = 0.000 sec
- It completes up to the BEGIN() macro at TIME = 0.000 sec
- Sets pin13 HIGH at TIME = 0.001 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC(1), the timestamp is TIME = 1.001 sec
- Sets pin13 LOW at TIME = 1.002 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC(1), the timestamp is TIME = 2.002 sec
- The while(1) loop ends and execution returns to the beginning of the loop.
- Sets pin13 HIGH again at TIME = 2.003 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC(1), the timestamp is TIME = 3.003 sec
- Sets pin13 LOW at TIME = 3.004 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC(1), the timestamp is TIME = 4.004 sec
- The process repeats.
As you can see, the execution time of Digital.write() gets added to the delay generated by DELAY_SEC(). In most applications this additional delay is very small, but over a long period of time it can accumulate and become noticeable.
Now let us understand the timestamps for Task-1 using the DELAY_SEC_PRECISE() macro:
- Task begins at TIME = 0.000 sec
- It completes up to the BEGIN() macro at TIME = 0.000 sec
- Sets pin13 HIGH at TIME = 0.001 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC_PRECISE(1), the timestamp is TIME = 1.000 sec
- Sets pin13 LOW at TIME = 1.001 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC_PRECISE(1), the timestamp is TIME = 2.000 sec
- The while(1) loop ends and execution returns to the beginning of the loop.
- Sets pin13 HIGH again at TIME = 2.001 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC_PRECISE(1), the timestamp is TIME = 3.000 sec
- Sets pin13 LOW at TIME = 3.001 sec
- Waits for 1 second. Therefore, at the end of DELAY_SEC_PRECISE(1), the timestamp is TIME = 4.000 sec
- The process repeats.
As you can see, the execution time of Digital.write() is not added to the delay generated by DELAY_SEC_PRECISE(). The DELAY_SEC_PRECISE() macro takes into account the execution time of other code and automatically adjusts itself to maintain the exact delay interval from the previous DELAY_SEC_PRECISE() call. This is the primary advantage of DELAY_SEC_PRECISE() over DELAY_SEC().