Interrupt Management
Embedded real-time systems have to take actions in response to events that originate from the environment. For example, a packet arriving on an Ethernet peripheral (the event) might require passing to a TCP/IP stack for processing (the action). Non-trivial systems will have to service events that originate from multiple sources, all of which will have different processing overhead and response time requirements. In each case, a judgment has to be made as to the best event processing implementation strategy:
- How should the event be detected? Interrupts are normally used, but inputs can also be polled.
- When interrupts are used, how much processing should be performed inside the interrupt service routine (ISR), and how much outside? It is normally desirable to keep each ISR as short as possible.
- How events are communicated to the main (non-ISR) code, and how can this code be structured to best accommodate processing of potentially asynchronous occurrences?
It is important to draw a distinction between the priority of a task, and the priority of an interrupt:
- A task is a software feature that is unrelated to the hardware on which FreeRTOS is running. The priority of a task is assigned in software by the application writer, and a software algorithm (the scheduler) decides which task will be in the Running state.
- Although written in software, an interrupt service routine is a hardware feature because the hardware controls which interrupt service routine will run, and when it will run. Tasks will only run when there are no ISRs running, so the lowest priority interrupt will interrupt the highest priority task, and there is no way for a task to pre-empt an ISR.
The Interrupt Safe API
Often it is necessary to use the functionality provided by a FreeRTOS API function from an interrupt service routine (ISR), but many FreeRTOS API functions perform actions that are not valid inside an ISR—the most notable of which is placing the task that called the API function into the Blocked state; if an API function is called from an ISR, then it is not being called from a task, so there is no calling task that can be placed into the Blocked state. FreeRTOS solves this problem by providing two versions of some API functions; one version for use from tasks, and one version for use from ISRs. Functions intended for use from ISRs have “FromISR” appended to their name.
Note: Never call a FreeRTOS API function that does not have “FromISR” in its name from an ISR.
The xHigherPriorityTaskWoken Parameter
If a context switch is performed by an interrupt, then the task running when the interrupt exits might be different to the task that was running when the interrupt was entered—the interrupt will have interrupted one task, but returned to a different task.
Some FreeRTOS API functions can move a task from the Blocked state to the Ready state. This has already been seen with functions such as xQueueSendToBack(), which will unblock a task if there was a task waiting in the Blocked state for data to become available on the subject queue.
If the priority of a task that is unblocked by a FreeRTOS API function is higher than the priority of the task in the Running state then, in accordance with the FreeRTOS scheduling policy, a switch to the higher priority task should occur. When the switch to the higher priority task actually occurs is dependent on the context from which the API function is called:
If the API function was called from a task
If configUSE_PREEMPTION is set to 1 in FreeRTOSConfig.h then the switch to the higher priority task occurs automatically within the API function—so before the API function has exited. This has already been seen in Figure 43, where writing to the timer command queue resulted in a switch to the RTOS daemon task before the function that wrote to the command queue had exited.
If the API function was called from an interrupt
A switch to a higher priority task will not occur automatically inside an interrupt. Instead, a variable is set to inform the application writer that a context switch should be performed. Interrupt safe API functions (those that end in “FromISR”) have a pointer parameter called pxHigherPriorityTaskWoken that is used for this purpose.
If a context switch should be performed, then the interrupt safe API function will set *pxHigherPriorityTaskWoken to pdTRUE. To be able to detect this has happened, the variable pointed to by pxHigherPriorityTaskWoken must be initialized to pdFALSE before it is used for the first time.
If the application writer opts not to request a context switch from the ISR, then the higher priority task will remain in the Ready state until the next time the scheduler runs—which in the worst case will be during the next tick interrupt.
FreeRTOS API functions can only set *pxHighPriorityTaskWoken to pdTRUE. If an ISR calls more than one FreeRTOS API function, then the same variable can be passed as the pxHigherPriorityTaskWoken parameter in each API function call, and the variable only needs to be initialized to pdFALSE before it is used for the first time.
Use of the pxHigherPriorityTaskWoken parameter is optional. If it is not required, then set pxHigherPriorityTaskWoken to NULL.
The portYIELD_FROM_ISR() and portEND_SWITCHING_ISR() Macros
taskYIELD() is a macro that can be called in a task to request a context switch. portYIELD_FROM_ISR() and portEND_SWITCHING_ISR() are both interrupt safe versions of taskYIELD(). portYIELD_FROM_ISR() and portEND_SWITCHING_ISR() are both used in the same way, and do the same thing1. Some FreeRTOS ports only provide one of the two macros. Newer FreeRTOS ports provide both macros. The examples in this book use portYIELD_FROM_ISR().
portEND_SWITCHING_ISR( xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
If the portYIELD_FROM_ISR() xHigherPriorityTaskWoken parameter is pdFALSE (zero), then a context switch is not requested, and the macro has no effect. If the portYIELD_FROM_ISR() xHigherPriorityTaskWoken parameter is not pdFALSE, then a context switch is requested, and the task in the Running state might change.
Deferred Interrupt Processing
It is normally considered best practice to keep ISRs as short as possible.
An interrupt service routine must record the cause of the interrupt, and clear the interrupt. Any other processing necessitated by the interrupt can often be performed in a task, allowing the interrupt service routine to exit as quickly as is practical. This is called ‘deferred interrupt processing’, because the processing necessitated by the interrupt is ‘deferred’ from the ISR to a task.
Deferring interrupt processing to a task also allows the application writer to prioritize the processing relative to other tasks in the application, and use all the FreeRTOS API functions.
If the priority of the task to which interrupt processing is deferred is above the priority of any other task, then the processing will be performed immediately, just as if the processing had been performed in the ISR itself.
There is no absolute rule as to when it is best to perform all processing necessitated by an interrupt in the ISR, and when it is best to defer part of the processing to a task.
Binary Semaphores Used for Synchronization
The interrupt safe version of the Binary Semaphore API can be used to unblock a task each time a particular interrupt occurs, effectively synchronizing the task with the interrupt.
This allows the majority of the interrupt event processing to be implemented within the synchronized task, with only a very fast and short portion remaining directly in the ISR.
As described in the previous section, the binary semaphore is used to ‘defer’ interrupt processing to a task.
if the interrupt processing is particularly time critical, then the priority of the deferred processing task can be set to ensure the task always preempts the other tasks in the system.
The ISR can then be implemented to include a call to portYIELD_FROM_ISR(), ensuring the ISR returns directly to the task to which interrupt processing is being deferred.
This has the effect of ensuring the entire event processing executes contiguously (without a break) in time, just as if it had all been implemented within the ISR itself.
The deferred processing task uses a blocking ‘take’ call to a semaphore as a means of entering the Blocked state to wait for the event to occur. When the event occurs, the ISR uses a ‘give’ operation on the same semaphore to unblock the task so that the required event processing can proceed.
‘Taking a semaphore’ and ‘giving a semaphore’ are concepts that have different meanings depending on their usage scenario. In this interrupt synchronization scenario, the binary semaphore can be considered conceptually as a queue with a length of one.
The queue can contain a maximum of one item at any time, so is always either empty or full (hence, binary).
By calling xSemaphoreTake(), the task to which interrupt processing is deferred effectively attempts to read from the queue with a block time, causing the task to enter the Blocked state if the queue is empty.
(延遲任務會藉由呼叫xSemaphoreTake()函式來進入block狀態,因為此函式作動的方式為他會去queue中讀取變數,若此queue中為空則將此任務推入block狀態)
When the event occurs, the ISR uses the xSemaphoreGiveFromISR() function to place a token (the semaphore) into the queue, making the queue full.
(當中斷事件發生時呼叫xSemaphoreGiveFromISR()函式,此函式會將一個數字輸入queue中)
This causes the task to exit the Blocked state and remove the token, leaving the queue empty once more.
(當queue中因為中斷事件放入一值後延遲任務會因為偵測到queue中有值而進入ready狀態準備被執行,並且queue自動將此傳入值清空,再次變為空的)
When the task has completed its processing, it once more attempts to read from the queue and, finding the queue empty, re-enters the Blocked state to wait for the next event.
The xSemaphoreCreateBinary() API Function
Before a semaphore can be used, it must be created. To create a binary semaphore, use the xSemaphoreCreateBinary() API function.
SemaphoreHandle_t xSemaphoreCreateBinary( void );
Returned value
If NULL is returned, then the semaphore cannot be created because there is insufficient heap memory available for FreeRTOS to allocate the semaphore data structures.
A non-NULL value being returned indicates that the semaphore has been created successfully. The returned value should be stored as the handle to the created semaphore.
The xSemaphoreTake() API Function
‘Taking’ a semaphore means to ‘obtain’ or ‘receive’ the semaphore. The semaphore can be taken only if it is available.
All the various types of FreeRTOS semaphore, except recursive mutexes, can be ‘taken’ using the xSemaphoreTake() function.
xSemaphoreTake() must not be used from an interrupt service routine.
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
xSemaphore
The semaphore being ‘taken’.
A semaphore is referenced by a variable of type SemaphoreHandle_t. It must be explicitly created before it can be used.
xTicksToWait
The maximum amount of time the task should remain in the Blocked state to wait for the semaphore if it is not already available.
If xTicksToWait is zero, then xSemaphoreTake() will return immediately if the semaphore is not available.
The block time is specified in tick periods, so the absolute time it represents is dependent on the tick frequency. The macro pdMS_TO_TICKS() can be used to convert a time specified in milliseconds to a time specified in ticks.
Setting xTicksToWait to portMAX_DELAY will cause the task to wait indefinitely (without a timeout) if INCLUDE_vTaskSuspend is set to 1 in FreeRTOSConfig.h.
Returned value
- pdPASS pdPASS is returned only if the call to xSemaphoreTake() was successful in obtaining the semaphore. If a block time was specified (xTicksToWait was not zero), then it is possible that the calling task was placed into the Blocked state to wait for the semaphore if it was not immediately available, but the semaphore became available before the block time expired.
- pdFALSE The semaphore is not available. If a block time was specified (xTicksToWait was not zero), then the calling task will have been placed into the Blocked state to wait for the semaphore to become available, but the block time expired before this happened.
The xSemaphoreGiveFromISR() API Function
Binary and counting semaphores can be ‘given’ using the xSemaphoreGiveFromISR() function.
xSemaphoreGiveFromISR() is the interrupt safe version of xSemaphoreGive(), so has the pxHigherPriorityTaskWoken parameter that was described at the start of this chapter.
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken );
xSemaphore
The semaphore being ‘given’.
A semaphore is referenced by a variable of type SemaphoreHandle_t, and must be explicitly created before being used.
pxHigherPriorityTaskWoken
It is possible that a single semaphore will have one or more tasks blocked on it waiting for the semaphore to become available. Calling xSemaphoreGiveFromISR() can make the semaphore available, and so cause a task that was waiting for the semaphore to leave the Blocked state. If calling xSemaphoreGiveFromISR() causes a task to leave the Blocked state, and the unblocked task has a priority higher than the currently executing task (the task that was interrupted), then, internally, xSemaphoreGiveFromISR() will set *pxHigherPriorityTaskWoken to pdTRUE.
If xSemaphoreGiveFromISR() sets this value to pdTRUE, then normally a context switch should be performed before the interrupt is exited. This will ensure that the interrupt returns directly to the highest priority Ready state task.
Returned value
- pdPASS pdPASS will be returned only if the call to xSemaphoreGiveFromISR() is successful.
- pdFAIL If a semaphore is already available, it cannot be given, and xSemaphoreGiveFromISR() will return pdFAIL.
留言
張貼留言