From the lesson 1, we already know how to communicate with hardware. However, most of the time the pattern of communication is not that simple. Usually, this pattern is asynchronous: we send some command to a device, but it doesn't respond immediately. Instead, it notifies us when the work is completed. Such asynchronous notifications are called "interrupts" because they interrupt normal execution flow and force the processor to execute an "interrupt handler".
There is one device that is particularly useful in operating system development: system timer. It is a device that can be configured to periodically interrupt a processor with some predefined frequency. One particular application of the timer that it is used in the process scheduling. A scheduler needs to measure for how long each process has been executed and use this information to select the next process to run. This measurement is based on timer interrupts.
We are going to talk about process scheduling in details in the next lesson, but for now, our task will be to initialize system timer and implement a timer interrupt handler.
In ARM.v8 architecture, interrupts are part of a more general term: exceptions. There are 4 types of exceptions
- Synchronous exception Exceptions of this type are always caused by the currently executed instruction. For example, you can use
str
instruction to store some data at an unexistent memory location. In this case, a synchronous exception is generated. Synchronous exceptions also can be used to generate a "software interrupt". Software interrupt is a synchronous exception that is generated on purpose bysvc
instruction. We will use this technique in lesson 5 to implement system calls. - IRQ (Interrupt Request) Those are normal interrupts. They are always asynchronous, which means that they have nothing to do with the currently executed instruction. In contrast to synchronous exceptions, they are always not generated by the processor itself, but by external hardware.
- FIQ (Fast Interrupt Request) This type of exception is called "fast interrupts" and exist solely for the purpose of prioritizing exceptions. It is possible to configure some interrupts as "normal" and other as "fast". Fast interrupts will be signaled first and will be handled by a separate exception handler. Linux doesn't use fast interrupts and we also are not going to do so.
- SError (System Error) Like
IRQ
andFIQ
,SError
exceptions are asynchronous and are generated by external hardware. UnlikeIRQ
andFIQ
,SError
always indicates some error condition. Here you can find an example explaining whenSError
can be generated.
Each exception type needs its own handler. Also, separate handlers should be defined for each different execution state, in which exception is generated. There are 4 execution states that are interesting from the exception handling standpoint. If we are working at EL1 those states can be defined as follows:
- EL1t Exception is taken from EL1 while stack pointer was shared with EL0. This happens when
SPSel
register holds the value0
. - EL1h Exception is taken from EL1 at the time when dedicated stack pointer was allocated for EL1. This means that
SPSel
holds the value1
and this is the mode that we are currently using. - EL0_64 Exception is taken from EL0 executing in 64-bit mode.
- EL0_32 Exception is taken from EL0 executing in 32-bit mode.
In total, we need to define 16 exception handlers (4 exception levels multiplied by 4 execution states) A special structure that holds addresses of all exception handlers is called exception vector table or just vector table. The structure of a vector table is defined in Table D1-7 Vector offsets from vector table base address
at page 1876 of the AArch64-Reference-Manual. You can think of a vector table as an array of exception vectors, where each exception vector (or handler) is a continuous sequence of instructions responsible for handling a particular exception. Accordingly, to Table D1-7
from AArch64-Reference-Manual
, each exception vector can ocupy 0x80
bytes maximum. This is not much, but nobody prevents us from jumping to some other memory location from an exception vector.
I think all of this will be much clearer with an example, so now it is time to see how exception vectors are implemented in the RPI-OS. Everything related to exception handling is defined in entry.S and we are going to start examining it right now.
The first useful macro is called ventry and it is used to create entries in the vector table.
.macro ventry label
.align 7
b \label
.endm
As you might infer from this definition, we are not going to handle exceptions right inside the exception vector, but instead, we jump to a label that is provided for the macro as label
argument. We need .align 7
instruction because all exception vectors should be located at offset 0x80
bytes one from another.
Vector table is defined here and it consists of 16 ventry
definitions. For now we are only interested in handling IRQ
from EL1h
but we still need to define all 16 handlers. This is not because of some hardware requirement, but rather because we want to see a meaningful error message in case something goes wrong. All handlers that should never be executed in normal flow have invalid
postfix and uses handle_invalid_entry macro. Let's take a look at how this macro is defined.
.macro handle_invalid_entry type
kernel_entry
mov x0, #\type
mrs x1, esr_el1
mrs x2, elr_el1
bl show_invalid_entry_message
b err_hang
.endm
In the first line, you can see that another macro is used: kernel_entry
. We will discuss it shortly.
Then we call show_invalid_entry_message and prepare 3 arguments for it. The first argument is exception type that can take one of these values. It tells us exactly which exception handler has been executed.
The second parameter is the most important one, it is called ESR
which stands for Exception Syndrome Register. This argument is taken from esr_el1
register, which is described on page 2431 of AArch64-Reference-Manual
. This register contains detailed information about what causes an exception.
The third argument is important mostly in case of synchronous exceptions. Its value is taken from already familiar to us elr_el1
register, which contains the address of the instruction that had been executed when the exception was generated. For synchronous exceptions, this is also the instruction that causes the exception.
After show_invalid_entry_message
function prints all this information to the screen we put the processor in an infinite loop because there is not much else we can do.
After an exception handler finishes execution, we want all general purpose registers to have the same values they had before the exception was generated. If we don't implement such functionality, an interrupt that has nothing to do with currently executing code, can influence the behavior of this code unpredictably. That's why the first thing we must do after an exception is generated is to save the processor state. This is done in the kernel_entry macro. This macro is very simple: it just stores registers x0 - x30
to the stack. There is also a corresponding macro kernel_exit, which is called after an exception handler finishes execution. kernel_exit
restores processor state by copying back the values of x0 - x30
registers. It also executes eret
instruction, which returns us back to normal execution flow. By the way, general purpose registers are not the only thing that needs to be saved before executing an exception handler, but it is enough for our simple kernel for now. In later lessons, we will add more functionality to the kernel_entry
and kernel_exit
macros.
Ok, now we have prepared the vector table, but the processor doesn't know where it is located and therefore can't use it. In order for the exception handling to work, we must set vbar_el1
(Vector Base Address Register) to the vector table address. This is done here.
.globl irq_vector_init
irq_vector_init:
adr x0, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x0 // vector table address
ret
Another thing that we need to do is to unmask all types of interrupts. Let me explain what I mean by "unmasking" an interrupt. Sometimes there is a need to tell that a particular piece of code must never be intercepted by an asynchronous interrupt. Imagine, for example, what happens if an interrupt occurs right in the middle of kernel_entry
macro? In this case, processor state would be overwritten and lost. That's why whenever an exception handler is executed, the processor automatically disables all types of interrupts. This is called "masking", and this also can be done manually if we need to do so.
Many people mistakenly think that interrupts must be masked for the whole duration of the exception handler. This isn't true - it is perfectly legal to unmask interrupts after you saved processor state and therefore it is also legal to have nested interrupts. We are not going to do this right now, but this is important information to keep in mind.
The following two functions are responsible for masking and unmasking interrupts.
.globl enable_irq
enable_irq:
msr daifclr, #2
ret
.globl disable_irq
disable_irq:
msr daifset, #2
ret
ARM processor state has 4 bits that are responsible for holding mask status for different types of interrupts. Those bits are defined as following.
- D Masks debug exceptions. These are a special type of synchronous exceptions. For obvious reasons, it is not possible to mask all synchronous exceptions, but it is convenient to have a separate flag that can mask debug exceptions.
- A Masks
SErrors
. It is calledA
becauseSErrors
sometimes are called asynchronous aborts. - I Masks
IRQs
- F Masks
FIQs
Now you can probably guess why registers that are responsible for changing interrupt mask status are called daifclr
and daifset
. Those registers set and clear interrupt mask status bits in the processor state.
The last thing you may wonder about is why do we use constant value 2
in both of the functions? This is because we only want to set and clear second (I
) bit.
Devices usually don't interrupt processor directly: instead, they rely on interrupt controller to do the job. Interrupt controller can be used to enable/disable interrupts sent by the hardware. We can also use interrupt controller to figure out which device generates an interrupt. Raspberry PI has its own interrupt controller that is described on page 109 of BCM2837 ARM Peripherals manual.
Raspberry Pi interrupt controller has 3 registers that hold enabled/disabled status for all types of interrupts. For now, we are only interested in timer interrupts, and those interrupts can be enabled using ENABLE_IRQS_1 register, which is described at page 116 of BCM2837 ARM Peripherals manual
. According to the documentation, interrupts are divided into 2 banks. The first bank consists of interrupts 0 - 31
, each of these interrupts can be enabled or disabled by setting different bits of ENABLE_IRQS_1
register. There is also a corresponding register for the last 32 interrupts - ENABLE_IRQS_2
and a register that controls some common interrupts together with ARM local interrupts - ENABLE_BASIC_IRQS
(We will talk about ARM local interrupts in the next chapter of this lesson). The Peripherals manual, however, has a lot of mistakes and one of those is directly relevant to our discussion. Peripheral interrupt table (which is described at page 113 of the manual) should contain 4 interrupts from system timer at lines 0 - 3
. From reverse engineering Linux source code and reading some other sources I was able to figure out that timer interrupts 0 and 2 are reserved and used by GPU and interrupts 1 and 3 can be used for any other purposes. So here is the function that enables system timer IRQ number 1.
void enable_interrupt_controller()
{
put32(ENABLE_IRQS_1, SYSTEM_TIMER_IRQ_1);
}
From our previous discussion, you should remember that we have a single exception handler that is responsible for handling all IRQs
. This handler is defined here.
void handle_irq(void)
{
unsigned int irq = get32(IRQ_PENDING_1);
switch (irq) {
case (SYSTEM_TIMER_IRQ_1):
handle_timer_irq();
break;
default:
printf("Unknown pending irq: %x\r\n", irq);
}
}
In the handler, we need a way to figure out what device was responsible for generating an interrupt. Interrupt controller can help us with this job: it has IRQ_PENDING_1
register that holds interrupt status for interrupts 0 - 31
. Using this register we can check whether the current interrupt was generated by the timer or by some other device and call device specific interrupt handler. Note, that multiple interrupts can be pending at the same time. That's why each device specific interrupt handler must acknowledge that it completed handling the interrupt and only after that interrupt pending bit in IRQ_PENDING_1
will be cleared. Because of the same reason, for a production ready OS you would probably want to wrap switch construct in the interrupt handler in a loop: in this way, you will be able to handle multiple interrupts during a single handler execution.
Raspberry Pi system timer is a very simple device. It has a counter that increases its value by 1 after each clock tick. It also has 4 interrupt lines that connect to the interrupt controller(so it can generate 4 different interrupts) and 4 corresponding compare registers. When the value of the counter becomes equal to the value stored in one of the compare registers the corresponding interrupt is fired. That's why, before we will be able to use system timer interrupts, we need to initialize one of the compare registers with a non-zero value, the larger the value is - the later an interrupt will be generated. This is done in timer_init function.
const unsigned int interval = 200000;
unsigned int curVal = 0;
void timer_init ( void )
{
curVal = get32(TIMER_CLO);
curVal += interval;
put32(TIMER_C1, curVal);
}
The first line reads current counter value, the second line increases it and the third line sets the value of the compare register for the interrupt number 1. By manipulating interval
value you can adjust how soon the first timer interrupt will be generated.
Finally, we got to the timer interrupt handler. It is actually very simple.
void handle_timer_irq( void )
{
curVal += interval;
put32(TIMER_C1, curVal);
put32(TIMER_CS, TIMER_CS_M1);
printf("Timer interrupt received\n\r");
}
Here we first update compare register so that that next interrupt will be generated after the same time interval. Next, we acknowledge the interrupt by writing 1 to the TIMER_CS
register. In the documentation TIMER_CS
is called "Timer Control/Status" register. Bits [0:3] of this register can be used to acknowledge interrupts coming from one of the 4 available interrupt lines.
The last thing that you might want to take a look at is the kernel_main function where all previously discussed functionality is orchestrated. After you compile and run the sample it should print "Timer interrupt received" message after an interrupt is taken. Please, try to do it by yourself and don't forget to carefully examine the code and experiment with it.
2.3 Processor initialization: Exercises
3.2 Interrupt handling: Low-level exception handling in Linux