Writing Device Drivers

Chapter 7 Interrupt Handlers

This chapter describes interrupt handling mechanisms, such as registering, servicing, and removing interrupts. This chapter provides information on the following subjects:

Interrupt Handler Overview

An interrupt is a hardware signal from a device to a CPU. It tells the CPU that the device needs attention and that the CPU should stop performing what it is doing and respond to the device. If a CPU is available (it is not performing a task with higher priority), it suspends the current thread and eventually invokes the interrupt handler for that device. The job of the interrupt handler is to service the device and stop it from interrupting. Once the handler returns, the CPU resumes what it was doing before the interrupt occurred.

The DDI/DKI provides interfaces for registering and servicing interrupts.

Interrupt Specification

The interrupt specification is information the system uses to bind a device interrupt source with a specific device interrupt handler. The specification describes the information provided by the hardware to the system when making an interrupt request. Because an interrupt specification is bus specific, the information it contains varies from bus to bus.

Interrupt specifications typically include a bus-interrupt level. For vectored interrupts the specifications include an interrupt vector. On IA platforms the interrupt specification defines the relative interrupt priority of the device. Because interrupt specifications are bus specific, see the man pages for isa(4), eisa(4), sbus(4), and pci(4) for information on interrupt specifications for these buses.

Interrupt Number

When registering interrupts the driver must provide the system with an interrupt number. This interrupt number identifies the interrupt specification for which the driver is registering a handler. Most devices have one interrupt: interrupt number 0. However, there are devices that have different interrupts for different events. A communications controller may have one interrupt for receive ready and one for transmit ready. The device driver normally knows how many interrupts the device has, but if the driver has to support several variations of a controller, it can call ddi_dev_nintrs(9F) to find out the number of device interrupts.

Interrupt Block Cookies

An iblock cookie is an opaque data structure that represents the information the system needs to block interrupts and is returned from ddi_get_iblock_cookie(9F) or ddi_get_soft_iblock_cookie(9F). This interface uses an interrupt number to return the iblock cookie associated with a specific interrupt source. The value of the iblock cookie (not the address) must be passed to mutex_init(9F) when initializing driver mutexes that will be used in the interrupt routine. The value of the iblock cookie is obtained by passing the address of the cookie to ddi_get_iblock_cookie() or ddi_get_soft_iblock_cookie(). For example:


       ddi_get_soft_iblock_cookie(dip, DDI_SOFTINT_HI,
               &xsp->low_iblock_cookie)
       mutex_init(&xsp->low_mu, NULL, MUTEX_DRIVER,
               (void *)xsp->low_iblock_cookie);

Device Interrupts

There are two common ways in which buses implement interrupts: vectored and polled. Both methods commonly supply a bus-interrupt priority level. However, vectored devices also supply an interrupt vector; polled devices do not.

High-Level Interrupts

Buses prioritize device interrupts at one of several bus-interrupt levels. These bus interrupt levels are then mapped to different processor-interrupt levels. A bus interrupt level that maps to a CPU interrupt priority level above the scheduler priority level is called a high-level interrupt. High-level interrupt handlers are restricted in what DDI interfaces they can call. In particular, the only DDI routines that high-level interrupt handlers are allowed to call are:

A bus-interrupt level by itself does not determine whether a device interrupts at high level: a given bus-interrupt level may map to a high-level interrupt on one platform, but map to an ordinary interrupt on another platform.

The driver can choose whether to support devices that have high-level interrupts, but it always has to check—it cannot assume that its interrupts are not high level. The function ddi_intr_hilevel(9F), given an interrupt number, returns a value indicating whether the interrupt is high level.

Normal Interrupts

The only information the system has about a device interrupt is either the bus interrupt priority level (IPL, on an SBus in a SPARC machine, for example) or the interrupt request number (IRQ on an ISA bus in an IA machine, for example).

When an interrupt handler is registered, the system adds the handler to a list of potential interrupt handlers for each IPL or IRQ. Once the interrupt occurs, the system must determine which device, of all the devices associated with a given IPL or IRQ, actually interrupted. It does this by calling all the interrupt handlers for the designated IPL or IRQ, until one handler claims the interrupt.

The SBus, ISA, EISA, and PCI buses are capable of supporting polled interrupts.

Software Interrupts

The Solaris 9 DDI/DKI supports software interrupts, also known as soft interrupts. Soft interrupts are initiated by software, rather than by a hardware device. Handlers for these interrupts must also be added to and removed from the system. Soft interrupt handlers run in interrupt context and therefore can be used to do many of the tasks that belong to an interrupt handler.

Hardware interrupt handlers are supposed to perform their tasks quickly, since they may suspend other system activity while running. This is particularly true for high-level interrupt handlers, which operate at priority levels greater than that of the system scheduler. High-level interrupt handlers mask the operations of all lower-priority interrupts—including those of the system clock. Consequently, the interrupt handler must avoid involving itself in an activity (such as acquiring a mutex) that might cause it to sleep.

If the handler sleeps, then the system may hang because the clock is masked and incapable of scheduling the sleeping thread. For this reason, high-level interrupt handlers normally perform a minimum amount of work at high-priority levels and delegate remaining tasks to software interrupts, which run below the priority level of the high-level interrupt handler. Because software interrupt handlers run below the priority level of the system scheduler, they can do the work that the high-level interrupt handler was incapable of doing.

Registering Interrupts

Before a device driver can receive and service interrupts, it must register an interrupt handler with the system by calling ddi_add_intr(9F). Registering interrupts provides the system with a way to associate an interrupt handler with an interrupt specification. The interrupt handler is called when the device might have been responsible for the interrupt. The handler has the responsibility of determining if it should handle the interrupt and, if so, of claiming it.


Caution – Caution –

There is a potential race condition between adding the interrupt handler and initializing mutexes. The interrupt routine is eligible to be called as soon as ddi_add_intr(9F) returns, as another device might interrupt and cause the handler to be invoked. This may result in the interrupt routine being called before any mutexes have been initialized with the returned interrupt block cookie. If the interrupt routine acquires the mutex before it has been initialized, undefined behavior may result. To ensure that this race condition does not occur, always initialize mutexes and any other data used in the interrupt handler before adding the interrupt.


To register a driver's interrupt handler, the driver usually performs the following steps in attach(9E).

  1. Test for high-level interrupts by calling ddi_intr_hilevel(9F) to find out if the interrupt specification maps to a high-level interrupt. If it does, one possibility is to post a message to that effect and return DDI_FAILURE. See Example 7–1.

  2. Get the iblock cookie by calling ddi_get_iblock_cookie(9F).

  3. Initialize any associated mutexes with the iblock cookie by calling mutex_init(9F).

  4. Register the interrupt handler by calling ddi_add_intr(9F).

Example 7–1 shows how to install an interrupt handler.


Example 7–1 attach(9E) Routine Installing an Interrupt Handler

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
      struct xxstate *xsp;
      switch (cmd) {
      case DDI_ATTACH:
              ...
              if (ddi_intr_hilevel(dip, inumber) != 0){
                    cmn_err(CE_CONT,
                        "xx: high-level interrupts are not supported\n");
              return (DDI_FAILURE);
              }
              ddi_get_iblock_cookie(dip, inumber, &xsp->iblock_cookie);
              mutex_init(&xsp->mu, NULL, MUTEX_DRIVER,
                  (void *)xsp->iblock_cookie);
              cv_init(&xsp->cv, NULL, CV_DRIVER, NULL);
              if (ddi_add_intr(dip, inumber, NULL, NULL, xxintr,
                  (caddr_t)xsp) != DDI_SUCCESS){
                      cmn_err(CE_WARN, "xx: cannot add interrupt handler.");
                      goto failed;
              }
              return (DDI_SUCCESS);
      case DDI_RESUME:
          For information, see Chapter 9, Power Management
      default:
          return (DDI_FAILURE);
      }
failed:
     remove interrupt handler if necessary, destroy mutex and condition variable
     return (DDI_FAILURE);
}

Interrupt Handler Responsibilities

The interrupt handler has a set of responsibilities to perform. Some are required by the framework, and some are required by the device. All interrupt handlers are required to do the following:

The following example shows an interrupt routine.


Example 7–2 Interrupt Example

static uint_t
xxintr(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    uint8_t         status; 
    volatile  uint8_t  temp;

    /*
     * Claim or reject the interrupt.This example assumes
     * that the device's CSR includes this information.
     */
    mutex_enter(&xsp->high_mu);
    /* use data access routines to read status */
    status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    if (!(status & INTERRUPTING)) {
            mutex_exit(&xsp->high_mu);
            return (DDI_INTR_UNCLAIMED); /* dev not interrupting */
    }
    /*
     * Inform the device that it is being serviced, and re-enable
     * interrupts. The example assumes that writing to the
     * CSR accomplishes this. The driver must ensure that this data
     * access operation makes it to the device before the interrupt
     * service routine returns. For example, using the data access
     * functions to read the CSR, if it does not result in unwanted
     * effects, can ensure this.
     */
    ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
            CLEAR_INTERRUPT | ENABLE_INTERRUPTS);
               /* flush store buffers */
    temp = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    
    mutex_exit(&xsp->mu);
    return (DDI_INTR_CLAIMED);
}

Most of the steps performed by the interrupt routine depend on the specifics of the device itself. Consult the hardware manual for the device to determine the cause of the interrupt, detect error conditions, and access the device data registers.

Handling High-Level Interrupts

High-level interrupts are those that interrupt at the level of the scheduler and above. This level does not allow the scheduler to run; therefore, high-level interrupt handlers cannot be preempted by the scheduler, nor can they rely on the scheduler (that is, they cannot block because of the scheduler)—they can only use mutual exclusion locks for locking.

Because of this, the driver must use ddi_intr_hilevel(9F) to determine if it is using high-level interrupts. If ddi_intr_hilevel(9F) returns true, the driver can fail to attach, or it can use a two-level scheme to handle interrupts.

The suggested method is to add a high-level interrupt handler, which simply triggers a lower-priority software interrupt to handle the device. The driver should allow more concurrency by using a separate mutex for protecting data from the high-level handler.

High-level Mutexes

A mutex initialized with the interrupt block cookie that represents a high-level interrupt is known as a high-level mutex. While holding a high-level mutex, the driver is subject to the same restrictions as a high-level interrupt handler.

High-Level Interrupt Handling Example

In the example presented in Example 7–3, the high-level mutex (xsp->high_mu) is used only to protect data shared between the high-level interrupt handler and the soft interrupt handler. This includes a queue that the high-level interrupt handler appends data to (and the low-level handler removes data from), and a flag that indicates the low-level handler is running. A separate low-level mutex (xsp->low_mu) protects the rest of the driver from the soft interrupt handler.


Example 7–3 attach(9E) Routine Handling High-Level Interrupts

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        struct xxstate *xsp;
        ...
        if (ddi_intr_hilevel(dip, inumber)) {
            ddi_get_iblock_cookie(dip, inumber,
                &xsp->high_iblock_cookie);
            mutex_init(&xsp->high_mu, NULL, MUTEX_DRIVER,
                (void *)xsp->high_iblock_cookie);
            if (ddi_add_intr(dip, inumber, &xsp->high_iblock_cookie,
                &xsp->high_idevice_cookie, xxhighintr, (caddr_t)xsp)
                != DDI_SUCCESS)
                goto failed;
            ddi_get_soft_iblock_cookie(dip, DDI_SOFTINT_HI,
                &xsp->low_iblock_cookie)
            mutex_init(&xsp->low_mu, NULL, MUTEX_DRIVER,
                (void *)xsp->low_iblock_cookie);
            if (ddi_add_softintr(dip, DDI_SOFTINT_HI, &xsp->id,
                &xsp->low_iblock_cookie, NULL,
                xxlowintr, (caddr_t)xsp) != DDI_SUCCESS)
                goto failed;
        } else {
            add normal interrupt handler
        }
        cv_init(&xsp->cv, NULL, CV_DRIVER, NULL);
        ...
        return (DDI_SUCCESS);
    failed:
            free allocated resources, remove interrupt handlers
        return (DDI_FAILURE);
}

The high-level interrupt routine services the device, and enqueues the data. The high-level routine triggers a software interrupt if the low-level routine is not running, as Example 7–4 demonstrates.


Example 7–4 High-level Interrupt Routine

static uint_t
xxhighintr(caddr_t arg)
{
    struct xxstate    *xsp = (struct xxstate *)arg;
    uint8_t        status;
    volatile  uint8_t  temp;
    int        need_softint;
    
    mutex_enter(&xsp->high_mu);
    /* read status */
    status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    if (!(status & INTERRUPTING)) {
        mutex_exit(&xsp->high_mu);
        return (DDI_INTR_UNCLAIMED); /* dev not interrupting */
    }

    ddi_put8(xsp->data_access_handle,&xsp->regp->csr,
        CLEAR_INTERRUPT | ENABLE_INTERRUPTS);
        /* flush store buffers */
    temp = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    read data from device and queue the data for the low-level interrupt handler;
    if (xsp->softint_running)
        need_softint = 0;
    else {
        xsp->softint_count++;
        need_softint = 1;
    }
    mutex_exit(&xsp->high_mu);
    /* read-only access to xsp->id, no mutex needed */
    if (need_softint)
        ddi_trigger_softintr(xsp->id);
    return (DDI_INTR_CLAIMED);
}

The low-level interrupt routine is started by the high-level interrupt routine triggering a software interrupt. Once running, it should continue to do so until there is nothing left to process, as Example 7–5 shows.


Example 7–5 Low-level Interrupt Routine

static uint_t
xxlowintr(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    ....
    mutex_enter(&xsp->low_mu);
    mutex_enter(&xsp->high_mu);
    if (xsp->softint_count > 1) {
        xsp->softint_count--;
        mutex_exit(&xsp->high_mu);
        mutex_exit(&xsp->low_mu);
        return (DDI_INTR_CLAIMED);
    }
    if (queue empty) {
        mutex_exit(&xsp->high_mu);
        mutex_exit(&xsp->low_mu);
        return (DDI_INTR_UNCLAIMED);
    }
    xsp->softint_running = 1;
    while (data on queue) {
        ASSERT(mutex_owned(&xsp->high_mu);
        dequeue data from high-level queue;
        mutex_exit(&xsp->high_mu);
        normal interrupt processing
        mutex_enter(&xsp->high_mu);
    }
    xsp->softint_running = 0;
    xsp->softint_count = 0;
    mutex_exit(&xsp->high_mu);
    mutex_exit(&xsp->low_mu);
    return (DDI_INTR_CLAIMED);
}