zephyr/doc/object/object_basic_interrupts.rst

359 lines
16 KiB
ReStructuredText

Interrupt Service Routines
##########################
General Information
*******************
Interrupt Service Routines are execution threads that run in response to
a hardware or software interrupt. They will preempt the execution of
any task or fiber running at the time the interrupt occurs.
Consequently, ISRs react fastest to hardware. Routines in the
nanokernel wake up with a very low overhead.
.. warning::
ISRs prevent other parts of the system from running. Therefore,
all code in these routines should be confined to short, simple
routines.
.. todo:: Insert how an ISR can be installed both static and dynamic.
Both dynamic and static ISRs can be installed. See
`Installing a Dynamic ISR`_ and `Installing a Static ISR`_ for more
details. An ISR cannot be installed in the project file, only a task or
a driver initialization call can install an ISR.
When an ISR wakes up a fiber, there is only one context switch directly
to the fiber. When an ISR wakes up a task, there is first a context
switch to the nanokernel, and then another context switch to the task
in the microkernel.
Interrupt Stubs
***************
Interrupts stubs are small pieces of assembler code that connect your
ISR to the Interrupt Descriptor Table (IDT). The interrupt stub informs
the kernel when an interrupt is in progress, performs interrupt
controller specific work, invokes your ISR and informs the kernel when
the interrupt processing is complete. The stub address is registered in
the Interrupt Descriptor Table. The stub references your ISR and the
stubs can either be generated dynamically or statically.
Interrupt Service Routine APIs
******************************
The table lists the ISR Application Program Interfaces. There are a
number of calls that an ISR can use to switch between different
processing levels.
.. note::
Application Program Interfaces of the ISRs are architecture-
specific because they are implemented in the interrupt controller
device driver for that processor or board. The architecture specific
implementation can be found in the corresponding documentation for
each architecture.
+-------------------------+---------------------------+
| Call | Description |
+=========================+===========================+
| :c:func:`irq_enable()` | Enables a specific IRQ. |
+-------------------------+---------------------------+
| :c:func:`irq_disable()` | Disables a specific IRQ. |
+-------------------------+---------------------------+
| :c:func:`irq_lock()` | Locks out all interrupts. |
+-------------------------+---------------------------+
| :c:func:`irq_unlock()` | Unlocks all interrupts. |
+-------------------------+---------------------------+
Installing a Dynamic ISR
************************
Use :c:func:`irq_connect()` to install and connect an ISR stub
dynamically. :c:func:`irq_connect()` is processor-specific. There is no
API method to uninstall a dynamic ISR.
Installing a Static ISR
***********************
The contents of a static interrupt stub are complex and board specific.
They are generally created manually as part of the BSP. A stub is
installed statically into the Interrupt Descriptor Table using one of
the macros detailed in following table. The table lists the macros you
can use to identify and register your static ISRs into the Interrupt
Descriptor Table. The IA-32 interrupt descriptor allows for the setting
of the privilege level, DPL, at which the interrupt can be triggered.
Tiny Mountain assumes all device drivers are kernel mode (ring 0) as
opposed to user-mode (ring 3). Therefore, these macros always set the
DPL to 0.
The IDT Macros
==============
+--------------------------+-------------------------------------------------------------------------+
| Call | Description |
+==========================+=========================================================================+
| NANO_CPU_INT_REGISTER( ) | Use this macro to register a driver's |
| | interrupt |
| | handler statically when the vector number is known at compile time. |
+--------------------------+-------------------------------------------------------------------------+
| SYS_INT_REGISTER( ) | Use this macro to register a driver's |
| | interrupt handler statically when |
| | the vector number is not known at compile time but the priority and IRQ |
| | line are. The BSP is responsible for implementing this macro in board.h |
| | to generate a vector from the priority and IRQ line at compile time. |
| | The macro is intended to provide a level of abstraction between the BSP |
| | and the driver. |
+--------------------------+-------------------------------------------------------------------------+
Interrupt Descriptor Table
**************************
The Interrupt Descriptor Table (IDT) is a data structure that implements
an interrupt vector table used by the processor to determine the
correct response to interrupts and exceptions. To optimize boot
performance and increase security, Tiny Mountain implements targets
using a statically created Interrupt Descriptor Table, interrupt stubs
and exception stubs. A static Interrupt Descriptor Table improves boot
performance because:
* No CPU cycles are used to construct the Interrupt Descriptor Table
at boot up.
* No CPU cycles are used to create interrupt stubs at boot up.
* No CPU cycles are used to create exceptions stubs at run-time.
The statically created Interrupt Descriptor Table can still be updated
at run-time despite being write-protected. There may be instances where
updating the Interrupt Descriptor Table at run-time is required, for
example, in order to install dynamic interrupts. The decision of
whether a target implements dynamic or static interrupts is determined
at compile time automatically based on the configuration.
Securing the Interrupt Descriptor Table
***************************************
Typically the IDT resides in the data section. Enable the Section Write
Protection feature to move the Interrupt Descriptor Table to the rodata
section and to mark all pages of memory in which the Interrupt
Descriptor Table resides as read-only. Enabling the Section Write
Protection feature places dynamic interrupt stubs into the text section
protecting them. A system where execute in place, XIP, support is
enabled, assumes the text section and read-only data section reside in
read-only memory, such as flash memory or ROM. In this scenario dynamic
interrupt stubs are not possible. The Interrupt Descriptor Table cannot
be updated at runtime. Therefore enabling the Section Write Protection
feature blocks generating dynamic interrupt stubs and updating the
Interrupt Descriptor Table at runtime.
Note This implementation of XIP does not support a ROM-resident
Interrupt Descriptor Table. When the segmentation feature is enabled,
execution of code in the data segment is not allowed. If the
segmentation feature is enabled and section write protection is not
enabled, dynamic interrupt stubs move to the text section, but they are
still writable.
The following is an example of a dynamic interrupt stub for x86:
.. code-block:: c
static NANO_CPU_INT_STUB_DECL (deviceStub);
void deviceDriver (void)
{
.
.
.
nanoCpuIntConnect (deviceIRQ, devicePrio, deviceIntHandler,
deviceStub);
.
.
.
}
This feature is part of Tiny Mountain's enhanced security profile.
Working with ISRs
*****************
Triggering Interrupts
=====================
The processor starts up an ISR when a hardware interrupt is received.
When one of the interrupt pins of the processor core is triggered, the
processor jumps to the appropriate interrupt routine. To interface this
hardware event with software, Tiny Mountain allows you to attach an ISR
to the interrupt signal.
An ISR can interface with a fiber using the nanokernel Application
Program Interfaces. The ISR can wake up a task using the microkernel
synchronization objects, an event or invoking the event handler. The
nanokernel affords them the lowest startup overhead because ISRs are
triggered from the hardware level. No context switch is needed to start
up an ISR.
When an interrupt occurs, all fibers and all tasks wait until the
interrupt is handled. If an application is executing a task or a fiber
is running, it is interrupted until the ISR finishes.
An ISR implementation is typically very hardware-specific because it
interfaces directly with a hardware interrupt and starts to run because
of it. The details of how this happens are described in your processors
documentation.
Prototype your hardware-specific functionality in a task, before you
move it to the ISR code.
If an ISR calls a channel service with a signal action, any fiber
rescheduling resulting from this call is delayed until all interrupt
handlers terminate. Therefore, use only the nano_Isr Application
Program Interfaces, as these do not invoke the system kernel scheduler
for a signal action. Keep in mind that there is no need for a swap at
this point; the caller has the highest priority already. Once the last
stacked interrupt terminates, the nanokernel scheduler must be called
to verify if a swap from the task to a fiber is necessary.
An ISR must never call any blocking channel Application Program
Interface. It would block the current fiber and all other interrupt
handlers that are stacked below the ISR.
Tiny Mountain supports interrupt nesting. When an ISR is running, it can
be interrupted when a new interrupt is received.
Using Interrupt Service Routines
================================
If interrupts come in at high speed, parts of your code can be at the
ISR level. If code is at the interrupt level, it avoids a context
switch making it faster. If interrupts come in at low speeds, the ISR
should only wake up a fiber or a task. That fiber or task should do all
the processing, not the ISR, even if the task can be interrupted by
fibers and ISRs. Keep fibers and ISRs short to ensure predictability.
For example, take an application that implements an algorithm in an ISR.
Suppose the algorithm takes one second to finish calculating. The
application has a task in the background that interfaces with a host
machine to plot data on the screen. The task updates the screen image
five times per second to provide a smooth screen display. This
application as a whole does not behave predictably if an interrupt is
received. The ISR starts calculating for one second and causes an
unexpected delay. The same holds true if the algorithm is implemented
using a fiber. The user sees an interleaved screen output. This example
is extreme but it shows that short fibers and short ISRs make the
system more predictable.
Implementing Interrupt Service Routines
***************************************
Most processors require that ISRs be coded in assembler. To make the
implementation easier, several assembler macros are available to do the
most common jobs. Because the ISRs block all other processing, always
implement the actual handling of the interrupt in a fiber or a task.
Where to handle the interrupt is a design choice that must be made
while considering the performance of the processor and the frequency of
the interrupt.
Coordinating ISRs and Events
****************************
An ISR can send a signal from the nanokernel to the microkernel to
trigger an event. Your setup can work with an event handler, or without
one. If there is no event handler and your task is waiting for the
event, the ISR wakes up the task when it triggers the event. If you
have an event handler, the ISR triggers the event handler routine. This
event handler then determines if the task wakes up or not.
.. warning::
Implement or process a buffer in an event handler if you input
comes in at a high speed.
Command Packet Sets
*******************
A command packet set is a group of statically-allocated command packets.
A command packet is accessible to any application running in kernel
space. They are necessary when signaling a semaphore from an ISR via
:c:func:`Isr_sem_give()` since command packets are processed after the
ISR finishes. That makes stack-allocated command packets unsafe for
this purpose. A statically-allocated command packet is implicitly
released after being processed. Consequently, the operating system does
not track the use-status of any statically-allocated command packet.
There is a small but unavoidable risk of a command packet's processing
being incomplete before the ISR runs again and tries to reuse the
packet. To further minimize this risk Tiny Mountain introduces command
packet sets. Fundamentally, a command packet set is a simple ring
buffer. Retrieve command packets from the set using
:c:func:`cmdPktGet()`. Each command packet has to be processed in a
near-FIFO since no use-status checking is performed a packet is
retrieved. In order to minimize the risk of packet corruption from
premature reuse, drivers that have an ISR component should use their
own command packet set and not use a common set for many drivers.
Create a command packet set in global memory using:
.. code-block:: c
CMD_PKT_SET_INSTANCE(setVariableName, #ofCommandPacketsInSet);
Task Level Interrupt Processing
*******************************
The task level interrupt processing feature permits to service
interrupts at the task level, without having to develop kernel level
ISRs. The *MAX_NUM_TASK_DEVS* kernel configuration option specifies the
total number of devices needing task-level interrupt support.
The default setting of 0 disables the following interfaces:
:c:func:`task_irq_alloc()`, :c:func:`task_irq_free()`,
:c:func:`task_irq_ack()` and :c:func:`task_irq_test()`. Each device has
a well-known identifier in the range from 0 to *MAX_NUM_TASK_DEVS*-1.
Tiny Mountain allows kernel tasks to bind to devices at run-time by
calling :c:func:`task_irq_alloc()`. A task may bind itself to multiple
devices by calling this routine multiple times but a given device can
be bound to only a single task at any point in time. The registering
task specifies the device it wishes to use, the associated IRQ and
priority level for the device's interrupt. It gets the assigned
interrupt vector in return. The interrupt associated with the device is
enabled once the task has registered to use a device. Whenever the
device generates an interrupt, the kernel automatically runs an ISR
that disables the interrupt and records its occurrence.
The task associated with the device can use :c:func:`taskDevIntTest()`
to determine if the device's interrupt has occurred. Alternatively, it
can use :c:func:`task_irq_test_wait()` or
:c:func:`task_irq_test_wait_timeout()` to wait until an interrupt is
detected.
After the task took the appropriate action to service an interrupt
generated by the device, it calls :c:func:`task_irq_ack()` to re-enable
the device's interrupt. The task can call :c:func:`task_irq_free()` to
unbind itself from a device that it no longer wishes to use. If the
registered device needs change its priority level, it must first
unregister and then register again with the new priority. To provide
security against device misuse, a device should only be tested,
acknowledged, and deregistered by a task if that task registered the
device. Restrict which task can register a given device or use the
device after registration, at the shim layer.