Skip to main content

3. Using delays


In this tutorial, we will illustrate the need for an OS-aware waiting mechanism in the tasks. Let-us compare the effect of using of the vTaskDelay() OS API function to a stupid delay implementation using the usual loop-counting idea.

Edit vTask2() function as follows:

/*
 *	Task1 toggles LED every 30ms
 */
void vTask1 (void *pvParameters)
{
	while(1)
	{
		BSP_LED_Toggle();
		vTaskDelay(30);
	}
}

/*
 *	Task2 sends a message to console every 100ms
 */
void vTask2 (void *pvParameters)
{
	uint16_t 	count;
	uint32_t	i;

	count = 0;

	while(1)
	{
		my_printf("Hello %2d from task2\r\n", count);
		count++;

		// Let us try a stupid delay
		//vTaskDelay(100);
		for(i=0; i<1000000; i++);	// <-- Replace vTaskDelay() by a stupid loop
	}
}

 

Save all saveall_edit, build build_exec, start the debugger and run resume_co the application. You'll get the console message:

image_002.png

 

but no blinking LED. Why is that?

 

Only you know that the counting loop is there for a delay. The OS scheduler just considers this is something the task has to do. Since the never ending loop of Task_2 never pauses, and because Task_2 is of highest priority, there is no chance for Task_1 to ever execute.

Below is the trace properties after several seconds of recording. Only 20 events have been captured, for a total duration of 75µs. How come?

image_000.png

 

The time line provides an explanation: startup events are first recorded, as expected. Then Task_2 is started. After this point in time, no kernel event never occurs. This is because Task_2 never stops. There's nothing left to record, and there is even no evidence (in the recorder) of a Task_1 waiting somewhere...

image_001.png

 

Note that above screenshot displays memory management events (such as malloc()). This is achieved by this setting in trcConfig.h:

/**
 * @def TRC_CFG_INCLUDE_MEMMANG_EVENTS
 * @brief Macro which should be defined as either zero (0) or one (1).
 *
 * This controls if malloc and free calls should be traced. Set this to zero (0)
 * to exclude malloc/free calls, or one (1) to include such events in the trace.
 *
 * Default value is 1.
 */
#define TRC_CFG_INCLUDE_MEMMANG_EVENTS 1

 

What if we apply the same idea to vTask1() instead?

/*
 *	Task1 toggles LED every 30ms
 */
void vTask1 (void *pvParameters)
{
	uint32_t	i;

	while(1)
	{
		BSP_LED_Toggle();

		// Let us try the stupid delay here, now.
		//vTaskDelay(30);
		for(i=0; i<100000; i++);	// <-- Stupid loop now here
	}
}

/*
 *	Task2 sends a message to console every 100ms
 */
void vTask2 (void *pvParameters)
{
	uint16_t 	count;

	count = 0;

	while(1)
	{
		my_printf("Hello %2d from task2\r\n", count);
		count++;
		vTaskDelay(100);
	}
}

 

This time, the application seems to work fine, both processes are executed as expected. Nevertheless, looking at the execution trace, on can see that Task_1 takes more than 90% of the CPU time... for blinking a LED, that's insane! And it only works because Task_1 has the lowest priority level. Any task with a priority level lower than the one of Task_1 would never execute. 

Here, every time Task_2 becomes ready to execute, the scheduled preempt Task_1, and allows for Task_2 execution because it has a higher priority level. So, OK, it works... but toggling a LED requires very few CPU cycles, so there is no reason to keep the CPU busy in such a idiot task. Running a waiting loop within a task must not be done, because there is no difference from the scheduler point of view between such delay, and something important the task must perform.

 

image_003.png

 

This little experiment clearly shows that you need, somewhere in every task loop, an OS aware waiting mechanism, otherwise there is no CPU time left to execute other tasks. When the task only need to wait for a known amount of time, vTaskDelay(), or vTaskDelayUntil() must be used, instead of any other non-OS delay functions. That's because those delays clearly inform the scheduler that for a given amount of time, there is no need for CPU here, and that the scheduler can take care of the other tasks, whatever their priority level.

When the task needs to wait an unknown time, for something to happen, say an event coming from other tasks, or from an external input, then other kernel objects are used. This includes semaphores, message queues, events, timers...

gitlab- commit Commit name "Wrong delay"
- push Push onto Gitlab

Stay tuned!