Interrupt Management

What is an interrupt? A simple explanation is that the system is processing a normal event, and is suddenly interrupted by another emergency event that needs to be handled immediately. The system turns to handle the emergency event, and after it is handled, it resumes the interrupted event. In life, we often encounter such scenarios:

When you are reading a book attentively, a phone call suddenly comes in, so you write down the page number of the book, go to answer the phone, and then continue reading from the previous page after hanging up the phone. This is a typical interruption process.

The call is from your teacher, asking you to hand in your homework quickly. You judge that handing in homework has a higher priority than reading a book, so you do your homework first after hanging up the phone, and then continue reading the book from the page you just left after handing in your homework. This is a typical process of task scheduling during interruption.

These scenarios are also common in embedded systems. When the CPU is processing internal data, an emergency occurs in the outside world, requiring the CPU to suspend the current work and handle this asynchronous event . After processing, it returns to the original interrupted address and continues the original work. This process is called an interrupt. The system that implements this function is called an interrupt system , and the source of the request for CPU interrupt is called an interrupt source . An interrupt is an exception. An exception is any event that causes the processor to deviate from normal operation and execute special code. If it is not handled in time, the system will fail at best, or even cause a devastating system paralysis. Therefore, correctly handling exceptions and avoiding errors is a very important part of improving software robustness (stability). The following figure is a simple interrupt diagram.

Interrupt handling is closely related to the CPU architecture. Therefore, this chapter will first introduce the ARM Cortex-M CPU architecture, and then introduce the interrupt management mechanism of RT-Thread in combination with the Cortex-M CPU architecture. After reading this chapter, you will have a deep understanding of the interrupt handling process of RT-Thread, how to add an interrupt service routine (ISR), and related precautions.

Unlike the old classic ARM processors (such as ARM7, ARM9), the ARM Cortex-M processor has a very different architecture. Cortex-M is a family series, including Cortex M0/M3/M4/M7. There are some differences between each model, such as Cortex-M4 has more floating-point calculation functions than Cortex-M3, but their programming models are basically the same. Therefore, the part of this book that introduces interrupt management and porting will not make too fine a distinction between Cortex M0/M3/M4/M7. This section mainly introduces the architecture related to RT-Thread interrupt management.

The register group of the Cortex-M series CPU includes 16 general register groups R0~R15 and several special function registers, as shown in the figure below.

R13 in the general register group is used as the stack pointer register (Stack Pointer, SP); R14 is used as the link register (Link Register, LR), which is used to store the return address when calling a subroutine; R15 is used as the program counter (Program Counter, PC), where the stack pointer register can be the main stack pointer (MSP) or the process stack pointer (PSP).

The special function registers include the program status word register group (PSRs), the interrupt mask register group (PRIMASK, FAULTMASK, BASEPRI), and the control register (CONTROL). The special function registers can be accessed through the MSR/MRS instructions, for example:

MRS R0, CONTROL ; 读取 CONTROL 到 R0 中
MSR CONTROL, R0 ; 写入 R0 到 CONTROL 寄存器中copymistakeCopy Success

The program status word register stores arithmetic and logic flags, such as negative flag, zero result flag, overflow flag, etc. The interrupt mask register group controls the interrupt enable of Cortex-M. The control register is used to define the privilege level and which stack pointer is currently used.

If it is a Cortex-M4 or Cortex-M7 with a floating point unit, the control register is also used to indicate whether the floating point unit is currently in use. The floating point unit contains 32 floating point general registers S0~S31 and a special FPSCR register (Floating point status and control register).

Cortex-M introduces the concepts of operation mode and privilege level, namely thread mode and processing mode. If an exception or interrupt is processed, the processor mode is entered, and otherwise the thread mode is entered.

Cortex-M has two operating levels, privileged level and user level. The thread mode can work at the privileged level or user level, while the processing mode always works at the privileged level and can be controlled by the CONTROL special register. The working mode state switching is shown in the figure above.

The stack register SP of Cortex-M corresponds to two physical registers MSP and PSP. MSP is the main stack and PSP is the process stack. Processing mode always uses MSP as the stack. Thread mode can choose to use MSP or PSP as the stack, which is also controlled by the CONTROL special register. After reset, Cortex-M enters thread mode, privilege level, and uses MSP stack by default.

The Cortex-M interrupt controller is called NVIC (Nested Vectored Interrupt Controller), which supports interrupt nesting. When an interrupt is triggered and the system responds, the processor hardware automatically pushes the context registers of the current running location into the interrupt stack. This part of the registers includes PSR, PC, LR, R12, R3-R0 registers.

When the system is servicing an interrupt, if a higher priority interrupt is triggered, the processor will also interrupt the currently running interrupt service routine, and then automatically save the PSR, PC, LR, R12, R3-R0 registers of the interrupt service routine context to the interrupt stack.

PendSV is also called a suspendable system call. It is an exception that can be suspended like a normal interrupt. It is specifically used to assist the operating system in context switching. The PendSV exception is initialized as the lowest priority exception. Every time a context switch is required, the PendSV exception is manually triggered and the context switch is performed in the PendSV exception handling function. In the next chapter "Kernel Porting", the detailed process of using the PendSV mechanism to perform operating system context switching will be introduced in detail.

The interrupt vector table is the entry point for all interrupt handlers. The following figure shows the interrupt handling process of the Cortex-M series: a function (user interrupt service routine) is associated with an interrupt vector in a virtual interrupt vector table. When the interrupt vector corresponds to an interrupt, the attached user interrupt service routine will be called and executed.

On the Cortex-M core, all interrupts are processed using the interrupt vector table, that is, when an interrupt is triggered, the processor will directly determine which interrupt source it is, and then jump directly to the corresponding fixed position for processing. Each interrupt service routine must be arranged together and placed at a unified address (this address must be set to the interrupt vector offset register of the NVIC). The interrupt vector table is generally defined by an array or given in the start code. The default is given by the start code:

  __Vectors     DCD     __initial_sp             ; Top of Stack
                DCD     Reset_Handler            ; Reset 处理函数
                DCD     NMI_Handler              ; NMI 处理函数
                DCD     HardFault_Handler        ; Hard Fault 处理函数
                DCD     MemManage_Handler        ; MPU Fault 处理函数
                DCD     BusFault_Handler         ; Bus Fault 处理函数
                DCD     UsageFault_Handler       ; Usage Fault 处理函数
                DCD     0                        ; 保留
                DCD     0                        ; 保留
                DCD     0                        ; 保留
                DCD     0                        ; 保留
                DCD     SVC_Handler              ; SVCall 处理函数
                DCD     DebugMon_Handler         ; Debug Monitor 处理函数
                DCD     0                        ; 保留
                DCD     PendSV_Handler           ; PendSV 处理函数
                DCD     SysTick_Handler          ; SysTick 处理函数

… …

NMI_Handler             PROC
                EXPORT NMI_Handler              [WEAK]
                B       .
                ENDP
HardFault_Handler       PROC
                EXPORT HardFault_Handler        [WEAK]
                B       .
                ENDP
… …copymistakeCopy Success

Please note the [WEAK] mark after the code, which is the symbol weakening mark. The symbol before [WEAK] (such as NMI_Handler, HardFault_Handler) will be weakened. If the entire code encounters a symbol with the same name during linking (such as a function with the same name as NMI_Handler), the code will use the symbol that is not weakened (the function with the same name as NMI_Handler), and the code related to the weakened symbol will be automatically discarded.

Taking the SysTick interrupt as an example, in the system startup code, you need to fill in the SysTick_Handler interrupt entry function, and then implement this function to respond to the SysTick interrupt. The interrupt handling function sample program is as follows:

void SysTick_Handler(void)
{
    /* enter interrupt */
    rt_interrupt_enter();

    rt_tick_increase();

    /* leave interrupt */
    rt_interrupt_leave();
}copymistakeCopy Success

In RT-Thread interrupt management, the interrupt handler is divided into three parts: interrupt leader, user interrupt service program, and interrupt follow-up program, as shown in the following figure:

Interrupt preamble

The main tasks of the interrupt leader are as follows:

1) Save the CPU interrupt scene. This part is related to the CPU architecture, and the implementation methods of different CPU architectures are different.

For Cortex-M, this work is done automatically by hardware. When an interrupt is triggered and the system responds, the processor hardware will automatically push the context registers of the current running part into the interrupt stack. This part of the registers includes PSR, PC, LR, R12, R3-R0 registers.

2) Notify the kernel to enter the interrupt state and call the rt_interrupt_enter() function, which adds 1 to the global variable rt_interrupt_nest to record the number of interrupt nesting levels. The code is shown below.

void rt_interrupt_enter(void)
{
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest ++;
    rt_hw_interrupt_enable(level);
}copymistakeCopy Success

User interrupt service routine

In the user interrupt service routine (ISR), there are two situations. The first situation is that no thread switching is performed. In this case, the user interrupt service routine and the subsequent interrupt program exit the interrupt mode after running and return to the interrupted thread.

Another situation is that thread switching is required during interrupt handling. In this case, the rt_hw_context_switch_interrupt() function is called to perform context switching. This function is related to the CPU architecture, and the implementation methods of different CPU architectures are different.

In the Cortex-M architecture, the function implementation flow of rt_hw_context_switch_interrupt() is shown in the figure below. It will set the rt_interrupt_to_thread variable of the thread to be switched, and then trigger the PendSV exception (PendSV exception is specifically used to assist context switching and is initialized as the lowest priority exception). After the PendSV exception is triggered, the PendSV exception interrupt handler will not be executed immediately because the interrupt is still being processed. Only when the interrupt follow-up program is completed and the interrupt processing is actually exited, the PendSV exception interrupt handler will be entered.

Interrupt subsequent programs

The main tasks of interrupting the subsequent program are:

1 Notifies the kernel to leave the interrupt state by calling the rt_interrupt_leave() function and reducing the global variable rt_interrupt_nest by 1. The code is as follows.

void rt_interrupt_leave(void)
{
    rt_base_t level;

    level = rt_hw_interrupt_disable();
    rt_interrupt_nest --;
    rt_hw_interrupt_enable(level);
}copymistakeCopy Success

2 Restore the CPU context before the interrupt. If no thread switch is performed during the interrupt processing, the CPU context of the from thread is restored. If a thread switch is performed during the interrupt, the CPU context of the to thread is restored. This part of the implementation is related to the CPU architecture. The implementation methods of different CPU architectures are different. The implementation process in the Cortex-M architecture is shown in the figure below.

When interrupt nesting is allowed, during the execution of the interrupt service program, if a high-priority interrupt occurs, the execution of the current interrupt service program will be interrupted to execute the interrupt service program of the high-priority interrupt. When the high-priority interrupt is processed, the interrupted interrupt service program will continue to be executed. If thread scheduling is required, the thread context switch will occur when all interrupt handlers are finished running, as shown in the following figure.

During the interrupt processing, before the system responds to the interrupt, the software code (or processor) needs to save the context of the current thread (usually saved in the thread stack of the current thread), and then call the interrupt service program to respond to and process the interrupt. When processing the interrupt (essentially calling the user's interrupt service program function), the interrupt processing function is likely to have its own local variables, which require corresponding stack space to save, so the interrupt response still requires a stack space as the context to run the interrupt processing function. The interrupt stack can be saved in the stack of the interrupted thread, and when exiting from the interrupt, it returns to the corresponding thread to continue execution.

The interrupt stack can also be completely separated from the thread stack, that is, each time an interrupt is entered, after saving the interrupt thread context, it switches to a new interrupt stack and runs independently. When the interrupt exits, the corresponding context is restored. Using an independent interrupt stack is relatively easier to implement, and it is also easier to understand and master the use of the thread stack (otherwise, space must be reserved for the interrupt stack. If the system supports interrupt nesting, it is also necessary to consider how much space should be reserved for nested interrupts).

RT-Thread provides an independent interrupt stack, that is, when an interrupt occurs, the interrupt pre-processor will replace the user's stack pointer with the interrupt stack space reserved by the system in advance, and restore the user's stack pointer when the interrupt exits. In this way, the interrupt will not occupy the thread's stack space, thereby improving the utilization of memory space, and as the number of threads increases, the effect of reducing memory usage becomes more obvious.

There are two stack pointers in the Cortex-M processor core. One is the main stack pointer (MSP), which is the default stack pointer and is used before running the first thread and in the interrupt and exception service routine. The other is the thread stack pointer (PSP), which is used in the thread. When the interrupt and exception service routine exits, the value of the second bit of the LR register is modified to 1, and the thread's SP is switched from MSP to PSP.

RT-Thread does not make any assumptions or restrictions on the processing time required for interrupt service routines, but like other real-time operating systems or non-real-time operating systems, users need to ensure that all interrupt service routines are completed in the shortest possible time (interrupt service routines have the highest priority in the system and will preempt all threads for execution). In this way, when interrupt nesting occurs or the corresponding interrupt source is shielded, the processing of other nested interrupts or the next interrupt signal of the interrupt source itself will not be delayed.

When an interrupt occurs, the interrupt service program needs to obtain the corresponding hardware status or data. If the interrupt service program then needs to perform simple processing on the status or data, such as the CPU clock interrupt, the interrupt service program only needs to add one to a system clock variable and then end the interrupt service program. Such interrupts often require a relatively short running time. However, for other interrupts, after obtaining the hardware status or data, the interrupt service program needs to perform a series of more time-consuming processing processes, and usually needs to divide the interrupt into two parts, namely the top half and the bottom half . In the top half, after obtaining the hardware status and data, the masked interrupt is turned on, a notification is sent to the relevant thread (which can be a semaphore, event, mailbox or message queue provided by RT-Thread), and then the interrupt service program is ended; and then, after receiving the notification, the relevant thread further processes the status or data, and this process is called bottom half processing .

In order to describe the implementation of bottom-half processing in RT-Thread in detail, we take a virtual network device receiving network data packets as an example, as shown in the following code, and assume that after receiving the data packet, the system's analysis and processing of the packet is a relatively time-consuming process that is much less important than the external interrupt source signal and can be processed without masking the interrupt source signal.

The program in this example creates an nwt thread. After starting, this thread will block on the nw_bh_sem signal. Once the semaphore is released, the next nw_packet_parser process will be executed to start the Bottom Half event processing.

/*
 * 程序清单:中断底半处理例子
 */

/* 用于唤醒线程的信号量 */
rt_sem_t nw_bh_sem;

/* 数据读取、分析的线程 */
void demo_nw_thread(void *param)
{
    /* 首先对设备进行必要的初始化工作 */
    device_init_setting();

    /*.. 其他的一些操作..*/

    /* 创建一个 semaphore 来响应 Bottom Half 的事件 */
    nw_bh_sem = rt_sem_create("bh_sem", 0, RT_IPC_FLAG_PRIO);

    while(1)
    {
        /* 最后,让 demo_nw_thread 等待在 nw_bh_sem 上 */
        rt_sem_take(nw_bh_sem, RT_WAITING_FOREVER);

        /* 接收到 semaphore 信号后,开始真正的 Bottom Half 处理过程 */
        nw_packet_parser (packet_buffer);
        nw_packet_process(packet_buffer);
    }
}

int main(void)
{
    rt_thread_t thread;

    /* 创建处理线程 */
    thread = rt_thread_create("nwt",demo_nw_thread, RT_NULL, 1024, 20, 5);

    if (thread != RT_NULL)
        rt_thread_startup(thread);
}copymistakeCopy Success

Next, let's take a look at how demo_nw_isr handles Top Half and enables Bottom Half, as shown in the following example.

void demo_nw_isr(int vector, void *param)
{
    /* 当 network 设备接收到数据后,陷入中断异常,开始执行此 ISR */
    /* 开始 Top Half 部分的处理,如读取硬件设备的状态以判断发生了何种中断 */
    nw_device_status_read();

    /*.. 其他一些数据操作等..*/

    /* 释放 nw_bh_sem,发送信号给 demo_nw_thread,准备开始 Bottom Half */
    rt_sem_release(nw_bh_sem);

    /* 然后退出中断的 Top Half 部分,结束 device 的 ISR */
}copymistakeCopy Success

From the two code snippets in the above example, we can see that the interrupt service routine completes the start and end of the interrupt Bottom Half by waiting and releasing a semaphore object. Since the interrupt processing is divided into two parts, Top and Bottom, the interrupt processing process becomes an asynchronous process. This part of the system overhead requires users to seriously consider whether the processing time of the interrupt service is greater than the time to send notifications to Bottom Half and process them when using RT-Thread.

In order to isolate the operating system from the underlying exceptions and interrupt hardware, RT-Thread encapsulates interrupts and exceptions into a set of abstract interfaces, as shown in the following figure:

The system associates the user's interrupt service program (handler) with the specified interrupt number. You can call the following interface to mount a new interrupt service program:

rt_isr_handler_t rt_hw_interrupt_install(int vector,
                                        rt_isr_handler_t  handler,
                                        void *param,
                                        char *name);copymistakeCopy Success

After calling rt_hw_interrupt_install(), when this interrupt source generates an interrupt, the system will automatically call the loaded interrupt service routine. The following table describes the input parameters and return values ​​of this function:

Input parameters and return value of rt_hw_interrupt_install()

parameter

describe

vector

vector is the mounted interrupt number

handler

Newly mounted interrupt service routine

param

param will be passed as a parameter to the interrupt service routine

name

The name of the interrupt

return

——

return

The handle of the interrupt service routine mounted before mounting this interrupt service routine

Note

Note: This API does not appear in every porting branch. For example, this API is not available in the porting branches of Cortex-M0/M3/M4.

The interrupt service routine is an operating environment that requires special attention. It runs in a non-threaded execution environment (generally a special operating mode of the chip (privileged mode)). In this operating environment, the operation of suspending the current thread cannot be used because the current thread does not exist. When performing related operations, there will be a print prompt message like "Function [abc_func] shall not used in ISR", which means that the function should not be called in the interrupt service routine).

Usually, before the ISR is ready to process an interrupt signal, we need to mask the interrupt source first. After the ISR processes the status or data, we should open the previously masked interrupt source in time.

Masking the interrupt source can ensure that the hardware status or data will not be disturbed in the subsequent processing. You can call the following function interface:

void rt_hw_interrupt_mask(int vector);copymistakeCopy Success

After calling the rt_hw_interrupt_mask function interface, the corresponding interrupt will be masked (usually when this interrupt is triggered, the interrupt status register will change accordingly, but it will not be sent to the processor for processing). The following table describes the input parameters of this function:

Input parameters of rt_hw_interrupt_mask()

parameter

describe

vector

The interrupt number to be masked

Note

Note: This API does not appear in every porting branch. For example, this API is not available in the porting branches of Cortex-M0/M3/M4.

In order to avoid losing the hardware interrupt signal as much as possible, you can call the following function interface to open the masked interrupt source:

void rt_hw_interrupt_umask(int vector);copymistakeCopy Success

After calling the rt_hw_interrupt_umask function interface, if the interrupt (and the corresponding peripheral) are configured correctly, the interrupt will be sent to the processor for processing after it is triggered. The following table describes the input parameters of this function:

Input parameters of rt_hw_interrupt_umask()

parameter

describe

vector

To open the masked interrupt number

Note

Note: This API does not appear in every porting branch. For example, this API is not available in the porting branches of Cortex-M0/M3/M4.

The global interrupt switch, also known as the interrupt lock, is the simplest way to prohibit multiple threads from accessing the critical section. That is, by disabling interrupts, the current thread is guaranteed not to be interrupted by other events (because the entire system no longer responds to external events that can trigger thread rescheduling). In other words, the current thread will not be preempted unless the thread actively gives up the processor control. When you need to disable interrupts for the entire system, you can call the following function interface:

rt_base_t rt_hw_interrupt_disable(void);copymistakeCopy Success

The following table describes the return values ​​of this function:

Return value of rt_hw_interrupt_disable()

return

describe

Interrupt Status

The interrupt status before the rt_hw_interrupt_disable function runs

Restoring interrupts is also called enabling interrupts. The rt_hw_interrupt_enable() function is used to "enable" interrupts. It restores the interrupt status before calling the rt_hw_interrupt_disable() function. If the interrupt status is disabled before calling the rt_hw_interrupt_disable() function, it will still be disabled after calling this function. Restoring interrupts is often used in pairs with disabling interrupts. The function interface is as follows:

void rt_hw_interrupt_enable(rt_base_t level);copymistakeCopy Success

The following table describes the input parameters of this function:

Input parameters of rt_hw_interrupt_enable()

parameter

describe

level

The interrupt status returned by the previous rt_hw_interrupt_disable

1) The method of using interrupt locks to operate critical sections can be applied to any occasion, and the other types of synchronization methods are all implemented by relying on interrupt locks. It can be said that interrupt locks are the most powerful and efficient synchronization method. However, the main problem with using interrupt locks is that the system will no longer respond to any interrupts during the interrupt shutdown period, and it cannot respond to external events. Therefore, the interrupt lock has a huge impact on the real-time performance of the system. When used improperly, it will cause the system to have no real-time performance at all (it may cause the system to completely deviate from the required time requirements); and when used properly, it will become a fast and efficient synchronization method.

For example, to ensure mutual exclusion for a line of code (such as an assignment), the fastest way is to use an interrupt lock instead of a semaphore or mutex:

    /* 关闭中断 */
    level = rt_hw_interrupt_disable();
    a = a + value;
    /* 恢复中断 */
    rt_hw_interrupt_enable(level);copymistakeCopy Success

When using an interrupt lock, you need to ensure that the interrupt is turned off for a very short time, such as a = a + value in the above code; you can also use another method, such as using a semaphore:

    /* 获得信号量锁 */
    rt_sem_take(sem_lock, RT_WAITING_FOREVER);
    a = a + value;
    /* 释放信号量锁 */
    rt_sem_release(sem_lock);copymistakeCopy Success

This code already uses an interrupt lock to protect the internal variables of the semaphore in the implementation of rt_sem_take and rt_sem_release. Therefore, for simple operations such as a = a + value;, using an interrupt lock will be simpler and faster.

2) The function rt_base_t rt_hw_interrupt_disable(void) and the function void rt_hw_interrupt_enable(rt_base_t level) generally need to be used in pair to ensure the correct interrupt status.

In RT-Thread, the API for switching global interrupts supports multi-level nesting. The code for simple nested interrupts is as follows:

Simple nested interrupt usage

#include <rthw.h>

void global_interrupt_demo(void)
{
    rt_base_t level0;
    rt_base_t level1;

    /* 第一次关闭全局中断,关闭之前的全局中断状态可能是打开的,也可能是关闭的 */
    level0 = rt_hw_interrupt_disable();
    /* 第二次关闭全局中断,关闭之前的全局中断是关闭的,关闭之后全局中断还是关闭的 */
    level1 = rt_hw_interrupt_disable();

    do_something();

    /* 恢复全局中断到第二次关闭之前的状态,所以本次 enable 之后全局中断还是关闭的 */
    rt_hw_interrupt_enable(level1);
    /* 恢复全局中断到第一次关闭之前的状态,这时候的全局中断状态可能是打开的,也可能是关闭的 */
    rt_hw_interrupt_enable(level0);
}copymistakeCopy Success

This feature can greatly facilitate code development. For example, if you disable interrupts in a function, then call some sub-functions and enable interrupts. These sub-functions may also contain code for enabling and disabling interrupts. Since the global interrupt API supports nested use, users do not need to do special processing for these codes.

When the entire system is interrupted and enters the interrupt processing function, it is necessary to notify the kernel that it has entered the interrupt state. In this case, the following interface can be used:

void rt_interrupt_enter(void);
void rt_interrupt_leave(void);copymistakeCopy Success

These two interfaces are used in the interrupt leader program and the interrupt follow-up program respectively, and both will modify the value of rt_interrupt_nest (interrupt nesting depth):

Whenever an interrupt is entered, the rt_interrupt_enter() function can be called to notify the kernel that the interrupt state has been entered and increase the interrupt nesting depth (execute rt_interrupt_nest++);

Whenever you exit an interrupt, you can call the rt_interrupt_leave() function to notify the kernel that you have left the interrupt state and reduce the interrupt nesting depth (execute rt_interrupt_nest --). Be careful not to call these two interface functions in your application.

The purpose of using rt_interrupt_enter/leave() is that in the interrupt service program, if kernel-related functions (such as releasing semaphores, etc.) are called, the kernel can adjust the corresponding behavior in time by judging the current interrupt status. For example, a semaphore is released in an interrupt to wake up a thread, but it is found that the current system is in an interrupt context environment. In this case, the strategy of thread switching in the interrupt should be adopted when switching threads, rather than switching immediately.

However, if the interrupt service routine does not call kernel-related functions (such as releasing semaphores), the rt_interrupt_enter/leave() function may not be called at this time.

In the upper layer application, when the kernel needs to know the current interrupt state or the current nested interrupt depth, it can call the rt_interrupt_get_nest() interface, which will return rt_interrupt_nest. As follows:

rt_uint8_t rt_interrupt_get_nest(void);copymistakeCopy Success

The following table describes the return values ​​of rt_interrupt_get_nest()

return

describe

0

The current system is not in an interrupt context

1

The current system is in the interrupt context

Greater than 1

Current interrupt nesting level

When the driver peripheral is working, whether its programming mode is triggered by interrupt mode or polling mode is often the first issue that the driver developer needs to consider, and this issue is very different in real-time operating systems and time-sharing operating systems. Because the polling mode itself uses a sequential execution method: query the corresponding event and then perform the corresponding processing. Therefore, the polling mode is relatively simple and clear in terms of implementation. For example, when writing data to the serial port, the program code will only write the next data when the serial port controller finishes writing one data (otherwise the data is discarded). The corresponding code can be as follows:

/* 轮询模式向串口写入数据 */
    while (size)
    {
        /* 判断 UART 外设中数据是否发送完毕 */
        while (!(uart->uart_device->SR & USART_FLAG_TXE));
        /* 当所有数据发送完毕后,才发送下一个数据 */
        uart->uart_device->DR = (*ptr & 0x1FF);

        ++ptr; --size;
    }copymistakeCopy Success

Polling mode may cause big problems in real-time systems, because in real-time operating systems, when a program is continuously executed (polling), its thread will always run, and threads with lower priority than it will not be run. In a time-sharing system, this is just the opposite. There is almost no priority difference. You can run this program in one time slice and then run another program in another time slice.

Therefore, in general, real-time systems use more interrupt modes to drive peripherals. When data arrives, the interrupt wakes up the relevant processing thread and then continues the subsequent actions. For example, for some serial port peripherals with FIFO (a first-in, first-out queue containing a certain amount of data), the writing process can be as follows:

The thread first writes data to the serial port's FIFO. When the FIFO is full, the thread suspends itself. The serial port controller continuously takes data out of the FIFO and sends it out at the configured baud rate (e.g. 115200bps). When all the data in the FIFO is sent, an interrupt will be triggered to the processor; when the interrupt service program is executed, the thread can be awakened. The example here is a FIFO type device. In reality, there are also DMA type devices with similar principles.

For low-speed devices, this mode is very good, because before the serial port peripheral sends the data in the FIFO, the processor can run other threads, which improves the overall operating efficiency of the system (even for time-sharing systems, such a design is very necessary). However, for some high-speed devices, for example, when the transmission speed reaches 10Mbps, assuming that the amount of data sent at a time is 32 bytes, we can calculate that the time required to send such a piece of data is: (32 X 8) X 1/10Mbps = 25us. When data needs to be transmitted continuously, the system will trigger an interrupt after 25us to wake up the upper thread to continue the next transmission. Assuming that the system's thread switching time is 8us (usually the thread context switching time of the real-time operating system is only a few us), then when the entire system is running, the data bandwidth utilization will only be 25/(25+8) =75.8%. However, using the polling mode, the data bandwidth utilization may reach 100%. This is also the reason why everyone generally believes that the data throughput in real-time systems is insufficient, and the system overhead is consumed by thread switching (some real-time systems will even use bottom-half processing and hierarchical interrupt processing as mentioned earlier in this chapter, which is equivalent to lengthening the time overhead from interrupt to sending thread, further reducing efficiency).

Through the above calculation process, we can see some key factors: the smaller the amount of data sent, the faster the sending speed, the greater the impact on data throughput. In the final analysis, it depends on the frequency of interrupts in the system. When a real-time system wants to improve data throughput, there are several ways to consider:

1) Increase the length of each data transmission, and let the peripheral send as much data as possible each time;

2) If necessary, change the interrupt mode to polling mode. At the same time, in order to solve the problem that the polling mode always occupies the processor and other low-priority threads cannot run, the priority of the polling thread can be appropriately lowered.

This is an interrupt application routine: when multiple threads access the same variable, use the global interrupt switch to protect the variable, as shown in the following code:

Using switch interrupts to access global variables

#include <rthw.h>
#include <rtthread.h>

#define THREAD_PRIORITY      20
#define THREAD_STACK_SIZE    512
#define THREAD_TIMESLICE     5

/* 同时访问的全局变量 */
static rt_uint32_t cnt;
void thread_entry(void *parameter)
{
    rt_uint32_t no;
    rt_uint32_t level;

    no = (rt_uint32_t) parameter;
    while (1)
    {
        /* 关闭全局中断 */
        level = rt_hw_interrupt_disable();
        cnt += no;
        /* 恢复全局中断 */
        rt_hw_interrupt_enable(level);

        rt_kprintf("protect thread[%d]'s counter is %d\n", no, cnt);
        rt_thread_mdelay(no * 10);
    }
}

/* 用户应用程序入口 */
int interrupt_sample(void)
{
    rt_thread_t thread;

    /* 创建 t1 线程 */
    thread = rt_thread_create("thread1", thread_entry, (void *)10,
                              THREAD_STACK_SIZE,
                              THREAD_PRIORITY, THREAD_TIMESLICE);
    if (thread != RT_NULL)
        rt_thread_startup(thread);


    /* 创建 t2 线程 */
    thread = rt_thread_create("thread2", thread_entry, (void *)20,
                              THREAD_STACK_SIZE,
                              THREAD_PRIORITY, THREAD_TIMESLICE);
    if (thread != RT_NULL)
        rt_thread_startup(thread);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(interrupt_sample, interrupt sample);copymistakeCopy Success

The simulation results are as follows:

 \ | /
- RT -     Thread Operating System
 / | \     3.1.0 build Aug 27 2018
 2006 - 2018 Copyright by rt-thread team
msh >interrupt_sample
msh >protect thread[10]'s counter is 10
protect thread[20]'s counter is 30
protect thread[10]'s counter is 40
protect thread[20]'s counter is 60
protect thread[10]'s counter is 70
protect thread[10]'s counter is 80
protect thread[20]'s counter is 100
protect thread[10]'s counter is 110
protect thread[10]'s counter is 120
protect thread[20]'s counter is 140
…copymistakeCopy Success

Note:

Since disabling global interrupts will cause the entire system to be unable to respond to interrupts, when using disabling global interrupts as a means of mutually exclusive access to critical sections, it is necessary to ensure that the time for disabling global interrupts is very short, such as the time to run several machine instructions.

Last updated

Assoc. Prof. Wiroon Sriborrirux, Founder of Advance Innovation Center (AIC) and Bangsaen Design House (BDH), Electrical Engineering Department, Faculty of Engineering, Burapha University