Interrupts are meant to act as an asynchronous means of getting the processor's attention. Due to the vast differences in the speeds and operation of devices with respect to the processor and the fact that external events do not coincide exactly with the states of the processor, there must be a means for devices to tell the processor that they need attention. Examples of situations where a device would need attention include having data to transfer, indicating that an assigned process is complete, or signalling that an error has occured. Because the processor is most likely tending to other operations, it needs to be stopped by the device to handle the situation.
There are two places where interrupts need to be enabled, globally and locally. In some cases, the process that the processor is handling may be critical enough to warrant ignoring other interrupts. It is for this reason that interrupts can be enabled and disabled on a global level. The global method for enabling and disabling interrupts is through the assembly language commands STI (set interrupt flag) and CLI (clear interrupt flag). Linux has two functions that serve these same purposes, sti() and cli().
There is a serious problem with simply using sti() and cli() in your application. It may affect the operation of other processes which depended on a specific setting of the interrupt enable flag. This is most critical for the operating system. It is for this reason that your code shouldn't take it upon itself to enable and disable the interrupts.
If you need to have interrupts enabled or disabled, you should use the following process:
The following example uses this process in the case of disabling the processor's interrupts.
unsigned long processor_flags;
save_flags(processor_flags); // Protect current processor flags values
cli(); // Disable interrupts
// Execute your code which requires disabled interrupts
restore_flags(processor_flags); // Return processor flags to previous values
It is important to note that caution must be taken when trying this technique. For example, do not perform the save_flags() and restore_flags() macros in different functions because it is possible to perform more calls to one than another or to perform them out of order. Disable interrupts, perform a short operation, then re-enable them as soon as you can.
On some SMP (multi-processor) systems, cli() disables the interrupts on all processors and can degrade the performance of the system.
Most interrupts also need to be individually enabled (locally) before they will interrupt the processor. This usually involves setting a second flag that points to a specific interrupt. In some cases, there may be more than one local interrupt enable flag. For example, the serial port has multiple interrupt enables including one to enable transmit interrupts, one to enable receive interrupts, and one to enable error interrupts. All types of local interrupts may call the same interrupt service routine (ISR), but they would occur at different times and for different reasons. Therefore, one of the tasks of the code used to initialize the use of a device's interrupts is to enable the interrupt locally. (Global enabling should be taken care of by the operating system.)
The application we wrote for the polled I/O lab was written in user space. Interrupts and device drivers, however, must be part of kernel space. User space applications are typically executed by the user and have access to the user I/O such as the mouse, keyboard, and monitor. Kernel space code interfaces with the components of the kernel and rarely if ever have any interaction with the user I/O. For example, in code for kernel space, there is no printf(). There is, however, a printk() function which is used to log events that occur in kernel space. printk() has a similar function to printf() except that the first three characters of the string to be printed should be of the form "<x>" where represents the priority of the message. We will be using printk() to indicate when a character has been received. (This is not what should be done in practice. In practice, the character received should be placed in a buffer.) The following is a list of the priority values for the printk() function taken directly from kernel.h. Some of the higher priorities are sent to the user output (monitor), but it is a bad idea to send all output to the monitor as this could make the user space unusable.
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
Running code in kernel space is also different. There is no main(). Instead, code in kernel space is made up of functions that are executed based on events. For example, we are going to "install" our interrupt code as a kernel module. A module is installed using insmod and removed using rmmod. When insmod is used to install a module, any initialization code to be performed should be contained in the function init_module(), not main(). When rmmod is used to remove a module, any closing code to be performed should be contained in the function cleanup_module(). The prototypes for these functions are shown below.
int init_module(void);
void cleanup_module(void);
The insmod and rmmod are the events that will call our module. For example, if we create a module called my_module.c, we would compile it using the gcc command
gcc -c my_module.c
The -c switch (compile and assemble, but do not link) is vital since we are not creating an application. insmod will install the object code to the kernel. To install my_module, use insmod.
insmod ./my_module.o
This command executes the module's function init_module() and install the module in the system. The command rmmod my_module will execute the module's function cleanup_module() and remove the module from the system.
The module will contain our interrupt code.
Last week we put together a simple program to watch the serial port and output any characters that were received. The only problem with the program was that it had to "watch" the port. Today, we are going to let the serial port do its own watching and notify the processor when something is received. To do this, we will be using interrupts, specifically the receive interrupt on the serial port.
If we examine the Interrupt Enable Register (IER) at BASE_ADDRESS + 1 with DLAB = 0, we see that it has six different conditions for which we can be interrupted:
For this lab, the only interrupt we will be interested in is the received data available interrupt. Setting this bit will cause the serial port to interrupt us when a character has been fully received by the port and is ready to be read. Remember that our code detected a received character by watching the least significant bit of the Line Status Register (LSR). When the bit turned to one, we knew there was a character in the receive buffer.
We need to begin setting up the interrupt by setting up the serial port for the correct communication parameters just as we did last week. In the case of the serial mouse, this means 1200 bps, 7 data bits, no parity, and 1 stop bit. Remember that we need to set permissions for reading and writing to the I/O ports using ioperm().
// Begin configuring serial port by setting up the divisor register
// To access the divisor register, DLAB (the most significant bit
// in the line control register, LCR) must be set.
byte_read = inb(BASE_ADDR+LCR_OFFSET);
byte_read |= 0x80; // Set first bit (DLAB) of LCR
outb(byte_read, BASE_ADDR+LCR_OFFSET);
// For a BAUD rate of 1200, the low byte of the divisor register
// must equal 0x60 and the high byte of the divisor must be cleared
outb(0x60, BASE_ADDR+DIV_LO_OFFSET);
outb(0, BASE_ADDR+DIV_HI_OFFSET);
// Now that the BAUD rate has been set, see need to set up the line
// control register. DLAB (bit 7) must be cleared so that we can
// access the receive register. Break enable must be disabled by
// clearing bit 6. Bits 5, 4, and 3 must be set to 000 for no parity.
// Bit 2 must be cleared for one stop bit, and bits 1 and 0 must be
// 10 for 7 data bits. This gives us a value of 00000010=0x2 for LCR.
outb(0x02, BASE_ADDR+LCR_OFFSET);
// We also need to initialize the FIFO Control Register (FCR).
// Let's make it so that interrupts are triggered when a single byte
// comes into the receive register. This is done by clearing the
// first two bits to 0. We also need to clear and enable the FIFOs.
// This is done by setting bits 2, 1, and 0 to 1.
outb(0x07, BASE_ADDR+FCR_OFFSET);
// Since we are not using any hardware control, i.e., we are going to
// ignore the modem lines, it might be a good idea to disable the
// modem control register.
outb(0, BASE_ADDR+MCR_OFFSET);
Initializing the serial port to use interrupts requires only one more step - setting the appropriate bits in IER. Last week's lab began by printing out all of the register values of the serial port. When we look at these values, we see that IER contained 0. This means that none of the interrupts were enabled.
We need to enable only the receive buffer interrupt. This means that we should set bit 0 and leave the rest cleared.
outb(1, BASE_ADDR+IER_OFFSET);
Since we are creating code for the kernel space and not the user space, all of this code should be contained in the init_module() routine of our module code.
It is possible to enable and disable interrupts with the operating system using three functions provided in <asm/irq.h>. These are:
disable_irq(int irq); -- Disables irq after any currently
executing handlers are finished.disable_irq_nosync(int irq); -- Disables irq without waiting for
any currently executing handlers to finish.enable_irq(int irq); -- Enables the irq.If you nest these functions, be sure to enable as many times as you disabled. For example, if you called disable_irq() twice, be sure to call enable_irq() twice to make sure the interrupt has been re-enabled.
When an interrupt occurs, the processor needs to have code to execute in response. This code may do things like
This is by no means a comprehensive list, but it should give you an idea of some of the things that might need to be done.
When an interrupt occurs, it's the processor's duty to jump to a special routine called an interrupt service routine (ISR) or interrupt handler. An ISR is simply a function that handles the needs of the interrupt. It is important to note that because interrupt "lines" to the processor are a limited resource, many times a device must share an interrupt with one or more other devices.
There are some things that must be done to initiate an interrupt. These include
This is to be done in the init_module() function.
We need to install the ISR before we initialize the device to generate an interrupt. This is because if you tell the system to begin generate interrupts, an interrupt may occur before you get the ISR installed putting the processor into an unpredictable state.
An ISR is just a function and can be written in C, but because the ISR needs to be called from the operating system, in our case the Linux kernel, we need to make sure that it follows some specific rules. For example:
The general prototype for an ISR is:
static void isr_function(int irq, void *dev_id, struct pt_regs *regs)
There are three arguments sent to the handler, and although none of them are required to be used, they do provide information that might be helpful.
int irq - This is the simply the interrupt number.
void *dev_id - When the ISR is installed (see the >next
section), a void * argument is passed to it to identify the device doing the interrupting.
This value is passed to the ISR in case the ISR services multiple devices.
struct pt_regs *regs - In case the ISR needs to access the content of the
processor's registers at the time the interrupt occurred, those values can be accessed here.
This is highly unlikely since the ISR should have no dependence whatsoever on the condition
of the processor's state at the time of the interrupt. It might be helpful if you need to
peform any debugging.
Our ISR will also be included in the module code.
Simply telling the hardware to generate an interrupt and writing an ISR isn't enough. We need to tell the operating system to expect the interrupt. In Linux, this is done by requesting an IRQ from the kernel with the function request_irq(). The prototype for request_irq() is shown below:
int request_irq (unsigned int irq, void(*handler)(int irq, void *, struct pt_regs *), unsigned long irqflags, const char *devname, void *dev_id)
The parameters for this function are as follows:
unsigned int irq - This is the interrupt number that the interrupt
handler will be assigned to. In Linux, a list of the currently active IRQs along with
the number of times they have been called can be retrieved using
cat /proc/interrupts. The serial port /dev/ttyS0 uses IRQ 4.
void(*handler)(int irq, void *devid, struct pt_regs *) - This is a
pointer to the interrupt handler. int irq allows the interrupt number
to be passed to the handler. void *devid is usually used to pass a
pointer to the interrupting device's data structure to allow the handler to identify
the specific device calling the interrupt. This is important in cases where the
handler is used for multiple devices. struct pt_regs * is a pointer
to a copy of the register values before the interrupt handler was started. Most
likely, these values will not be used.
unsigned long irqflags - This is a set of flags defining the
operation of the interrupt.
const char *devname - A string used to identify the device using the interrupt.
void *dev_id - This is a unique identifier used to point to the
specific device doing the interrupting.
Once the interrupt is enabled, we can request the IRQ using request_irq(). To see a list of all of the interrupts that have been enabled, enter the following command at the command line.
cat /proc/interrupts
When rmmod is executed thereby calling cleanup_module(), we need to disable the interrupt and remove it from the IRQ list. Before removing an ISR, we need to make sure that the device will not be generating any more interrupts. In the case of the serial port, this is done by clearing the IER.
outb(0, BASE_ADDR+IER_OFFSET);
After the device has been closed and is unable to generate an interrupt, the
interrupt handler can be removed. This is done using the function void
free_irq(unsigned int irq, void *dev_id). Its arguments are the same as
those by the same name and type described in the request_irq() section.
Now we need to write the software to be executed when the interrupt occurs. In the polling lab, we created an infinite loop that read the Line Status Register over and over again. When bit 0 was set, i.e., the serial port was telling us that receive data was available, we would read from the receive data register and output it to the screen.
Interrupts will make it so that we don't have to watch the LSR for bit 0 to be
set. We still, however, will need to read the data from the receive data register.
That is the function of the interrupt service routine (ISR). This means that the inb(BASE_ADDR+RX_BUF_OFFSET) will be moved from the infinite loop in
our code to the ISR.
When an interrupt occurs, the installed ISR will be called to handle the interrupt. In the case of the serial port, all we really need to do is
Checking whether an interrupt has occurred and determining the type of interrupt are both handled with the Interrupt Identification Register (IIR) (Use the Interfacing the Serial/RS-232 Port site as a reference.) Bit 0 which shows whether an interrupt has occurred, so your ISR should check to see if that bit is a 1. After we have checked for bit 0 to be set in the IIR, we need to make sure that it was an interrupt for a received character. Your code should probably check for and handle all possible interrupt conditions. If an interrupt has occurred, the type of interrupt is shown in bits 1 and 2. A setting of 10 in these bits indicates received data is available. Have your code check to see that this is what happened.
Once the interrupt condition has been verified, we need to receive the data. This is done the same way we did it for the polled scheme, simply read the receive buffer. The following code implements these steps.
/*************** First, check to see if an interrupt has occurred *****************/
if(inb(BASE_ADDR+IIR_OFFSET)&0x01)
{
/*********** Now, check to see if interrupt was for received character ************/
if((inb(BASE_ADDR+IIR_OFFSET)&0x06) == 0x04)
{
/*** If we get here, we know data has been received and we should grab the data ***/
printf("Data received: %x\n", inb(BASE_ADDR+RX_BUF_OFFSET));
}
}
The previous sections present a step-by-step discussion of how to write the code to install a serial port ISR. It might help, however, to see the code in its final form.
|
/* The following two lines indicate to the
compiler that we are compiling a module */ /* module.h includes the appropriate kernel
includes to allow us to work in the */ /* kernel space.
*/ #define MODULE #include <linux/module.h> /* Basic include for the outb() and inb()
functions.
*/ #include <asm/io.h> /* The following defines the address space for
the serial port. */ #define BASE_ADDR 0x3f8 #define ADDR_SPACE 8 #define THB_OFFSET 0 #define RX_BUF_OFFSET 0 #define DIV_LO_OFFSET 0 #define DIV_HI_OFFSET 1 #define IER_OFFSET 1 #define IIR_OFFSET 2 #define FCR_OFFSET 2 #define LCR_OFFSET 3 #define MCR_OFFSET 4 #define LSR_OFFSET 5 #define MSR_OFFSET 6 /* We also need to define the serial port IRQ
number. Remember that /dev/ttyS0 */ /* uses IRQ 4. */ #define TTYS0_IRQ 4 /******************************
serial_interrupt() ********************************/ /* This is our serial port interrupt service
routine. It is the one that will be */ /* executed whenever an IRQ 4 occurs. */ /**********************************************************************************/ static void
serial_interrupt(int irq, void *dev_id, struct
pt_regs *regs) { /*************** First, check to see if an
interrupt has occurred *****************/ if(inb(BASE_ADDR+IIR_OFFSET)&0x01) { /*********** Now, check to see if interrupt was
for received character ************/ if((inb(BASE_ADDR+IIR_OFFSET)&0x06) == 0x04) { /*** If we get here, we know data has been
received and we should grab the data ***/ printk("Data received: %x\n",
inb(BASE_ADDR+RX_BUF_OFFSET)); } } } /************************** End of
serial_interrupt() *****************************/ /***************************** init_module()
**************************************/ int init_module(void) { int byte_read; printk("<1>Attempting to initialize serial port
receive interrupt."); /* NOTE: This code is not complete! Availability of interrupts along with */ /* appropriate error checking is needed. */ // Begin by granting permission to the I/O space ioperm(BASE_ADDR,ADDR_SPACE,1);ioperm(0x60,1,1); // Begin configuring serial port by setting up
the divisor register // To access the divisor register, DLAB (the most
significant bit // in the line control register, LCR) must be
set. byte_read =
inb(BASE_ADDR+LCR_OFFSET); byte_read |= 0x80; // Set first bit (DLAB) of LCR outb(byte_read,
BASE_ADDR+LCR_OFFSET); // For a BAUD rate of 1200, the low byte of the
divisor register // must equal 0x60 and the high byte of the
divisor must be cleared outb(0x60,
BASE_ADDR+DIV_LO_OFFSET); outb(0,
BASE_ADDR+DIV_HI_OFFSET); // Now that the BAUD rate has been set, see need
to set up the line // control register. DLAB (bit 7) must be cleared so that we can // access the receive register. Break enable must be disabled by // clearing bit 6. Bits 5, 4, and 3 must be set to 000 for no
parity. // Bit 2 must be cleared for one stop bit, and
bits 1 and 0 must be // 10 for 7 data bits. This gives us a value of 00000010=0x2 for
LCR. outb(0x02,
BASE_ADDR+LCR_OFFSET); // We also need to initialize the FIFO Control
Register (FCR). // Let's make it so that interrupts are triggered
when a single byte // comes into the receive register. This is done
by clearing the // first two bits to 0. We also need to clear and enable the FIFOs. // This is done by setting bits 2, 1, and 0 to 1. outb(0x07,
BASE_ADDR+FCR_OFFSET); // Since we are not using any hardware control,
i.e., we are going to // ignore the modem lines, it might be a good
idea to disable the // modem control register. outb(0,
BASE_ADDR+MCR_OFFSET); // Before we enable the interrupt, we need to
point the kernel to the // ISR serial_interrupt() using
request_irq(). if(request_irq(TTYS0_IRQ, serial_interrupt, 0, "serial_port", 0)) { printk("<1>Request for IRQ %d granted.",
TTYS0_IRQ); // If we get the IRQ, enable the interrupt in the
Interrupt Enable Register (IER) outb(1,
BASE_ADDR+IER_OFFSET); } else printk("<1>Request for IRQ %d denied.",
TTYS0_IRQ); } /******************************* End of init_module()
******************************/ /*********************************
cleanup_module() ********************************/ void cleanup_module(void) { /********** Begin by disabling the interrupt
locally at the serial port ************/ outb(0,
BASE_ADDR+IER_OFFSET); /********************* Next, releast the IRQ
using free_irq() **********************/ free_irq(TTYS0_IRQ,
0); } /****************************** End of
cleanup_module() ****************************/ |
Developed by David Tarnoff for CSCI 4717 -- Computer Architecture at ETSU