5. More on delays
1. What's the problem with vTaskDelay()
When called, the vTaskDelay() function turns the calling task into a blocked state for a given time duration. It is not an ideal approach for implementing a clean periodic task activation. It doesn't take into account the task self-execution time, that may include extra time if the task is preempted while executing. Therefore, if the task has variable execution time, the period between task activation is also variable.
Using the example code of the previous tutorial, the figure below is an oscilloscope capture of the actual LED pin toggling. One can see that every 10 toggles we have a 12ms period due to the message printing processed in Task_2 that suspends Task_1, adding a 2ms delay before its termination.
What happens is best illustrated in figure below:
Since Task_1 duration is very short is this case (just a LED toggling), a simple workaround to address this issue here could be to invert the priority hierarchy between Task_1 and Task_2. Having Task_1 at highest priority level prevents any preemption and guaranties a regular periodic activation. Let's try this...
xTaskCreate(vTask1, "Task_1", 256, NULL, 2, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 1, NULL);
The result is as expected. Task_1 give the semaphore but now, it is allowed to complete before Task_2 sends the console message. The result is a more uniform delay between wake-ups of Task_1.
Still, this is not a perfect solution because Task_1 has slightly (not visible here) varying execution time $\epsilon$ (depending whether it gives or not the semaphore), and even if it is small, its execution time comes as an offset (small constant error) to the desired 10ms activation period.
2. The solution : vTaskDelayUntil()
The correct way to implement a uniform delay between task wake up events is shown below:
/*
* Task1 toggles LED every 10ms
*/
void vTask1 (void *pvParameters)
{
portTickType xLastWakeTime;
uint16_t count;
count = 0;
// Initialize timing
xLastWakeTime = xTaskGetTickCount();
while(1)
{
BSP_LED_Toggle();
count++;
// Release semaphore every 10 count
if (count == 10)
{
xSemaphoreGive(xSem);
count = 0;
}
// Wait here for 10ms since last wakeup
vTaskDelayUntil (&xLastWakeTime, (10/portTICK_RATE_MS));
}
}
Note that writing:
vTaskDelayUntil (&xLastWakeTime, (10/portTICK_RATE_MS));
Is the correct way to specify waiting times in milliseconds instead of number of OS ticks. It also applies to traditional vTaskDelay() and timeouts.
Considering the current definitions hidden behind portTICK_RATE_MS:
#define portTICK_RATE_MS portTICK_PERIOD_MS
#define portTICK_PERIOD_MS ( ( TickType_t ) 1000 / configTICK_RATE_HZ )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
We can conclude that in our case portTICK_RATE_MS = 1 so writing:
vTaskDelayUntil (&xLastWakeTime, (10));
would have been the same. This is only true because we have set the tick period to 1ms. Using the recommended writing produces a code that is robust to changes in the FreeRTOSConfig.h header, and is therefore a better practice.
Going back to our matter, using vTaskDelayUntil() function instead of vTaskDelay(), provides a precise, uniform delay between Task_1 activations, no matter the priority hierarchy. See below:
With Task_1 having the highest priority level:
xTaskCreate(vTask1, "Task_1", 256, NULL, 2, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 1, NULL);
With Task_2 having the highest priority level, we have same result:
xTaskCreate(vTask1, "Task_1", 256, NULL, 1, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 2, NULL);
- Commit name "Uniform delay" - Push onto Gitlab |
3. Understanding vTaskDelayUntil()
3.1. Basic principles
First, we need to make sure that he concept of "OS ticks" is well understood.
When the scheduler is called for first time, a system-level hardware timer (i.e., a counter) is started. This timer is part of the ARM Cortex-M core and is called SysTick. It works pretty much like any timer you know. It is a counter, that counts... time!
With our clock settings, and the configuration in FreeRTOSConfig.h, the SysTick generates an interrupt every 1ms (1kHz).
#define configCPU_CLOCK_HZ ( SystemCoreClock )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
A SysTick interrupt is basically what we call a "tick". Every time a tick is produced a FreeRTOS internal variable representing the time is incremented. This process is done within an interrupt handler of high hardware priority. No matter what your application is doing, time counting is robust. What we call "OS ticks" is the value of that variable. It is therefore a measure of the time elapsed since the program started. Its unit depends on the tick period. In our case, it's in milliseconds.
Now, it is also important to understand how the vTaskDelayUntil() function works, because it can explain some odd situations. I'll give an example soon. For now let us discuss the figure below:
The first time the task function is executed, the xLastWakeTime variable is initialized with the current OS tick counter value. In the above example, let say xLastWakeTime = 2.
xLastWakeTime = xTaskGetTickCount();
Then, when the task is ready to be blocked, the vTaskDelayUntil() function actually computes the next meeting point in time by adding the requested delay (10) to the current xLastWakeTime value. So next meeting point in time is @tick=10+2=12. The xLastWakeTime is updated to that meeting point before the task enters the blocked state.
vTaskDelayUntil (&xLastWakeTime, (10/portTICK_RATE_MS));
When time reaches 12, the task switches from blocked to ready mode.
For all the subsequent wake-ups, there is no initialization anymore. The xLastWakeTime is simply updated by summing its current value with the desired delay, so that next meeting points will be 22, 32, 42, 52 and so on... Again, at these point in time, the task switches from blocked to ready mode.
Well, being ready doesn't mean that task will actually execute. We need the scheduler to allow task execution in order to update our meeting points. What if the task miss one (or several) meeting points because some other tasks of higher priorities have been very busy during a long period of time?
Lets try to setup such a scenario and see what happens!
3.2. A case of odd behavior
In the case study below, we implement two tasks with activation based on delays:
Task_1 has a precise periodic wake-up every seconds using vTaskDelayUntil(). It only reads and prints the current time (tick counter).
Task_2 has a higher priority level than Task_1. It prints a simple '.' in the console every 100ms using a vTaskDelay() suspension. Yet a little malicious blocking loop is there for the (evil) purpose of Task_1 CPU starvation. A long as the user push-button is kept pressed, Task_2 is caught in a never ending loop, and since Task_2 has higher priority, Task_1 never executes.
/*
* Task_1
*
* - Prints current time (OS tick) every 1s
* - Using precise period delay
*/
void vTask1 (void *pvParameters)
{
portTickType now, xLastWakeTime;
uint16_t n = 0;
// Initialize timing
xLastWakeTime = xTaskGetTickCount();
while(1)
{
n++;
now = xTaskGetTickCount();
my_printf("\r\n Task_1 #%2d @tick = %6d ", n, now);
// Wait here for 1s since last wake-up
vTaskDelayUntil (&xLastWakeTime, (1000/portTICK_RATE_MS));
}
}
/*
* Task_2
*
* - Toggles LED and prints '.' every 100ms
* - Internal (blocking) loop if button is pressed
*/
void vTask2 (void *pvParameters)
{
while(1)
{
// Loop here if button is pressed
while(BSP_PB_GetState()==1);
// Otherwise toggle LED and suspend task for 100ms
BSP_LED_Toggle();
my_printf(".");
vTaskDelay(100);
}
}
Note that we have no semaphore in this case study. So you can clean the code, and experiment.
Save , build and start a debug session. Run the application without pressing the button :
Everything looks very good indeed. Task_1 reports every meeting points perfectly, and in between we have precisely 10 dots printed by Task_2. That works well as expected.
Now, reset the program . Run the application again, and after few seconds, press the user button for about 3~4 seconds, and then release it. You'll get something similar to this:
Can you explain what you see? If not, see below...
Task_1 # 1 @tick = 0 .......... -> Task_1 OK. Next meeting point @1000
Task_1 # 2 @tick = 1000 .......... -> Task_1 OK. Next meeting point @2000
Task_1 # 3 @tick = 2000 .......... -> Task_1 OK. Next meeting point @3000
Task_1 # 4 @tick = 3000 ..... -> Task_1 OK. Next meeting point @4000. Button has been
pressed @3500 (5 dots). Task_2 is now trapped in the loop
@tick = 4000 -> Task_1 is now ready, but Task_2 still trapped
5000 -> So scheduler keeps executing Task_2
6000 -> again
7000 -> and again, until...
Task_1 # 5 @tick = 7886 -> Button has just been released! Task_1 resumes and prints
current tick (7886). Then compute next meeting point @5000.
But 5000 is in the past, so the vTaskDelayUntil() does not
stop execution and Task_1 loops immediately
Task_1 # 6 @tick = 7889 -> Task_1 immediate looping (+3ms since previous loop)
New meeting point @6000 which is still in the past,
so Task_1 loops again
Task_1 # 7 @tick = 7891 -> New meeting point @7000, Task_1 loops again
Task_1 # 8 @tick = 7894 . -> And again. But this time next meeting point @8000 which
is now 106ms in the future. So Task_1 finally enters the
blocked state after the vTaskDelayUntil() and we'got
time for only 1 dot from Task_2
Task_1 # 9 @tick = 8000 .......... -> And we've going back to normal. Next meeting point = @9000
Task_1 #10 @tick = 9000 .......... -> And so on...
Interesting, isn't it? And very instructive regarding the vTaskDelayUntil() behavior. The above case caused me headaches in the real life, with a radio transmitting task that was "running after the time" at startup because other tasks took time and CPU to complete their initialization. So believe me, that's not so unusual.
- Commit name "Odd delay behavior" - Push onto Gitlab |
Add new comment