mirror of
https://github.com/isometimes/rpi4-osdev
synced 2024-11-21 17:50:40 +00:00
Added first draft of README.md to part13-interrupts
This commit is contained in:
parent
04a58dfad4
commit
a1a0a25d92
4 changed files with 163 additions and 35 deletions
|
@ -87,22 +87,22 @@ el1_entry:
|
|||
|
||||
.ltorg
|
||||
|
||||
.org 0x100
|
||||
.org 0x110
|
||||
.globl spin_cpu0
|
||||
spin_cpu0:
|
||||
.quad 0
|
||||
|
||||
.org 0x108
|
||||
.org 0x118
|
||||
.globl spin_cpu1
|
||||
spin_cpu1:
|
||||
.quad 0
|
||||
|
||||
.org 0x110
|
||||
.org 0x120
|
||||
.globl spin_cpu2
|
||||
spin_cpu2:
|
||||
.quad 0
|
||||
|
||||
.org 0x118
|
||||
.org 0x128
|
||||
.globl spin_cpu3
|
||||
spin_cpu3:
|
||||
.quad 0
|
||||
|
|
144
part13-interrupts/README.md
Normal file
144
part13-interrupts/README.md
Normal file
|
@ -0,0 +1,144 @@
|
|||
Writing a "bare metal" operating system for Raspberry Pi 4 (Part 13)
|
||||
====================================================================
|
||||
|
||||
What are interrupts?
|
||||
--------------------
|
||||
If you've spent any time looking at the Bluetooth code in these tutorials, you'll notice we're always "polling" for updates. In fact, in _part11-breakout-smp_ we tie up an entire core just waiting around for something to happen. This clearly isn't the best use of CPU time. Fortunately, the world solved that problem for us years ago with _interrupts_.
|
||||
|
||||
Ideally, we want to tell a piece of hardware to do something and have it simply notify us when the work is complete so we can move on with our lives in the meantime. These notifications are known as _interrupts_ because they disrupt normal program execution and force the CPU to immediately run an _interrupt handler_.
|
||||
|
||||
The simplest device that interrupts
|
||||
-----------------------------------
|
||||
One useful piece of built-in hardware is a system timer, which can be programmed to interrupt at regular intervals e.g. every second. You'll need this if you want to schedule multiple processes to run on a single core e.g. using the principle of time slicing.
|
||||
|
||||
For now, however, we're simply going to learn how to program the timer and respond to its interrupts.
|
||||
|
||||
The codebase
|
||||
------------
|
||||
Let me quickly explain what you're looking at in the _part13-interrupts_ code:
|
||||
|
||||
* _boot/_ : the same _boot_ code directory from _part12-wgt_
|
||||
* _include/_ : some useful headers copied directly from _part11-multicore_
|
||||
* _lib/_ : some useful libraries copied directly from _part11-multicore_
|
||||
* _kernel/_ : the only new code we need to concern ourselves with in this tutorial
|
||||
|
||||
Please note: I have also done some work to tidy up the _Makefile_ and respect this directory structure, but nothing to write home about!
|
||||
|
||||
The new code
|
||||
------------
|
||||
You'll recognise a lot of _kernel.c_ from _part10-multicore_, except instead of showing four cores at work and playing sound, we're now only using core 0 & 1 and, in addition, making use of two distinct timers to show four progress bars. So, the `main()` routine kicks off core 1, then the timers, and then finally core 0's workload.
|
||||
|
||||
The timers are set up using these calls:
|
||||
|
||||
```c
|
||||
irq_init_vectors();
|
||||
enable_interrupt_controller();
|
||||
irq_enable();
|
||||
timer_init();
|
||||
```
|
||||
|
||||
Initialising the exception vector table
|
||||
---------------------------------------
|
||||
In fact, interrupts are a more specific kind of _exception_ - something that, when "raised", needs the immediate attention of the processor. A perfect example of when an exception might occur is when bad code tries to do soemthing "impossible" e.g. divide by zero. The CPU needs to know how to respond when/if this happens i.e. an address of some code to run which handles this exception e.g. by printing an error to the screen. These addresses are stored in an _exception vector table_.
|
||||
|
||||
_irqentry.S_ sets up a list called `vectors` which contains individual _vector entries_. These vector entries are simply jump instructions to handler code.
|
||||
|
||||
The CPU is told where the vector table is stored during the `irq_init_vectors()` call from `main()` in _kernel.c_. You'll find this code in _utils.S_:
|
||||
|
||||
```c
|
||||
irq_init_vectors:
|
||||
adr x0, vectors
|
||||
msr vbar_el1, x0
|
||||
ret
|
||||
```
|
||||
|
||||
It simply sets the Vector Base Address Register to the address of the `vectors` list.
|
||||
|
||||
Interrupt handling
|
||||
------------------
|
||||
The only vector entry we really care about for the purposes of this tutorial is `handle_el1_irq`. This is a generic handler for any interrupt request (IRQ) that comes in at EL1 (kernel execution level), and please do note that we're only allowed one of these.
|
||||
|
||||
If you do want a deeper understanding, I highly recommend reading s-matyukevich's work [here](https://github.com/s-matyukevich/raspberry-pi-os/blob/master/docs/lesson03/rpi-os.md).
|
||||
|
||||
```c
|
||||
handle_el1_irq:
|
||||
kernel_entry
|
||||
bl handle_irq
|
||||
kernel_exit
|
||||
```
|
||||
|
||||
Put simply, `kernel_entry` saves the register state before the interrupt handler runs, and `kernel_exit` restores this register state before we return. As we're _interrupting_ normal program execution, we want to be sure that we put things back to how they were so that nothing unpredictable happens as our kernel code resumes.
|
||||
|
||||
In the middle we simply call a function called `handle_irq()` which is written in the C language in _irq.c_. It's purpose is to look more closely at the interrupt request, figure out what device was responsible for generating an interrupt, and run the right sub-handler:
|
||||
|
||||
```c
|
||||
void handle_irq() {
|
||||
unsigned int irq = REGS_IRQ->irq0_pending_0;
|
||||
|
||||
while(irq) {
|
||||
if (irq & SYS_TIMER_IRQ_1) {
|
||||
irq &= ~SYS_TIMER_IRQ_1;
|
||||
|
||||
handle_timer_1();
|
||||
}
|
||||
|
||||
if (irq & SYS_TIMER_IRQ_3) {
|
||||
irq &= ~SYS_TIMER_IRQ_3;
|
||||
|
||||
handle_timer_3();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we're handling two different timer interrupts in this code. In fact, `handle_timer_1()` and `handler_timer_3()` are implemented in _kernel.c_ and serve to demonstrate that the timer has fired by incrementing a progress counter and updating a graphical representation of its value. Timer 3 is configured to progress at four times the speed of Timer 1.
|
||||
|
||||
The interrupt controller
|
||||
------------------------
|
||||
The interrupt controller is the hardware responsible for telling the CPU about interrupts as they occur. We can use the interrupt controller to allow/block (or enable/disable) interrupts. We can also use it to figure out which device generated the interrupt, as we did in `handle_irq()`.
|
||||
|
||||
In `enable_interrupt_controller()`, called from `main()` in _kernel.c_, we allow the Timer 1, Timer 3 and AUX interrupts through. Similarly, in `disable_interrupt_controller()` we block the Timer 1 and Timer 3 interrupts:
|
||||
|
||||
```c
|
||||
void enable_interrupt_controller() {
|
||||
REGS_IRQ->irq0_enable_0 = AUX_IRQ | SYS_TIMER_IRQ_1 | SYS_TIMER_IRQ_3;
|
||||
}
|
||||
|
||||
void disable_interrupt_controller() {
|
||||
REGS_IRQ->irq0_enable_0 = AUX_IRQ;
|
||||
}
|
||||
```
|
||||
|
||||
Masking/unmasking interrupts
|
||||
----------------------------
|
||||
To begin receiving interrupts, we need to take one more step: unmasking all types of interrupts.
|
||||
|
||||
Masking is a technique used by the CPU to stop a particular piece of code from being stopped in its tracks by an interrupt. It's used to protect important code that *must* complete. Imagine what would happen if our `kernel_entry` code (that saves register state) was interrupted halfway through! In this case, the register state would be overwritten and lost. This is why the CPU automatically masks all interrupts when an exception handler is executed.
|
||||
|
||||
The `irq_enable` and `irq_disable` functions in _utils.S_ are responsible for masking and unmasking interrupts:
|
||||
|
||||
```c
|
||||
.globl irq_enable
|
||||
irq_enable:
|
||||
msr daifclr, #2
|
||||
ret
|
||||
|
||||
.globl irq_disable
|
||||
irq_disable:
|
||||
msr daifset, #2
|
||||
ret
|
||||
```
|
||||
|
||||
As soon as `irq_enable()` called from `main()` in _kernel.c_, the timer handlers are run when the timer fires. Well, sort of...!
|
||||
|
||||
Initialising the timers
|
||||
-----------------------
|
||||
The RPi4's system timer couldn't be simpler. It has a counter which increases by 1 with each clock tick. It then has 4 interrupt lines (0 & 2 reserved for the GPU, 1 & 3 used by us in this tutorial!) with 4 corresponding compare registers. When the value of the counter becomes equal to a value in one of the compare registers, the corresponding interrupt is fired.
|
||||
|
||||
So before we receive any timer interrupts, we must also set the right compare registers to have a non-zero value. The `timer_init()` function (called from `main()` in _kernel.c_) gets the current timer value, adds the timer interval and sets the compare register to that total, so when the right number of clock ticks pass, the interrupt fires. It does this for both Timer 1 and Timer 3, setting Timer 3 to run 4 times as fast.
|
||||
|
||||
Handling the timer interrupts
|
||||
-----------------------------
|
||||
This is the simplest bit. We update the compare register so the next interrupt will be generated after the same interval again. Importantly we then acknowledge the interrupt by setting the right bit of the Control Status register. Then we update the screen!
|
||||
|
||||
And... hey presto...
|
|
@ -1,27 +1,5 @@
|
|||
#include "kernel.h"
|
||||
|
||||
const char entry_error_messages[16][32] = {
|
||||
"SYNC_INVALID_EL1t",
|
||||
"IRQ_INVALID_EL1t",
|
||||
"FIQ_INVALID_EL1t",
|
||||
"ERROR_INVALID_EL1T",
|
||||
|
||||
"SYNC_INVALID_EL1h",
|
||||
"IRQ_INVALID_EL1h",
|
||||
"FIQ_INVALID_EL1h",
|
||||
"ERROR_INVALID_EL1h",
|
||||
|
||||
"SYNC_INVALID_EL0_64",
|
||||
"IRQ_INVALID_EL0_64",
|
||||
"FIQ_INVALID_EL0_64",
|
||||
"ERROR_INVALID_EL0_64",
|
||||
|
||||
"SYNC_INVALID_EL0_32",
|
||||
"IRQ_INVALID_EL0_32",
|
||||
"FIQ_INVALID_EL0_32",
|
||||
"ERROR_INVALID_EL0_32"
|
||||
};
|
||||
|
||||
void enable_interrupt_controller() {
|
||||
REGS_IRQ->irq0_enable_0 = AUX_IRQ | SYS_TIMER_IRQ_1 | SYS_TIMER_IRQ_3;
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
#define S_FRAME_SIZE 256
|
||||
|
||||
.macro kernel_entry
|
||||
sub sp, sp, #S_FRAME_SIZE
|
||||
stp x0, x1, [sp, #16 * 0]
|
||||
stp x2, x3, [sp, #16 * 1]
|
||||
stp x4, x5, [sp, #16 * 2]
|
||||
sub sp, sp, #S_FRAME_SIZE
|
||||
stp x0, x1, [sp, #16 * 0]
|
||||
stp x2, x3, [sp, #16 * 1]
|
||||
stp x4, x5, [sp, #16 * 2]
|
||||
stp x6, x7, [sp, #16 * 3]
|
||||
stp x8, x9, [sp, #16 * 4]
|
||||
stp x10, x11, [sp, #16 * 5]
|
||||
|
@ -42,9 +42,9 @@
|
|||
.endm
|
||||
|
||||
.macro kernel_exit
|
||||
ldp x0, x1, [sp, #16 * 0]
|
||||
ldp x2, x3, [sp, #16 * 1]
|
||||
ldp x4, x5, [sp, #16 * 2]
|
||||
ldp x0, x1, [sp, #16 * 0]
|
||||
ldp x2, x3, [sp, #16 * 1]
|
||||
ldp x4, x5, [sp, #16 * 2]
|
||||
ldp x6, x7, [sp, #16 * 3]
|
||||
ldp x8, x9, [sp, #16 * 4]
|
||||
ldp x10, x11, [sp, #16 * 5]
|
||||
|
@ -59,7 +59,7 @@
|
|||
ldp x28, x29, [sp, #16 * 14]
|
||||
ldr x30, [sp, #16 * 15]
|
||||
add sp, sp, #S_FRAME_SIZE
|
||||
eret
|
||||
eret
|
||||
.endm
|
||||
|
||||
.macro handle_invalid_entry type
|
||||
|
@ -67,12 +67,18 @@
|
|||
mov x0, #\type
|
||||
mrs x1, esr_el1
|
||||
mrs x2, elr_el1
|
||||
|
||||
// We could pass this to a function to print an error here
|
||||
// e.g. bl show_invalid_entry_message
|
||||
//
|
||||
// For now we'll just hang
|
||||
|
||||
b err_hang
|
||||
.endm
|
||||
|
||||
.macro ventry label
|
||||
.align 7
|
||||
b \label
|
||||
b \label
|
||||
.endm
|
||||
|
||||
//Exception vectors table
|
||||
|
|
Loading…
Reference in a new issue