|Operating Systems Development Series|
This series is intended to demonstrate and teach operating system development from the ground up.Please note: This tutorial covers hardware interrupt handling, not software interrupt handling. If you are looking for software interrupts, please see Tutorial 15. This tutorial requires knowledge of software interrupt handling.
IntroductionWelcome to...what? Tutorial 16 already? In the last tutorial, we have dived deep into the world of interrupt handling. We have covered, and even implimented, interfaces for the GDT and IDT inside of our hardware abstraction layer. We have covered almost everything we needed for software interrupt handling to work. ...But WAIT! What about hardware interrupts? o_0 Because alot of critical system devices use interrupts, it is necessary for us to be able to handle and catch interrupts triggered by hardware devices. The good news? This is already done for us! By what? The 8259 Programmable Interrupt Controller (PIC). We will look closer in the next section. Even if we get hardware interrupts working by itself, we will still be running into problems do to the system timer. As long as the system timer does not use a valid interrupt handler that we have set up for it, it will triple fault a few milliseconds after you enable hardware interrupts. After all, it will call an invalid interrupt handler, remember? Thus, we will also fix this small problem by reprogramming the system timer, otherwise known as the Programmable Interval Timer (PIT). Alot of stuff in this tutorial. We will look at:
Hardware InterruptsThere are two types of interrupts, those generated by software (Useually by an instruction, such as INT, INT 3, BOUND, INTO), and an interrupt generated by hardware. Hardware interrupts are very important for PC's. It allows other hardware devices to signal the CPU that something is about to happen. For example, a keystroke on the keyboard, or a single clock tick on the internal timer, for example. We will need to map what Interrupt Request (IRQ) to generate when these interrupts happen. This way, we have a way to track these hardware changes. Lets take a look at these hardware interrupts.
You do not need to worry to much about each device just yet. The 8259A Pins are described in detail in the 8259 PIC tutorial. The Interrupt Numbers listed in this table are the default DOS interrupt requests (IRQ) to execute when these events trigger.
In most cases, we will need to recreate a new interrupt table. As such, most operating systems need to remap the interrupts the PIC's use to insure they call the proper IRQ within their IVT. This is done for us by the BIOS for the real mode IVT. We will cover how to do this later in this tutorial as well.Wait...What is this PIC thing? All of these hardware devices that can signal hardware devices are connected indirectly to the 8259A Programmable Interrupt Controller (PIC). This is a special, and very important microcontroller that is used to signal the microprocessor when it needs to fire a hardware interrupt. We will be programming this microcontroller a little later in this tutorial. Because this microcontroller is fairly complex, we have dedicated another tutorial for it. Please read it here.
Interrupt ChainingWe will be able to install our own interrupt handlers within the Interrupt Descriptor Table (IDT) very easily. We create interrupt handlers to handle not only software interrupts, but interrupts triggered by hardware devices. Remember: The hardware devices signal the Programmable Interrupt Controller to signal the processor to request a hardware interrupt to be triggered. The PIC lets the processor know what Interrupt Request (IRQ) to call within out Interrupt Descriptor Table (IDT). But wait... How does the PIC know what IRQs to call within our IDT? We tell it. This is why we must reprogram the PIC in order to let it know what interrupts to use. Okay, lets say we now have interrupt handlers to handle software and hardware interrupts. What now? How does this work from our perspecitive? Sure, we can easily install handlers for different devices, but what if multiple devices require the same interrupt? What about multiple functions for a software interrupt? This is where Interrupt Chaining comes in. Interrupt chaning is a technique used to restore and call all of the interrupt handlers that share that same interrupt number. This is done by saving the previus Interrupt Routine (IR) in a function pointer. Then, installing the new handler, and calling the previus interrupt handler whenever the new IR is called. Here is an example:
As you can see, interrupt chaining is rather easy. Notice how the previus interrupt handlers will always be called whenever this interrupt fires? setvect() installs a new interrupt vector. getvect() returns an interrupt vector. These interrupt vectors can be stored in either the Interrupt Vector Table (IVT) or Interrupt Descriptor Table (IDT). Wait, what? Thats right--ours :)
Get Ready - Implimenting Interrupt HandlingWe have covered alot of ground with interrupts and interrupt handling. Text alone can only go so far. We have even looked a little bit about how hardware interrupt handling works, but not enough to get very far. We cannot impliment hardware interrupt handling until we learn how to program the Programmable Interrupt Controller. Also, we cannot enable hardware interrupts until we fix the timing problem (Remember that the Programmable Interval Timer is still connected to IRQ8 thanks to the BIOS? That means, as soon as we re-enable hadware interrupts, the next timer tick will result in a double fault.) Because of this, we also have to learn how to reprogram the Programmable Interval Timer. This, dear readers, is where things get complex. Welcome back to the world of hardware programming :) There is good news, however... None of these microcontrollers are very complex. However, to keep the main series from getting too complex, I have decided to write 2 tutorials dedicated to these microcontrollers. This is required reading for understanding of the demo and code that lies ahead. Because of this, I recommend for all of our readers to read the following tutorials before continuing:
Hardware AbstractionThe first interface that we will look at is the one provided by the hardware abstraction layer. This can be seen by looking at include/hal.h and hal/hal.cpp. I will not be describing the routines in depth as most of it is very simple and simply use the other interfaces (GDT, IDT, CPU, PIC, PIT, etc) that we have developed (And are about to develop). Instead, I want to look at the interface itself. This will be the interface used by the kernel and device drivers, so why not?
The new hal.hThis is where we start seeing how useful hardware abstraction can be. I wanted to provide a "DOS"-like interface that is just as easy to use as programming 16bit DOS is. In doing so, I came up with a very easy list of routines that can be used for alot of different purposes. Looking at these routines, you will see there is absolutley no refrence to the hardware devices or tables that are used by them. This is what hardware abstraction is all about. It does not abstract the architecture; but rather the hardware that it uses. Alot of the code that we use later use the routines within the HAL to perform its task. Because of this, I wanted you to take a look at the hardware abstraction layer now, and the routines it provides.
If you have ever programmed 16bit DOS, you should feel at home right about now! :)
Programmable Interrupt Controller
8259: MicrocontrollerThe 8259 Microcontroller familiy is a set of Programmable Interrupt Controller (PIC) Integrated Circuits (ICs). Hardware controllers are indirectly connected to the PIC when a hardware interrupt is requested. Because of this, in order to handle hardware interrupts, we must have an understanding of how to program this microcontroller. I will still go over everything here, however the 8259 is a complex microcontroller. Because of this, we have dedicated a full tutorial to cover just this controller. Because of this, in order to get the most out of this section, Please see (and refrence) the following tutorial to learn about the PIC: 8259A Programmable Interrupt Controller Tutorial Please note: We will not cover everything about the PIC nor hardware interrupt handling here. Please see the above tutorial for this.
8259: AbstractThe Programmable Interrupt Controller (PIC) is a microcontroller used to provide the connection between devices and the processor through interrupt lines. This allows devices to signal the processor whenever it requires attention from the system software or executive. This is the Interrupt Request (IRQ). The PIC controls all of the hardware interrupt requests. It allows us to recieve signals from different hardware devices whenever they require attention. When a device, such as the Floppy Disk Controller (FDC) requires attention, it tells the PIC to fire the IRQ it is assigned to. From here, the PIC will signal the processor, and give the interrupt number to call. The processor then offsets into the IDT, and executes the interrupt handler at ring 0. We define all of the interrupt handlers, so we now take control. The best thing about this is that it is all automatic thanks to the PIC. Whenever a device signals the PIC, our interrupt handler will be executed automatically. The processor also performs a task switch to ring 0, so we will always end up in kernal land to handle the request. Cool, huh? The PIC itself is a complex microcontroller. I will try to cover everything in detail here, but please keep in mind that--in order to get the most out of this tutorial, we encourage our readers to read the above PIC tutorial. With all of that in mind--lets dive into the interface. All of this code can be found in the demo at the end of this tutorial.
Operation CommandsAn Operation Command is a special command that is composed of a bit pattern. This bit pattern must be set up to describe the command for a microcontroller. There are basically two types of operation commands: Initialization Command Words (ICWs) and Operation Command Words (OCWs). ICWs are operation commands that must only be used during the initialization of the device. OCWs are used to control the device after the device has been initialized.
pic.h: InterfaceThis file provides the overall minidriver interface for the rest of the system. This is the interface to controlling and managing the PIC. I define a "minidriver" as a driver embedded in a peice of software, and not as stand alone software.
pic.h: Device ConnectionsIn the PIC tutorial, we have looked at hardware interrupts in alot of depth. We have looked at how hardware devices signal the PIC whenever it requires attention of the system software or executive. For this to work, each device is indirectly connected to an Interrupt Request (IR) line on the PIC. This line not only represents the Interrupt Request (IRQ) the device uses but also its pririty level (The lower the IRQ number, the higher pririty.) To help when working with individual devices and their IRQs, we will want to abstract the IRQ they use. This helps increase portability but also readability as they are behind nice constants. Remember: Magic numbers are bad!
The above constants list all of the devices (along with their IRQ line/number) that they use. There are only 8 IR lines per PIC, hence only 8 possible IRQs per PIC. Remember that PIC's can be cascaded with secondary PICs (Up to 8 PICs can be cascaded with each other.) Typical x86 architectures only have 2--One primary and one secondary. The two most important devices for us right now are the timer (I86_PIC_IRQ_TIMER) and keyboard (I86_PIC_IRQ_KEYBOARD). We will be using I86_PIC_IRQ_TIMER in this tutorial, so you will see how everything works together, cool?
pic: 8259 CommandsSetting up the PIC is farily complex. It is done through a series of Command Words, which are a bit pattern that containes various of states used for initialization and operation. This might seem a little complex, but it is not to hard. We will first look at the Operation Command Word (OCW) that are used to control the PIC. We will look at the initialization commands a little later.
pic: Operation Command Word 1This represents the value in the Interrupt Mask Register (IMR). It does not have a special format, so it is handled directly in the implimentation file to enable and disable hardware interrupts. It is a single byte in size. We enable and disable ("mask and unmask") an interrupt request line by setting the correct bit. Remember that there are only 8 IRQ's per PIC? So, bit 0 in the IMR is IRQ 0, bit 1 is IRQ 1, bit 2 is IRQ 2, and so on. We will take a look at the Interrupt Mask register a little later on, cool?
pic: Operation Command Word 2This is the primary control word used to control the PIC. Lets take a look...
There you have it! This is an important command word for us. We will be required to send this command word from all interrupt handlers. Remember that the PIC masks off the interrupt when it gets executed? This means that no more interrupt requests on that IR line can execute until the processor acknowledges the PIC. This is done by sending an End of Interrupt command word to the correct PIC. We can do this by masking off the EOI bit in the command word. This is what I86_PIC_OCW2_MASK_EOI is used for. A little later on, you will see that the interface has a i86_pic_send_command routine that is used to...erm...send commands to the PIC. Lets look at an example of sending the EOI command using this routine so that you can see how it works:
The above code will send an EOI command to the pic in picNumber, cool? I suppose thats it for OCW 2. On to the next one!
pic: Operation Command Word 3*I plan on adding to this section.*
pic.cpp: ImplimentationOkay...Everything was easy so far, right? You are probably asking "Where is the challenge!??" Well, okay then. pic.cpp provides the implimentation for our PIC interface. First thing we must look at are the registers.
pic.cpp: Register constantsThis is where we define the constants to abstract the port locations for the PICs. Notice that I have defined constants for all register names, even though they share the same port address. The reason is for completness: Even though they share the same port location, they still are different registers.
Not to hard. We send commands to the command register, and read data from the data register. If we are writing from a data register, we are accessing the Interrupt Mask Register (IMR) which can be used to manually mask off or unmask interrupt requests. This is how we enable or disable interrupt requests. The register we are accessing depends on wether it is a write or read operation. If we write to port 0x20, we are accessing the command register. If we are reading from it, we are accessing the status register. Lastly, because this is an implimentation detail, it is part of the implimentation (pic.cpp), not interface. Lets take a look at the constants used during initialization next.
pic.cpp: Initialization Control Word 1This is the primary control word used when initializing the PICs. This is a 7 bit value that must be put in the primary PIC command register. This is the format:
As you can see, there is alot going on here. We have seen some of these before. This is not as hard as it seems, as most of these bits are not used on the x86 platform.There are two types of constants for each command word. The first type are bit masks that are used to mask off the bits that the data represents. The second type of constants used are command control bits which are used in conjunction with the masks to set them to their correct values. Lets look closer. Here are the ICW 1 bit masks. Notice how they follow the format shown in the above table. We do not define anything for the last three bits as they are always zero for x86 architectures.
Okay...We can easily use the above bit masks to set the bits in the ICW 1. But, how do we know what they mean? That is, when we mask off the bits that we are wanting to set, how do we know what the value we are setting them to mean? This is where command control bits come in. They contain the constant values that may be used to set the above masked off bits to. This helps increase readability and extendability alot. Here are the command control bits for ICW 1. Lets take a look...
Not to hard. The naming convention used allows us to easily know what to use, and where. For example, I86_PIC_ICW1_SNGL_YES is used with I86_PIC_ICW1_MASK_SNGL, I86_PIC_ICW1_LTIM_EDGETRIGGERED is used with I86_PIC_ICW1_MASK_LTIM. Here is an example of how they work together. When we are initializing the PIC, we will need to enable initialization, and to send ICW 4. To do this, we simply set up ICW 1 like this:
Thats it!? Yep. Notice how everything works and fits together. This is used throughout the implimentations to set specific bits (or a series of bits) to known values. The best thing here as that--just by looking at the above code--you know what it is doing. (Begin initialization, and to expect ICW 4). Pretty cool, huh? We will be using this method throughout this series when needed when setting and masking off bits.
Initialization Control Word 2This control word is used to map the base address of the IVT of which the PIC are to use.
During initialization, we need to send ICW 2 to the PICs to tell them where the base address of the IRQ's to use. If an ICW1 was sent to the PICs (With the initialization bit set), you must send ICW2 next. Not doing so can result in undefined results. Most likley the incorrect interrupt handler will be executed.Because this command does not have a complex format, it is handled directly inside of pic.cpp and does not have any constants.
Initialization Control Word 3This command word is used to let the PIC controllers know how they are cascaded. To cascade multiple PICs, we must connect one of the PIC's IR lines to each other. We use this command word to let them know what line it is.
Because this command does not have a complex format, it is handled directly inside of pic.cpp and does not have any constants.
Initialization Control Word 4Yey! This is the final initialization control word. This controls how everything is to operate.
This is a pretty complex command word, but not to bad. Lets take a look at our defined bit masks. Notice how they follow the format shown above.
Simular to ICW 1, we have a set of control bits that are used in conjunction with the bit masks to set properties. Here they are...
This is simple snough, huh? ^_^ We can use the above control bits in conjunction with the bit masks to build up the control word. The naming convention used allows us to easily identify what bit masks they are used with. I suppose thats it for the constants used in the implimentation. Lets get on with the functions...
i86_pic_send_command (): Sends a command to a PICThis routine sends a command byte to the PIC's command register. picNum is a zero-based index representing the PIC we are accessing. On x86, this should either be a 0 or 1. Notice that we test what PIC we are working with in order to get the correct command register. While this is part of the interface, it should not be used that much outside of the interface. It provides a method so we can manually send and control the PICs, if needed. This will be required by the interrupt handlers to send the EOI command.
i86_pic_send_data () and i86_pic_read_data (): Send and return a data byte to or from a PICThese routine are very simular to the above routine, however it writes or reads to the PIC's data register depending on the PIC in picNum. Notice how both of these routines are inline. Because these routines are small, we want to take out the function call.
i86_pic_initialize (): Initializes the PICsThis is the final routine for the PIC interface. This initializes both PICs for operation using all of the routines above, and our constants defined for the initialization control words. This routine is not too complex. Or, rather, not as complex as it looks ;) All it does is send the initialization command to the PIC. It does this by setting the I86_PIC_ICW1_INIT_YES bit in the command word. We also set the I86_PIC_ICW1_IC4_EXPECT bit. This insures that the controller expects us to send ICW 4. Notice how the constants help improve readability? The ICW is stored in..well... icw. We send the command to both PICs using our i86_pic_send_command() routine. After ICW 1 is sent, we begin initialization by sending ICW 2. Remember that ICW 2 containes the base interrupt numbers? This is passed into the base0 and base1 parameters. Afterwords, we simply send ICW 3. Remember that ICW 3 provides the connection between the master and secondary PIC controllers. Lastly is ICW 4. We set up x86 mode by setting the I86_PIC_ICW4_UPM_86MODE bit. Compare this routine with the example found in the PIC tutorial and be amazed...very amazed on their simularities!
*Whew*, I guess thats all of the big stuff for the PIC. All that is left is reprogramming the PIT. Don't worry--its not as complex as the PIC is. Lets take a look...
Programmable Interval TimerOkay... So the PIC is ready to go, so we can now enable hardware interrupts, right? Yep--Kind of. While everything is okay so far, we still do not have an interrupt handler installed for the PIT yet. So, what will happen on the next timer tick? ...I think you know where I am getting at :) A Programmable Interval Timer (PIT) is a counter which triggers an interrupts when they reach their programmed count. The 8253 and 8254 microcontrollers are PITs avialble for the i86 architectures used as timer for i86-compatable systems. On x86 architectures, The PIT acts as the system timer, and is connected to the PIC's IR0 line. This allows the PIT to fire IRQ 0 each timer tick. Because of this, we will need to reprogram this microcontroller before we can use it. The PIT is a complex microcontroller to program. Because of this, we have created a separate tutorial for it. While I will still try to cover everything in detail, I will not cover everything about the PIT here. Please see (and refrence) the following tutorial to learn about the PIT: 8253 Programmable Interval Timer Tutorial
pit.h: InterfaceThe good thing about the PIT is that it is not that complex to program. It does not contain that much commands, and yet does not need that much commands. It is a small, but powerful chip used for hardware timing and requests.
Operation Command WordThe PIT only containes one Operation Command Word (OCW) which is used to initialize a counter. It sets up the counters counting mode, operation mode, and allows us to set up an initil count value. The command word is a little complex. Here is the complete command word:
Okay...While this is smaller then the ICWs and OCWs we set up in the PIC, this is actually more complex. The commands used in the PIC are simple in that they are 1 bit in size. The commands used in this operation command word are not. This is where Command Control Bits shine. These help define the different settings and bit combinations for the different bit masks above. Here they are.
Lets look at an example. Lets say we want to initialize counter 0 as a square wave generator in binary count mode. This is how we can do it:
I think I am making this too easy, what do you think? :p This is all you need to do, and ocw will contain the operation command word that can be sent to the PIC. Notice how using these constants help both improve readabilty, but also to decrease the possibility for errors. I guess thats all there is to pit.h. Lets dive into pit.cpp next, shall we? Wee...!!
pit.cpp: ImplimentationThis containes the bulk of the PIT minidriver. It containes the implimentations of each routine used by both the interface and implimentation.
pit.cpp: RegistersThis is where we define the constants to abstract the port locations for the PIT.
Not to bad. I86_PIT_REG_COUNTER0, I86_PIT_REG_COUNTER1, and I86_PIT_REG_COUNTER2 are the data registers for each counter. Remember that the PIT has three internal counters? I86_PIT_REG_COMMAND is our command register. We will need to write commands to the command register to control and operate the PIT. Also, notice _pit_ticks. This is a very special and important global. Remember that the PIT counter 0 connects to the IR0 line on the PIC? This means, when counter 0 fires, it will generate Interrupt Request (IRQ) 0. We will need to create and install an interrupt handler to handle this request. All the interrupt handler needs to do is update the Global Tick Count for the system. That is what _pit_ticks is for.
i86_pit_irq(): PIT Counter 0 Interrupt HandlerThis is the interrupt handler that handles the IRQ 0 request. Whenever Counter 0 fires, it will call this interrupt handler. All it does is increment the global tick count whenever it fires. Note the general format for an interrupt handler. intstart() is a macro used to disable hardware interrupts and save the stack frame so that we can return to the task without missing up its stack. intret() is a macro that disables hardware interrupts, restors the stack frame and returns from the handler using the IRETD instruction. The purpose of this is simply so that we can protect the current stack from being changed, and return back to the task with its stack intact. These macros are defined in asm/system.h so they can be used by the kernel and device drivers interrupt handlers. interrupt is a special constant that is only used on certain compiliers. For MSVC++, it is defined as __declspec (naked). This is so we don't need to worry about the compiliers added code. Some compiliers support this keyword directly (Most notably 16 bit compiliers). Others (Like MSVC++) do not, so we must define it. interruptdone() is a special routine defined in the Hardware Abstraction Layer. It is resposible for sending the End of Interrupt commands to the PIC. This is the generic format that all of our interrupt handlers will use.
i86_pit_send_command (): Send Command to PITThis is a very important routine that allows us to send command to the PIT. This hides the command port we are sending it to, which is nice if we need to change the port name. The command is in the form of an Operation Command Word (OCW).
For an example, we can build up an OCW using our bit masks and command control bits above. Then, we can use i86_pit_send_command() to send the OCW to the PIT.
i86_pit_send_data() and i86_pit_read_data(): Sends and reads data from counterThese routines help abstract the port name used when reading or writing to a counter. These are used to set and get the current count value. All they do is test the counter passed in counter to insure we get the correct port. Then, its just a simple read or write operation through that port.
i86_pit_initialize (): Initialize the PITOkay, lets talk about initializing the PIT. Yeah! Well... er.. There really is not much to talk about, as it really does not require initialization. What we will need to do, however, is provide a way to install our interrupt handler. irq is the interrupt number to use and irCodeSeg is the code seletor offset into the Global Descriptor Table (GDT). We use our i86_install_ir() routine to install our interrupt handler (i86_pit_irq) into the Interrupt Descriptor Table. From here on out, IRQ 0 is mapped to our interrupt handler at irq. irq should be the same base IRQ number that the primary PIC was mapped to use to insure it is mapped to IRQ 0.
i86_pit_start_counter (): Starts an internal counterThis is the final routine in the PIT interface. This starts up a counter. We pass the counter into counter that we want to start (Such as I86_PIT_REG_COUNTER0). mode containes the operation mode that we want the counter to use (Such as I86_PIT_OCW_MODE_SQUAREWAVEGEN). freq containes the frequency rate that we want the counter to operate at. This routine builds up the operational command word based on the paramaters passed into the routine.
ConclusionFrom here on out, all of the basics are completed. We have covered alot in this series, from processor modes and architecture, to processor tables, interrupts, interrupt management, and more. This is the beginning of the kernel, and where the kernel builds off from. In this tutorial, we have added support for the PIC, PIT, exceptions, and hardware interrupt management. This is a important steps, as alot of important devices use hardware interrupts. Also, this gives us a chanch to re-enable hardware interrupts (Remember that we needed to disable hardware interrupts before the switch to protected mode?) In the next tutorial, we will go back to the kernel itself. Its time to talk about one of the most fundemental aspects of any computer system: Paging and Low Level Memory Management. This will also be the foundation of our own System API. I'll see you there... ;)
Until next time,