Documentation: added ESP32S3 interrupt allocation doc

pull/8172/head
Omar Chebib 2021-12-16 15:39:57 +08:00
rodzic 5cc4bceb2a
commit 8accc6f6d4
2 zmienionych plików z 44 dodań i 38 usunięć

Wyświetl plik

@ -4,7 +4,7 @@ High-Level Interrupts
.. toctree::
:maxdepth: 1
The Xtensa architecture has support for 32 interrupts, divided over 8 levels, plus an assortment of exceptions. On the {IDF_TARGET_NAME}, the interrupt mux allows most interrupt sources to be routed to these interrupts using the :doc:`interrupt allocator <../api-reference/system/intr_alloc>`. Normally, interrupts will be written in C, but ESP-IDF allows high-level interrupts to be written in assembly as well, allowing for very low interrupt latencies.
The Xtensa architecture has support for 32 interrupts, divided over 7 levels (levels 1 to 7, with 7 being an NMI), plus an assortment of exceptions. On the {IDF_TARGET_NAME}, the interrupt mux allows most interrupt sources to be routed to these interrupts using the :doc:`interrupt allocator <../api-reference/system/intr_alloc>`. Normally, interrupts will be written in C, but ESP-IDF allows high-level interrupts to be written in assembly as well, resulting in very low interrupt latencies.
Interrupt Levels
----------------
@ -60,32 +60,31 @@ For a real-life example, see the :component_file:`esp_system/port/soc/{IDF_TARGE
Notes
-----
- Do not call C code from a high-level interrupt; because these interrupts still run in critical sections, this can cause crashes.
(The panic handler interrupt does call normal C code, but this is OK because there is no intention of returning to the normal code
flow afterwards.)
- Do not call C code from a high-level interrupt; as these interrupts are run from a critical section, this can cause the target to crash.
Note that although the panic handler interrupt does call normal C code, this exception is allowed due to the fact that this handler never returns (i.e., the application will not continue to run after the panic handler).
so breaking C code execution flow is not a problem.
.. only:: esp32
And if :ref:`CONFIG_BTDM_CTRL_HLI` is enabled, it does call normal C code in high-level interrupt, but this is OK becase we add some protection for it.
When :ref:`CONFIG_BTDM_CTRL_HLI` is enabled, C code is also called from a high-level interrupt, this is possible thanks to some additional protection added to it.
- Make sure your assembly code gets linked in. If the interrupt handler symbol is the only symbol the rest of the code uses from this
file, the linker will take the default ISR instead and not link the assembly file into the final project. To get around this, in the
assembly file, define a symbol, like this::
- Make sure your assembly code gets linked in. Indeed, as the free-to-use symbols are declared as weak, the linker may discard the file containing the symbol. This will
happen if the only symbol defined, or used, from the user file is the ``xt_*`` free-to-use symbol. To avoid this, in the assembly file containing the ``xt_*`` symbol,
define another symbol, like::
.global ld_include_my_isr_file
ld_include_my_isr_file:
Here it is called ``ld_include_my_isr_file`` but can have any name, as long as it is not defined anywhere else in the project.
The symbol is called ``ld_include_my_isr_file`` here but can have any arbitrary name not defined anywhere else.
Then, in the component ``CMakeLists.txt``, add this name as an unresolved symbol to the ld command line arguments::
Then, in the component CMakeLists.txt, add this file as an unresolved symbol to the ld command line arguments::
target_link_libraries(${COMPONENT_TARGET} "-u ld_include_my_isr_file")
target_link_libraries(${COMPONENT_TARGET} "-u ld_include_my_isr_file")
This should cause the linker to always include the file defining ``ld_include_my_isr_file``, causing the ISR to always be linked in.
This should cause the linker to always include a file defining ``ld_include_my_isr_file``, causing the ISR to always be linked in.
- High-level interrupts can be routed and handled using :cpp:func:`esp_intr_alloc` and associated functions. The handler and handler arguments
to :cpp:func:`esp_intr_alloc` must be NULL, however.
- High-level interrupts can be routed and handled using esp_intr_alloc and associated functions. The handler and handler arguments
to esp_intr_alloc must be NULL, however.
- In theory, medium priority interrupts could also be handled in this way. For now, ESP-IDF does not support this.
- In theory, medium priority interrupts could also be handled in this way. ESP-IDF does not support this yet.

Wyświetl plik

@ -12,6 +12,10 @@ Overview
The {IDF_TARGET_NAME} has one core, with 32 interrupts. Each interrupt has a certain priority level, most (but not all) interrupts are connected to the interrupt mux.
.. only:: esp32s3
The {IDF_TARGET_NAME} has two cores, with 32 interrupts. Each interrupt has a certain priority level, most (but not all) interrupts are connected to the interrupt mux.
.. only:: esp32c3
The {IDF_TARGET_NAME} has one core, with 31 interrupts. Each interrupt has a programmable priority level.
@ -19,27 +23,28 @@ Overview
Because there are more interrupt sources than interrupts, sometimes it makes sense to share an interrupt in multiple drivers. The :cpp:func:`esp_intr_alloc` abstraction exists to hide all these implementation details.
A driver can allocate an interrupt for a certain peripheral by calling :cpp:func:`esp_intr_alloc` (or :cpp:func:`esp_intr_alloc_intrstatus`). It can use
the flags passed to this function to set the type of interrupt allocated, specifying a specific level or trigger method. The
the flags passed to this function to set the type of interrupt allocated, specifying a particular level or trigger method. The
interrupt allocation code will then find an applicable interrupt, use the interrupt mux to hook it up to the peripheral, and
install the given interrupt handler and ISR to it.
This code has two different types of interrupts it handles differently: Shared interrupts and non-shared interrupts. The simplest
of the two are non-shared interrupts: a separate interrupt is allocated per esp_intr_alloc call and this interrupt is solely used for
the peripheral attached to it, with only one ISR that will get called. Shared interrupts can have multiple peripherals triggering
it, with multiple ISRs being called when one of the peripherals attached signals an interrupt. Thus, ISRs that are intended for shared
interrupts should check the interrupt status of the peripheral they service in order to see if any action is required.
This code presents two different types of interrupts, handled differently: shared interrupts and non-shared interrupts. The simplest
ones are non-shared interrupts: a separate interrupt is allocated per :cpp:func:`esp_intr_alloc` call and this interrupt is solely used for
the peripheral attached to it, with only one ISR that will get called. On the other hand, shared interrupts can have multiple peripherals triggering
them, with multiple ISRs being called when one of the peripherals attached signals an interrupt. Thus, ISRs that are intended for shared
interrupts should check the interrupt status of the peripheral they service in order to check if any action is required.
Non-shared interrupts can be either level- or edge-triggered. Shared interrupts can
only be level interrupts (because of the chance of missed interrupts when edge interrupts are
used.)
(The logic behind this: DevA and DevB share an int. DevB signals an int. Int line goes high. ISR handler
calls code for DevA -> does nothing. ISR handler calls code for DevB, but while doing that,
DevA signals an int. ISR DevB is done, clears int for DevB, exits interrupt code. Now an
interrupt for DevA is still pending, but because the int line never went low (DevA kept it high
even when the int for DevB was cleared) the interrupt is never serviced.)
only be level interrupts due to the chance of missed interrupts when edge interrupts are
used.
For example, let's say DevA and DevB share an interrupt. DevB signals an interrupt, so INT line goes high. The ISR handler
calls code for DevA but does nothing. Then, ISR handler calls code for DevB, but while doing that,
DevA signals an interrupt. DevB's ISR is done, it clears interrupt status for DevB and exits interrupt code. Now, an
interrupt for DevA is still pending, but because the INT line never went low, as DevA kept it high
even when the interrupt for DevB was cleared, the interrupt is never serviced.
.. only:: CONFIG_IDF_TARGET_ARCH_XTENSA
.. only:: esp32 or esp32s3
Multicore issues
----------------
@ -63,9 +68,9 @@ even when the int for DevB was cleared) the interrupt is never serviced.)
Internal interrupt sources are defined in esp_intr_alloc.h as ``ETS_INTERNAL_*_INTR_SOURCE``.
These peripherals can only be configured from the core they are associated with. When generating an interrupt,
the interrupt they generate is hard-wired to their associated core; it's not possible to have e.g. an internal
the interrupt they generate is hard-wired to their associated core; it's not possible to have, for example, an internal
timer comparator of one core generate an interrupt on another core. That is why these sources can only be managed
using a task running on that specific core. Internal interrupt sources are still allocatable using esp_intr_alloc
using a task running on that specific core. Internal interrupt sources are still allocatable using :cpp:func:`esp_intr_alloc`
as normal, but they cannot be shared and will always have a fixed interrupt level (namely, the one associated in
hardware with the peripheral).
@ -80,9 +85,10 @@ even when the int for DevB was cleared) the interrupt is never serviced.)
- Allocating an external interrupt will always allocate it on the core that does the allocation.
- Freeing an external interrupt must always happen on the same core it was allocated on.
- Disabling and enabling external interrupts from another core is allowed.
- Multiple external interrupt sources can share an interrupt slot by passing ``ESP_INTR_FLAG_SHARED`` as a flag to esp_intr_alloc().
- Multiple external interrupt sources can share an interrupt slot by passing ``ESP_INTR_FLAG_SHARED`` as a flag to :cpp:func:`esp_intr_alloc`.
Care should be taken when calling esp_intr_alloc() from a task which is not pinned to a core. During task switching, these tasks can migrate between cores. Therefore it is impossible to tell which CPU the interrupt is allocated on, which makes it difficult to free the interrupt handle and may also cause debugging difficulties. It is advised to use xTaskCreatePinnedToCore() with a specific CoreID argument to create tasks that will allocate interrupts. In the case of internal interrupt sources, this is required.
Care should be taken when calling :cpp:func:`esp_intr_alloc` from a task which is not pinned to a core. During task switching, these tasks can migrate between cores. Therefore it is impossible to tell which CPU the interrupt is allocated on, which makes it difficult to free the interrupt handle and may also
cause debugging difficulties. It is advised to use :cpp:func:`xTaskCreatePinnedToCore` with a specific CoreID argument to create tasks that will allocate interrupts. In the case of internal interrupt sources, this is required.
IRAM-Safe Interrupt Handlers
----------------------------
@ -99,15 +105,16 @@ Multiple Handlers Sharing A Source
----------------------------------
Several handlers can be assigned to a same source, given that all handlers are allocated using the ``ESP_INTR_FLAG_SHARED`` flag.
They'll be all allocated to the interrupt, which the source is attached to, and called sequentially when the source is active.
They will all be allocated to the interrupt, which the source is attached to, and called sequentially when the source is active.
The handlers can be disabled and freed individually. The source is attached to the interrupt (enabled), if one or more handlers are enabled, otherwise detached.
A handler will never be called when disabled, while **its source may still be triggered** if any one of its handler enabled.
Sources attached to non-shared interrupt do not support this feature.
Though the framework support this feature, you have to use it *very carefully*. There usually exist 2 ways to stop a interrupt from being triggered: *disable the source* or *mask peripheral interrupt status*.
IDF only handles the enabling and disabling of the source itself, leaving status and mask bits to be handled by users. **Status bits should always be masked before the handler responsible for it is disabled,
or the status should be handled in other enabled interrupt properly**. You may leave some status bits unhandled if you just disable one of all the handlers without masking the status bits, which causes the interrupt to trigger infinitely resulting in a system crash.
Though the framework support this feature, you have to use it *very carefully*. There usually exist two ways to stop an interrupt from being triggered: *disable the source* or *mask peripheral interrupt status*.
IDF only handles enabling and disabling of the source itself, leaving status and mask bits to be handled by users.
**Status bits shall either be masked before the handler responsible for it is disabled, either be masked and then properly handled in another enabled interrupt**.
Please note that leaving some status bits unhandled without masking them, while disabling the handlers for them, will cause the interrupt(s) to be triggered indefinitely, resulting therefore in a system crash.
API Reference
-------------