Chapter 6. Control of External Interrupts

Some SGI computer systems can generate and receive external interrupt signals. These are simple, two-state signal lines that cause an interrupt in the receiving system.

The external interrupt hardware is managed by a kernel-level device driver that is distributed with IRIX and automatically configured when the system supports external interrupts. The driver provides two abilities to user-level processes:

External interrupt support is closely tied to the hardware of the system. The features described in this chapter are hardware-dependent and in many cases cannot be ported from one system type to another without making software changes. System architectures are covered in separate sections:

External Interrupts in Challenge and Onyx Systems

The hardware architecture of the Challenge/Onyx series supports external interrupt signals as follows:

  • Four jacks for outgoing signals are available on the master IO4 board. A user-level program can change the level of these lines individually.

  • Two jacks for incoming interrupt signals are also provided. The input lines are combined with logical OR and presented as a single interrupt; a program cannot distinguish one input line from another.

The electrical interface to the external interrupt lines is documented at the end of the ei(7) reference page.

A program controls the outgoing signals by interacting with the external interrupt device driver. This driver is associated with the device special file /dev/ei, and is documented in the ei(7) reference page.

Generating Outgoing Signals

A program can generate an outgoing signal on any one of the four external interrupt lines. To do so, first open /dev/ei. Then apply ioctl() on the file descriptor to switch the outgoing lines. The principal ioctl command codes are summarized in Table 6-1.

Table 6-1. Functions for Outgoing External Signals in Challenge

Operation

Typical ioctl() Call

Set pulse width to N microseconds.

ioctl(eifd, EIIOCSETOPW, N)

Return current output pulse width.

ioctl(eifd,EIIOCGETOPW,&var)

Send a pulse on some lines M.[a] 

ioctl(eifd, EIIOCSTROBE, M)

Set a high (active, asserted) level on lines M.

ioctl(eifd, EIIOCSETHI, M)

Set a low (inactive, deasserted) level on lines M.

ioctl(eifd, EIIOCSETLO, M)

[a] M is an unsigned integer whose bits 0, 1, 2, and 3 correspond to the external interrupt lines 0, 1, 2, and 3. At least one bit must be set.

In the Challenge and Onyx series, the level on an outgoing external interrupt line is set directly from a CPU. The device driver generates a pulse (function EIIOCSTROBE) by asserting the line, then spinning in a disabled loop until the specified pulse time has elapsed, and finally deasserting the line. Clearly, if the pulse width is set to much more than the default of 5 microseconds, pulse generation could interfere with the handling of other interrupts in that CPU.

The calls to assert and deassert the outgoing lines (functions EIIOCSETHI and EIIOCSETLO) do not tie up the kernel. Direct assertion of the outgoing signal should be used only when the desired signal frequency and pulse duration are measured in milliseconds or seconds. No user-level program, running in a CPU that is not isolated and reserved, can hope to generate repeatable pulse durations measured in microseconds using these functions. (A single interrupt occurring between the call to assert the signal and the call to deassert it can stretch the intended pulse width by as much as 200 microseconds.) A real-time program, running in a CPU that is reserved and isolated from interrupts—perhaps a program that uses the Frame Scheduler—could generate repeatable millisecond-duration pulses using these functions.

Responding to Incoming External Interrupts

An important feature of the Challenge and Onyx external input line is that interrupts are triggered by the level of the signal, not by the transition from deasserted to asserted. This means that, whenever external interrupts are enabled and any of the input lines are in the asserted state, an external interrupt occurs. The interface between your program and the external interrupt device driver is affected by this hardware design. The functions for incoming signals are summarized in Table 6-2.

Table 6-2. Functions for Incoming External Interrupts

Operation

Typical ioctl() Call

Enable receipt of external interrupts.

ioctl(eifd, EIIOCENABLE)
eicinit();

Disable receipt of external interrupts.

ioctl(eifd, EIIOCDISABLE)

Specify which CPU will handle external interrupts.

ioctl(eifd, EIIOCINTRCPU, cpu)

Specify which CPU will execute driver ioctl calls, or -1 for the CPU where the call is made.

ioctl(eifd, EIIOCSETSYSCPU, cpu)

Block in the driver until an interrupt occurs.

ioctl(eifd, EIIOCRECV, &eiargs)

Request a signal when an interrupt occurs.

ioctl(eifd, EIIOCSTSIG, signumber)

Wait in an enabled loop for an interrupt.

eicbusywait(1)

Set expected pulse width of incoming signal.

ioctl(eifd, EIIOCSETIPW, microsec)

Set expected time between incoming signals.

ioctl(eifd, EIIOCSETSPW, microsec)

Return current expected time values.

ioctl(eifd, EIIOCGETIPW, &var)
ioctl(eifd, EIIOCGETSPW, &var)


Directing Interrupts to a CPU

In real-time applications, certain CPUs can be reserved for critical processing. In this case you may want to use EIIOCINTRCPU, either to direct interrupt handling away from a critical CPU, or to direct onto a CPU that you know has available capacity. Use of this ioctl requires installation of patch 1257 or a successor patch.

Detecting Invalid External Interrupts

The external interrupt handler maintains two important numbers:

  • the expected input pulse duration in microseconds

  • the minimum pulse-to-pulse interval, called the “stuck” pulse width because it is used to detect when an input line is “stuck” in the asserted state

When the external interrupt device driver is entered to handle an interrupt, it waits with interrupts disabled until time equal to the expected input pulse duration has passed since the interrupt occurred. The default pulse duration is 5 microseconds, and it typically takes longer than this to recognize and process the interrupt, so no time is wasted in the usual case. However, if a long expected pulse duration is set, the interrupt handler might have to waste some cycles waiting for the end of the pulse.

At the end of the expected pulse duration, the interrupt handler counts one external interrupt and returns to the kernel, which enables interrupts and returns to the interrupted process.

Normally the input line is deasserted within the expected duration. However, if the input line is still asserted when the time expires, another external interrupt occurs immediately. The external interrupt handler notes that it has been reentered within the “stuck” pulse time since the last interrupt. It assumes that this is still the same input pulse as before. In order to prevent the stuck pulse from saturating the CPU with interrupts, the interrupt handler disables interrupts from the external interrupt signal.

External interrupts remain disabled for one timer tick (10 milliseconds). Then the device driver re-enables external interrupts. If an interrupt occurs immediately, the input line is still asserted. The handler disables external interrupts for another, longer delay. It continues to delay and to test the input signal in this manner until it finds the signal deasserted.

Setting the Expected Pulse Width

You can set the expected input pulse width and the minimum pulse-to-pulse time using ioctl(). For example, you could set the expected pulse width using a function like the one shown in Example 6-1.

Example 6-1. Challenge Function to Test and Set External Interrupt Pulse Width

int setEIPulseWidth(int eifd, int newWidth)
{
   int oldWidth;
   if ( (0==ioctl(eifd, EIIOCGETIPW, &oldWidth))
   &&   (0==ioctl(eifd, EIIOCSETIPW, newWidth)) )
      return oldWidth;
   perror("setEIPulseWidth");
   return 0;
}

The function retrieves the original pulse width and returns it. If either ioctl() call fails, it returns 0.

The default pulse width is 5 microseconds. Pulse widths shorter than 4 microseconds are not recommended.

Since the interrupt handler keeps interrupts disabled for the duration of the expected width, you want to specify as short an expected width as possible. However, it is also important that all legitimate input pulses terminate within the expected time. When a pulse persists past the expected time, the interrupt handler is likely to detect a “stuck” pulse, and disable external interrupts for several milliseconds.

Set the expected pulse width to the duration of the longest valid pulse. It is not necessary to set the expected width longer than the longest valid pulse. A few microseconds are spent just reaching the external interrupt handler, which provides a small margin for error.

Setting the Stuck Pulse Width

You can set the minimum pulse-to-pulse width using code like that in Example 6-1, using constants EIIOCGETSPW and EIIOCSETSPW.

The default stuck-pulse time is 500 microseconds. Set this time to the nominal pulse-to-pulse interval, minus the largest amount of “jitter” that you anticipate in the signal. In the event that external signals are not produced by a regular oscillator, set this value to the expected pulse width plus the duration of the shortest expected “off” time, with a minimum of twice the expected pulse width.

For example, suppose you expect the input signal to be a 10 microsecond pulse at 1000 Hz, both numbers plus or minus 10%. Set the expected pulse width to 10 microseconds to ensure that all pulses are seen to complete. Set the stuck pulse width to 900 microseconds, so as to permit a legitimate pulse to arrive 10% early.

Receiving Interrupts

The external interrupt device driver offers you four different methods of receiving notification of an interrupt. You can

  • have a signal of your choice delivered to your process

  • test for interrupt-received using either an ioctl() call or a library function

  • sleep until an interrupt arrives or a specified time expires

  • spin-loop until an interrupt arrives

You would use a signal (EIIOCSETSIG) when interrupts are infrequent and irregular, and when it is not important to know the precise arrival time. Use a signal when, for example, the external interrupt represents a human-operated switch or some kind of out-of-range alarm condition.

The EIIOCRECV call can be used to poll for an interrupt. This is a relatively expensive method of polling because it entails entry to and exit from the kernel. The overhead is not significant if the polling is infrequent—for example, if one poll call is made every 60th of a second.

The EIIOCRECV call can be used to suspend the caller until an interrupt arrives or a timeout expires (see the ei(7) reference page for details). Use this method when interrupts arrive frequently enough that it is worthwhile devoting a process to handling them. An unknown amount of time can pass between the moment when the interrupt handler unblocks the process and the moment when the kernel dispatches the process. This makes it impossible to timestamp the interrupt at the microsecond level.

In order to poll for, or detect, an incoming interrupt with minimum overhead, use the library function eicbusywait() (see the ei(7) reference page). You use the eicinit() function to open /dev/ei and prepare to use eicbusywait().

The eicbusywait() function does not switch into kernel mode, so it can perform a low-overhead poll for a received interrupt. If you ask it to wait until an interrupt occurs, it waits by spinning on a repeated test for an interrupt. This monopolizes the CPU, so this form of waiting is normally used by a process running in an isolated CPU. The benefit is that control returns to the calling process in negligible time after the interrupt handler detects the interrupt, so the interrupt can be handled quickly and timed precisely.

External Interrupts In Origin 2000 and Origin 200

The miscellaneous I/O attachment logic in the Origin 2000 and Origin 200 architecture is provided by the IOC3 ASIC. Among many other I/O functions, this chip dedicates one input line and one output line for external interrupts.

There is one IOC3 chip on the motherboard in a Origin 200 deskside unit. There is one IOC3 chip on the IO6 board which provides the base I/O functions in each Origin 2000 module; hence in a Origin 2000 system there can be as many unique external interrupt signal pairs as there are physical modules.

The electrical interface to the external interrupt line is documented at the end of the ei(7) reference page.

A program controls the outgoing signals by interacting with the external interrupt device driver. This driver is associated with device special files /hw/external_interrupt/n, where n is an integer. The name /hw/external_interrupt/1 designates the only external interrupt device in a Origin 200, or the external interrupt device on the system console module of a Origin 2000 system.  

There is also a symbolic link /dev/ei that refers to /hw/external_interrupt/1.

Generating Outgoing Signals

A program can generate an outgoing signal—as a level, a pulse, a pulse train, or a square wave—on any external interrupt line. To do so, first open the device special file. Then apply ioctl() on the file descriptor to command the output.

A command to initiate one kind of output (level, pulse, pulse train or square wave) automatically terminates any other kind of output that might be going on. When all processes have closed the external interrupt device, the output line is forced to a low level.

In the Origin 2000 and Origin 200 systems, the level on an outgoing external interrupt line is set by the IOC3 chip. The device driver issues a command by PIO to the chip, and the pulse or level is generated asynchronously while control returns to the calling process. Owing to the speed of the R10000 CPU and its ability to do out-of-order execution, it is entirely possible for your program to enter the device driver, command a level, and receive control back to program code before the output line has had time to change state.

Generating Fixed Output Levels

The ioctl command codes for fixed output levels are summarized in Table 6-3.

Table 6-3. Functions for Fixed External Levels in Origin 2000

Operation

Typical ioctl() Call

Set a high (active, asserted) level.

ioctl(eifd, EIIOCSETHI)

Set a low (inactive, deasserted) level.

ioctl(eifd, EIIOCSETLO)

Direct assertion of the outgoing signal (using EIIOCSETHI and EIIOCSETLO) should be used only when the desired signal frequency and pulse duration are measured in milliseconds or seconds. A typical user-level program, running in a CPU that is not isolated and reserved, cannot hope to generate repeatable pulse durations measured in microseconds using these functions. A real-time program, running in a CPU that is reserved and isolated from interrupts may be able to generate repeatable millisecond-duration pulses using these functions.

Generating Pulses and Pulse Trains

You can command single pulse of this width, or a train of pulses with a specified repetition period. The ioctl functions are summarized in Table 6-4.

Table 6-4. Functions for Pulses and Pulse Trains in Origin 2000

Operation

Typical ioctl() Call

Set pulse width to N microseconds (ignored).

ioctl(eifd, EIIOCSETOPW, N)

Return current output pulse width (23).

ioctl(eifd,EIIOCGETOPW,&var)

Send a 23.4 microsecond pulse.

ioctl(eifd, EIIOCSTROBE)

Set the repetition interval to N microseconds.

ioctl(eifd, EIIOCSETPERIOD, N)

Return the current repetition interval.

ioctl(eifd,EIIOCGETPERIOD,&var)

Initiate regular pulses at the current period.

ioctl(eifd, EIIOCPULSE)

The IOC3 supports only one pulse width: 23.4 microseconds. The EIIOCSETOPW command is accepted for compatibility with the Challenge driver, but is ignored. The EIIOCGETOPW function always returns 23 microseconds.

The repetition period can be as short as 23.4 microseconds (pass N=24) or as long as slightly more than 500000 microseconds (0.5 second). Any period is truncated to a multiple of 7,800 nanoseconds.

Generating a Square Wave

You can command a square wave at a specified frequency. The ioctl functions are summarized in Table 6-5.

Table 6-5. Functions for Outgoing External Signals in Origin 2000

Operation

Typical ioctl() Call

Set the toggle interval to N microseconds.

ioctl(eifd, EIIOCSETPERIOD, N)

Return the current toggle interval.

ioctl(eifd,EIIOCGETPERIOD,&var)

Initiate a square wave.

ioctl(eifd, EIIOCSQUARE)

The period set by EIIOCSETPERIOD determines the interval between changes of state on the output—in other words, the period of the square wave is twice the interval. The repetition period can be as short as 23.4 microseconds (pass N=24) or as long as slightly more than 500000 microseconds (0.5 second). Any period is truncated to a multiple of 23.4 microseconds.

Responding to Incoming External Interrupts

The IOC3 external input line (unlike the input to the Challenge and Onyx external input line) is edge-triggered by a transition to the asserted state, and has no dependence on the level of the signal. There is no concept of an “expected” pulse width or a “stuck” pulse width as in the Challenge (see “Detecting Invalid External Interrupts”).

The external interrupt device driver offers you four different methods of receiving notification of an interrupt. You can

  • have a signal of your choice delivered to your process

  • test for interrupt-received using either an ioctl() call or a library function

  • sleep until an interrupt arrives or a specified time expires

  • spin-loop until an interrupt arrives

The functions for incoming signals are summarized in Table 6-6. The details of the function calls are found in the ei(7) reference page.

Table 6-6. Functions for Incoming External Interrupts in Challenge

Operation

Typical Function Call

Enable receipt of external interrupts.

ioctl(eifd, EIIOCENABLE)

eicinit();

eihandle = eicinit_f(eifd);

Disable receipt of external interrupts.

ioctl(eifd, EIIOCDISABLE)

Request a signal when an interrupt occurs, or clear that request by passing signumber=0.

ioctl(eifd, EIIOCSETSIG, signumber)

Poll for an interrupt received.

eicbusywait(0);

eicbusywait_f(eifd,0);

ioctl(eifd,EIIOCRECV,&eiargs)

Block in the driver until an interrupt occurs, or until a specified time has elapsed.

ioctl(eifd,EIIOCRECV,&eiargs)

Wait in an enabled loop for an interrupt.

eicbusywait(1);

eicbusywait_f(eihandle,1);

You would use a signal (EIIOCSETSIG) when interrupts are infrequent and irregular, and when it is not important to know the precise arrival time. Use a signal when, for example, the external interrupt represents a human-operated switch or some kind of out-of-range alarm condition.

The EIIOCRECV call can be used to poll for an interrupt. This is a relatively expensive method of polling because it entails entry to and exit from the kernel. This is not significant if the polling is infrequent—for example, if one poll call is made every 60th of a second.

The EIIOCRECV call can be used to suspend the caller until an interrupt arrives or a timeout expires (see the ei(7) reference page for details). Use this method when interrupts arrive frequently enough that it is worthwhile devoting a process to handling them. An unknown amount of time can pass between the moment when the interrupt handler unblocks the process and the moment when the kernel dispatches the process. This makes it impossible to timestamp the interrupt at the microsecond level.

In order to poll for, or detect, an incoming interrupt with minimum overhead, use the library function eicbusywait() (see the ei(7) reference page). You use the eicinit() function to open /dev/ei and prepare to use eicbusywait(); or you can open one of the other special device files and pass the file descriptor to eicinit_f().

The eicbusywait() function does not switch into kernel mode, so it can perform a low-overhead poll for a received interrupt. If you ask it to wait until an interrupt occurs, it waits by spinning on a repeated test for an interrupt. This monopolizes the CPU, so this form of waiting is normally used by a process running in an isolated CPU. The benefit is that control returns to the calling process in negligible time after the interrupt handler detects the interrupt, so the interrupt can be handled quickly and timed precisely.