freeRTOS series of tutorials [Chapter 3] task management

Need to get Better reading experience My classmate, please visit I set it up Site view , address: http://rtos.100ask.net/

Series of tutorials

There will be many chapters in the series of this tutorial. For the convenience of students, click here to view the contents of the article Directory list , directory list page address: https://blog.csdn.net/thisway_diy/article/details/121399484

summary

In this chapter, the following will be covered:

  • How does FreeRTOS allocate CPU time to each task
  • How to select a task to run
  • How does task priority work
  • What are the status of the task
  • How to implement the task
  • How to use task parameters
  • How to modify task priority
  • How to delete a task
  • How to achieve periodic tasks
  • How to use idle tasks

3.1 basic concepts

For the whole MCU program, we call it application.

When using FreeRTOS, we can create multiple tasks in the application. Some documents also call tasks threads.

Take daily life as an example. For example, the mother has to do two things at the same time:

  • Hello: This is a task
  • Message back: This is another task

This can introduce many concepts:

  • Task state:
    • Currently feeding, it is in running status; Another task of "returning information" is the "not running" status
    • The "not running" status can also be subdivided:
      • Ready: ready to run
      • Blocked: blocked, stuck. Mother is waiting for her colleagues to return information
      • suspended: hang up. My colleague has too much nonsense. I don't care about him
  • Priority
    • I give consideration to both work and life: feeding and returning information have the same priority, and I take turns to do it
    • I'm busy: I still have free tasks. Take a break
    • The kitchen is on fire. Don't say anything. Put out the fire first: higher priority
  • Stack
    • When feeding the children, I should remember that the last one was fed with rice, and this one was fed with green vegetables
    • When I return a message, I want to remember what I just talked about
    • Do different tasks, these details are different
    • For people, of course, it's in their mind
    • For the program, it is recorded in the stack
    • Each task has its own stack
  • event driven
    • The child eats too slowly: take a rest first, wait until he swallows it, wait until he reminds me, and then feed the next bite
  • Co operative scheduling
    • You're sending a message back to your colleagues
      • The colleague said: Well, you can go and feed the child before you leave
      • If your colleagues don't let you go, you can't go even if the child cries
    • You're not easy. You can feed the children
      • The child said: Well, mom, you can deal with your work before you leave
      • If your child doesn't let you go, you can't go even if your colleagues send messages

This involves many concepts, which are analyzed in detail in subsequent chapters.

3.2 task creation and deletion

3.2.1 what is a task

In FreeRTOS, a task is a function. The prototype is as follows:

void ATaskFunction( void *pvParameters );

Note that:

  • This function cannot return
  • The same function can be used to create multiple tasks; In other words, multiple tasks can run the same function
  • Inside the function, try to use local variables:
    • Each task has its own stack
    • When each task runs this function
      • The local variables of task A are placed in the stack of task A and the local variables of Task B are placed in the stack of task B
      • Local variables of different tasks have their own copies
    • If the function uses global variables and static variables
      • Only one copy: multiple tasks use the same copy
      • Conflict prevention (to be discussed later)

Here is an example:

void ATaskFunction( void *pvParameters )
{
	/* For different tasks, local variables are placed in the task stack and have their own copies */
	int32_t lVariableExample = 0;
	
    /* The task function is usually implemented as an infinite loop */
	for( ;; )
	{
		/* Task code */
	}

    /* If the program exits from the loop, be sure to use vtask delete to delete itself
     * NULL Indicates that you are deleting
     */
	vTaskDelete( NULL );
    
    /* The program will not be executed here. If it is executed here, there will be an error */
}

3.2.2 create task

The functions used to create the task are as follows:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // Function pointer
                        const char * const pcName, // Task name
                        const configSTACK_DEPTH_TYPE usStackDepth, // Stack size, in word,10 means 40 bytes
                        void * const pvParameters, // Parameters passed in when calling the task function
                        UBaseType_t uxPriority,    // priority
                        TaskHandle_t * const pxCreatedTask ); // Task handle, which can be used to operate the task later

Parameter Description:

parameterdescribe
pvTaskCodeFunction pointer, you can simply think that the task is a C function.
It's a little special: never exit, or call "vtask delete (null)" when you exit
pcNameThe name of the task. FreeRTOS does not use it internally, but only for debugging.
Length: configMAX_TASK_NAME_LEN
usStackDepthEach task has its own stack, and the stack size is specified here.
The unit is word. For example, if 100 is passed in, it means that the stack size is 100 word s, that is, 400 bytes.
The maximum value is uint16_ Maximum value of T.
How to determine the stack size is not easy. It is often estimated.
The exact way is to look at the reverse exchange code.
pvParameterspvTaskCode(pvParameters) is used when calling the pvTaskCode function pointer
uxPriorityPriority range: 0~(configMAX_PRIORITIES – 1)
The smaller the value, the lower the priority,
If too large a value is passed in, xTaskCreate will adjust it to (configMAX_PRIORITIES – 1)
pxCreatedTaskUsed to save the output result of xTaskCreate: task handle.
In the future, if you want to operate this task, such as modifying its priority, you need this handle.
If you don't want to use this handle, you can pass in NULL.
Return valueSuccess: pdPASS;
Failed: errcould_ NOT_ ALLOCATE_ REQUIRED_ Memory (the only reason for failure is insufficient memory)
Note: the document says that the return value of failure is pdFAIL, which is wrong.
pdFAIL is 0, errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY is - 1.

3.2.3 example 1: create task

Code: FreeRTOS_01_create_task

Use 2 functions to create 2 tasks respectively.

Code for task 1:

void vTask1( void *pvParameters )
{
	const char *pcTaskName = "T1 run\r\n";
	volatile uint32_t ul; /* volatile To avoid being optimized */
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print information for task 1 */
		printf( pcTaskName );
		
		/* Delay for a while (relatively simple and rough) */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		}
	}
}

Code for task 2:

void vTask2( void *pvParameters )
{
	const char *pcTaskName = "T2 run\r\n";
	volatile uint32_t ul; /* volatile To avoid being optimized */
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print information for task 1 */
		printf( pcTaskName );
		
		/* Delay for a while (relatively simple and rough) */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		}
	}
}

main function:

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

The operation results are as follows:

be careful:

  • task 2 runs first!
  • Only by analyzing the code of xtask create can we know the reason: tasks with higher priority or created later run first.

Task diagram:

  • At t1: Task 2 enters the running state and runs until t2
  • At t2: Task1 enters the running state and runs until t3; At t3, task 2 re enters the running state

3.2.4 example 2: using task parameters

Code: FreeRTOS_02_create_task_use_params

As we said, multiple tasks can use the same function. How do you reflect their differences?

  • Stack different
  • Different parameters can be passed in when creating a task

We create two tasks and use the same function. The code is as follows:

void vTaskFunction( void *pvParameters )
{
	const char *pcTaskText = pvParameters;
	volatile uint32_t ul; /* volatile To avoid being optimized */
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print task information */
		printf(pcTaskText);
		
		/* Delay for a while (relatively simple and rough) */
		for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
		{
		}
	}
}

pcTaskText in the above code comes from pvParameters. Where do pvParameters come from? Passed in when the task was created.

The code is as follows:

  • When using xTaskCreate to create two tasks, the fourth parameter is pvParameters
  • pvParameters are different for different tasks
static const char *pcTextForTask1 = "T1 run\r\n";
static const char *pcTextForTask2 = "T2 run\r\n";

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTaskFunction, "Task 1", 1000, (void *)pcTextForTask1, 1, NULL);
	xTaskCreate(vTaskFunction, "Task 2", 1000, (void *)pcTextForTask2, 1, NULL);

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

3.2.5 deletion of tasks

The functions used to delete a task are as follows:

void vTaskDelete( TaskHandle_t xTaskToDelete );

Parameter Description:

parameterdescribe
pvTaskCodeTask handle. You can get a handle when creating a task with xTaskCreate.
You can also pass in NULL, which means deleting yourself.

How do I delete a task? A bad example:

  • Suicide: vtask delete (null)
  • Killed: other tasks execute vtask delete (pvTaskCode), which is their own handle
  • Kill: execute vtask delete (pvTaskCode), which is the handle of other tasks

3.2.6 example 3: delete task

Code: FreeRTOS_03_delete_task

The code in this section will cover the knowledge of priority. You can only look at the usage of vtask delete and ignore the explanation of priority.

We need to do these things:

  • Create task 1: in the big cycle of task 1, create task 2, and then sleep for a period of time
  • Task 2: print a sentence and delete yourself

The code of task 1 is as follows:

void vTask1( void *pvParameters )
{
	const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );		
	BaseType_t ret;
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print task information */
		printf("Task1 is running\r\n");
		
		ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
		if (ret != pdPASS)
			printf("Create Task2 Failed\r\n");
		
		// If you do not sleep, the Idle task cannot be executed
		// The Idel task cleans up the memory used by task 2
		// If you do not sleep, the Idle task cannot be executed, and finally the memory runs out
		vTaskDelay( xDelay100ms );
	}

The code for task 2 is as follows:

void vTask2( void *pvParameters )
{	
	/* Print task information */
	printf("Task2 is running and about to delete itself\r\n");

	// You can directly pass in the parameter NULL. This is just to demonstrate the function usage
	vTaskDelete(xTask2Handle);
}

The main function code is as follows:

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

The operation results are as follows:

Task diagram:

  • Create task 1 in main function with priority of 1. When task 1 runs, it creates task 2, whose priority is 2.
  • Task 2 has the highest priority and is executed immediately.
  • Task 2 deletes itself after printing a sentence.
  • After task 2 is deleted, task 1 has the highest priority. It is task 1's turn to continue running. It calls vtask delay() to enter the Block state
  • During task 1 Block, the Idle task is executed: it releases the memory (TCB, stack) of task 2
  • When the time expires, task 1 becomes the highest priority task and continues to be executed.
  • So cycle.

In the function of task 1, if vtask delay is not called, the Idle task is used for no chance to execute, and it cannot release the memory allocated by task 2.

Task 1 is constantly creating tasks and consuming memory. Finally, memory is exhausted and new tasks can no longer be created.

The phenomena are as follows:

In the code of task 1, you should pay attention to the return value of xTaskCreate.

  • Many manuals say that when it fails, the return value is pdFAIL, and this macro is 0
  • In fact, the return value of failure is errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY, this macro is - 1
  • To avoid confusion, we compare the return value with pdPASS. This macro is 1

3.3 task priority and Tick

3.3.1 task priority

In the previous example, we experienced the use of priority: high priority tasks run first.

The value range of priority is: 0~(configMAX_PRIORITIES – 1). The higher the value, the higher the priority.

The scheduler of FreeRTOS can use two methods to quickly find the tasks with the highest priority and can be run. When using different methods, configmax_ The values of priorities are different.

  • General method
    Using C function implementation, it is the same code for all architectures. To configmax_ There is no limit on the value of priorities. But configmax_ The value of priorities should be as small as possible, because the larger the value, the more memory and time will be wasted.
    configUSE_ PORT_ OPTIMISED_ TASK_ This method is used when the selection is defined as 0 or undefined.
  • Architecture related optimization methods
    Architecture related assembly instructions can quickly find the highest bit of 1 from a 32-bit number. Using these instructions, you can quickly find the tasks with the highest priority and can be run.
    When using this method, configmax_ The value of priorities cannot exceed 32.
    configUSE_ PORT_ OPTIMISED_ TASK_ This method is used when selection is defined as 1.

Before learning the scheduling method, you just need to know briefly:

  • FreeRTOS ensures that the highest priority, runnable tasks can be executed immediately
  • For runnable tasks with the same priority, take turns

There is no need to remember, as in our example:

  • The kitchen is on fire. Of course, priority should be given to putting out the fire
  • Feeding and answering messages are equally important. Take turns

3.3.2 Tick

For tasks of the same priority, they are executed "in turn". How do you take turns? You do it for a while, I do it for a while.

What is the definition of "one minute"?

People have a heartbeat, and the heartbeat interval is basically constant.

FreeRTOS also has a heartbeat, which uses a timer to generate fixed interval interrupts. This is called Tick and Tick. For example, a clock interrupt occurs every 10ms.

As shown below:

  • Suppose that t1, t2 and t3 have clock interrupts
  • The time between two interrupts is called time slice and tick period
  • The length of the time slice is determined by configTICK_RATE_HZ decision, assuming configtick_ RATE_ If Hz is 100, the time slice length is 10ms

How to switch tasks with the same priority? See the figure below:

  • Task 2 is performed from t1 to t2
  • When a tick interrupt occurs at t2, enter the tick interrupt processing function:
    • Select the next task to run
    • After executing the interrupt processing function, switch to a new task: Task 1
  • Task 1 runs from t2 to t3
  • As can be seen from the figure below, the running time of the task does not strictly start from T1, T2 and T3

With the concept of Tick, we can use Tick to measure time, for example:

vTaskDelay(2);  // Wait for 2 ticks, assuming configtick_ RATE_ Hz = 100, 10ms in Tick cycle, 20ms waiting

// You can also use PDMS_ TO_ The ticks macro converts ms to tick
vTaskDelay(pdMS_TO_TICKS(100));	 // Wait for 100ms

Note that the delay based on Tick is not accurate. For example, vtask delay (2) is intended to delay two Tick cycles, and it may return after a little more than one Tick.

As shown below:

When using vtask delay function, it is recommended to use PDMS in ms_ TO_ Ticks converts time to Tick.

Such code is similar to configTICK_RATE_HZ is irrelevant, even if the configuration item configTICK_RATE_HZ has changed, and we don't have to modify the code.

3.3.3 example 4: priority experiment

Code: FreeRTOS_04_task_priority

This program will create 3 tasks:

  • Task 1 and task 2: have the same priority, both of which are 1
  • Task 3: the highest priority is 2

The codes of tasks 1 and 2 are as follows:

void vTask1( void *pvParameters )
{
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print task information */
		printf("T1\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print task information */
		printf("T2\r\n");				
	}
}

Task 3 code is as follows:

void vTask3( void *pvParameters )
{	
	const TickType_t xDelay3000ms = pdMS_TO_TICKS( 3000UL );		
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		/* Print task information */
		printf("T3\r\n");				

		// If you do not sleep, other tasks cannot be performed
		vTaskDelay( xDelay3000ms );
	}
}

The main function code is as follows:

{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 1, NULL);
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

The operation is shown in the figure below:

  • Task 3 takes precedence until it invokes vtask delay to actively give up running
  • Task 1 and task 2: take turns

The scheduling is shown in the figure below:

3.3.4 example 5: modify priority

Code in this section: FreeRTOS_05_change_priority.

Use uxTaskPriorityGet to get the priority of a task:

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );

Use the parameter xTask to specify the task, and set it to NULL to obtain its own priority.

Use vtask priorityset to set the priority of a task:

void vTaskPrioritySet( TaskHandle_t xTask,
                       UBaseType_t uxNewPriority );

Use the parameter xTask to specify tasks. Setting it to NULL means setting your own priority;
The parameter uxNewPriority indicates the new priority. The value range is 0~(configMAX_PRIORITIES – 1).

The code of the main function is as follows. It creates two tasks: Task 1 has higher priority and executes first:

int main( void )
{
	prvSetupHardware();
	
	/* Task1 The priority of task 1 is higher, and task 1 is executed first */
	xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

The code of task 1 is as follows:

void vTask1( void *pvParameters )
{
	UBaseType_t uxPriority;
	
	/* Task1,Task2 Will not enter a blocked or suspended state
	 * Decide who can run according to priority
	 */
	
	/* Get Task1's own priority */
	uxPriority = uxTaskPriorityGet( NULL );
	
	for( ;; )
	{
		printf( "Task 1 is running\r\n" );

		printf("About to raise the Task 2 priority\r\n" );
		
		/* Elevate task 2 above task 1
		 * Task2 Will be executed immediately
 		 */
		vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
		
		/* If task 1 can run here, it means that its priority is higher than Task 2
		* That means that task 2 must have lowered its priority
 		 */
	}
}

The code for task 2 is as follows:

void vTask2( void *pvParameters )
{
	UBaseType_t uxPriority;

	/* Task1,Task2 Will not enter a blocked or suspended state
	 * Decide who can run according to priority
	 */
	
	/* Get Task2's own priority */
	uxPriority = uxTaskPriorityGet( NULL );
	
	for( ;; )
	{
		/* Running here means that task 2 has a higher priority than task 1
		 * Task1 Increased the priority of task 2
		 */
		printf( "Task 2 is running\r\n" );
		
		printf( "About to lower the Task 2 priority\r\n" );

		/* Lower task 2's own priority so that it is less than task 1
		 * Task1 Can run
 		 */
		vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
	}
}

The scheduling is shown in the figure below:

  • 1: At first, task 1 has the highest priority, and it executes first. It raises the priority of task 2.
  • 2: Task 2 has the highest priority and it executes. It lowered its priority.
  • 3: Task 1 has the highest priority and is executed again. It raises the priority of task 2.
  • So cycle.
  • Note: the priority of task 1 is always 2, and the priority of task 2 is 3 or 1, both of which are greater than 0. So the Idel mission has no chance to execute.

3.4 task status

Previously, we simply divided the status of tasks into 2: running and not running.

For non running states, you can continue to subdivide them, such as FreeRTOS_ 04_ task_ In priority:

  • After Task3 executes vTaskDelay, it is in a non running state and cannot run again until 3 seconds have elapsed
  • While task 3 is running, task 1 and task 2 are also in non running status, but they can run at any time
  • The two "non running" states are different and can be subdivided into:
    • Blocked status
    • Suspended status
    • Ready status (Ready)

3.4.1 blocked

In the example of daily life, when the mother communicates with her colleagues in front of the computer, if the colleagues have not replied, the mother's work will be stuck, blocked and blocked. The point is: mother is waiting.

In FreeRTOS_ 04_ task_ In the priority experiment, if the vtask delay call in task 3 is commented out, task 1 and task 2 have no chance to execute, and task 1 and task 2 are "starved".

In the actual product, instead of running a task all the time, we use the "event driven" method to run it:

  • A task needs to wait for an event before it can run
  • It does not consume CPU resources while waiting for events
  • While waiting for the event, the task is blocked

For tasks in blocking state, it can wait for two types of events:

  • Time related events
    • Can wait for a while: I'll wait 2 minutes
    • You can also wait until some absolute time: I wait until 3 p.m
  • Synchronization event: this event is generated by another task or interrupt program
    • Example 1: task A waits for Task B to send data to it
    • Example 2: task A waits for the user to press the key
    • There are many sources of synchronization events (these concepts will be described in detail later):
      • Queue
      • Binary semaphores
      • Counting semaphores
      • Mutexes
      • Recursive mutexes, recursive mutexes
      • Event groups
      • Task notifications

You can add a timeout when waiting for a synchronization event. For example, when waiting for data in the team, the timeout is set to 10ms:

  • Data arrival within 10ms: successful return
  • After 10ms, there is still no data: timeout return

3.4.2 suspended

In the example of daily life, the mother is communicating with her colleagues in front of the computer, and the mother can pause:

  • It's annoying. I'll pause for a while
  • The leader said: you pause

Tasks in FreeRTOS can also be suspended. The only way is through the vtask suspend function. The function prototype is as follows:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

The parameter xtask tosuspend indicates the task to be suspended. If it is NULL, it indicates to suspend itself.

To exit the pause state, you can only be operated by others:

  • Call another task: vtask resume
  • Interrupt program call: xtask resumefromisr

In actual development, the pause state is not used much.

3.4.3 ready status (Ready)

The task is completely ready and ready to run: it's just not round yet. At this time, it is ready.

3.4.4 complete state transition diagram

3.5 Delay function

3.5.1 two Delay functions

There are two Delay functions:

  • vTaskDelay: wait for at least the specified number of tick interrupts to become ready
  • vTaskDelayUntil: wait until the specified absolute time before it becomes ready.

The prototypes of these two functions are as follows:

void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: How much to wait for Tick */

/* pxPreviousWakeTime: Last wake-up time
 * xTimeIncrement: To block to (pxPreviousWakeTime + xTimeIncrement)
 * The units are Tick Count
 */
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                            const TickType_t xTimeIncrement );

The following drawing Description:

  • When vtask delay (n) is used, the time interval between entering and exiting vtask delay is at least n Tick interrupts
  • When using xTaskDelayUntil (& pre, n), the time to exit xTaskDelayUntil before and after two times is at least n Tick interrupts
    • When you exit xTaskDelayUntil, the task will enter the ready state and generally get the opportunity to execute
    • Therefore, you can use xtask delayuntil to make the task run periodically

3.5.2 example 6: Delay

Code in this section: FreeRTOS_06_taskdelay.

This program will create 2 tasks:

  • Task1:
    • High priority
    • Set the variable flag to 1, then call vTaskDelay(xDelay50ms). Or vtaskdelayuntil (& xlastwaketime, xdelay50ms);
  • Task2:
    • Low priority
    • Set the variable flag to 0

The main function code is as follows:

int main( void )
{
	prvSetupHardware();
	
	/* Task1 The priority of task 1 is higher, and task 1 is executed first */
	xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
	xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

In the code of task 1, use the condition switch to select the Delay function. Change #if 1 to #if 0 to use vtask delayuntil. The code is as follows:

void vTask1( void *pvParameters )
{
	const TickType_t xDelay50ms = pdMS_TO_TICKS( 50UL );
	TickType_t xLastWakeTime;
	int i;
	
	/* Get the current Tick Count */
	xLastWakeTime = xTaskGetTickCount();
			
	for( ;; )
	{
		flag = 1;
		
		/* Deliberately add multiple loops to make the program run longer */
		for (i = 0; i <5; i++)
			printf( "Task 1 is running\r\n" );

#if 1		
		vTaskDelay(xDelay50ms);
#else		
		vTaskDelayUntil(&xLastWakeTime, xDelay50ms);
#endif		
	}
}

The code of task 2 is as follows:

void vTask2( void *pvParameters )
{
	for( ;; )
	{
		flag = 0;
		printf( "Task 2 is running\r\n" );
	}
}

Use Keil's logic analysis to observe the bit waveform of flag variable, as follows:

  • When the flag is 1, it means that Task1 is running, and when the flag is 0, it means that Task2 is running, that is, Task1 is blocked
  • Vtask delay: Specifies the blocking time
  • Vtask delayuntil: Specifies the interval and cycle of task execution

3.6 idle tasks and their hook functions

3.6.1 introduction

In FreeRTOS_ 03_ delete_ In the task experiment, we experienced the role of Idle tasks: freeing the memory of deleted tasks.

In addition to the above purposes, why do you have to have free tasks? For a good program, its tasks are event driven: it is blocked most of the time. It is possible that all tasks created by ourselves cannot be executed, but the scheduler must be able to find a running task: therefore, we need to provide idle tasks. When using vTaskStartScheduler() function to create and start a scheduler, idle tasks will be created inside this function:

  • Idle task priority is 0: it cannot prevent user tasks from running
  • Idle tasks are either ready or running and will never block

The priority of idle tasks is 0, which means that once a user's task becomes ready, the idle task will be switched out immediately to let the user's task run. In this case, we say that the user task "pre empt" the idle task, which is implemented by the scheduler.

Note: if you use vtask delete() to delete a task, you must ensure that the idle task has a chance to execute, otherwise you cannot free the memory of the deleted task.

We can add an idle task hook function. Every time the loop of an idle task executes, the hook function will be called. Hook functions are used to:

  • Execute some low priority, background functions that need to be executed continuously
  • Measure the idle time of the system: if idle tasks can be executed, it means that all high priority tasks have stopped. Therefore, by measuring the occupied time of idle tasks, the processor occupancy can be calculated.
  • Let the system enter the power saving mode: idle tasks can be executed, which means that there are no important things to do. Of course, you can enter the power saving mode.

Limitations of hook functions for idle tasks:

  • It cannot cause idle tasks to enter the blocking state or suspended state
  • If you can use vtask delete() to delete tasks, the hook function should be executed very efficiently. If the idle task migration is stuck in the hook function, it cannot free memory.

3.6.2 premise of using hook function

In FreeRTOS\Source\tasks.c, you can see the following code, so the premise is:

  • Define this macro as 1: configure_ IDLE_ HOOK
  • Implement the vApplicationIdleHook function

3.7 scheduling algorithm

3.7.1 important concepts

This knowledge has been mentioned above, and it is summarized here.

The running task, called "processor in use", is running. In a single processing system, only one task can be running at any time.

A non running task that is in one of the three states: blocked, suspended, and ready. Ready tasks can be selected by the scheduler to switch to the running state. The scheduler always selects the highest priority ready task and puts it into the running state.

A task in blocking state, which is waiting for an "event". When an event occurs, the task will enter the ready state. Events are divided into two categories: time related events and synchronous events. The so-called time-related event is to set the timeout: block within the specified time, and enter the ready state when the time is up. Using time-related events, you can achieve periodic functions and timeout functions. Synchronization events are: a task is waiting for some information, and other tasks or interrupt service programs will send information to it. How to "send a message"? There are many methods, including task notification, queue, event group, semaphore, mutex, etc. These methods are used to send synchronization information, such as indicating that a peripheral has received data.

3.7.2 configure scheduling algorithm

The so-called scheduling algorithm is how to determine which ready task can be switched to the running state.

Configure the scheduling algorithm through two configuration items in the configuration file FreeRTOSConfig.h: configure_ PREEMPTION,configUSE_TIME_SLICING.

There is also a third configuration item: configure_ TICKLESS_ Idle, an advanced option, is used to turn off Tick interrupt to save power. It will be explained separately later. Now let's assume configure_ TICKLESS_ Idle is set to 0. Do not use this function first.

The behavior of scheduling algorithm is mainly reflected in two aspects: how high priority tasks run first and how ready tasks with the same priority are selected. The scheduling algorithm should ensure that ready state tasks with the same priority can run "in turn", and the strategy is "round robin scheduling". Rotation scheduling does not guarantee that the running time of tasks is allocated fairly. We can also refine the time allocation method.

Understand a variety of scheduling algorithms from three perspectives:

  • Can you preempt? Can high priority tasks be executed first (configuration item: configure_preemption)

    • Yes: it is called "pre preemptive scheduling". High priority ready tasks are executed immediately, which will be detailed below.
    • No: if you can't grab it, you can only negotiate. It's called "cooperative scheduling"
      • When the current task is executed, higher priority tasks are ready and cannot run immediately. You can only wait for the current task to actively give up CPU resources.
      • Other tasks with the same priority can only wait: tasks with higher priority cannot be preempted, and those with lower priority should be more honest
  • Whether tasks with the same priority can be executed in turn on the premise of preemption (configuration item: configure_time_slicing)

    • Execute in turn: it is called "time slicing". Tasks with the same priority are executed in turn. You execute one time slice and I execute another time slice
    • Do not execute in turn: English is "without Time Slicing". The current task will be executed until it is voluntarily abandoned or preempted by high priority tasks
  • On the premise of "preemptive" + "time slice rotation", further refine: whether idle tasks give way to user tasks (configuration item: configure_show_year)

    • Idle tasks are inferior to others. Each time a cycle is executed, we will see whether to actively give way to the user task
    • Idle tasks are the same as user tasks. Everyone executes them in turn. No one is more special

The list is as follows:

Configuration itemABCDE
configUSE_PREEMPTION11110
configUSE_TIME_SLICING1100x
configIDLE_SHOULD_YIELD1010x
explainCommonly usedRarely usedRarely usedRarely usedHardly

Note:

  • A: Preemptive + time slice rotation + idle task concession
  • B: Preemptive + time slice rotation + no concession for idle tasks
  • C: Preemptive + non time slice rotation + idle task concession
  • D: Preemptive + non time slice rotation + no concession for idle tasks
  • E: Cooperative scheduling

3.7.3 Example 7: scheduling

Code in this section: FreeRTOS_07_scheduler. Subsequent experiments are based on this program and observe the effect by modifying the configuration items.

Three tasks are created in the code: the priority of task 1 and task 2 is 0. Like idle tasks, the highest priority of task 3 is 2. Four global variables are defined in the program. When a task is executed, the corresponding variable is set to 1. You can check the task switching through Keil's logic analyzer:

static volatile int flagIdleTaskrun = 0;  // When idle task is running, flagIdleTaskrun=1
static volatile int flagTask1run = 0;     // Task 1 runtime flagTask1run=1
static volatile int flagTask2run = 0;     // Task 2 runtime flagTask2run=1
static volatile int flagTask3run = 0;     // Task 3 runtime flagTask3run=1

The main function code is as follows:

int main( void )
{
	prvSetupHardware();
	
	xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL);
	xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL);
	xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL);

	/* Start scheduler */
	vTaskStartScheduler();

	/* If the program runs here, it means an error. Generally, there is insufficient memory */
	return 0;
}

The codes of task 1 and task 2 are as follows. They are "continuous tasks":

void vTask1( void *pvParameters )
{
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 1;
		flagTask2run = 0;
		flagTask3run = 0;
		
		/* Print task information */
		printf("T1\r\n");				
	}
}

void vTask2( void *pvParameters )
{	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 1;
		flagTask3run = 0;
		
		/* Print task information */
		printf("T2\r\n");				
	}
}

The code of task 3 is as follows. It will call vtask delay so that other tasks can run:

void vTask3( void *pvParameters )
{	
	const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL );		
	
	/* The body of a task function is generally an infinite loop */
	for( ;; )
	{
		flagIdleTaskrun = 0;
		flagTask1run = 0;
		flagTask2run = 0;
		flagTask3run = 1;
		
		/* Print task information */
		printf("T3\r\n");				

		// If you do not sleep, other tasks cannot be performed
		vTaskDelay( xDelay5ms );
	}
}

Provides a hook function for idle tasks:

void vApplicationIdleHook(void)
{
	flagIdleTaskrun = 1;
	flagTask1run = 0;
	flagTask2run = 0;
	flagTask3run = 0;	
	
	/* Deliberately adding printing to make the flagIdleTaskrun become 1 for a long time */
	printf("Id\r\n");				
}

3.7.4 comparison effect: preemption or not

In FreeRTOSConfig.h, define such macros to compare the effects of logic analyzer:

// Experiment 1: Preemption
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// Experiment 2: no preemption
#define configUSE_PREEMPTION		0
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

It can be seen from the following comparison diagram:

  • Preemption: when high priority tasks are ready, they can be executed immediately
  • When preemption is not allowed: the priority is meaningless. Since preemption is not allowed, we can only negotiate. Task 1 in the figure has been running (there is no negotiation spirit at all), and other tasks cannot be executed. Even if the vtask delay of task 3 has timed out and its priority is higher, it cannot be executed.

3.7.5 comparison effect: whether the time slice rotates or not

In FreeRTOSConfig.h, define such macros to compare the effects of logic analyzer:

// Experiment 1: time slice rotation
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// Experiment 2: time slice does not rotate
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      0
#define configIDLE_SHOULD_YIELD		1

It can be seen from the following comparison diagram:

  • Time slice rotation: task switching will be caused in Tick interrupt
  • No rotation of time slice: task switching will be caused when high priority tasks are ready, and when high priority tasks are no longer running. It can be seen that task 3 can be executed immediately after it is ready. After it runs, it leads to task switching. There is no task switching at other times. You can see that task 1 and task 2 have been running for a long time.

3.7.6 comparison effect: idle task concession

In FreeRTOSConfig.h, define such macros to compare the effects of logic analyzer:

// Experiment 1: idle task concession
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		1

// Experiment 2: no concession for idle tasks
#define configUSE_PREEMPTION		1
#define configUSE_TIME_SLICING      1
#define configIDLE_SHOULD_YIELD		0

It can be seen from the following comparison diagram:

  • During concession: in each cycle of idle tasks, the processor will be surrendered actively. As can be seen from the figure, the waveform of flagidltaskrun is very small
  • When there is no concession: idle tasks are treated the same as task 1 and task 2, and their waveform width is about the same

Tags: Embedded system Single-Chip Microcomputer stm32 FreeRTOS RTOS

Posted on Sat, 20 Nov 2021 02:17:06 -0500 by mlnsharma