Operating system FreeRTOS: software timer, event flag group, task notification

Posted by sandsquid on Sun, 30 Jan 2022 14:21:50 +0100

Software Timers

Timer can be said to be a peripheral of each MCU. Some MCU have extremely powerful timer functions, such as providing PWM, input capture and other functions. But the most commonly used is the most basic function of the timer - timing, which completes the transactions that need to be processed periodically through the timer. The timer of MCU belongs to hardware timer. The number of hardware timers of different MCU is different, because the cost should be considered

FreeRTOS also provides timer function, but it is a software timer. The accuracy of software timer is certainly not as high as that of hardware timer, but it is enough for ordinary periodic processing tasks with low accuracy requirements. When the hardware timer of MCU is not enough, we can consider using the software timer of FreeRTOS

The software timer can be set for a period of time. When the set time arrives, the specified function will be executed. The function called by the timer is called the callback function of the timer. The interval between two executions of the callback function is called the timer's timing cycle. In short, when the timer's timing cycle arrives, the callback function will be executed.

Note for compiling callback function: the callback function of the software timer is executed in the timer service task, so it is impossible to call any API function that blocks the task in the callback function. For example, vtask delay() and vtask delayunti() cannot be called in the timer callback function, and some API functions that access queues or semaphores with non-zero blocking time cannot be called.

Timer is an optional function that does not belong to FreeRTOS kernel. It is provided by timer service (or Daemon) task. FreeRTOS provides many timer related API functions. Most of these API functions use FreeRTOS queue to send commands to timer service tasks. This queue is called the timer command queue. The timer command queue is provided to the software timer of FreeRTOS, and users cannot access it directly

The left part is part of the user application and is invoked in a user task created by a user. The right part of the figure is the task function of the timer service task. The timer command queue connects the user application task and the timer service task. In this example, the application calls the function xtimerreset (). As a result, the reset command will be sent to the timer command queue, and the timer service task will process this command. The application sends the reset command to the timer command queue indirectly through the function xTimerReset(), rather than directly calling the queue operation function such as xQueueSend().

Related macro definitions

If you want to use a software timer, use the macro configure_ Timers must be set to 1. When it is set to 1, the timer service task will be created automatically when the FreeRTOS scheduler is started.

vTaskStartScheduler();          //When task scheduling is started, a timer is created here

Finally comes to defining functions

BaseType_t xTimerCreateTimerTask( void )

Set the task priority of the software timer service task, which can be 0 ~ (configmax_priorities-1). The priority must be set according to the actual application requirements. If the priority of timer service task is set high, the commands in timer command queue and timer callback function will be processed in time.

configTIMER_QUEUE_LENGTH this macro is used to set the queue length of the timer command queue

configTIMER_TASK_STACK_DEPTH this macro is used to set the task stack size of timer service task. The unit is word, not byte!, For STM32, a word is 4 bytes. Since the timer service task will execute the callback function of the timer, the size of the task stack must be set according to the callback function of the timer

Timer classification

There are two kinds of software timers: single timer and periodic timer

For a single timer, the timer callback function will be executed once, such as timing 1s. When the timing time is up, the callback function will be executed once, and then the timer will stop running. For the single timer, we can manually restart it again (just call the corresponding API function), but the single timer cannot restart automatically.

On the contrary, once the cycle timer is started, it will automatically restart after executing the callback function, so that the callback function will execute periodically

Corresponding API


The newly created software timers are in sleep mode, so you should start them manually

Start the software timer. The function xTimerStartFromISR() is the interrupt version of this function and can be used in the interrupt service function. If the software timer is not running, calling the function xTimerStart() will calculate the timer expiration time. If the software timer is running, the result of calling the function xTimerStart() is the same as xTimerReset(). This function is a macro that actually executes the function xtimergeneralcommand

There is a blocking time option in the start parameter. This is because calling the function xTimerStart() to start the software timer is actually sending a tmrcommand to the timer command queue_ Since the start command sends messages to the queue, it will certainly involve the setting of queue blocking time. Similar logic, other commands also need blocking time.

Experimental code

Basic variable configuration

TimerHandle_t 	AutoReloadTimer_Handle;			//Cycle timer handle
TimerHandle_t	OneShotTimer_Handle;			//Single timer handle
void AutoReloadCallback(TimerHandle_t xTimer); 	//Cycle timer callback function
void OneShotCallback(TimerHandle_t xTimer);		//Single timer callback function

Timer creation

//Create software cycle timer
    AutoReloadTimer_Handle=xTimerCreate((const char*		)"AutoReloadTimer",
									    (TickType_t			)1000,
							            (UBaseType_t		)pdTRUE,
							            (void*				)1,
							            (TimerCallbackFunction_t)AutoReloadCallback); //Cycle timer, cycle 1s(1000 clock beats), cycle mode
    //Create a single timer
	OneShotTimer_Handle=xTimerCreate((const char*			)"OneShotTimer",
							         (TickType_t			)2000,
							         (UBaseType_t			)pdFALSE,
							         (void*					)2,
							         (TimerCallbackFunction_t)OneShotCallback); //Single timer, cycle 2s(2000 clock beats), single mode	

Callback function (a callback function is a function that has been declared in advance when creating a timer and pointed to this function with a function pointer)

//Callback function of cycle timer
void AutoReloadCallback(TimerHandle_t xTimer)
{
	static u8 tmr1_num=0;
	tmr1_num++;									//Cycle timer execution times plus 1
	LCD_ShowxNum(70,111,tmr1_num,3,16,0x80); 	//Displays the number of times the cycle timer is executed
	LCD_Fill(6,131,114,313,lcd_discolor[tmr1_num%14]);//Filled area
}

//Callback function of single timer
void OneShotCallback(TimerHandle_t xTimer)
{
	static u8 tmr2_num = 0;
	tmr2_num++;		//Cycle timer execution times plus 1
	LCD_ShowxNum(190,111,tmr2_num,3,16,0x80);  //Displays the execution times of a single timer
	LCD_Fill(126,131,233,313,lcd_discolor[tmr2_num%14]); //Filled area
	LED1=!LED1;
    printf("Timer 2 operation ends\r\n");
}

At this time, the timer has been created, but it still does not start running. We can handle it by pressing the key

//Task function of TimerControl
void timercontrol_task(void *pvParameters)
{
	u8 key,num;
	while(1)
	{
		//Only when both timers are created successfully can they be operated
		if((AutoReloadTimer_Handle!=NULL)&&(OneShotTimer_Handle!=NULL))
		{
			key = KEY_Scan(0);
			switch(key)
			{
				case WKUP_PRES:     //When key_ Press up to turn on the cycle timer
					xTimerStart(AutoReloadTimer_Handle,0);	//Turn on cycle timer
					printf("Start timer 1\r\n");
					break;
				case KEY0_PRES:		//When key0 is pressed, the single timer is turned on
					xTimerStart(OneShotTimer_Handle,0);		//Turn on the single timer
					printf("Start timer 2\r\n");
					break;
				case KEY1_PRES:		//When key1 is pressed, the timer is turned off
					xTimerStop(AutoReloadTimer_Handle,0); 	//Turn off cycle timer
					xTimerStop(OneShotTimer_Handle,0); 		//Turn off the single timer
					printf("Turn off timers 1 and 2\r\n");
					break;	
			}
		}
		num++;
		if(num==50) 	//Flash once every 500msLED0
		{
			num=0;
			LED0=!LED0;	
		}
        vTaskDelay(10); //Delay 10ms, that is, 10 clock beats
	}
}

Event flag group

Event bit (event flag) the event bit is used to indicate whether an event occurs. The event bit is usually used as an event flag, such as the following examples:

● when a message is received and processed, a bit (flag) can be set to 1. When there is no message to be processed in the queue, this bit (flag) can be set to 0.

● when the messages in the queue are sent and output through the network, a bit (flag) can be set to 1. When there is no data to be sent from the network, this bit (flag) can be set to 0.

● now you need to send a heartbeat message to the network and set a bit (flag) to 1. Now there is no need to send heartbeat information to the network. This bit (flag) is set to 0.

Event group an event group is a group of event bits. The event bits in the event group are accessed through bit numbers. Similarly, they are listed above
Take three examples:

● bit0 of the event flag group indicates whether the message in the queue is processed.

● bit1 of the event flag group indicates whether a message needs to be sent from the network.

● bit2 of the event flag group indicates whether it is necessary to send heartbeat information to the network now.

The core is to debug a 32-bit variable: event flag group and event bit data type:

The data type of the event flag group is EventGroupHandle_t. When configure_ 16_ BIT_ When ticks is 1, the event flag group can store 8 event bits. When configure_ 16_ BIT_ When ticks is 0, the event flag group stores 24 event bits. Not just 16, 32 bits, because the high eight bits are used as reserved bits.

API call

Create event group

EventGroupHandler=xEventGroupCreate();	 //Create event flag group

There is a special variable

EventGroup_t *pxEventBits;

The event flag structure stores the structure used to describe some attributes of the event, as well as the list of events to occur.

Set event group

The parameter is the handle, and the number of bits to be cleared (0x08 clears the third bit), of course, you can also clear multiple bits (0x09 clears 0 and 3). The basic operations are bit operations.

Because the task waiting event bit is set to 1, and then there will be tasks running. Therefore, when the task is set, the function will be started to judge whether there is a task to start running.

Wait for the specified event bit

A task may need to synchronize with multiple events, so the task needs to wait and judge multiple event bits (flags). This function can be completed by using the function xEventGroupWaitBits(). After calling the function, if the event bit the task is waiting for is not ready (set to 1 or clear), the task will enter the blocking state until the blocking time arrives or the waiting event bit is ready. The function prototype is as follows:

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
const TickType_t xTicksToWait );

Parameter meaning:

xEventGroup: Specifies the event flag group to wait for.

uxBitsToWaitFord: Specifies the event bits to wait for. For example, this parameter is 0X05 when waiting for bit0 and / or bit2. If waiting for bit0 and / or bit1 and / or bit2, this parameter is 0X07, and so on.

xClearOnExit: if this parameter is pdTRUE, these event bits set by the parameter uxBitsToWaitFor will be cleared before exiting this function. If the bit pdFALSE is set, these event bits will not change.

xWaitForAllBits: if this parameter is set to pdTRUE, the function xEventGroupWaitBits() will return only when the event bits set by uxBitsToWaitFor are set to 1 or the specified blocking time expires. When this function is pdFALSE, as long as any one of these event bits set by uxBitsToWaitFor is set to 1, or the specified blocking time expires, the function

xEventGroupWaitBits() will return.

xTicksToWait: sets the blocking time in beats.

For example, using one task requires three other tasks to be synchronized with the task. Then you need to use this function.

Task notification

Task notification is an optional function in FreeRTOS. To use task notification, you need to configure the macro_ TASK_ Notifications is defined as 1. Each task of FreeRTOS has a 32-bit notification value, which is the member variable ulNotifiedValue in the task control block. Task notification is an event. If the receiving task of a task notification is blocked due to waiting for task notification, the blocking state of the task will be relieved after sending task notification to the receiving task. You can also update the task notification value of the received task

It can replace event flag group and queue as part of the function of semaphore. The difference is that a semaphore can be used by multiple tasks, but task notification can only realize one-to-one notification.

Reasonable and flexible use of the above methods to change the task notification value can replace queues, binary semaphores, counting semaphores and event flag groups in some occasions. When using task notification to realize the binary semaphore function, the time to unblock the task is 45% faster than using the binary semaphore directly (FreeRTOS official test results, using the binary semaphore in v8.1.2, GCC compiler, tested under the condition of - O2 optimization, without enabling the assertion function configASSERT()), and less RAM is used

The task notification is sent by using the function xTaskNotify() or xtasknotifygive() (and the interrupted version of this function). This notification value will be saved until the receiving task calls the function xTaskNotifyWait() or ulTaskNotifyTake() to obtain this notification value. If the receiving task is blocked by waiting for the task notification, the blocking state will be released after receiving the task notification.

Although task notification can improve the speed and reduce the use of RAM, there are restrictions on the use of task notification: FreeRTOS task notification can only have one receiving task, which is actually the case for most applications. The receiving task can enter the blocking state due to receiving the task notification, but the sending task will not be blocked due to the failure of sending the task notification.

Task notification API


Task notification sending function: change the notification value and check whether there are tasks waiting for the notification value.

ulTaskNotifyTake() is a function to obtain task notification. When task notification is used as binary semaphore or counting semaphore, this function can be used to obtain semaphore

xTaskNotifyWait() is also used to obtain task notifications, but this function is more powerful than ulTaskNotifyTake(). This function can be used to obtain task notifications regardless of which of the task notifications are used as binary semaphores, counting semaphores, queues and event flag groups. However, when task notifications are used as position semaphores and count semaphores, it is recommended to use the function ulTaskNotifyTake().

Task management can replace other types of semaphores, but task management can only be for one task. Therefore, instead of counting semaphores, multiple tasks cannot be controlled to access the same resource at the same time (Resource Management), which can only be used for event counting.

Task notification API experiment

Binary semaphore experiment

void USART1_IRQHandler(void)                	//Serial port 1 interrupt service program
{
//Send task notification
	if((USART_RX_STA&0x8000)&&(DataProcess_Handler!=NULL))//The data is received and the task receiving the task notification is valid
	{
		vTaskNotifyGiveFromISR(DataProcess_Handler,&xHigherPriorityTaskWoken);//Send task notification
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//Make a task switch if necessary
	}
}

//In data processing tasks
		NotifyValue=ulTaskNotifyTake(pdTRUE,portMAX_DELAY);	//Get task notification
		if(NotifyValue==1)									//The task notification value before clearing is 1, indicating that the task notification is valid
		...Start processing task

Counting semaphore experiment

Press key to send task notification

xTaskNotifyGive(SemapTakeTask_Handler);//Send task notification

Create a function to wait

//Get count semaphore task function
void SemapTake_task(void *pvParameters)
{
	uint32_t NotifyValue;
	while(1)
	{
		NotifyValue=ulTaskNotifyTake(pdFALSE,portMAX_DELAY);//Get task notification
		LED1=!LED1;
        vTaskDelay(1000);                               	//Delay 1s, that is, 1000 clock beats	
	}
}

In this, take will process the signal value.

Task notification simulation mailbox experiment

If the variable of the task is a 32-bit value, it can be used to store pointers.

Complete mailbox simulation with no reservation notification

Send task processing

//task1 task function
void task1_task(void *pvParameters)
{
	u8 key,i=0;
    BaseType_t err;
	while(1)
	{
		key=KEY_Scan(0);            			//Scan key
        if((Keyprocess_Handler!=NULL)&&(key))   
        {
			err=xTaskNotify((TaskHandle_t	)Keyprocess_Handler,		//Task handle to receive task notification
									 (uint32_t		)key,						//Task notification value
									 (eNotifyAction	)eSetValueWithOverwrite);	//Send task notification by overwriting
			if(err==pdFAIL)
			{
				printf("Task notification sending failed\r\n");
			}
        }
        vTaskDelay(10);           //Delay 10ms, that is, 10 clock beats	
	}
}

The receiving function can only use xtask notifywait because take is a binary semaphore

//Keyprocess_task function
void Keyprocess_task(void *pvParameters)
{
	u8 num;
	uint32_t NotifyValue;
	BaseType_t err;
	
	while(1)
	{
		err=xTaskNotifyWait((uint32_t	)0x00,				//The task bit is not cleared when entering the function
							(uint32_t	)ULONG_MAX,			//Clear all bit s when exiting the function
							(uint32_t*	)&NotifyValue,		//Save task notification value
							(TickType_t	)portMAX_DELAY);	//Blocking time
		if(err==pdTRUE)				//Get task notification succeeded
		{
			switch((u8)NotifyValue)//The data type sent is u32, which should be changed to u8
			{
                case WKUP_PRES:		//KEY_UP control LED1
                    LED1=!LED1;
					break;
				case KEY0_PRES:		//KEY0 refresh LCD background
                    num++;
					LCD_Fill(6,126,233,313,lcd_discolor[num%14]);
                    break;
			}
		}
	}

Final effect: press the key to complete the task sending, and the other task has been waiting for the task notification to control the behavior according to the received data.

Simulated event flag group

Sender processing

#define EVENTBIT_0 	 (1<<0) 				// Event bit
#define EVENTBIT_1	(1<<1)
#define EVENTBIT_2	(1<<2)


//Task of setting event bits
void eventsetbit_task(void *pvParameters)
{
	u8 key,i;
	while(1)
	{
		if(EventGroupTask_Handler!=NULL)
		{
			key=KEY_Scan(0);
			switch(key)
			{
				case KEY1_PRES:
					xTaskNotify((TaskHandle_t	)EventGroupTask_Handler,//Task handle to receive task notification
								(uint32_t		)EVENTBIT_1,			//bit to update
								(eNotifyAction	)eSetBits);				//Update the specified bit
					break;
				case WKUP_PRES:
					xTaskNotify((TaskHandle_t	)EventGroupTask_Handler,//Task handle to receive task notification
								(uint32_t		)EVENTBIT_2,			//bit to update
								(eNotifyAction	)eSetBits);				//Update the specified bit
					break;	
			}
		}
		i++;
		if(i==50)
		{
			i=0;
			LED0=!LED0;
		}
        vTaskDelay(10); //Delay 10ms, that is, 10 clock beats
	}
}

//task handle 
extern TaskHandle_t EventGroupTask_Handler;
BaseType_t xHigherPriorityTaskWoken;

void EXTI9_5_IRQHandler(void)
{	
	delay_xms(20);   //Debounce			 
	if(KEY0==0)	
	{
		xTaskNotifyFromISR((TaskHandle_t	)EventGroupTask_Handler, 	//task handle 
						   (uint32_t		)EVENTBIT_0, 				//bit to update
						   (eNotifyAction	)eSetBits, 					//Update the specified bit
						   (BaseType_t*		)xHigherPriorityTaskWoken);

		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	}
 	 EXTI_ClearITPendingBit(EXTI_Line5);    //Clear the interrupt flag bit on LINE5  
}

Event receiving end processing

//Event flag group processing task
void eventgroup_task(void *pvParameters)
{
	u8 num=0,enevtvalue;
	static u8 event0flag,event1flag,event2flag;
	uint32_t NotifyValue;
	BaseType_t err;
	
	while(1)
	{
		//Get task notification value
		err=xTaskNotifyWait((uint32_t	)0x00,				//The task bit is not cleared when entering the function
							(uint32_t	)ULONG_MAX,			//Clear all bit s when exiting the function
							(uint32_t*	)&NotifyValue,		//Save task notification value
							(TickType_t	)portMAX_DELAY);	//Blocking time
		
		if(err==pdPASS)	   //Task notification obtained successfully
		{
			if((NotifyValue&EVENTBIT_0)!=0)			//Event 0 occurred	
			{
				event0flag=1;	
			}				
			else if((NotifyValue&EVENTBIT_1)!=0)	//Event 1 occurred	
			{
				event1flag=1;
			}
			else if((NotifyValue&EVENTBIT_2)!=0)	//Event 2 occurred	
			{
				event2flag=1;	
			}
	
			enevtvalue=event0flag|(event1flag<<1)|(event2flag<<2);	//Analog event flag group value
			printf("Task notification value is:%d\r\n",enevtvalue);
			LCD_ShowxNum(174,110,enevtvalue,1,16,0);				//Displays the current event value on the LCD
			
			if((event0flag==1)&&(event1flag==1)&&(event2flag==1))	//All three events occurred at the same time
			{
				num++;
				LED1=!LED1;	
				LCD_Fill(6,131,233,313,lcd_discolor[num%14]);
				event0flag=0;								//Flag reset
				event1flag=0;
				event2flag=0;
			}
		}
		else
		{
			printf("Task notification acquisition failed\r\n");
		}

	}