![]() |
|
||||
Writing Device Drivers for LynxOS |
Interrupt and Timeout Handling
This chapter discusses issues related to the design and implementation of interrupt service routines (ISRs) and timeout handlers.
Introduction
Interrupts are external hardware exception conditions that are delivered to the processor to indicate the occurrence of a specific event. ISRs are useful for:
For example, an interrupt could be generated indicating the completion of a DMA (Direct Memory Access) transfer. The device driver would give a command to the DMA controller to transfer a block of data and set the vector for the interrupt generated by the controller to a specific driver function. This, in turn, would signal a semaphore to wake up any system or user threads waiting on the completion of the DMA transfer.
The availability of data at a port is often indicated by an interrupt. A tty driver receives an interrupt when a character is ready to be read from the port, for example.
A printer generates an interrupt when it has printed a character and is ready to print the next character.
Timeout Interrupts
LynxOS timeout handlers are called by the clock interrupt handler and therefore are considered to be similar to any interrupt handler. Instructions for setting up timeout handlers are provided in "Timeout Handlers".
Interrupts and Real-Time Response
A task, regardless of its priority, is interrupted if an interrupt is pending and interrupts are enabled. This could result in low priority interrupt service routines executing before high priority tasks that have real-time constraints.
To offload processing from interrupt-based sections of a device driver, LynxOS offers a feature known as kernel threads. Kernel threads are independently schedulable entities that closely resemble processes but do not have the memory overhead associated with processes.
Using kernel threads, delays are significantly reduced. Instead of the interrupt service routine handling all the servicing of the interrupt, a kernel thread is used to perform the function previously performed by the interrupt routine. A kernel thread is scheduled according to process priority and not hardware priority. This ensures that the interrupt service time is kept to a minimum and the task response time is kept short. The use of kernel threads is covered in detail in "Kernel Threads and Priority Tracking"
LynxOS Interrupt Handlers
Interrupt handlers in LynxOS are specified in the install() or open() entry point functions and are cleared in the uninstall() or close() entry points. Interrupt handlers run before any other kernel or application processing is completed.
Interrupt handlers are declared and reset using the functions iointset() and iointclr(). The table below summarizes iointset() and iointclr().
Interrupt handlers cannot directly use application virtual addresses. The application virtual addresses must be translated to kernel virtual addresses before they can be accessed by driver routines. Refer to "Memory Management" for more information on address translation.
iointset()
The device driver registers its interrupt handler routine with the LynxOS interrupt dispatcher using the iointset() function call. The interrupt dispatcher subsequently calls the interrupt handler routine when an interrupt occurs. iointset() cannot be called from within an ISR.
The prototype for iointset() is:
The interrupt vector number used by the hardware sending the interrupt is specified by the vector argument. The interrupt dispatcher calls the routine given by function. The interrupt dispatcher passes arguments to the interrupt handler.
On x86 and SPARC systems, iointset() returns the index of the previous interrupt vector. This can be used with ioint_link() to share interrupt vectors.
iointclr()
The iointclr() function clears an interrupt vector from the stack of interrupt handlers. iointclr() cannot be called from within an ISR.
The prototype for iointclr() is:
The vector argument specifies the interrupt vector number to clear.
For each interrupt vector, the kernel maintains a stack of interrupt handlers. If a device driver installs a new interrupt handler at a position occupied by an existing handler, the old handler is reinstalled when iointclr() is called for vector.
Sharing IRQs
ioint_link() is used on hardware where two or more drivers must share the same interrupt vector. To share an interrupt, each interrupt routine must check if its device has interrupted and then act accordingly; it must also call the next interrupt routine on the list that has the same hardware vector.
When a device driver shares an interrupt with other drivers, it can use the value returned by iointset() as a key to the next interrupt handler on the stack of interrupt handlers. After processing the interrupt, the interrupt handler can use ioint_link() to cause the next interrupt handler in the stack to be dispatched. For example:
dev_install(info)
{...
s=sysbrk(...);
...
s->key = iointset(vector, int_handler, s);
...
int_handler(s)
{
...
...
ioint_link(s->key);
}
Interrupt Vector Values
x86
On the x86 platform, iointset() and iointclr() require the interrupt vector number, not the address of the vector. The interrupt vector number (0-15) must also be offset by 32. For example:
PowerPC
Many PowerPC systems have two or more interrupt controllers. For example the Motorola 16xx series and the PowerStack series have an i8259-compatible interrupt controller to handle ISA interrupts and the VMECHIP2 handles VME interrupts. On the Motorola PowerPlus systems, there are three interrupt controllers. There is an MPIC device, which is the master interrupt controller, and handles interrupts from PCI, timers, and cascaded interrupt controllers. There is a i8259-compatible interrupt controller to handle ISA-based interrupts. The Universe VME controller handles the VME interrupts on some of the PowerPlus systems.
The PowerPC processor has only one interrupt input. The LynxOS interrupt dispatch routine determines the source of the interrupt and uses a table with 256 entries to dispatch the interrupt service routine. The vector parameter to iointset() is the index to this table.
To maintain compatibility with x86-based device drivers, the ISA interrupt controller uses MASTER_BASE (32) as the base vector. The PowerPlus systems use the MPIC_BASE (4) as the base vector for the MPIC. The VME controllers use the VME_IRQBASE (48) as the base vector.
The interrupt vector space is divided as follows:
0...3 Reserved vectors 4...28 MPIC vectors 29...31 Reserved vectors 32...48 ISA vectors 48...255 VME vectorsAs in the x86-based systems, iointset() returns a key, which can be used in ioint_link() to share interrupts. For device drivers using the PCI service functions, the pci_what_irq(slot) returns a vector, which can be used directly in the iointset() function.
Interrupt Levels
When a device generates an interrupt, the interrupt signal is propagated from the device through the bridge or bus controller. Some boards may have a single interrupt controller, while others may have two or more.
The figure below shows a typical configuration on x86-based computers, where two Intel 8259 interrupt controllers are cascaded. INT1 is called the master controller, because it is closest to the CPU. INT2 is called the slave controller.
When one of the IRQ input lines of an interrupt controller is asserted, the controller asserts its int+ output line. For INT1, this signals the CPU that an interrupt has occurred. For INT2, the interrupt signal is passed to INT1, which then signals the CPU.
When the CPU is signaled, it communicates with the interrupt controllers to determine which IRQ line has been asserted. The CPU executes the interrupt handler that was most recently registered for that IRQ. When the handler terminates, the CPU continues where it left off before executing the handler.
The Intel 8259 interrupt controllers are cascaded as shown in the figure below.
![]()
Intel 8259 Interrupt Controller ConfigurationThe interrupt controllers are programmed so that each IRQ input has a distinct interrupt priority. When an interrupt handler is servicing an interrupt, it may be preempted when another device asserts an interrupt with a higher priority.
The following table shows the IRQs, sorted by interrupt priority, with the most favorable priority at the top, and the least favorable at the bottom.
Interrupt Priority 1(cascade)2345 IRQ 1IRQ 2IRQ 8IRQ 9IRQ 10IRQ 11 678910 IRQ 12IRQ 13IRQ 14IRQ 15IRQ 3 1112131415 IRQ 4IRQ 5IRQ 6IRQ 7IRQ 0On all Board Support Packages (BSPs), IRQ 1 is used for the keyboard and has the most favorable priority. IRQ 0 is used for the real-time clock and has the least favorable priority. This means that the clock interrupt handler could be preempted by any other interrupt, whereas the keyboard handler cannot.
IRQ 2 is used to cascade interrupts from Controller INT2 to Controller INT1. The use of the other IRQs may vary for different BSPs.
The assignment of priorities to IRQs is made by the kernel code at boot time. The assignments are found in the file /sys/bsp.xxx/bsp.intr.c. In some BSPs, the assignment can be configured at kernel build time.
Implementing an Interrupt Handler
A typical approach to the organizational structure of a device driver that contains an interrupt handler is to divide the code into two components called process context functions and kernel context functions (see figure below).
Top/Bottom Model for Device Drivers![]()
Process context functions are the driver entry point functions and other supporting code, and the kernel context functions are the interrupt handler, and its subroutine send. Between the two halves are shared data structures (read and write queues in this example) internal to the device driver.
The send routine is a device driver supporting function used to send data to the hardware. (See "send() Routine" for an example.)
Use of Queues
Queues are often used to communicate between the process context and kernel context functions. Examples of the use of queues to communicate between entry points and interrupt handlers are:
A counting semaphore, initialized to the size of the queue, tracks the free space in the write() entry point. The swait() function is called, and if space is available in the queue a character is queued. The interrupt handler subsequently removes the character from queue and signals the semaphore using ssignal().
The semaphore in the read() entry point tracks data availability in the queue. The swait() routine blocks until data is available in the queue. The interrupt handler posts the data to the queue, signaling the semaphore that data is available, if queue space is available. If queue space is unavailable, an error flag is set.
Following is an example code structure.
dev_read()
{
swait(&receive_data_available,SEM_SIGIGNORE);
disable();
dequeue_receive_data();
restore();
}
dev_write()
{
disable();
swait(&space_on_queue_available,SEM_SIGIGNORE);
enqueue_send_data();
if (no_interrupt_pending)
output_data();
restore();
}
interrupt_handler()
{
if (data_received)
{
enqueue_receive_data();
ssignal(&receive_data_available);
}
else
{
if (dequeue_send_data())
output_data();
else no_interrupt_pending = 1;
}
}
Interrupt Handler Considerations
Following are programming considerations to use when creating an interrupt handler.
- Use disable() and restore() in the entry point functions and their subroutines to prevent interrupts from accessing data structures that are being modified.
- Application virtual addresses cannot be directly accessed from the interrupt handler.
- Translate application virtual addresses to kernel addresses in the entry point functions prior to making them available to the interrupt routines.
Example Code
Following is an example of an interrupt-based printer device driver.
Device Information Definition
The device information definition has three variables associated with the interrupting driver. The port variable is the port number for the printer; irq is the interrupt line on which the printer interrupts the system; and qlen is the length of the queue used to communicate between the top and bottom halves of the driver.
Device Information Declaration
The device information declaration for the device information definition gives the port address for the printer port, which is 0x378. The IRQ line on which the printer interrupts is 7, and the queue length is 100. The program ptrinfo.c is compiled and executed to create a data file to be passed to the install routine during dynamic installation. (See "Installation and Debugging" for more on dynamic installation.)
Declaration for ioctl
The ioctl() routine in this driver returns the number of characters and lines printed out so far. Thus, the user can issue the ioctl system call with the appropriate command and pointer to the structure defined above.
Driver Source Code
#define PP_DATA 0 /* data port offset */
#define PP_STATUS 1 /* status port offset */
#define PP_CONTROL 2 /* control port offset */
#define PP_BUSY 0x80 /* printer busy */
#define PP_PE 0x20 /* out of paper */
#define PP_SLCT 0x10 /* printer is selected */
#define PP_ERROR 0x08 /* printer detected error */
#define PP_IENABLE 0x10 /* interrupt enable */
#define PP_SLCTIN 0x08 /* select printer */
#define PP_INIT 0x04 /* start printer */
#define PP_AUTOLF 0x02 /* auto line feed */
#define PP_STROBE 0x01 /* strobe printer */
#define port_in(addr) __inb /* copy 1 byte from port */
#define port_out(data,addr) __outb(addr,data) /* copy 1 byte to port */
Statics Structure
The statics structure for the interrupt handling printer driver is shown above. The IRQ (irq) number, the queue length (qlen) and port address (dport) are copied from the device information definition.
chars and lines store the number of characters and lines printed out so far. q is the base address of the queue. head and tail keep track of data in the queue.
close_sem is a semaphore used for ensuring that the output queue is fully drained before the device is closed. expecting is initially reset to indicate that the first character has to be output before an interrupt is received by the system. nextnl is used for mapping \n (newline) to \r\n (carriage return/line feed).
install() Entry Point
The install() entry point function checks for the existence of the printer. The data is written onto the port and read back immediately. If the data is not the same, then there is no printer. Once the presence of the printer is confirmed, the statics structure and the queue are allocated. The fields within the structure are initialized.
free_sem is initialized to the queue length and the irq number is copied into the statics data structure. The routine iointset() is called to initialize an interrupt handler for the given interrupt vector. (The offset of 32 is added to the irq number before passing it to the routine on the x86.) Finally, an initialization sequence is sent to the printer.
Notice that there is a timing loop between the two port_out calls. This is required for the transmitted data to be latched properly. The pointer to the statics structure is returned.
uninstall() Entry Point
The uninstall() entry point clears the interrupt vector by using the iointclr() function. uninstall() frees the memory associated with the queue and the statics structure.
open() Entry Point
The open() entry point checks for the access mode of the device and returns an error if the application has tried to open it in read mode.
close() Entry Point
The close() entry point function ensures that the characters to be output are complete before the device is closed. It checks whether s>expecting is 1. This indicates that there are characters present in the queue. If this is true, it sets the s>closing flag and waits in the swait() routine for the interrupt handler to signal that all characters are output and the device can be closed. It also resets the chars and lines fields in the statics structure definition.
send() Routine
This routine outputs a character into the port by dequeuing from the queue. If a new line is found it puts out a \r and resets the nextnl flag. If not, it dequeues from the head of the queue, adjusts the head pointer to wrap around, and signals the semaphore while keeping track of the free space in the queue. This routine then increments the number of characters s>chars.
The routine then puts a character onto the printer port. The while loop to check the status port is no longer necessary because the interrupt signifies that it is safe to write. The character is just put onto the data port. After this, to latch the byte onto the printer, a high-low strobe is put onto the control port.
Interrupt() Handler
If the queue has data in it, or if a new line is indicated, the routine send() is called to output the character onto the port. If not, it indicates that the queue is empty and thus s>expecting is set to zero. Further, if the closing flag is set, it indicates that the close() entry point routine is in an swait() state. Thus, an ssignal routine is called to awaken the semaphore. Finally, an initialization sequence is sent to the control port.
write() Entry Point
The write() entry point has a loop for count characters to be output onto the queue. The swait() inside the loop tracks the free space in the queue. If there is space in the queue a character is queued.
The disable() and restore() function calls provide protection for the critical region of code, which is used by the interrupt handler. The character is queued to the tail of the queue. The qdata variable (which keeps track of the number of characters in the queue) is incremented. If expecting is zero, the first character on the queue is sent to the port and expecting is set to one.
ioctl() Entry Point
The ioctl() entry point handles the case for PTRSTATUS. The user can invoke this from an application program to determine the number of characters actually output onto the port. arg is the pointer to the user buffer. The check by wbounds() confirms that the user pointer has writable memory allocated to it.
x86 IRQ Device Defaults
The following table is a list of devices with their interrupt (also called IRQ) levels, I/O addresses and additional default information. No two devices with the same IRQ can be configured into the LynxOS kernel at the same time..
x86 Default Device Configuration IRQ1 DMA Channel3 Device Comments 0 N/A N/A 1 0x3B4 0x3D4 0x3C4 0x3C5 0x3CE 0x3CF Keyboard 2 N/A N/A Cascade 3 0x2F8 N/A 3 N/A N/A 4 0x3F8 N/A 5 0x3E8 N/A 5 0x240 N/A 5 0x3F0 N/A Slot number 7TP = 0, AUI = 1, BNC = 3 (default 3) 5 0x240 N/A Slot number 0TP = 0, AUI = 1, BNC = 3 (default 3) 5 0x240 N/A On board RAM address4: 0xcc00016 bit access 1 6 0x3F2-0x3F7 N/A 8237 DMA controller 7 0x378 N/A 7 0x200 N/A On board RAMbase: 0xd0000 8 N/A Real-time clock 9 N/A Vertical sync. interrupt 9 0x3E0 N/A 10 Unused by default. 11 0x330-0x332 5 bus_on5: 8bus_off6: 40 11 0x3307 5 Edge/Level8: edge(1) 11 N/A 5 EISA slot9: 2 12 Unused by default. 13 Unused by default. 14 0x1F1-0x1F7, 0x3F6-0x3F7 Defaults are hard coded in driver; primary controller. 15 Unused by default. - 0x340 N/A PIO mode only N/A N/A No user configurable options.
4On board RAM address: Some boards have memory mapped to a particular range in the I/O space. This number is the start of such memory. The size of memory varies from card to card.
Timeout Handlers
A timeout handler can be set up using the timeout driver service call, timeout().
The prototype for timeout() is:
The timeout causes the function handler to be called with one argument, arg, after interval has expired. timeout() returns a non-negative timeout ID if there is a timer available. This ID can be used to track or cancel the timeout.
A timeout can be canceled before the routine is called using cancel_timeout() and passing it the timeout ID. Care should be taken to cancel only a pending timeout. The check for a timeout expiration and cancelling timeout should be done atomically with interrupts disabled.
Following is a basic timeout handler algorithm.
entry_point()
{
int ps;
sem = 0;
/* start device operations */
timeoutID = timeout(timeoutHandler, arg, 1);
swait(&sem, -1);
disable(ps);
if (timeoutID)
{
cancel_timeout(timeoutID);
timeoutID = 0;
}
restore(ps);
}
timeoutHandler(arg)
{
/* do timeout processing */
timeoutID = 0;
ssignal(&sem);
}
ISR()
{
ssignal(&sem);
}
LynxOS timeout handlers are called by the clock interrupt handler. Timeout routines are handled in the kernel using a delta queue to check for all expired timers. Because the timeout handler is called from the clock interrupt handler, it should not execute for a long period of time. If a lengthy timeout processing is needed inside the timeout handler routine, it is better to handle the timeout inside a kernel thread. (See "Kernel Threads and Priority Tracking" for more information on kernel threads.) The timeout handler can simply increment a variable indicating the number of timeouts accumulated and signal the kernel thread. The kernel thread wakes up and handles the more complicated processing associated with the timeout.
Using kernel threads ensures that the thread can call routines, which can use semaphores for mutual exclusion instead of interrupt disabling, thus improving real-time response. The thread can handle all accumulated timeouts and once this is completed it blocks on the semaphore.
Following is an example timeout handler algorithm implemented with kernel threads.
entry_point()
{
event_sem = 0;
timeoutCt = 0;
tid = timeout(timeoutHandler, arg, ticks);
}
timeoutHandler(arg)
{
timeoutCt++;
ssignal(&event_sem);
}
Thread()
{
int ps;
int touts;
for (;;)
{
swait(&event_sem, -1);
disable(ps);
touts = timeoutCt;
timeoutCt = 0;
restore(ps);
while (touts--)
timeout_processing();
}
}
timeout_processing()
{
swait(&mutex_sem, -1);
/* do timeout processing */
ssignal(&mutex_sem);
}
![]() LynuxWorks, Inc. 855 Branham Lane East San Jose, CA 95138 http://www.lynuxworks.com 1.800.255.5969 |
![]() |
![]() |
![]() |
![]() |