Interrupts

Hardware events can generate interrupts on the CPU. Processing events asynchronically is more efficient and introduces less latency than constantly polling a hardware resource. Not only does the efficiency improvement provide more processing power to other tasks, it also greatly reduces power consumption on low power systems by entering sleep mode when the CPU is inactive.

Design

Usually, an interrupt service routing (ISR) has a specific function name replacing a weak symbol in the interrupt vector table at link time. As soon as the corresponding interrupt request (IRQ) is issued by the hardware, the CPU jumps to the ISR.

interrupts
Figure: Overview of kernel interrupt handling.

As illustrated in the figure an IRQ triggers the ISR handler within the kernel. Then, the kernel calls the ISR registered by the user. The user can then pass a signal or data to a thread for further processing.

If an interrupt handler does not interact with the kernel or a thread, it can still be declared in the interrupt vector table. However, if the custom ISR exists in the vector table, the kernel ISR handler will not be called anymore.

interrupt-seq
Figure: Interrupt handler sequence diagram.

The diagram in the figure details the setup and the IRQ call sequence. At startup the user creates and registers a new ISR handler at kernel. When the IRQ occurs, the kernel ISR is called. With the IRQ number, the kernel selects which user ISRs to call. If there are multiple ISR handlers for one IRQ, they will be called in sequence. The user handler can then do some short work such as clearing flags or passing new data to a thread. The ISR is then exited and the kernel checks if a context switch is necessary. If a thread is awaiting an event, it can the process incoming data.

The goal is to keep interrupts short and to defer heavy work from ISRs to threads for multiple reasons. First, even the lowest interrupt priority is higher than any thread priority. A low priority interrupt might unnecessarily block a high priority thread. Secondly, interrupts run in kernel mode with the same privileges as the kernel. Fewer lines of code in kernel mode reduce the probability of a software defect. Also, keep in mind that the ISR is executed on the kernel stack.

\notebox{ Strict memory boundaries could be enforced if all interrupt handling was defferd to thread mode. An issue arises when an interrupt must be cleared in a microcontroller register the kernel does not know about. If the kernel were to exit the ISR without clearing the register, the handler would be called again resulting a deadlock. As a solution, the kernel could deactivate the interrupt until the user ISR processes it. This might lead to missed interrupts, which is to be avoided as well. }

Usage

Setting up an interrupt handler follows the concept of the thread builder.

static PROC: &Process = bern_kernel::new_process!(my_process, 8192);
#[entry]
fn main() -> ! {
    // init 
    PROC.init(move |c| {
        InterruptHandler::new(c)1
            .stack(InterruptStack::Kernel)2
            .connect_interrupt(stm32f4xx_hal::interrupt::EXTI15_10 as u16)3
            .handler(move |_c| {4
                // clear IRQ flags
                // send events to threads
            });

        /*..*/
    }).unwrap();
    /*..*/
}

Listing: Creating a new interrupt handler.

Similar to spawning a thread, an interrupt handler is set up within a process context 1 in the listing. Currently, an interrupt handler can only run on the kernel stack 2. The handler is connected to an IRQ using the interrupt index 3. An interrupt handler can be connected to multiple IRQs. alternatively, on IRQ can trigger multiple interrupt handlers. This is useful as some interrupts combine signal (i.e. multiple GPIOs trigger one IRQ) or one peripheral has multiple IRQs (i.e. an UART has multiple TX and RX interrupts that can be processed in one handler).

Lastly, the handler code is placed in a closure 4. Using a closure again can capture its environment. Therefore, synchronization primitives and channels can be passed to an interrupt handler. In contrast to a static ISR a closure can guarantee that a synchronization object is initialized before its used for the first time. The handler itself should only clear IRQ flags if necessary and send data or signals to a thread. Processing data is meant to be done in threads.

Note that the example excludes enabling the interrupt on microcontroller and interrupt controller level. It is up to the user to activate the interrupt. The kernel only handles interrupt calls.