Interrupts

An interrupt is an asynchronous event, generated by a device when it needs servicing. For example, an interrupt is generated when data is available on a serial port, or an ethernet packet has arrived. Interrupts allow a driver to know about an event as soon as it occurs, but without the driver spending time polling (actively waiting) for it.

The general architecture of a driver that uses interrupts is that a background Interrupt Handling Thread (IHT) is created during the driver startup / binding operation. This thread waits for an interrupt to happen, and, when it does, performs some kind of servicing action.

As an example, consider a serial port driver. It may receive interrupts due to any of the following events happening:

  • one or more characters have arrived,
  • room is now available to transmit one or more characters,
  • a control line (like DTR, for example) has changed state.

The interrupt wakes up the IHT. The IHT determines the cause of the event, usually by reading some status registers. Then, it runs an appropriate service function to handle the event. Once done, the IHT goes back to sleep, waiting for the next interrupt.

For example, if a character arrives, the IHT wakes up, reads a status register that indicates "data is available," and then calls a function that drains all available characters from the serial port FIFO into the driver's buffer.

No kernel-level code required

You may be familiar with other operating systems which use Interrupt Service Routines (ISR). These are kernel-level handlers that run in privileged mode and interface with the interrupt controller hardware.

In Fuchsia, the kernel deals with the privileged part of the interrupt handling, and provides thread-level functions for driver use.

The difference is that the IHT runs at thread level, whereas the ISR runs at kernel level in a very restricted (and sometimes fragile) environment. A principal advantage is that if the IHT crashes, it takes out only the driver, whereas a failing ISR can take out the entire operating system.

Attaching to an interrupt

Currently, the only bus that provides interrupts is the PCI bus. It supports two kinds: legacy and Message Signaled Interrupts (MSI).

Therefore, in order to use interrupts on PCI:

  1. determine which kind your device supports (legacy or MSI),
  2. set the interrupt mode to match,
  3. get a handle to your device's interrupt vector (usually one, but may be multiple),
  4. start IHT background thread,
  5. arrange for IHT thread to wait for interrupts (on handle(s) from step 3).

Steps 1 and 2 are handled by the `Pci::ConfigureInterruptMode helper function:

// Configure interrupt mode.
uint32_t irq_cnt = 1;
fuchsia_hardware_pci::InterruptMode mode;
zx_status_t status = pci.ConfigureInterruptMode(irq_cnt, &mode);
if (status != ZX_OK) {
  // handle error
}

Pci::ConfigureInterruptMode() takes two arguments:

#include <lib/device-protocol/pci.h>

zx_status_t Pci::ConfigureInterruptMode(uint32_t requested_irq_count,
                                        fpci::InterruptMode* out_mode);

The first argument, requested_irq_count, is number of interrupts you would like.

The second argument is an out parameter for the interrupt mode that the device supports.

Having configured interrupt support, you call Pci::MapInterrupt() to create a handle to the selected interrupt. Note that Pci::MapInterrupt() has the following prototype:

#include <lib/device-protocol/pci.h>

zx_status_t Pci::MapInterrupt(uint32_t which_irq, zx::interrupt* out_interrupt);

The first argument, which_irq indicates the device-relative interrupt number you'd like, and the second argument is a pointer to the created interrupt object.

You now have an interrupt handle.

Note that the vast majority of devices have just one interrupt, so simply passing 0 for which_irq is normal. If your device does have more than one interrupt, the common practice is to run the pci::MapInterrupt() function in a for loop and bind handles to each interrupt.

Waiting for the interrupt

In your IHT, you call zx::interrupt::wait() to wait for the interrupt. The following prototype applies:

zx_status_t zx::interrupt::wait(zx::time* out_timestamp);

The first parameter can be nullptr (typical), or it can be a pointer to a time stamp that indicates when the interrupt was triggered (in nanoseconds, relative to the monotonic clock source fetched with zx_clock_get_monotonic()).

Therefore, a typical IHT would have the following shape:

int MyDevice::IrqThread() {
    for (;;) {
        zx_status_t status = dev->irq_.wait(nullptr);
        // do stuff
    }
}

The device class has a member (here irq_) that is the object you obtained from Pci::MapInterrupt().

Edge vs level interrupt mode

The interrupt hardware can operate in one of two modes; "edge" or "level".

In edge mode, the interrupt is armed on the active-going edge (when the hardware signal goes from inactive to active), and works as a one-shot. That is, the signal must go back to inactive before it can be recognized again.

In level mode, the interrupt is active when the hardware signal is in the active state.

Typically, edge mode is used when the interrupt is dedicated, and level mode is used when the interrupt is shared by multiple devices (because you want the interrupt to remain active until all devices have de-asserted their request line).

The Zircon kernel automatically masks and unmasks the interrupt as appropriate. For level-triggered hardware interrupts, zx::interrupt::wait() masks the interrupt before returning, and unmasks it when called the next time. For edge-triggered interrupts, the interrupt remains unmasked.

The IHT should not perform any long-running tasks. For drivers that perform lengthy tasks, use a worker thread.

Shutting down a driver that uses interrupts

In order to cleanly shut down a driver that uses interrupts, you can use zx::interrupt::destroy() to abort the zx::interrupt::wait() call.

The idea is that when the foreground thread determines that the driver should be shut down, it simply destroys the interrupt handle, causing the IHT to shut down:

void MyDevice::DdkRelease() {
  // destroy the handle, this will cause zx_interrupt_wait() to pop
  irq_.destroy();
  irq_thread_.join();
  ...
}

int MyDevice::IrqThread() {
    ...
    for(;;) {
        zx_status_t status = irq_.wait(nullptr);
        if (status == ZX_ERR_CANCELED) {
            // we are being shut down, do any cleanups required
            ...
            break;
        }
        ...
    }
}

The main thread, when requested to shut down, destroys the interrupt handle. This causes the IHT's zx::interrupt::wait() call to wake up with an error code. The IHT looks at the error code (in this case, ZX_ERR_CANCELED) and makes the decision to end. Meanwhile, the main thread is waiting to join the IHT with the call to thread::join(). Once the IHT exits, thread::join() returns, and the main thread can finish its processing.

The advanced reader is invited to look at some of the other interrupt related functions available: