Getting keyboard response


Interrupts?! I hate to be interrupted!

So far, we managed to clear the screen from all the nonsense that is there when we start our computer or emulator. We've also learned how to make beautiful little characters appear where we want them to. Everything we've done has been static or predetermined. In a real OS, you can move the mouse or press some keys and hopefully make something happen. We want that in our OS as well :). This tutorial with teach you how to interact with one of the computer's oldest accessories - the keyboard.

If you already know what an interrupt in computer terms is, you can jump to the next section. I don't tolerate people sitting here getting bored, they can read some other tutorials! ;)

As far as I know, there are two ways to get the CPU to know what's happening outside it's little world. Either it asks every device if something has happened (this method is called polling), or the devices is letting the CPU know when something has happened (interrupt based communication). Asking takes resources from the CPU, so if we let the devices shout instead of answering letters, it would release the CPU from writing the letters and just answer the calls instead. Very convenient and effective!

An interrupt is simply what the name says. It interrupts the CPU. There are two types, internal and external. The internal interrupt is accually a program asking for a service by the Operating System or the BIOS. Something it can't do by itself, like reading from a disk. An external interrupt is related to physical little connections to the CPU that indicates that something needs or should be done. A perfect example for our little tutorial is the keyboard. Every time you press a key, it sends an electronic signal to the motherboard that forwards the signal to the PIC (Programmable Interrupt Controller).

As the name says, it can be programmed to do things. Not very much, but still :). It's actually just a mask for the interrupts. Any interrupts that comes to the PIC, is simply run through the mask and if it passes, the signal is sent to the CPU and the code running at the moment is interrupted. The CPU saves its present state and looks in a little thing called the interrupt table or the interrupt vector. Say the CPU received interrupt 34. Then it looks at offset 34 in the interrupt table and sees there an address which it jumps to. This is the interrupt routine. It's usually very short. A keyboard interrupt for example, could read the key value, save it in a buffer and end. Short and simple! After an interrupt is finished, the CPU restores the pre-interrupt state and continues to execute the code like nothing ever happened!

And now for some acual coding!

As always before, we start with the boot sector. We first tell the compiler that we are writing 16-bit instructions and start at address 0x7C00. Then we read a number of sectors from the disk to memory address 0x1000. After that we just fill in the GDT and jump to our main() function. If you don't know the following piece of code, please look at my previous tutorial.

[BITS 16]

[ORG 0x7C00]

reset_drive:

mov ah, 0
int 13h
or ah, ah
jnz reset_drive

mov ax, 0
mov es, ax
mov bx, 0x1000

mov ah, 02h
mov al, 02h
mov ch, 0
mov cl, 02h
mov dh, 0
int 13h
or ah, ah
jnz reset_drive
cli
xor ax, ax
mov ds, ax

lgdt [gdt_desc]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 08h:clear_pipe

[BITS 32]
clear_pipe:

mov ax, 10h
mov ds, ax
mov ss, ax
mov esp, 090000h
mov 08h:01000h

In the previous tutorial, we had three segments in the GDT: null, code and data. Now we need to add another that should hold our interrupt code. This segment should start at address 0x1000, because when we fill the interrupt table, the addresses are relative. So the new entry is almost a copy of the first code entry, except the memory address. The GDT should look like this:

gdt:

gdt_null:

dd 0
dd 0

gdt_code:

dw 0FFFFh
dw 0
db 0
db 10011010b
db 11001111b
db 0

gdt_data:

dw 0FFFFh
dw 0
db 0
db 10010010b
db 11001111b
db 0

gdt_interrupts:

dw 0FFFFh
dw 01000h
db 0
db 10011010b
db 11001111b
db 0

gdt_end:

gdt_desc:

dw gdt_end - gdt - 1
dd gdt

Don't forget to fill up the boot sector!

times 510-($-$$) db 0

dw 0AA55h

All this should be saved into a .asm file. I call mine bootsect.asm. Then it's time for jumping to C and main(). The function is plain and simple. Just two function calls and an infinite loop.

void main()
{

clrscr();
init_interrupts();
for(;;);

}

If you remember, we've already written clrsrc(). This was done in the tutorial 3. The main focus of this tutorial is coming up, init_interrupts()!

The main event of tutorial 4

To set up interrupts, we will need to fill in the IDT or the Interrupt Descriptor Table. It basically wants to know where to jump when a special interrupt occurs. Interrupts have numbers. The first 20 interrupts are hardware interrupts in the CPU. They are related to faults that can occur when executing code. After the 20 reserved interrupts, there comes another 12 interrupts that are reserved by Intel. Nobody knows if something exists there or what they might do, but you shouldn't touch them. So, our first interrupt should be placed at index 32 (starting with 0) in the IDT.

InterruptDescription
0Divide by Zero
1Debug: Trap
2Nonmaskable external interrupt
3Debug: Breakpoint
4Overflow
5BOUND range exceeded(?)
6Invalid opcode
7No Math Cooprocessor (when trying to use it)
8Double fault (something went terrably wrong!)
9Cooprocessor segment overrun(?)
10Invalid Task switch
11Memory: Segment not present
12Memory: Stack-segment fault
13Memory: General protection
14Memory: Page fault
15Reserved by Intel
16FPU error or WAIT/FWAIT instruction
17Memory: Alignment check
18Machine Check(?)
19SSE/SSE2-Exception
20-31Reserved by Intel
32-255Available to the programmer!

First, fire up your favourite text editor and create a new file. An entry in the IDT is 64 bits long. Because the largest integer we can have is 32 bit long, we need to create a struct for our entry. This is simply just two unsigned longs.

typedef struct {

unsigned long dword0;
unsigned long dword1;

} segment_desc;

Before we begin setting up interrupts, we need a function to take care of the call. We'll write this later, for now we just declare it.

extern void keyb_int();

Then we define the init_interrupts()-function we call from main()...

void init_interrupts()
{

...and define some local variables.

segment_desc idt[0x100];
unsigned long idt_desc[2];
unsigned long idt_address;
unsigned long keyb_address;

A short explaination of the variables. The idt structure contains the addresses to the interrupt routines (we only have one, but the IDT must be filled to at least 0x21 where our keyboard interrupt lays, but I've set it to maximum size, 256). To make the CPU find the IDT, we must also have a variable and that's what idt_desc is there for. Both idt_address and keyb_address are simply helping variables (you'll understand later).

Now... To get interrupts up and running we first need to program the PIC to redirect the hardware interrupts to the right place. This is done through ports 0x20-0x21 and 0xA0-0xA1. There's acually two PICs (one master and one slave) and each one can handle eight interrupts. There are newer versions on modern hardware called APIC (Advanced Programmable Interrupt Controller), but it's backwards compatible, so this should run on older hardware as well.

The first words sent to the ports are called ICWs (Interrupt Control Words). This is kind of much to explain, but I'll try. There are at least two controll words you can send and at maximum four. How many is dicided in the first ICW as follows.

The LSB (Least Significant Bit) controls whether there is fourth ICW or not. In our case, yes, so this should be set. The next bit controls whether there are one or two PICs available and hence a third ICW. This should not be set in our case, specifying two PICs. The two next bits should be set to 0. They control stuff that I don't really know :) and specifying that the interrupts is edge triggered instead of level triggered. If you really care about the difference, make a search on Google or something. The fifth bit (bit 4), should be set, telling the PIC that this is the ICW1. The last three bits should be ignored and set to 0. We send the same ICW1 to both the PICs.

out(0x20, 0x11);
out(0xA0, 0x11);

The first ICW is always sent to port 0x20. All the other ICWs are instead sent to 0x21 and 0xA1, just for the record. The second ICW is straight forward. It tells the PICs what interrupt they should begin raising on the different pins. Each PIC had eight interrupt pins, as I wrote before. Our first interrupt we can program is 0x20, so we send this value to the first PIC. Because it occupies eight interrupts, the second one gets interrupts 0x28-0x2F.

out(0x21, 0x20);
out(0xA1, 0x28);

The third ICW is also quite easy to understand. The two PICs are connected to each other and the third ICW simply tells the PICs, how they are connected to each other. A standard way is connecting IRQ2 (bit 2) on PIC1 with IRQ9 (bit 1) on PIC2. Not even sure if this can be changed on Intel's hardware...

out(0x21, 0x4);
out(0xA1, 0x2);

The fourth ICW is a more compicated one, like the first. The LSB should be set, because this controlls whether to operate in 8085 mode or in 8086/8088 mode and we want the later. The second bit tells the CPU if the EOF (End Of Interrupt) is Automatic or normal. On Intel's hardware this bit must be set to 0 (normal) and the EOF must be done via software. The following two bits, specifies if the interrupts should be buffered or not. We don't need that, so clear these two bits. After four comes five, but here's a little problem. I don't really know what this does... The documentation says "Sets Special fully-nested mode" and I have absolutely no clue what that means. A standard way seems to be 0, and that works for us. The last three bits are reserved, just like in ICW1. That sums up to:

out(0x21, 0x1);
out(0xA1, 0x1);

There... finally done with the ICWs! Now for the masking. Masking is like a filter to turn interrupts on and off. The masking looks like this:

PIC 1PIC 2
BitInterrupt to mask
0System Timer
1Keyboard
2Redirect to IRQ9 (PIC2)
3Serial Port 1 (COM2/4)
4Serial Port 2 (COM1/3)
5Sound Card
6Floppy Disk
7Parallell Port
BitInterrupt to mask
0Real-Time Clock
1Redirect from IRQ2 (PIC1)
2Reserved
3Reserved
4PS/2 Mouse
5Math Co-Processor
6Hard Disk
7Reserved


A set bit in the mask, tells the CPU to skip the interrupt. The only interrupt we need is the keyboard interrupt. It's enabled through bit 1 on PIC1. Note: When disabling IRQ2 on PIC1, the entire PIC2 is disabled through the master<->slave relationship!

out(0x21, 0xFD);
out(0xA1, 0xFF);

An entry in the IDT is 64 bits long. The first 16 bits are the lower 16 bits of a 32 bit offset (address to the interrupt routine). The next 16 bits tells the CPU what segment the offset refers to (gdt_interrupt in our case and that converts to offset 0x18 in the GDT). This is where one of the help variables come in. The interrupt routine's address needs to be converted to a long variable and it's stored in this help variable for easy access.

keyb_address = (unsigned long)keyb_int;
idt[0x21].dword0 = (keyb_address & 0xFFFF) | (0x18 << 16);

Hold your horses there mister! You can't just set interrupt 0x21 and ignore the rest?! Well, that's exactly what you can. There's an enable flag for every interrupt and since we haven't set any bits, the other interrupts are just ignored!

Bits 32-63 begins with 5 reserved bits and 3 bits that should always be set to 0 (yeah, I know it sounds silly, but just set them all to 0).

The following two bits decides whether the entry is an interrupt gate, a trap gate or a task gate. We can say that it isn't a task gate, that's for sure. So, is it an interrupt gate or a trap gate and what's the diffecence? It sounds like it should be an interrupt gate and that's the correct answer. The only real thing that seperates the two are the way they handle the IF, or the Interrupt Flag. If this is set, interrupts are enabled and otherwice, disabled. Interrupts wants to work alone, so the IF should not be set. This is represented by 10 (acually it's 01, but usually, you put the least significant bit at the far left).

All this is followed by a set bit and the bit after that should also be set, because it decides whether the gate is a 16 bit gate or a 32 bit gate and I wish that you find that answer rather obvious. After another 0, there are two bits that's called DPL or the Descriptor Privilege Level. As the name tell us, it has something to do with privilege or in this case security. This is the ring level from where the interrupt can be called. There are four different security levels in the Protected Mode, but I'll talk about it in a later tutorial. Just put 00 here at the moment.

The last bit before the last 16 bits of the offset, is called the Present flag. This should always be set, to let the IDT know that here's an active entry. Sum all the bits and we get 0x8E in hex. This was a long non-coding section I just wrote, but I hope you are still with me.

idt[0x21].dword1 = (keyb_address & 0xFFFF0000) | 0x8E00;

With that done, all we need to do is to let the CPU know where our IDT is located. Here's where our second help variable comes in to play.

idt_address = (unsigned long)idt;

The IDT descriptor is rather simple to understand. No strange bits to set here :). It's actually very similar to the LGDT (used to load the GDT). First we have the size or limit of the IDT which I set to 256 (max) multiplied by the size of the struct which is two longs, 8 bytes. After this 16 bits, the address of the IDT descriptor is specified. As you might have noticed, the entry is just 48 bits long, the last 16 bits are ignored.

idt_desc[0] = 0x800 + ((idt_address & 0xFFFF) << 16);
idt_desc[1] = idt_address >> 16;

For our next trick, we must use our little assembly knowledge. We need to execute the command LIDT to load the IDT, and execute STI to enable interrupts (which was disabled in the boot sector). A couple of NOPs is needed to ensure that the CPU has time to actually

asm("lidt %0\n"
"sti"
:"=m"(idt_desc));

Phew! Finally! That leaves just one thing left - the interrupt it self :). Again create a new file. I call mine keyb.asm. Hey! Wasn't we suppose to code in C the rest of the time?! Well, almost. Normally, we would look in a table and jump to a C routine in this routine, but since we only have one small interrupt, we will make it 'inline'.


Oh no! Not more assembly!

The file contains 32-bit code and a function that should be accessible from outside this file called keyb_int().

[BITS 32]

[GLOBAL _keyb_int]

[SECTION .text]

_keyb_int:

The first thing in our interrupt should be to read the input buffer from the keyboard. This is done from port 0x60. We'll save it in the AL register, which is the lowest 8 bits in the EAX register.

in al, 0x60

What we want our interrupt to do, is to print numbers we type on the keyboard. The codes returned from the keyboard is called Scan Codes. These codes are different depending on the country the keyboard was made in.

There are also variants, but those are software controlled. We'll use US standard scan codes. Often, numbers' scan codes are the same even if your keyboard is made in a different country. The first scan code 0x1 is ESC. After that follows the numbers, so '1' has scan code 0x2, etc.

We just want to write those numbers, so we'll make two if statements in our code (weee, more assembly!). The first one checks if the scan code is greater than 10 and the other one if the scan code is less than 2.

cmp al, 10
jg .return

cmp al, 2
jl .return

We'll define the return section later. The '.' just tells the compiler that it is a local address.

To be able to print the scan code, we need to convert it to standard ASCII. This is simply done by adding 47 to the scan code. The ASCII number for '1' is 49 and since the scan code for '1' was 0x2, only 47 needs to be added.

add al, 47

We choose a static position to print our number, memory address: 0xB8000 (upper left corner like in the previous tutorial). Since the AL register contains the ASCII code, we simply move the contents to the memory address:

mov [0xB8000], al

Our goal is reached, we printed the character we typed on the keyboard in the upper left corner of the screen. Now for the EOF and return. Since the interrupt we received were located on PIC1, we just need to send 0x20 to port 0x20. If the interrupt have been located on PIC2, then we would need to send 0x20 to both port 0xA0 (slave) and 0x20 (master).

.return:

mov al, 0x20
out 0x20, al

If this was a normal funtion, we would have used RET to return, but since this is an interrupt we have to use IRET. The difference is that IRET also saves a bunch of registers which RET doesn't (they use them for parameters instead). This is one of the reasons C isn't very good to write pure interrupt functions, because they use RET.

iret


The only thing left is the compiling part! First the boot sector to plain binary. Then our interrupt routine, but it should be linked together with the rest of the kernel, so we use the coff format when compiling.

nasm -f bin bootsect.asm -o bootsect.bin
nasm -f coff keyb.asm -o keyb.o

gcc -ffreestanding -c main.c -o main.o
gcc -c video.c -o video.o
gcc -c ports.c -o ports.o
gcc -c ints.c -o ints.o
ld -e _main -Ttext 0x0 -o kernel.o main.o video.o ports.o keyb.o ints.o
ld -i -e _main -Ttext 0x0 -o kernel.o main.o video.o ports.o keyb.o ints.o
objcopy -R .note -R .comment -S -O binary kernel.o kernel.bin

Use some program to put the files together. If you use my makeboot program, the syntax is the same as the previous tutorial:

makeboot a.img bootsect.bin kernel.bin

If you are a Windows user, you can use plain old copy to do the trick!

copy /b bootsect.bin +kernel.bin a.img

Just run Bochs with a.img and this is what you should get when you press '1' on your keyboard:


Download the complete source for this tutorial, including makeboot and a .bat-file for compiling.

Download my example configuration file for Bochs 2.0.2 - Win32.

Download my example configuration file for Bochs (paths to the BIOS may have to be changed, if you're using another distributions than Win32 1.4.1).

Any comments, improvments or found errors? Mail me: gregor.brunmar@home.se.