embedded real-time systems need to respond to events generated by the whole system environment. These events have different requirements for processing time and response time. Events are usually detected by interrupt, and the processing capacity in interrupt service routine (ISR) should be as short as possible.
note: only "FromISR" or "from"_ Only API functions or macros ending with "ISR" can be used in interrupt service routines.
1, Binary semaphore
1.1. Use binary semaphores to introduce tasks and interrupts synchronously
binary semaphore can unblock the task when a special interrupt occurs, which is equivalent to synchronizing the task with the interrupt. In this way, the work with a large amount of interrupt event processing can be completed in the synchronization task, and only a small part of the work can be processed quickly in the interrupt service routine (ISR). In this way, interrupt processing can be said to be "deferred" to a "handler" task.
if an interrupt processing request is particularly urgent, the priority of the delayed processing task can be set to the highest to ensure that the delayed processing task preempts other tasks in the system at any time. In this way, the delayed processing task becomes the first task to be executed after the corresponding ISR exits. In time, it is immediately executed by the ISR, which is equivalent to that all processing is completed in the ISR. This scheme is shown in the figure:
create binary semaphores using vsemaphotorecreatebinary() API function
void vSemaphoreCreateBinary( xSemaphoreHandle xSemaphore );
The acquisition of semaphore xSemaphoreTake() and xSemaphoreTake() cannot be invoked in the interrupt service routine.
portBASE_TYPE xSemaphoreTake( xSemaphoreHandle xSemaphore, portTickType xTicksToWait );
set binary semaphore xsephoregivefromisr() in interrupt
portBASE_TYPE xSemaphoreGiveFromISR( xSemaphoreHandle xSemaphore, portBASE_TYPE *pxHigherPriorityTaskWoken );
1.2. Use binary semaphores to synchronize tasks and interrupts
main() function is very simple. Create binary semaphores and tasks, install interrupt service routines, and then start the scheduler.
int main( void ) { init(); /* Semaphores must be created before use. In this example, a binary semaphore is created */ vSemaphoreCreateBinary( xBinarySemaphore ); /* Check whether the semaphore was created successfully */ if( xBinarySemaphore != NULL ) { /* Create deferred processing tasks. This task will be synchronized with the interrupt. Deferred processing tasks are created with a higher priority to ensure that The interrupt will be executed immediately after exiting. In this example, the deferred processing task is given priority 3 */ xTaskCreate( My_TaskTest, "My_TaskTest", 1000, NULL, 3, NULL ); /* Start the scheduler so the created tasks start executing. */ vTaskStartScheduler(); } }
delay processing task My_TaskTest uses binary semaphores to synchronize with interrupts. This task also prints out a message in each cycle. The purpose of this is to visually see the execution process of the task and interrupt in the execution output result of the program.
static void My_TaskTest( void *pvParameters ) { /* As per most tasks, this task is implemented within an infinite loop. */ while(true) { /* Use semaphores to wait for an event. The semaphore is executed before the scheduler starts, that is, this task Has been created before. The task is blocked without timeout, so this function call will only return after successfully obtaining the semaphore. There is no need to detect the return value here */ xSemaphoreTake( xBinarySemaphore, portMAX_DELAY ); /* When the program runs here, the event must have happened. The event handling in this example simply prints out a message */ vPrintString( "My_TaskTest work!\r\n" ); } }
the interrupt service handler does very little, just gives a semaphore to unblock the delayed processing task. Notice how the parameter pxHigherPriorityTaskWoken is used here. This parameter is set to pdFALSE before calling xsemaphotoregivefromisr(). If it is set to pdTRUE after the call is completed, a context switch is required.
void My_IRQHandler(void) { xHigherPriorityTaskWoken = pdFALSE; //Release binary semaphore xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken ); if( xHigherPriorityTaskWoken == pdTRUE ) { /* A semaphore is given to unblock the task waiting for this semaphore. If the priority of the unblocked task is higher than that of the current task - force a task switch, To ensure that the interrupt returns directly to the unblocked task (preferably higher). */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//Make a task switch if necessary } }
this demonstrates that a binary semaphore is used to synchronize tasks and interrupts. The whole execution process can be described as:
1. Interrupt generation.
2. The interrupt service routine starts and gives a semaphore to unblock the delayed processing task.
3. When the interrupt service routine exits, the delay processing task is executed. The first thing a deferred processing task does is get semaphores.
4. After the delay processing task completes the interrupt event processing, try to obtain the semaphore again - if the semaphore is invalid at this time, the task will cut in and block the waiting event.
2, Count semaphore
when the interrupt occurs at a relatively slow frequency, the binary semaphore synchronization task and interrupt can work effectively. However, a binary semaphore can latch only one interrupt event at most. Before the latched event is processed, if there is an interrupt event, the subsequent interrupt events will be lost. If the count semaphore is used instead of the binary semaphore, this loss of interrupt can be avoided.
2.1. Synchronous introduction of tasks and interrupts by counting semaphores
there are two typical uses of counting semaphores:
1. Event count
in this usage, each time an event occurs, the interrupt service routine will "give" the semaphore, and the count value of the semaphore is increased by 1 each time it is given. Delayed processing task will "take" a semaphore every time a task is processed, and the count value of the semaphore will be reduced by 1 every time it is obtained. The count value of a semaphore is actually the difference between the number of events that have occurred and the number of events that have been processed. this
the count semaphore used for event counting is initialized to 0 when it is created.
2. Resource management
in this usage, the count value of the semaphore is used to represent the number of available resources. To obtain control of resources, a task must first obtain the semaphore and reduce the count value of the semaphore by 1. When the count value decreases to 0, it indicates that no resources are available. When the task completes the work with resources, the semaphore will be given (returned) to increase the count value of the semaphore by 1.
a semaphore used for resource management. When it is created, its count value is initialized to the total number of available resources.
semaphores must be created before use. Use the xsemaphotorecreatecounting() API function to create a count semaphore.
xSemaphoreHandle xSemaphoreCreateCounting( unsigned portBASE_TYPE uxMaxCount,unsigned portBASE_TYPE uxInitialCount );
2.2. Use counting semaphores to synchronize tasks and interrupts
main() function is very simple. Count semaphores and tasks, install interrupt service routines, and then start the scheduler.
int main( void ) { init(); /* A semaphore must be created before it can be used. In this example, a count semaphore is created. The maximum count value of this semaphore is 10 and the initial count value is 0 */ xCountingSemaphore = xSemaphoreCreateCounting( 10, 0 ); /* Check whether the semaphore was created successfully */ if( xCountingSemaphore != NULL ) { /* Create deferred processing tasks. This task will be synchronized with the interrupt. Deferred processing tasks are created with a higher priority to ensure that The interrupt will be executed immediately after exiting. In this example, the deferred processing task is given priority 3 */ xTaskCreate( My_TaskTest, "My_TaskTest", 1000, NULL, 3, NULL ); /* Start the scheduler so the created tasks start executing. */ vTaskStartScheduler(); } }
delay processing task My_TaskTest uses count semaphores to synchronize with interrupts. This task also prints out a message in each cycle. The purpose of this is to visually see the execution process of the task and interrupt in the execution output result of the program.
static void My_TaskTest( void *pvParameters ) { /* As per most tasks, this task is implemented within an infinite loop. */ while(true) { /* Use semaphores to wait for an event. The semaphore is executed before the scheduler starts, that is, this task Has been created before. The task is blocked without timeout, so this function call will only return after successfully obtaining the semaphore. There is no need to detect the return value here */ xSemaphoreTake( xCountingSemaphore , portMAX_DELAY ); /* When the program runs here, the event must have happened. The event handling in this example simply prints out a message */ vPrintString( "My_TaskTest work!\r\n" ); } }
the interrupt service handler does very little, just gives a semaphore to unblock the delayed processing task. Notice how the parameter pxHigherPriorityTaskWoken is used here. This parameter is set to pdFALSE before calling xsemaphotoregivefromisr(). If it is set to pdTRUE after the call is completed, a context switch is required.
void My_IRQHandler(void) { xHigherPriorityTaskWoken = pdFALSE; /* The semaphore is given many times. When given for the first time, the delayed processing task is unblocked. Later, it is given to demonstrate the use of semaphore latched events, In order to delay processing, these interrupt events are processed in sequence without losing interrupts. In this way, the analog processor generates multiple interrupts, although These events are only simulated in a single interrupt */ xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken ); xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken ); xSemaphoreGiveFromISR( xCountingSemaphore, &xHigherPriorityTaskWoken ); if( xHigherPriorityTaskWoken == pdTRUE ) { /* A semaphore is given to unblock the task waiting for this semaphore. If the priority of the unblocked task is higher than that of the current task - force a task switch, To ensure that the interrupt returns directly to the unblocked task (preferably higher). */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken);//Make a task switch if necessary } }
demonstrates that a count semaphore is used to synchronize tasks and interrupts. The whole execution process can be described as:
1. Interrupt generation.
2. The interrupt service routine starts and gives a semaphore to unblock the delayed processing task.
3. When the interrupt service routine exits, the delay processing task is executed. The first thing a deferred processing task does is get semaphores. After each interrupt occurs, the delay processing task processes all three events generated by the interrupt [simulated]. These events are latched into the count value of the semaphore so that the delayed processing task can process them in order.
4. After the delay processing task completes the interrupt event processing, try to obtain the semaphore again - if the semaphore is invalid at this time, the task will cut in and block the waiting event.
3, Queue
xQueueSendToFrontFromISR(), xQueueSendToBackFromISR() and xQueueReceiveFromISR() are interrupt safe versions of xQueueSendToFront(), xQueueSendToBack() and xQueueReceive() respectively, which are specially used in interrupt service routines.
semaphores are used for event communication. Queues can be used not only for event communication, but also for transmitting data.