In this lecture we will learn how to deal with hardware interrupts. The programmable interrupt controller
is used to manage the interrupt requests.
So first we should initialize the PIC before we can handle the hardware interrupts.
In this video,
we will set up the timer as an example.
In order to do that, we also need to initialize the programmable interval timer which will fire the interrupt periodically
.
We start off by talking about the interrupt controller.
As you can see, there are two chips linked together. Each chip has 8 IRQ signals.
The slave is linked to the master via IRQ2.
So actually we have a total of 15 interrupts.
The programmable interval timer, for example, uses IRQ0 of the master chip. The keyboard uses
IRQ 1 of the master chip, etc.
When we press a key,
the keyboard will send a signal to the interrupt controller and the controller will then send signals to the processor
according to the settings we write to the controller.
The processor finds the handler for that interrupt by the vector number and call that handler which we have seen
in the previous lectures.
There are three registers in each chip. These registers are used to service the interrupts.
They are 8-bit registers with each bit representing the corresponding IRQ.
When a device, keyboard for example, sends a signal to the chip, the corresponding bit
in Interrupt request register IRR will be set.
If the corresponding bit in the interrupt mask register IMR is 0
meaning that the IRQ is not masked, the PIC will send the interrupt to the processor
and set the corresponding bit in the in-service register ISR.
Generally, we could have multiple IRQs come at the same which means more than one bit in the IRR
will be set.
The PIC will choose which one should be processed first according to the priority. Normally, the IRQ0
has the highest priority and IRQ7 the lowest priority.
And note that the slave is attached to the master via IRQ2.
So, all the IRQs on the slave have higher priority than IRQ3 of the master.
OK, let's open the project.
In the kernel entry,
we initialize the PIT and PIC which requires several steps. First we look at the PIT.
There are three channels in the PIT, through channel 0 to channel 2.
Channel 1 and 2 may not exist and we don’t use them in our system. So we only talk about channel 0
.
The PIT has four registers, one mode command register and three data registers
for the three channels respectively.
We set the command and data registers to make the PIT works as we expect, that is
fire an interrupt periodically. The mode command register has four parts in it.
The bit 0 indicates that
the value PIT uses
is in binary or BCD forms.
We set it to 0 which means in binary form. Bit 1 through 3 is operating mode.
There are several operating modes we can select. In this example, we set to 010 which is mode 2
rate generator used for reoccurring interrupt. Bit 4 to 5 is Access mode.
The data registers are 8 bits.
If we want to write 16-bit value to the PIT, we need to write two bytes in a row. And access mode specifies
in which order the data will be written to the data register, such as low byte only, high byte only,
etc.
in this example, we set the access mode to 11 which means we want to write the low byte first
and then the high byte.
The last part is for selecting the channel with 0 being channel 0, 1 being channel 1 and so on.
Since we only use channel 0, we simply set it to 0.
We move the value to al register.
So we define the label
init pit
We move al
and the value
The address of mode command register is 43. We use out instruction to write the value in al to the register
.
We out 43
al.
Now we can write the settings to data register to make the PIT fire the interrupt as we expect.
In fact, the value we want to write to the PIT is an interval value which specifies when the interrupt is fired
.
The PIT works simply like this, we load a counter value and PIT will decrement this value at a rate of
about 1.2 mega HZ which means it will decrement the value roughly 1.2 million times per second
.
In our system, we want the interrupt fired at 100 HZ which is 100 times per second.
So let's do a simple calculation.
we divide the 1.2 million by 100,
the result is 11 931 which is the count value
we want to write to the register. Note that the counter value is 16-bit value.
So we write the low byte of the value and then write the high byte.
OK, we're move ax 11931
The address of data register of channel 0 is 40. We out 40
al.
Then we move
al ah
And now al holds the high byte of the value. we out
40
al.
Ok the PIT is now running. The next thing we are going to do is we are going to
set up the interrupt controller.
The PIC also has command register and data register. Each chip has its own register set.
The address for the command register of the master chip is 20 and the address of the slave is a0.
So the bit 0 and bit 4 is 1.
The bit 4 means that this is the initialization command followed by another three initialization command words we are about to write.
The bit 0 indicate that we use the last initialization command word
.
OK, we defines a label init pic
We move al
11.
and write it to the command registers using out instruction.
The address of the slave is a0.
Next we write these three command words. The first one specifies the starting vector number of the first IRQ.
Remember the processor has defined the first 32 vectors for its own use. So we can define the vectors number
from 32 to 255.
Here we simply specify the starting vector number of the first IRQ is 32. So we move
al,32.
Instead of writing the data to command register, we write it to the data register,
the address of which is 21 for the master and a1 for the slave.
So we write
the value to 21
As for the slave, we simply place the starting vector number right after the master.
Each chip has 8 IRQs and the first vector number of the master is 32, the second vector is 33 and so on.
So the starting vector number for the slave is 40 in this case.
So we move al 40.
and write it to a1.
The next command word indicate that which IRQ is used for connecting the two PIC chips.
On a regular system, the slave is attached to the master via IRQ 2 as we saw in the slide. If the bit 2 of the word is set,
it means that the IRQ2 is used.
So we write the value 4 to the data register.
we move al 4
And write it to the data register.
The word for the slave meaning the identification which should be 2.
So we write 2 to the register.
The last command word is used for selecting mode. The bit 0 should be 1 meaning that x86 system is used.
The bit 1 is automatic end of interrupt. We don’t use it
and simply set it to 0. The bit 2 to 3 are set to 0 which means buffered mode is not used.
The bit 4 specify the fully nested mode. We don’t use this mode either, and set it to 0.
OK, we write
1 to the register.
Right now the interrupt controller is working. But we have one more thing to do.
Since we have a total of 15 IRQs in the PIC, we only set up one device,
the PIT.
Therefore, we mask all the IRQs except the IRQ0 of the master which PIT uses. Masking an interrupt is simple
.
All we need to do is set the corresponding bit of the IRQ and write the value to the data register.
So here we set the bits 1 through 7
and write it to the data register.
All the IRQs in the slave are not used in this course, we set all the bits
and write it to the data register.
Ok only the IRQ0 of the master will fire interrupts.
The next thing we are going to do is we are going to set the idt entry for the timer.
The vector number of the timer is set to 32 in the PIC, so the address of the entry is
the base of the idt plus 32*16.
Remember each entry takes up 16-byte space.
The handler is called, for example, timer.
Then we can set the offset.
We copy the code here.
You can wrap the code as a function and call the function to set the idt entry. We simply copy and paste here
.
The register rdi holds the address of the idt and we need to add rdi
32*16 to make it point to the timer entry.
Ok. With the idt entry is set,
let’s write the timer handler
.
The process of interrupt handling is pretty much the same.
First off, we save the state of the CPU by pushing the registers on the stack and pop them back
before the handler returns.
The return instruction is interrupt return.
Since this is the first time we test the timer handler, we just print the character
T to indicate that we enter the timer interrupt.
We print the character in different location
and in different color, for example, we choose the color yellow.
And jump to end.
Alright, the last thing we need to do is enable interrupt by using the instruction set interrupt flag
.
Remember when we switch from real mode to protected mode in the loader file, we disabled the interrupt.
The processor will not respond the interrupt request after that.
We have set up the timer and the interrupt controller at this point,
in order to handle the interrupt, we need to enable the interrupt and we are good to go
.
Ok we save the file and build the project.
We opened the boot folder and run the bochs.
OK, as you can see, we have two characters.
The character K and the new character T
So the timer interrupt is actually fired.