Google is committed to advancing racial equity for Black communities. See how.


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 usually done closely together, for example:

// Query whether we have MSI or Legacy interrupts.
uint32_t irq_cnt = 0;
if ((pci_query_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_MSI, &irq_cnt) == ZX_OK) &&
    (pci_set_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_MSI, 1) == ZX_OK)) {
    // using MSI interrupts
} else if ((pci_query_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_LEGACY, &irq_cnt) == ZX_OK) &&
           (pci_set_irq_mode(&edev->pci, ZX_PCIE_IRQ_MODE_LEGACY, 1) == ZX_OK)) {
    // using legacy interrupts
} else {
    // an error

The pci_query_irq_mode() function takes three arguments:

zx_status_t pci_query_irq_mode(const pci_protocol_t* pci,
                               zx_pci_irq_mode_t mode,
                               uint32_t* out_max_irqs);

The first argument, pci, is a pointer to the PCI protocol stack bound to your device just like we saw above, in the BAR documentation.

The second argument, mode, is the kind of interrupt that you are interested in; it's one of the two constants shown in the example.

The third argument is a pointer to integer that returns how many interrupts of the specified type your device supports.

Having determined the kind of interrupt supported, you then call pci_set_irq_mode() to indicate that this is indeed the kind of interrupt that you wish to use.

Finally, you call pci_map_interrupt() to create a handle to the selected interrupt. Note that pci_map_interrupt() has the following prototype:

zx_status_t pci_map_interrupt(const pci_protocol_t* pci,
                              int which_irq,
                              zx_handle_t* out_handle);

The first argument is the same as in the previous call, the second argument, which_irq indicates the device-relative interrupt number you'd like, and the third argument is a pointer to the created interrupt handle.

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_map_interrupt() 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_handle_t handle,
                              zx_time_t* out_timestamp);

The first argument is the handle you obtained from the call to pci_map_interrupt(), and the second parameter can be NULL (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:

static int irq_thread(void* arg) {
    my_device_t* dev = arg;
    for (;;) {
        zx_status_t rc;
        rc = zx_interrupt_wait(dev->irq_handle, NULL);
        // do stuff

The convention is that the argument passed to the IHT is your device context block. The context block has a member (here irq_handle) that is the handle you obtained from pci_map_interrupt().

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:

static void main_thread() {
    if (shutdown_requested) {
        // destroy the handle, this will cause zx_interrupt_wait() to pop

        // wait for the IHT to finish
        thrd_join(dev->iht, NULL);

static int irq_thread(void* arg) {
    for(;;) {
        zx_status_t rc;
        rc = zx_interrupt_wait(dev->irq_handle, NULL);
        if (rc == ZX_ERR_CANCELED) {
            // we are being shut down, do any cleanups required

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 thrd_join(). Once the IHT exits, thrd_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: