| # 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 usually done closely together, for example: |
| |
| ```c |
| // 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: |
| |
| ```c |
| 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. |
| |
| <!--- there's also a `ZX_PCIE_IRQ_MODE_MSI_X` in the syscalls/pci.h file; should I say anything |
| about that? How would we use it in the above case, just make a third condition? --> |
| |
| 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: |
| |
| ```c |
| 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()**](/docs/reference/syscalls/interrupt_wait.md) |
| to wait for the interrupt. |
| The following prototype applies: |
| |
| ```c |
| 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()`](/docs/reference/syscalls/clock_get_monotonic.md)). |
| |
| Therefore, a typical IHT would have the following shape: |
| |
| ```c |
| 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()**](/docs/reference/syscalls/interrupt_wait.md) |
| 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()**](/docs/reference/syscalls/interrupt_destroy.md) |
| to abort the |
| [**zx_interrupt_wait()**](/docs/reference/syscalls/interrupt_wait.md) |
| 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: |
| |
| ```c |
| static void main_thread() { |
| ... |
| if (shutdown_requested) { |
| // destroy the handle, this will cause zx_interrupt_wait() to pop |
| zx_interrupt_destroy(dev->irq_handle); |
| |
| // 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 |
| ... |
| return; |
| } |
| ... |
| } |
| } |
| ``` |
| |
| The main thread, when requested to shut down, destroys the interrupt handle. |
| This causes the IHT's |
| [**zx_interrupt_wait()**](/docs/reference/syscalls/interrupt_wait.md) |
| 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: |
| |
| * [**zx_interrupt_ack()**](/docs/reference/syscalls/interrupt_ack.md) |
| * [**zx_interrupt_bind()**](/docs/reference/syscalls/interrupt_bind.md) |
| * [**zx_interrupt_create()**](/docs/reference/syscalls/interrupt_create.md) |
| * [**zx_interrupt_trigger()**](/docs/reference/syscalls/interrupt_trigger.md) |