Interrupt management function
In the previous article, we have explained the functions of T-Kernel, mainly focusing on the processing that is mainly performed by tasks. On the other hand, interrupt is a process that is executed independently of the task. So, in addition to how to use interrupts in T-Kernel, this section explains the difference in behavior caused by the difference in the context at runtime, how to create interrupt handlers, and details of operation.
What is an interrupt?
As the name implies, an interrupt is the interruption (interrupting) of an ongoing process to perform another process. An interrupt is mainly used when a peripheral device notifies the CPU of a change in state or when a program handles a CPU exception. For example, when a keyboard is tapped, the letters corresponding to the keys are displayed on the screen, and this “key being tapped” is often notified to the CPU using an interrupt.
What is the advantage of interrupts? The use of interrupts can improve responsiveness while using CPU resources efficiently. If interrupts are not used, the CPU must periodically check the status of the peripheral devices. This is called polling. In the worst-case scenario, polling requires a delay of only a period of time before it can detect a change in peripheral device status. Therefore, polling must be done at sufficiently short intervals to achieve high responsiveness. However, if the polling time interval is shortened, the CPU processing increases by the amount of time, and the CPU load increases. On the other hand, with interrupts, the CPU can perform other tasks until a peripheral device requests an interrupt. If there is no other process to be executed, the CPU can be put to rest. If no other process is to be executed, the CPU can rest if there is no other process to be executed, and the CPU can suspend the current process and perform the corresponding process in response to the interruption when an interrupt is requested.
However, interrupts are not only an advantage. Interruption is not only an advantage, but it is also necessary to return to the original process after the interruption is completed. For this purpose, the current state must be saved appropriately so that the process can be restored when it is interrupted after an interruption occurs. When returning to the original processing, the current state must also be properly restored from the saved information. Thus, there is some overhead in interrupt handling, and if the interruptions occur at a very high frequency, it may be more efficient to poll the system periodically to check for state changes.
Interrupt Management Function of T-Kernel
When an interrupt occurs, the ongoing process is interrupted and the callback routine corresponding to the interrupt is executed. The interrupt management function of T-Kernel is a function to define interrupt handler, etc. for external interrupts from peripheral devices and CPU exceptions.
tk_def_int | Interrupt handler definition |
---|---|
tk_ret_int | Returning from an interrupt handler |
Interrupt handlers registered in tk_def_int
can be written in high-level language (C language) or assembly language.
When writing in high-level language, interrupt handlers are invoked via the kernel’s high-level language handling routines. The high-level language handling routine saves and restores the running state. Specifically, it preserves the running state by saving and restoring registers. The interrupt handler is terminated by a return from a C function. The kernel’s high-level language handling routine then calls tk_ret_int
.
On the other hand, when written in assembly language, the kernel does not, as a rule, intervene when the interrupt handler is invoked. Some implementations may include programmatic processing, but when an interrupt occurs, the interrupt handler defined in tk_def_int
is directly invoked by the hardware interrupt handling function. At the beginning and end of the interrupt handler, it is necessary to save and return the registers used by the interrupt handler. The interrupt handler is terminated by tk_ret_int
or the CPU’s interrupt return instruction (or equivalent means).
In addition to system calls, there are also APIs that are provided as library functions or C macros.
DI | All external interrupts prohibited |
---|---|
EI | All external interrupts allowed |
isDI | Obtain all external interrupt prohibition states |
DINTNO | Conversion from interrupt vector to interrupt handler number |
---|---|
EnableInt | Allows interrupts |
DisableInt | Interruption prohibited |
ClearInt | Clear interrupt generation |
EndOfInt | Issuing EOI to the interrupt controller |
CheckInt | Check for interruptions |
SetIntMode | setting interrupt mode |
However, these library functions and macros may not be implemented in some cases because the functions related to interrupts are strongly dependent on the hardware. Please refer to the implementation specifications for details. In fact, the T-Kernel 2.0 specification includes the following description.
The interrupts have a high degree of hardware dependence and differ from system to system, making it difficult to standardize their implementation. Although the following specifications are defined as the standard specifications, it may be difficult to implement them in accordance with these specifications depending on the system. Implementation of the standard specification is required as much as possible, but implementation of unimplementable features is not required. Although it is permissible to add functions other than those in the standard specification, function names and so on should be different from those in the standard specification. However, DI(), EI(), and isDI()
must be implemented in accordance with the standard specification.
Context for running an interrupt handler
Interrupt handlers are initiated by a completely different factor than the progression of the task and are processed in a context that does not belong to any task. In other words, when an interrupt occurs, an interrupt handler is processed in a context unrelated to the task being executed. In T-Kernel, the context that does not belong to the task such as interrupts is called a task-independent part and the context that belongs to the latter task is called a task part.
A task-independent part can issue a system call in the same format as a task part. A feature of a task-independent part is that it does not make sense to specify the task that was being executed immediately before entering the task-independent part. Therefore, the concept of “self-task” does not exist. Therefore, the task-independent portion cannot issue a system call to enter the waiting state or a system call that implicitly specifies its own task.
Also, task switching (dispatching) does not occur because the task-independent part cannot specify the task currently being executed. When dispatching becomes necessary, it is delayed until it leaves the task-independent part. This is called the principle of delayed dispatching. In this way, interrupt processing can take precedence over task processing.
Now let’s consider the case where interrupts are nested and generated. For example, Figure 5 shows that an interrupt X occurs during the execution of task A, and an interrupt Y with a higher priority occurs in the interrupt handler. In this case, if dispatching occurs immediately after tk_wup_tsk
is issued in interrupt handler B, subsequent interrupt handlers will be processed later than the task. In addition, if task B is activated immediately upon the return of interrupt Y in (1), interrupts (2) to (3) of interrupt X will be executed later than task B, and (2) to (3) will be executed only when task A is in the execution state. This means that a handler for low-priority interrupt X is in danger of being preempted by not only high-priority interrupts, but also by task B, which is invoked by it. Thus, there is no longer any guarantee that the interrupt handler will be executed before the task, and the interrupt handler cannot be written. To avoid this, when an interrupt is nested, execution of the executable task is resumed only after all interrupt handlers have been processed.
To summarize the above, interrupt handlers are limited to the following
- A system call that implicitly specifies its own task cannot be issued.
- You cannot issue a system call to put your task in the wait state.
- Delayed dispatching is applied and no task dispatching occurs until the interrupt handler has finished processing.
In addition to these constraints, other interrupts of the same (or lower) priority are generally prohibited while the interrupt handler is running, so interrupt handlers basically don’t do any complex processing. If you need complex processing, you need to design your program to perform the necessary processing there by using tasks.
Detecting Push-Switch Presses Using an Interrupt
Now, let’s look at a sample program using an interrupt. This sample program uses the T-Kernel 2.0 function to detect presses of push switches on the T-Engine reference board by interrupts.
There are four types of push switches (SW1, SW2, SW3, SW4
), and each generates a different interrupt. In the sample program, when a push switch is pressed, the program outputs which one is pressed to the console. This “output to console” process takes a lot of time to complete. Executing such a long process in an interrupt handler will prohibit other interrupts and delay task dispatch, so such a program design is not appropriate. So in this article, we create a task that outputs characters to the console separately from the interrupt handler, and the interrupt handler uses an event flag to notify the task that a push switch has been pressed.
First, let’s define an interrupt handler, as shown in the following code sample for SW1.
/* Define a flag pattern for each push button */ #define FLGPTN_SW1 (0x01) #define FLGPTN_SW2 (0x02) #define FLGPTN_SW3 (0x04) #define FLGPTN_SW4 (0x08) #define FLGPTN_ALL (FLGPTN_SW1 | FLGPTN_SW2 | FLGPTN_SW3 | FLGPTN_SW4) /* Number of push buttons */ #define NUM_SW (4) /* * An interrupt handler for each push button */ LOCAL void inthdr_sw1(UINT dintno) { tk_set_flg(flgid, FLGPTN_SW1); ClearInt(dintno); }
This interrupt handler sets the flag pattern corresponding to the pressed push button to the event flag, and clears the occurrence of the interrupt with ClearInt
. The same functions are defined for SW2, SW3, SW4
as for inthdr_sw1
.
By the way, Inthdr_sw1
clears interrupts at the end with ClearInt
, but what will happen if ClearInt
does not clear the interruptions? The interrupt controller continues to keep the interrupt occurring, so when you try to return from the interrupt handler, the same interrupt handler keeps being called again. As a result, the task will not be called and nothing will be displayed in the console. Although it is necessary to call ClearInt
on the T-Engine reference board, it may not be necessary to call ClearInt
on some systems. Check the contents of the hardware specification or implementation specification and write the code to suit your system.
We then define an interrupt handler using tk_def_int
.
/* Function to register an interrupt handler */ LOCAL void define_inthdr(UINT dintno, FP inthdr) { T_DINT dint; dint.intatr = TA_HLNG; dint.inthdr = inthdr; tk_def_int(dintno, &dint); } EXPORT INT usermain(void) { .... /* Registering an interrupt handler for each push button */ define_inthdr(DINTNO(IV_GPIO(8)), inthdr_sw1); define_inthdr(DINTNO(IV_GPIO(7)), inthdr_sw2); define_inthdr(DINTNO(IV_GPIO(6)), inthdr_sw3); define_inthdr(DINTNO(IV_GPIO(4)), inthdr_sw4); .... }
IV_GPIO
is a T-Engine reference board specific macro to obtain the interrupt vector of GPIO interrupts. SW1, SW2, SW3, SW4
corresponds to GPIO8, GPIO7, GPIO6, GPIO4
respectively. And since the interrupt handler is written in C language (high-level language), TA_HLNG
attribute is specified in tk_def_int
.
Then you can set up interrupts.
/* Functions to allow interrupts */ LOCAL void enable_int(INTVEC intvec) { SetIntMode(intvec, IM_ENA | IM_EDGE | IM_HI); ClearInt(intvec); EnableInt(intvec); } EXPORT INT usermain(void) { .... /* Allow interrupts for each push button. */ enable_int(IV_GPIO(8)); enable_int(IV_GPIO(7)); enable_int(IV_GPIO(6)); enable_int(IV_GPIO(4)); .... }
First, set the interrupt mode with SetIntMode
. The items that can be set in SetIntMode
are implementation-dependent and can be specified by the T-Engine reference board as follows
#define IM_ENA 0x0001 /* interrupt enabled */ #define IM_DIS 0x0000 /* interrupts disabled */ #define IM_INV 0x0002 /* polarity reversal */ #define IM_LEVEL 0x0200 /* level */ #define IM_EDGE 0x0000 /* edge */ #define IM_HI 0x0000 /* High level/rising edge */ #define IM_LOW 0x0100 /* Low level/falling edge */ #define IM_BOTH 0x0400 /* Both edges */ #define IM_ASYN 0x0800 /* asynchronous */
Since the logic of the push switch is 1 when pushed down and 0 when released, set the interrupt mode to detect the rising edge. The item corresponding to a rising edge is the logical sum of IM_EDGE
(edge interrupt) and IM_HI
(rising edge). Then it clears the occurrence of interrupts with ClearInt
and finally allows interrupts with EnableInt
.
Interrupt Vector Table
So far, we have mainly explained how to use the interrupt management function of T-Kernel with some sample programs. This section explains how interrupt handlers are managed in the system.
The interrupt vector table is a table that registers information about interrupt handlers for each interruption factor. The contents of the interrupt vector table include the first address of an interrupt handler and a branch instruction to the interrupt handler. The interrupt handler registered in tk_def_int
is set in this interrupt vector table.
The T-Engine reference board implements the following vector table.
Until the interrupt handler is called
Now, let’s take a look at the flow of how the interrupt handler registered in tk_def_int
is actually called. This section explains the operation of T-Kernel 2.0 on the T-Engine reference board (tef_em1d
).
The T-Engine reference board has Arm11 on board, and the software performs branching to the interrupt handler corresponding to each interrupt factor in Arm11’s interrupt processing.
- When an interrupt signal is received from a peripheral device, the interrupt controller sends an interrupt request to the CPU.
- When the CPU is notified of an interrupt request, it branches from the CPU’s exception vector to the interrupt entry routine.
- The interrupt entry routine identifies the interrupt factor from the state of the interrupt controller and branches to the corresponding interrupt handler using the interrupt vector table.
- If the interrupt handler is written in high-level language, the interrupt handler written in C is called once via the high-level language handling routines to conform to the register rules of the C function calls.
- When a C-written interrupt handler exits with a return statement, it returns to the high-level language handling routine and calls
tk_ret_int
in it. This causes it to return from the interrupt handler, and the delayed dispatch is processed.
In the above procedure, “1” and “2” are processed by hardware, and “3”, “4” and “5” are processed by software.
For more information on Arm’s exception handling, please refer to Cortex-M, Part 13” is helpful.
interrupt handler number and interrupt vector
Finally, this section explains how to specify interrupts in the interrupt management function of T-Kernel.
The method to specify interrupts differs between the system call and other methods. In the system call, interrupt is specified using the interrupt handler number, and in the library function or macro, interrupt is specified using the interrupt vector.
The system call (tk_def_int
) specifies interrupts by interrupt handler number, and one interrupt handler can be defined per interrupt handler number. An interrupt handler number is used to distinguish between multiple interrupt handlers. Its specific meaning is defined for each implementation. In general, the interrupt vector defined in the CPU hardware interrupt handling is used as it is, or some number that can be mapped to the interrupt vector is used. The DINTNO macro is used to obtain the interrupt handler number from the interrupt vector.
However, the T-Engine reference board uses the interrupt vector as it is for the interrupt handler number. For this reason, the DINTNO macro is defined as follows, and returns the value as it is.
#define DINTNO(intvec) (intvec) /* convert to interrupt definition number */
Summary
In order to build highly responsive real-time systems, interrupts are an inevitable part of the process. In general, however, interrupt-related functions are difficult to implement because they are strongly dependent on hardware, and knowledge about both hardware and software is required to utilize them effectively.
By using T-Kernel, users can reduce the amount of time and effort to implement the interrupt handling. In fact, as shown in the sample program, the interrupt processing can be written in T-Kernel API and standard C language, except for some parts such as interrupt handler numbers and interrupt modes. As a result, you can develop highly portable software.
In addition, the processing flow tends to be complicated when multiple tasks and interrupts are used. However, the priority of processing becomes clearer and it is easier to achieve the processing expected by users due to the appropriate restrictions on processing of the interrupt handler of T-Kernel and the principle of delayed dispatch.
Please make effective use of the T-Kernel functions to build a system with high real-time performance.
“もっと見る” カテゴリーなし
Mbed TLS overview and features
In this article, I'd like to discuss Mbed TLS, which I've touched on a few times in the past, Transport …
What is an “IoT device development platform”?
I started using Mbed because I wanted a microcontroller board that could connect natively to the Internet. At that time, …
Mbed OS overview and features
In this article, I would like to write about one of the components of Arm Mbed, and probably the most …