In the last part of the series about keyboard input we have seen that we can read the state of the keyboard by actively polling it. This means the CPU is busy waiting for keyboard input and can’t complete other tasks in the meantime. This is not a good way to handle keyboad input.
As an example if we think of common OSes that show a clock on their task bars, with busy waiting this clock could only be updated when a button is pressed, because only by pressing a button would you let the CPU take control, as it leaves the loop that actively waits for keyboard input, to update the clock on the screen.
A solution to this predicament are interrupts. With interrupts the CPU can be configured to react to certain events by “interrupting” what it is currently doing and jumping to a function we provide.
What to expect
At the end of this part of the series we will have keyboard interrupts set up. With interrupts the CPU does not continuously check the keyboard anymore and is free to do other tasks.
This can be seen by repeatidly printing a character on the screen within a loop, while still reacting to keyboard input.
When
Why interrupts so early on
Since we need interrupts sooner or later and since they make developement and error handling a lot easier, most OS developers would suggest adding interrupts early on. They are also unavoidable for any kind of device drivers or in order to, for instance, render graphics as we’re waiting for keyboard input.
Why
Why interrupts
As seen in the last part, we can get around interrupt handling by actively polling, but this suboptimal. For device drivers such as for Floppy Disks that have long delays until they are ready, there is no practical way around interrupts.
Once we have interrupts set up we can handle a wide range of important features:
Get Interrupts from the keyboard and avoid busy-waiting
Likewise from the floppy drive controller
Handle common errors such as divisions by zero, out of bounds, etc
Handle exceptions
Background
Interrupts are simple conceptionally, but complicated to set up on x86
On microcontrollers such as the Atmega’s setting up interrupts is merely three calls to library functions.
What we are attempting to achieve is to
define a custom function (also called the "interrupt service routing", ISR) to be called when a given interrupt occurs.
We then configure the CPU, so that it knows what to call for what interrupt.
Now on x86 CPUs setting up interrupts is fairly complicated and online forums are full of developers asking questions on how to debug their interrupt handlers.
In contrast to Atmega’s we need to provide the CPU with multiple correctly aligned table in a memory block that we define in C-Code and load into the CPU in assembler.
Steps to set up interrupts
To get interrupts working and to set an interrupt for keyboard input we need to:
Allocate the "Global Descriptor Table" (GDT) and load it in assembly
Allocate the "Interrupt Descriptor Table" (IDT) and load it in assembly
Add functionality to register handler functions for interrupts "Interrupt Service Routine" (ISR).
Define a keyboard interrupt handler function
Set the address to that handler function in our IDT
Enable interrupts in the CPU
Debugging Interrupt Handlers is difficult, because we’re dealing with VMs that lock-up as things go wrong.
There are also numerous pitfalls when dealing with sample codes found online:
The osdev-Wiki helps in getting a basic overview [1,2] of what we are trying to achieve. After looking at some other custom OSes on GitHub, two were particularly close to what we’re attemping to achieve here: Interrupt handling on i686-elf compiler with NASM, compilable on Linux that is kept as simple as possible [3,4]. I used both of them and the osdev tutorials and wiki [5] to build the solution in the following. A detailed description on GDT can be found here [6].
How
The Assembly Boot code
The code comes in two parts: the assembly and the C-code. The GDT and IDT’s are very similar in structure.
First we need the assembly code for our GDT and IDT. It will allocate the interrupt handling routines and allow us to call “gdt_flush” and “idt_load” from C. Inside of the “irq_common_stub” and “isr_common_stub” are calls to our C-Code we will later add, the “irq_handler” and “isr_handler”.
Take not of the “lgdt” and “lidt”, where the “l” stands for “load”, they are used to “load” the gdt and idt tables into the CPU.
boot.asm
The Assembly ISR code
I’ve separated the code for the isr’s and irq’s to includes, because they are fairly long.
Both isr.asm and isq.asm are very similar. This is because they use the same type of GDT table. Their purpose differs: one is for handling processor errors while the other is for handling handware interrupts.
Die to the similarities in structure we could use NASM macros to automatically generate the isr and irq handlers, but that makes the code a lot less readable.
For the ISR’s let’s create isr.asm.
isr.asm (new file)
The Assembly IRQ code
And for the IRQ’s irq.asm.
irq.asm (new file)
The C-Code for the GDT
So at this point we have the Assembler-Code set up. Next we will build the code that can allocate the required structures in C, call the assembly load functions to load them into the CPU and the C-Functions that get called when an interrupt occurs.
For this we need our “General Descriptor Table” (GDT).
gdt.h (new file)
The C-Code for the IDT
The “Interrupt Descriptor Tables” (IDT)
idt.h (new file)
The C-Code for the Interrupt Handlers
And our handler functions, so-called “Interrupt Service Routines” (ISRs).
isrs.h (new file)
One last bit
I’ve moved the inportb/outportb/delay functions to a common.h as we’re now using them from the keyboard and interrupts.
common.h (new)
Include and Build
Now we can include all our headers in kernel.c
kernel.c (modify)
Modify the line for the assembler of our build script to include the parent directory by adding “-i ../” for the “%include” statements.
A Keyboard interrupt
With all of the above we can now finally register an interrupt for our keyboard.
For this, inside kernel.c, setup a handler function for the keyboard interrupt.
and modify our kernel_main initialize and register it with
We can now adapt our code from the last part of the series to print “.” on the screen continiously and still react to key presses.
The “1” in “IRQ(1)” is what specifies that we’re interested in interrupts from the keyboard.
For the Floppy Disk Controller we would use “IRQ(6)”, see [7].
See the video above for the result.
We could of course easily request the scancode from the keyboard within the “cb_keyboardInterrupt()” interrupt callback by calling “keyboardActivePolling_getScancode()”. We could then convert it to ASCII and print it on the screen as seen in the keyboard by polling part of the series.
Progress
With interrupts working and the keyboard interrupt, we can now focus on loading and saving data to disk.
The easiest approach to getting functional disk input/output is to write a small floppy disk driver as seen in the next part of the series. Interrupts are useful, because we need to react to the drive changing state.