[FreeRTOS check-in station 5 opens] Interrupts and task switching, closing time is August 26
[Copy link]
Activity Overview: Click here to view (including activity encouragement and activity learning content)
Check-in start and end time for this site: August 24-August 26 (3 days)
Check-in tasks :
1. Read Cruelfox's fifth article: FreeRTOS Learning Notes (5) Interrupts and Task Switching
2. Please briefly describe the function of the portYIELD_FROM_ISR() macro call. ( Reading the FreeRTOS official documentation and viewing the implementation of this macro in the source code will help you understand it.)
FreeRTOS Learning Notes (5) Interrupts and Task Switching
For your convenience, I copied cruelfox's " FreeRTOS Learning Notes (5) Interrupts and Task Switching " here~
After FreeRTOS has the task's memory resource - stack management mechanism, can perform context switching of CPU execution according to task status and priority, and provides inter-task communication channels to achieve necessary task synchronization and mutual exclusion, multiple tasks can work together. However, since it is called a Real-Time operating system, it also needs to be able to respond quickly to external (hardware) events. Especially for applications on microcontrollers, after a hardware interrupt (IRQ) is generated, it is necessary for the operating system to immediately wake up a task to handle this event. From the perspective of tasks, there are actually many tasks that need to be scheduled according to hardware events (such as transmission completion, device ready, data received, etc.), otherwise it will keep testing the hardware device status register flag bit, wasting CPU time.
The time slice management of FreeRTOS actually borrows the timer interrupt behind the scenes. Otherwise, it is impossible for a task to be interrupted without applying for scheduling and execute other tasks of the same priority. Similarly, any hardware interrupt will execute the corresponding interrupt service routine (ISR, also called IRQ Handler). After the ISR is executed, will it return to the current task or schedule other tasks? This is entirely determined by the ISR.
1. ISR is independent of all tasks
Although in effect, ISR, that is, interrupt service routine, serves the function of a certain task, it must be emphasized first: ISR code does not belong to any part of FreeRTOS task code. Each ISR is a C language function, but it is not a task and will not be called by any task.
ISR uses the stack differently from tasks. As mentioned in the previous series, FreeRTOS allocates independent stack space for each task to save local variables of functions, etc. When an interrupt occurs, some registers of the CPU will be saved to the current stack (rather than the stack of a specified task), and then the ISR program will be executed. If the code of a task is currently being executed, the stack of the task will be occupied; if the code of another ISR is currently being executed, that is, interrupt nesting occurs, then the stack of the task that was interrupted earlier may continue to be used (Note: This is platform-related. For the implementation on the ARM Cortex-m series platform, FreeRTOS allows the task to run in thread mode, using PSP as the stack pointer, and the ISR will switch to handler mode, using MSP as the stack pointer, so all ISRs will share a stack).
The execution of the ISR can be independent of the FreeRTOS kernel. As long as the FreeRTOS API is not used in the ISR, FreeRTOS will not know that the interrupt has occurred, because it can save the scene and restore the scene after execution regardless of where the current stack is. Similarly, the execution of the ISR itself will not cause any task switching. When the FreeRTOS code is introduced into an existing project, the original ISR can still operate without modification.
The ISR does not change the state of the current task. Although the execution of the currently running task is suspended after the IRQ occurs, and the CPU switches to execute the ISR code, the state of the current task is still Running, and it does not change to other states - this is obviously different from the task being preempted. Even if the FreeRTOS API is called in the ISR to wake up other tasks with higher priority than the current task (change to the Ready state), the task switching operation will be performed after the ISR returns, and the running task will be reselected. In fact, the ISR does not know what the currently running task is, and it is meaningless to actively change the current task state.
2. Critical Section Concept
When I analyzed the FreeRTOS implementation details earlier, I encountered the two calls taskENTER_CRITICAL() and taskEXIT_CRITICAL() many times. From the name, it means that a very important operation is to be performed at this time, which is not allowed to be interrupted, such as accessing the task status list. If it is not handled in this way, the data to be accessed may be overwritten in the middle, or the data modification is not completed and is accessed by other tasks or the FreeRTOS kernel, which will cause erroneous results. Therefore, a section of code is defined as a critical section, and taskENTER_CRITICAL() and taskEXIT_CRITICAL() are used to protect it, prohibiting task scheduling and prohibiting other interrupt ISRs from accessing FreeRTOS core data.
After this treatment, this section of code is temporarily given a very high priority, regardless of the priority of the current task. Guess, first mask the interrupt, and then allow it after execution, isn't it? In fact, it is not that simple. Let's take a look at how FreeRTOS defines these two operations.
There are two macro definitions in the task.h header file:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
Next, in the portmacro.h file (CM3 platform), it is defined as
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
Find the implementation of vPortEnterCritical() and vPortExitCritical() functions in the port.c file:
- void vPortEnterCritical(void)
- {
- portDISABLE_INTERRUPTS();
- uxCriticalNesting++;
- if( uxCriticalNesting == 1 )
- {
- configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
- }
- }
- void vPortExitCritical(void)
- {
- configASSERT(uxCriticalNesting);
- uxCriticalNesting--;
- if( uxCriticalNesting == 0 )
- {
- portENABLE_INTERRUPTS();
- }
- }
Copy code
There is a little more operation than disabling interrupts: a counting variable is used. The configASSERT() code can be removed and ignored. So why do we need to count? The answer is to nest calls. After a certain number of vPortEnterCritical() calls, the same number of vPortExitCritical() calls are required to allow interrupts.
Let's take a look at how to disable interrupts on the Cortex-m3 platform:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
Take a closer look at the functions implemented in the assembly code
- portFORCE_INLINE static void vPortRaiseBASEPRI( void )
- {
- uint32_t ulNewBASEPRI;
- __asm volatile
- (
- " mov %0, %1 undefined" \
- " msr basepri, %0 undefined" \
- " isb undefined" \
- "dsb undefined" \
- :"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
- );
- }
Copy code
This operation modifies the BASEPRI register and masks some hardware interrupts: interrupts with a priority equal to or lower than configMAX_SYSCALL_INTERRUPT_PRIORITY. Why is it only partially masked? Because if an interrupt ISR does not access FreeRTOS core data or call any FreeRTOS API, then its interruption is harmless. However, partially masking interrupts requires hardware support. For example, there is no BASEPRI register on the ARM Cortex-m0 platform, so the corresponding implementation code is simple:
#define portDISABLE_INTERRUPTS() __asm volatile (" cpsid i ")
#define portENABLE_INTERRUPTS() __asm volatile (" cpsie i ")
There can also be a critical section in the ISR, but it is necessary to call taskENTER_CRITICAL_FROM_ISR() and taskEXIT_CRITICAL_FROM_ISR(), whose parameters and return values are different, and the current interrupt level status needs to be saved and restored. On the Cortex-m3 platform, the corresponding value is to save and restore the BASEPRI register.
The meaning of the value of configMAX_SYSCALL_INTERRUPT_PRIORITY is that only ISRs with a priority not higher than this one are allowed to call the FreeRTOS API, that is, just because they have the opportunity to call the API, they must be blocked when entering the critical section. As for the higher the interrupt priority, whether the value is larger or smaller depends on the hardware platform. Be sure not to confuse the interrupt priority (a hardware concept) with the task priority of FreeRTOS.
3. FreeRTOS API functions that can be used in ISR
In the FreeRTOS documentation, it has always been emphasized that in ISR, API functions ending with FromISR must be called , and regular APIs cannot be called. This is because the execution environment and tasks of ISR are different. In addition to considering efficiency, some APIs have to be distinguished. The
API called in the ISR requires a quick return and does not allow waiting. The system does not allow interrupt processing to take up too much time, let alone waiting for other interrupts to occur. Some APIs cannot be used in ISRs because they have blocking functions, or the functions are changed, including parameter passing requirements.
Task scheduling is optional in ISRs. For example, the operation of the communication object may wake up other tasks with higher priority than the current task; if it is performed in a task, it will immediately cause a task switch. However, in ISRs, it may not be necessary to switch tasks so frequently, and it is beneficial to operating efficiency to treat it as a freely selectable operation. This xxxxFromISR() API will have a BaseType_t *pxHigherPriorityTaskWoken parameter to determine whether a higher priority task has been awakened, and then the ISR will decide whether to switch tasks.
I have extracted the ISR-specific API functions from the manual and their corresponding ordinary API versions, and listed them in the table below. Some ordinary versions of APIs have a parameter to specify the waiting time, which is cancelled in the ISR version.
ISR dedicated function name |
Conventional API correspondence |
Other Features |
xTaskGetTickCountFromISR |
xTaskGetTickCount |
|
xTaskNotifyFromISR |
xTaskNotify |
Additional parameters |
xTaskNotifyAndQueryFromISR |
xTaskNotifyAndQuery |
Additional parameters |
vTaskNotifyGiveFromISR |
xTaskNotifyGive |
Additional parameters |
xTaskResumeFromISR |
vTaskResume |
Return Value |
xQueueIsQueueEmptyFromISR |
--- |
|
xQueueIsQueueFullFromISR |
--- |
|
uxQueueMessagesWaitingFromISR |
uxQueueMessagesWaiting |
|
xQueueOverwriteFromISR |
xQueueOverwrite |
Additional parameters |
xQueuePeekFromISR |
xQueuePeek |
Cancel Wait |
xQueueReceiveFromISR |
xQueueReceive |
Additional parameters, cancel waiting |
xQueueSelectFromSetFromISR |
xQueueSelectFromSet |
Cancel Wait |
xQueueSendFromISR |
xQueueSend |
Additional parameters, cancel waiting |
xQueueSendToBackFromISR |
xQueueSendToBack |
Additional parameters |
xQueueSendToFrontFromISR |
xQueueSendToFront |
Additional parameters |
xSemaphoreGiveFromISR |
xSemaphoreGive |
Additional parameters |
xSemaphoreTakeFromISR |
xSemaphoreTake |
Additional parameters, cancel waiting |
xTimerChangePeriodFromISR |
xTimerChangePeriod |
Additional parameters, cancel waiting |
xTimerPendFunctionCallFromISR |
xTimerPendFunctionCall |
Additional parameters, cancel waiting |
xTimerResetFromISR |
xTimerReset |
Additional parameters, cancel waiting |
xTimerStartFromISR |
xTimerStart |
Additional parameters, cancel waiting |
xTimerStopFromISR |
xTimerStop |
Additional parameters, cancel waiting |
xEventGroupClearBitsFromISR |
xEventGroupClearBits |
Executed in Daemon Task |
xEventGroupGetBitsFromISR |
xEventGroupGetBits |
Executed in Daemon Task |
xEventGroupSetBitsFromISR |
xEventGroupSetBits |
Additional parameters, executed in Daemon Task |
When the ISR needs task scheduling (for example, when an API returns *pxHigherPriorityTaskWoken equal to pdTRUE), portYIELD_FROM_ISR(pdTRUE) should be executed before the ISR returns to let the scheduler switch tasks. For the Cortex-m3 platform, portYIELD_FROM_ISR() implements scheduling in the same way as portYIELD(), except for checking whether the parameter is true, which is to set the PendSV bit in the NVIC (interrupt controller). In this way, after all hardware interrupt request ISRs return, the ISR of the PendSV interrupt is executed and the scheduler performs task switching. (See my previous post "FreeRTOS Learning Notes (3) Task Status and Switching")
Using ISR to trigger task scheduling logically assigns part of the processing of external interrupt events to a certain task (or some tasks), and only performs some urgent and time-saving processing in the ISR (such as reading the registers of hardware devices, clearing flags, transferring buffer data, etc.). The remaining work handled by the task is then managed by the FreeRTOS scheduler according to the task priority. From the software's point of view, it is as if the task is waiting for an interrupt to occur and then immediately handles it.
4. Daemon Task
It is natural to assign the more complex and time-consuming work of hardware interrupt processing to a separate task, but FreeRTOS also provides a mechanism to avoid creating a separate task. This is to use the system's Daemon Task.
The xTimerPendFunctionCallFromISR() function "submits" a normal function as a parameter to the system service, allowing the system's own Daemon Task to execute this function. When submitting, specify two parameters to pass to this function. Daemon Task is managed by the scheduler, and its task priority is specified by configTIMER_TASK_PRIORITY. When Daemon Task executes the submitted function, it depends on whether the system is idle. When it gets the execution opportunity, it will take out the function entry address and parameters to be executed from the command queue to execute. Borrowing a picture from the manual:
The FreeRTOS Event Group implementation borrows the Daemon Task to handle operations in the ISR, such as the xEventGroupSetBitsFromISR() call listed in the table above. The reason described in the manual is that this is not a "deterministic operation" (it may take too long). In event_groups.h, #define
xEventGroupClearBitsFromISR(xEventGroup, uxBitsToClear) \
xTimerPendFunctionCallFromISR(vEventGroupClearBitsCallback, \
(void *) xEventGroup, (uint32_t)uxBitsToClear, NULL)
is defined
, thus delaying a FromISR call to the Daemon Task to execute the normal version call.
The main body of the Daemon Task is as follows:
- static void prvTimerTask( void *pvParameters )
- {
- TickType_t xNextExpireTime;
- BaseType_t xListWasEmpty;
- #if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
- {
- extern void vApplicationDaemonTaskStartupHook( void );
- }
- #endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */
- for( ;; )
- {
- xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
- prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty);
- prvProcessReceivedCommands();
- }
- }
Copy code
The loop is processing software timer events, sorting them by expiration time (executing the corresponding functions). This involves the function of software timer - FreeRTOS, which will be studied later. In order to understand how the function submitted from ISR is executed, let's first look at what xTimerPendFunctionCallFromISR() does:
- BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, BaseType_t *pxHigherPriorityTaskWoken )
- {
- DaemonTaskMessage_t xMessage;
- BaseType_t xReturn;
- xMessage.xMessageID = tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR;
- xMessage.u.xCallbackParameters.pxCallbackFunction = xFunctionToPend;
- xMessage.u.xCallbackParameters.pvParameter1 = pvParameter1;
- xMessage.u.xCallbackParameters.ulParameter2 = ulParameter2;
- xReturn = xQueueSendFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
- tracePEND_FUNC_CALL_FROM_ISR( xFunctionToPend, pvParameter1, ulParameter2, xReturn );
- return xReturn;
- }
Copy code
It is easy to understand. Fill in the function address and parameters to be executed in the DaemonTaskMessage_t data structure and add it to the xTimerQueue queue. In the prvProcessTimerOrBlockTask() function in the task loop above, there is a call (the complete code is not listed here):
vQueueWaitForMessageRestricted(xTimerQueue, (xNextExpireTime - xTimeNow), xListWasEmpty);
that is, wait for messages in the xTimerQueue queue until the next software timer expires. Therefore, when the Daemon Task receives the message sent from the ISR, it will execute the command (function call) specified by the message.
Summary
In order to support real-time response to hardware events, the interrupt service routine (ISR) must be executed as early as possible. Because the system may have multiple interrupts, the ISR needs to be written as short as possible and return after executing the key operations to allow other interrupts to be processed. FreeRTOS provides a series of mechanisms to allow ISR to hand over operations that need to be processed but are not so urgent to tasks to complete, and reasonably allocate CPU resources.
Welcome to reply to this post and the question cruelfox left for everyone: Please briefly describe the function of the portYIELD_FROM_ISR() macro call. ( Reading the FreeRTOS official documentation and viewing the implementation of this macro in the source code will help you understand it.)
|