An interrupt is not a programmed element. An interrupt is more of an event. When the processor detects one of these events, it goes to a table of addresses, each of which is mapped to a different type of interrupt. For example, the 16-bit Intel x86 processor uses an interrupt vector table (IVT) that starts at 0000:0000. Each address in the table requires a segment and an offset pointer and hence requires 4 bytes (32 bits). there are 256 of these vectors allowing for 256 different interrupt vectors. The table below is a summary of some of these vectors. (Source: http://www.beyondlogic.org/interrupts/interupt.htm)
Vector Table ID |
Purpose |
Typical Application |
Non-Maskable IRQ (Parity Errors) | ||
System Timer | ||
Keyboard | ||
Redirected | ||
Serial Comms. COM2/COM4 | ||
Serial Comms. COM1/COM3 | ||
Reserved/Sound Card | ||
Floppy Disk Controller | ||
Parallel Comms. | ||
Real Time Clock | ||
Redirected IRQ2 | ||
Reserved | ||
Reserved | ||
PS/2 Mouse | ||
Math's Co-Processor | ||
Hard Disk Drive | ||
Reserved | ||
It is easy to confuse interrupts with exceptions or even program calls. For example, in 2160, you used a software interrupt, i.e., the assembly language command INT, to access the BIOS service routines. That is not an interrupt - it is a service call.
The interrupt vector points to the starting address of the routine that is meant to be run in response to an interrupt occurring. This routine is referred to as the Interrupt Service Routine or ISR. Since interrupts are not "called," the ISR cannot utilize any of the basic programming structures that programmers have come to rely on. For example, no arguments can be passed to an ISR nor can the ISR return any values. An ISR cannot be part of an object either – it is a stand-alone entity. This implies that the format of an ISR should be:
void ISR_function(void)
{
/* interrupt handler code */
}
There is an additional requirement in the creation of an ISR. Since an interrupt stops the current execution of code and can occur anywhere and at anytime, it cannot affect registers. The standard method of calling a function usually affects registers and flags, a process which is made okay by having the "calling" code expect those changes. Since there is no calling code the interrupt is unexpected, it cannot affect registers. Therefore, there must be a method for defining an ISR which does not affect registers. Some programming languages require a keyword such as "__interrupt" in the function definition in order to achieve this.
void __interrupt ISR_function(void)
{
/* interrupt handler code */
}
Once the interrupt is written, we have to give the processor a way to call it, i.e., we need to enter a pointer to our function in the interrupt vector table. In the old days, it was a straightforward process. You simply called a function called setvect(). There were two arguments for setvect(). The first one was an integer pointing to the row in the IVT pertaining to the interrupt you wanted to set. For example, the keyboard interrupt would be '9'. The second argument was the address of the new ISR. This would give us the code that looked like:
setvect(INTERRUPT_NUMBER, ISR_function);
It is bad practice to simply replace an interrupt without respect for the interrupt that was there before your code was executed. Therefore, before you set your vector, you should get the old vector. This is done with getvect().
old_ISR_function = getvect(INTERRUPT_NUMBER);
Typically, it was good practice to then call the old interrupt after you completed the execution of the code in your ISR. The modified ISR below does this in addition to disabling interrupts during the execution of the ISR.
void __interrupt ISR_function(void)
{
disable(); // Disable interrupts during execution of ISR
/* interrupt handler code */
old_ISR_function(); // Call original ISR
enable(); // Re-enable interrupts before leaving
}
This then gives us the final main() code too.
void main(void)
{
old_ISR_function = getvect(INTERRUPT_NUMBER); // Get old vector
setvect(INTERRUPT_NUMBER, ISR_function); // Insert new vector
/* main code */
setvect(INTERRUPT_NUMBER, old_ISR_function); // Replace old vector
return(0);
}
Unfortunately, the developers of Visual Studio 2005 believe we should have nothing to do with interrupts, so in place of the real thing, I've written a little exercise here to "simulate" the difference between programmed I/O and interrupt driven I/O. Let's begin with programmed I/O.
A necessary part of any processor system is timing. The program we are going to write is going to access the system timers in order to output a period once every second to the console. In programmed I/O, we are going to need to do the following procedure.
The primary item to note is that the loop contains a "read timer" function. This is what makes this portion of the exercise a "polled" or programmed I/O implementation.
The function we are going to use to read the timer is QueryPerformanceCounter(). This function reads the processors high resolution timer which is incremented once with every clock pulse of the processor's clock. This means that for a 3.59 GHz machine, this timer is incremented approximately 3,590,000,000 times in a second. QueryPerformanceCounter() takes as its single parameter a pointer to a LARGE_INTEGER which you define in your code. This gives us a way to read how many clock cycles have occurred since the machine was turned on.
LARGE_INTEGER current_time;
QueryPerformanceCounter(¤t_time);
The number returned by QueryPerformanceCounter() is huge and not very useful for timing at the "seconds" level. As I mentioned earlier, it is incremented once with every pulse of the system clock. If we knew the system clock frequency, we could figure out how many seconds had passed since turning on the machine. This can be done with QueryPerformanceFrequency(). This function returns the frequency of the processor once again using a LARGE_INTEGER passed as a pointer for the function's only parameter.
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
Dividing the time by the frequency will convert the time to seconds.
Assignment: Using these two functions, create the C++ code that will perform the operation shown in the previous flowchart. Create a console application in Visual Studio 2005 with the following features:
This method of time keeping is unuseable for a number of reasons. The most important reason is that it takes processing power away from other duties of the application. Second, and more importantly, the code that needs to be notified of the passing of a specified amount of time may not even be in control of the processor. Take for example an operating system. An operating system my give a specified amount of time to an application to run. After that period, the operating system is going to take control back. The application isn't going to give up control, so the O/S must take it. But since the O/S is not running, the processor must have an alternate way to stop the application and return control to the O/S. Timer interrupts do this.
As I said earlier, Visual Studio won't give us access to the IVT. We are going to use a function provided by Microsoft to simulate a timer interrupt. This function is called SetTimer(). SetTimer() allows a programmer to specify a time duration after which the O/S will call a specified function. This is sort of like an interrupt in that we get to say something like, "in 250 milliseconds, call this function." It shouldn't matter what code is being executed, although we'll see in a moment that that is not entirely true.
The prototype for SetTimer() is:
UINT_PTR SetTimer(HWND hWnd, UINT_PTR nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc);
The UINT_PTR return value will be an integer. If the SetTimer function failed, it will equal zero. Otherwise, it will be an integer identifying the assigned timer.
The first parameter, hWnd, is a handle identifying the window. Since we are going to be running this as a console application, there will be no window. Just set this value to NULL.
The second parameter, nIDEvent, is used to identify the timer in case you are using multiple timers or reusing a timer in your application. Microsoft claims that if hWnd parameter is NULL, nIDEvent is ignored. Just put a non-zero value here like 1.
The third parameter, uElapse, is the amount of time you wish to have expire before calling the function. It is in milliseconds.
The fourth parameter, lpTimerFunc, is a pointer to your CALLBACK function which is what will be executed when the timer expires. The important thing to note about this function is that it requires a special prototype. The prototype includes a number of parameters, all of which we will be ignoring. The CALLBACK function prototype should look like the following:
void CALLBACK yourFunction(HWND hWnd, UINT uMessage, UINT uTimerID, DWORD dwTickCount);
Once the timer is set, it will continue to interrupt once every uElapse seconds until you kill it with KillTimer().
Assignment: Write a console application using Visual Studio 2005 that does the following:
cout << ".";
IMPORTANT: If this was a true interrupt, there would be no code whatsoever in the while loop. Each time the timer expired, the CALLBACK function would be executed without any intervention from the main code. This is where the simulation falls apart. We need to make sure that the console knows to check the message queue which is where the request to execute the CALLBACK function is contained. THIS IS NOT A TRUE INTERRUPT. Within your while loop, enter the following code to check the message queue.
GetMessage(&msg, NULL, 0, 0);
TranslateMessage (&msg);
DispatchMessage (&msg);
Be able to compare and contrast the two pieces of code you've written today. It's important to understand the difference between programmed I/O and interrupt driven I/O.
Developed by David Tarnoff for CSCI 4717 -- Computer Architecture at ETSU