How RTOS Kernel Manages Interrupts

Source | Mculover666

To develop “advanced” applications based on RTOS, many principles of the kernel must be mastered.

This article will explain how the RTOS kernel manages interrupts? How should users write interrupt handler functions? And how can users set critical sections?

1. Review of Knowledge — Interrupts

1.1. Interrupt Mechanism

The interrupt mechanism is an important mechanism for embedded systems to implement “asynchronous event handling”. In summary, it can be divided into three steps:

  • ① The peripheral generates an interrupt request (such as GPIO external interrupt, serial port interrupt, timer interrupt, etc.)
  • ② The CPU determines whether to respond to the interrupt request. If responding, the CPU stops executing the current program and executes the interrupt service routine (ISR);
  • ③ After the interrupt service routine is completed, it returns to the interrupted point and continues executing the program that was interrupted;

When executing low-priority interrupt service routines, if the CPU allows interrupt nesting, it will execute higher-priority interrupt service routines, returning sequentially, as follows: How RTOS Kernel Manages Interrupts

1.2. Interrupt Management in Cortex-M Kernel

The ARM Cortex-M kernel has a dedicated peripheral for managing interrupts — NVIC, which stands for Nested Vectored Interrupt Controller, used to determine interrupt priority.

In the ARM Cortex-M kernel, NVIC is configured with an 8-bit register, which can configure a total of levels of interrupts. However, when ST produced STM32, they found that a small microcontroller could not use so many levels, which was a waste, so they set the low 4 bits of this register to 0 and only used the high 4 bits for configuration, resulting in STM32 having only level interrupts.

After simplifying to 16 levels of interrupts, ST found that the rich peripherals inside STM32 were still inconvenient to configure, so they manually grouped these 4 bits into 5 groups:

Priority Group Preemptive Priority Bits Sub-Priority Bits
NVIC_PriorityGroup_0 0 bit 4 bit
NVIC_PriorityGroup_1 1 bit 3 bit
NVIC_PriorityGroup_2 2 bit 2 bit
NVIC_PriorityGroup_3 3 bit 1 bit
NVIC_PriorityGroup_4 4 bit 0 bit

These 5 interrupt grouping rules are arbitrary, and any rule can be used. The default rule used by STM32 is NVIC_PriorityGroup_0.

During program execution, STM32 determines the interrupt priority according to the following rules:

  • First, check the preemptive priority; the smaller the number, the higher the priority;
  • If the preemptive priority is the same, check the sub-priority; similarly, the smaller the number, the higher the priority;

1.3. Interrupt Handling in STM32 HAL Library

The HAL library provided by STM32 includes default interrupt handling functions. For external interrupts, as an example: How RTOS Kernel Manages Interrupts Through the implementation mechanism of the HAL library, it will ultimately call the weakly defined callback function:

__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { UNUSED(GPIO_Pin); }

This allows users to redefine this callback function as the interrupt handler:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { switch(GPIO_Pin) { case GPIO_PIN_2: HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET); break; default: break; } }

2. Management of Interrupts by RTOS Kernel

2.1. Changes in Interrupt Settings?

The settings for enabling interrupts and setting interrupt priorities ultimately come down to setting the values of registers. Whatever changes you want, it works the same way in RTOS as it does in bare metal!

2.2. Changes in Interrupt Handling?

In the RTOS kernel, there is not just a single main function running, but rather “multiple tasks existing simultaneously”, scheduled and executed preemptively by the kernel based on different task priorities.

As mentioned in the second article, the priority of the PendSV exception (task switching/scheduling) in the RTOS kernel is set to the lowest, which has three advantages:

① It allows “any task” to be interrupted by interrupt requests generated by peripherals (assuming interrupts have been enabled);

② It avoids task switching within interrupt handlers;

③ It allows interrupt handlers to nest normally according to interrupt priority without being affected by tasks;

Thus, even with the RTOS kernel, “the entire process from interrupt generation to executing the interrupt handler is no different from bare metal programs”.

2.3. Changes in Interrupt Return

In bare metal programs, interrupt handlers (including nested executions) will ultimately return to the point where the main function was interrupted.

However, in RTOS, since multiple tasks exist simultaneously, when the interrupt handler finishes executing and returns, it is unclear which task to return to???

It’s actually simple. Let me explain slowly~

Because current RTOS kernels use “preemptive scheduling mechanisms”, if the interrupt handler returns to the original task after execution, and there are higher-priority tasks in the ready list, it violates the rules of preemptive scheduling.

Therefore, users need to call the tos_knl_irq_leave function just before exiting the interrupt service routine. In this function, “the task with the highest priority in the current kernel ready list is found, and a switch is performed to execute it, forcibly changing the normal return path of the interrupt program” to comply with the rules of preemptive scheduling.

This method has a flaw. When interrupts occur in a nested manner, finishing the highest-priority interrupt handler will jump out, missing all lower-priority interrupt handlers, which is very dangerous, as shown in the figure: How RTOS Kernel Manages Interrupts To solve this problem, the RTOS kernel has devised a clever method: “set a global variable to record the current number of interrupt nesting, and only when it is 0 will it jump out to execute the highest-priority task; otherwise, it returns normally.”

Next, let’s look at the source code!

① Definition of the variable

The global variable used to record this value in TencentOS-tiny is defined in tos_global.c:

k_nesting_t         k_irq_nest_cnt              = (k_nesting_t)0;

Where the definition of the type k_nesting_t is as follows:

typedef uint8_t             k_nesting_t;

The maximum value of this variable is K_NESTING_LIMIT_IRQ, indicating the maximum number of interrupt nestings allowed in TencentOS-tiny:

#define K_NESTING_LIMIT_IRQ             (k_nesting_t)250u

② Usage of the variable

When entering the interrupt service function, the user needs to call the following API to increment this variable:

__API__ void tos_knl_irq_enter(void) { if (!tos_knl_is_running()) { return; } if (unlikely(k_irq_nest_cnt >= K_NESTING_LIMIT_IRQ)) { return; } ++k_irq_nest_cnt; }

When exiting the interrupt service function, the following API should be called to decrement this variable. If the variable value is 0, it indicates that this is the last layer of the interrupt, and the operation to schedule to the highest-priority task in the system begins; otherwise, it returns directly:

__API__ void tos_knl_irq_leave(void) { TOS_CPU_CPSR_ALLOC(); if (!tos_knl_is_running()) { return; } TOS_CPU_INT_DISABLE(); if (!knl_is_inirq()) { TOS_CPU_INT_ENABLE(); return; } --k_irq_nest_cnt; if (knl_is_inirq()) { TOS_CPU_INT_ENABLE(); return; } if (knl_is_sched_locked()) { TOS_CPU_INT_ENABLE(); return; } k_next_task = readyqueue_highest_ready_task_get(); if (knl_is_self(k_next_task)) { TOS_CPU_INT_ENABLE(); return; } cpu_irq_context_switch(); TOS_CPU_INT_ENABLE(); }

Careful readers may notice that in this function, the task switch is called not with the ordinary cpu_context_switch, but with cpu_irq_context_switch.

This is because when the CPU calls the interrupt handler, the context of the interrupted task has already been automatically saved, so only the context switch operation is needed here.

To summarize:

“The RTOS interrupt handler does not return to the originally interrupted task, but to the highest-priority task in the system. The only differences in writing the interrupt handler compared to bare metal programs are two points: the rest are identical”:

  • ① Call tos_knl_irq_enter once after entering;
  • ② Call tos_knl_irq_leave once before exiting;

For example, if I were to write the key interrupt handler from the knowledge review in TencentOS-tiny, it would be as follows:

void EXTI2_IRQHandler(void) { tos_knl_irq_enter(); HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); tos_knl_irq_leave(); }

3. Critical Sections in RTOS

Critical sections sound impressive and advanced, but they are actually just a paper tiger~

In RTOS, there are some pieces of code that “should not be interrupted by interrupt programs during execution”, and these pieces of code are called critical sections.

Writing critical section code is also very simple and straightforward: “Directly disable interrupts before the critical section code and enable interrupts after the critical section code”.

For example, in TencentOS-tiny, the method to write critical section code is as follows:

void task1_entry(void) { //…… /* Start of critical section, disable interrupts */ TOS_CPU_INT_DISABLE(); /* .... The code in between is called the critical section and will not be interrupted by any interrupts */ /* End of critical section, enable interrupts */ TOS_CPU_INT_ENABLE(); //…… }

4. Summary

As per the usual practice, let’s summarize the points that can be applied in program writing through the study of this article:

“In RTOS, writing interrupt handler functions requires calling tos_knl_irq_enter after entering and tos_knl_irq_leave before exiting.”

“In RTOS, more complex interrupt handlers should be designed as tasks that activate/wake up the task for execution in the interrupt handler.”

③ Do not call various APIs in critical section code, as it will affect system real-time performance and may cause system crashes.

———— END ————
How RTOS Kernel Manages Interrupts
● Column “Embedded Tools”
● Column “Embedded Development”
● Column “Keil Tutorials”
● Selected Tutorials on Embedded Topics
Follow our public accountReply “Add Group” to join the technical exchange group according to the rules, reply “1024” to see more content.
Click “Read Original” to see more shares.

Leave a Comment

Your email address will not be published. Required fields are marked *