# Inter-Processor Call (IPC)

[中文]

Note

IPC stands for an "Inter-Processor Call" and NOT "Inter-Process Communication" as found on other operating systems.

## Overview

Due to the dual core nature of the ESP32, there are some scenarios where a certain callback must be executed from a particular core such as:

- When allocating an ISR to an interrupt source of a particular core (applies to freeing a particular core's interrupt source as well)

- On particular chips (such as the ESP32), accessing memory that is exclusive to a particular core (such as RTC Fast Memory)

- Reading the registers/state of another core

The IPC (Inter-Processor Call) feature allows a particular core (the calling core) to trigger the execution of a callback function on another core (the target core). The IPC feature allows execution of a callback function on the target core in either a task context, or an interrupt context. Depending on the context that the callback function is executed in, different restrictions apply to the implementation of the callback function.

## IPC in Task Context

The IPC feature implements callback execution in a task context by creating an IPC task for each core during application startup. When the calling core needs to execute a callback on the target core, the callback will execute in the context of the target core's IPC task.

When using IPCs in a task context, users need to consider the following:

- IPC callbacks should ideally be simple and short. An IPC callback must never block or yield.

- The IPC tasks are created at the highest possible priority (i.e., `configMAX_PRIORITIES - 1`).

  > - If CONFIG_ESP_IPC_USES_CALLERS_PRIORITY is enabled, the target core's IPC task will be lowered to the current priority of the target core before executing the callback.

  > - If CONFIG_ESP_IPC_USES_CALLERS_PRIORITY is disabled, the target core will always execute the callback at the highest possible priority.

- Depending on the complexity of the callback, users may need to configure the stack size of the IPC task via CONFIG_ESP_IPC_TASK_STACK_SIZE.

- The IPC feature is internally protected by a mutex. Therefore, simultaneous IPC calls from two or more calling core's are serialized on a first come first serve basis.

### API Usage

Task Context IPC callbacks have the following restrictions:

- The callback must be of the `esp_ipc_func_t` type.

- The callback must never block or yield as this will result in the target core's IPC task blocking or yielding.

- The callback must avoid changing any aspect of the IPC task's state, e.g., by calling `vTaskPrioritySet(NULL, x)`.

The IPC feature offers the API listed below to execute a callback in a task context on a target core. The API allows the calling core to block until the callback's execution has completed, or return immediately once the callback's execution has started.

- `esp_ipc_call()` triggers an IPC call on the target core. This function will block until the target core's IPC task begins execution of the callback.

- `esp_ipc_call_blocking()` triggers an IPC on the target core. This function will block until the target core's IPC task completes execution of the callback.

## IPC in Interrupt Context

In some cases, we need to quickly obtain the state of another core such as in a core dump, GDB stub, various unit tests, and hardware errata workarounds. The IPC ISR feature implements callback execution from a High Priority Interrupt context by reserving a High Priority Interrupt on each core for IPC usage. When a calling core needs to execute a callback on the target core, the callback will execute in the context of the High Priority Interrupt of the target core.

For such scenarios, the IPC ISR feature supports execution of callbacks in a High Priority Interrupt context.

When using IPCs in High Priority Interrupt context, users need to consider the following:

- Since the callback is executed in a High Priority Interrupt context, the callback must be written entirely in assembly. See the API Usage below for more details regarding writing assembly callbacks.

- The priority of the reserved High Priority Interrupt is dependent on the CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL option.

When the callback executes, users need to consider the following:

- The calling core will disable interrupts of priority level 3 and lower.

- Although the priority of the reserved interrupt depends on CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL, during the execution of IPC ISR callback, the target core will disable interrupts of priority level 5 and lower regardless of what CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL is set to.

### API Usage

High Priority Interrupt IPC callbacks have the following restrictions:

- The callback must be of type `esp_ipc_isr_func_t` but implemented entirely in assembly.

- The callback is invoked via the `CALLX0` instruction with register windowing disabled, thus the callback:

    - must not call any register window related instructions, e.g., `entry` and `retw`.

    - must not call other C functions as register windowing is disabled.

- The callback should be placed in IRAM at a 4-byte aligned address.

- On invocation of, or after returning from the callback, the registers `a2, a3, a4` are saved/restored automatically, thus can be used in the callback. The callback should ONLY use those registers.

    - `a2` contains the `void *arg` of the callback.

    - `a3/a4` are free to use as scratch registers.

The IPC feature offers the API listed below to execute a callback in a High Priority Interrupt context:

- `esp_ipc_isr_call()` triggers an IPC call on the target core. This function will busy-wait until the target core begins execution of the callback.

- `esp_ipc_isr_call_blocking()` triggers an IPC call on the target core. This function will busy-wait until the target core completes execution of the callback.

These functions interrupt the other CPU and execute the callback in the context of a High Priority Interrupt. There are two common usage patterns:

- For simple callbacks that do not enter critical sections shared with the other CPU, call `esp_ipc_isr_call()` or `esp_ipc_isr_call_blocking()` directly.

- If the calling CPU may enter critical sections used by the other CPU, or if several callbacks must run while the other CPU remains stopped, first stall the other CPU using `esp_ipc_isr_stall_other_cpu()` or `esp_ipc_isr_stall_other_cpu_safe()`. Then use `esp_ipc_isr_call()` or `esp_ipc_isr_call_blocking()` to execute callbacks. After the operation is complete, release the other CPU with `esp_ipc_isr_release_other_cpu()`.

The following code-blocks demonstrates a High Priority Interrupt IPC callback written in assembly that simply reads the target core's cycle count:

    /* esp_test_ipc_isr_get_cycle_count_other_cpu(void *arg) */
    // this function reads CCOUNT of the target core and stores it in arg.
    // use only a2, a3 and a4 regs here.
    .section    .iram1, "ax"
    .align      4
    .global     esp_test_ipc_isr_get_cycle_count_other_cpu
    .type       esp_test_ipc_isr_get_cycle_count_other_cpu, @function
    // Args:
    // a2 - void* arg
    esp_test_ipc_isr_get_cycle_count_other_cpu:
    rsr.ccount a3
    s32i    a3, a2, 0
    ret

The callback can be called directly when no shared critical section can deadlock:

    uint32_t cycle_count;
    esp_ipc_isr_call_blocking(esp_test_ipc_isr_get_cycle_count_other_cpu, (void *)&cycle_count);

Alternatively, safely stall the other CPU before making one or more IPC calls:

    while (esp_ipc_isr_stall_other_cpu_safe() != ESP_OK) {
        // Optionally, add a timeout or yield to avoid infinite loop
    }
    uint32_t cycle_count;
    esp_ipc_isr_call_blocking(esp_test_ipc_isr_get_cycle_count_other_cpu, (void *)&cycle_count);
    esp_ipc_isr_release_other_cpu();

Note

The number of scratch registers available for use is sufficient for most simple use cases. But if your callback requires more scratch registers, `void *arg` can point to a buffer that is used as a register save area. The callback can then save and restore more registers. See the system/ipc/ipc_isr.

Note

For more examples of High Priority Interrupt IPC callbacks, you can refer to components/esp_system/port/arch/xtensa/esp_ipc_isr_routines.S and components/esp_system/test_apps/esp_system_unity_tests/main/port/arch/xtensa/test_ipc_isr.S.

See examples/system/ipc/ipc_isr/xtensa/main/main.c for an example of its use.

The High Priority Interrupt IPC API also provides the following convenience functions that can stall/resume the target core. These APIs utilize the High Priority Interrupt IPC, but supply their own internal callbacks:

- `esp_ipc_isr_stall_other_cpu()` stalls the target core. The calling core disables interrupts of level 3 and lower while the target core will busy-wait with interrupts of level 5 and lower disabled. The target core will busy-wait until `esp_ipc_isr_release_other_cpu()` is called.

- `esp_ipc_isr_stall_other_cpu_safe()` attempts to stall the other core only if it is not in a critical section or ISR context. If the other core is in such a state, the function considers it unsafe to stall, releases the core, and returns an error.

- `esp_ipc_isr_release_other_cpu()` resumes the target core.

## Application Examples

- system/ipc/ipc_isr/xtensa demonstrates how to use the IPC ISR feature to run an IPC in the context of a High Priority Interrupt, allowing users to quickly get the state of the other CPU, with two assembly callbacks that return the PS register of the other core and save/restore registers while performing tasks based on input arguments.

## API Reference

### Header File

- components/esp_system/include/esp_ipc.h

- This header file can be included with:

  >     #include "esp_ipc.h"

### Functions

esp_err_t esp_ipc_call(uint32_t cpu_id, esp_ipc_func_t func, void *arg)

  Execute a callback on a given CPU.

  Execute a given callback on a particular CPU. The callback must be of type "esp_ipc_func_t" and will be invoked in the context of the target CPU's IPC task.

  - This function will block the target CPU's IPC task has begun execution of the callback

  - If another IPC call is ongoing, this function will block until the ongoing IPC call completes

  - The stack size of the IPC task can be configured via the CONFIG_ESP_IPC_TASK_STACK_SIZE option

  Note

  In single-core mode, returns ESP_ERR_INVALID_ARG for cpu_id 1.

  Parameters:

    - cpu_id -- [in] CPU where the given function should be executed (0 or 1)

    - func -- [in] Pointer to a function of type void func(void* arg) to be executed

    - arg -- [in] Arbitrary argument of type void* to be passed into the function

  Returns:

    - ESP_ERR_INVALID_ARG if cpu_id is invalid

    - ESP_ERR_INVALID_STATE if the FreeRTOS scheduler is not running

    - ESP_OK otherwise

esp_err_t esp_ipc_call_blocking(uint32_t cpu_id, esp_ipc_func_t func, void *arg)

  Execute a callback on a given CPU until and block until it completes.

  This function is identical to esp_ipc_call() except that this function will block until the execution of the callback completes.

  Note

  In single-core mode, returns ESP_ERR_INVALID_ARG for cpu_id 1.

  Parameters:

    - cpu_id -- [in] CPU where the given function should be executed (0 or 1)

    - func -- [in] Pointer to a function of type void func(void* arg) to be executed

    - arg -- [in] Arbitrary argument of type void* to be passed into the function

  Returns:

    - ESP_ERR_INVALID_ARG if cpu_id is invalid

    - ESP_ERR_INVALID_STATE if the FreeRTOS scheduler is not running

    - ESP_OK otherwise

### Type Definitions

typedef void (*esp_ipc_func_t)(void *arg)

  IPC Callback.

  A callback of this type should be provided as an argument when calling esp_ipc_call() or esp_ipc_call_blocking().

### Header File

- components/esp_system/include/esp_ipc_isr.h

- This header file can be included with:

  >     #include "esp_ipc_isr.h"

### Functions

void esp_ipc_isr_call(esp_ipc_isr_func_t func, void *arg)

  Execute an ISR callback on the other CPU.

  Execute a given callback on the other CPU in the context of a High Priority Interrupt.

  - This function will busy-wait in a critical section until the other CPU has started execution of the callback

  - The callback must be written:

    - in assembly for XTENSA chips (such as ESP32, ESP32S3). The function is invoked using a CALLX0 instruction and can use only a2, a3, a4 registers. See :doc:`IPC in Interrupt Context </api-reference/system/ipc>` doc for more details.

    - in C or assembly for RISCV chips (such as ESP32P4).

  Note

  This function is not available in single-core mode.

  Note

  This function can be called even if the other core is stalled (either via esp_ipc_isr_stall_other_cpu() or esp_ipc_isr_stall_other_cpu_safe()). This allows safely stalling the other CPU and executing one or more callbacks before releasing it.

  Parameters:

    - func -- [in] Pointer to a function of type void func(void* arg) to be executed

    - arg -- [in] Arbitrary argument of type void* to be passed into the function

void esp_ipc_isr_call_blocking(esp_ipc_isr_func_t func, void *arg)

  Execute an ISR callback on the other CPU and busy-wait until it completes.

  This function is identical to esp_ipc_isr_call() except that this function will busy-wait until the execution of the callback completes.

  Note

  This function is not available in single-core mode.

  Note

  This function can be called even if the other core is stalled (either via esp_ipc_isr_stall_other_cpu() or esp_ipc_isr_stall_other_cpu_safe()). This allows safely stalling the other CPU and executing one or more callbacks before releasing it.

  Parameters:

    - func -- [in] Pointer to a function of type void func(void* arg) to be executed

    - arg -- [in] Arbitrary argument of type void* to be passed into the function

void esp_ipc_isr_stall_other_cpu(void)

  Stall the other CPU unconditionally.

  This function forces the other CPU to enter a busy-wait loop within a High Priority Interrupt context, effectively stalling its execution until esp_ipc_isr_release_other_cpu() is called.

  Typical use cases include:

  - DPORT workaround (e.g., DPORT or APB registers) on dual-core ESP32 chips prior to v2.0.

  - Scenarios where the state of the other CPU does not need to be checked, stalling unconditionally.

  Implementation details:

  - The function is initialized after FreeRTOS startup.

  - When invoked, it signals the other CPU to enter a high-priority interrupt and remain stalled.

  - If the other CPU is already in a high-priority interrupt, it is considered stalled.

  - The other CPU will remain stalled until esp_ipc_isr_release_other_cpu() is called.

  - This function does not check the current state (e.g., critical section or ISR) of the other CPU. To avoid potential deadlocks on spinlocks, use esp_ipc_isr_stall_other_cpu_safe() instead.

  - If the stall feature is paused via esp_ipc_isr_stall_pause(), this function has no effect.

  Note

  Not available in single-core mode.

  Note

  The caller is responsible for ensuring no deadlocks on spinlocks occur. Use esp_ipc_isr_stall_other_cpu_safe() for safer operation.

esp_err_t esp_ipc_isr_stall_other_cpu_safe(void)

  Safely stall the other CPU.

  Attempts to stall the other CPU only if it is not currently in a critical section or ISR context. If the other CPU is in a critical section or ISR, the function will return an error.

  This helps to prevent potential deadlocks when both CPUs may access shared resources/spinlocks.

  Note

  This function is intended for scenarios where safe stalling is required and the state of the other CPU must be checked.

  Returns:

    - ESP_OK: The other CPU was successfully stalled.

    - ESP_ERR_NOT_ALLOWED: The other CPU is in a critical section or ISR context and cannot be stalled.

bool esp_ipc_isr_is_other_cpu_stalled(void)

  Check whether the other CPU is currently stalled.

  Returns:

    true if the other CPU has entered the IPC ISR stall loop, false otherwise.

void esp_ipc_isr_release_other_cpu(void)

  Release the other CPU.

  This function will release the other CPU that was previously stalled from calling esp_ipc_isr_stall_other_cpu()

  - This function is used for DPORT workaround.

  - If the stall feature is paused using esp_ipc_isr_stall_pause(), this function will have no effect

  Note

  This function is not available in single-core mode.

void esp_ipc_isr_stall_pause(void)

  Puase the CPU stall feature.

  This function will pause the CPU stall feature. Once paused, calls to esp_ipc_isr_stall_other_cpu() and esp_ipc_isr_release_other_cpu() will have no effect. If a IPC ISR call is already in progress, this function will busy-wait until the call completes before pausing the CPU stall feature.

void esp_ipc_isr_stall_abort(void)

  Abort a CPU stall.

  This function will abort any stalling routine of the other CPU due to a previous call to esp_ipc_isr_stall_other_cpu(). This function aborts the stall in a non-recoverable manner, thus should only be called in case of a panic().

  - This function is used in panic handling code

void esp_ipc_isr_stall_resume(void)

  Resume the CPU stall feature.

  This function will resume the CPU stall feature that was previously paused by calling esp_ipc_isr_stall_pause(). Once resumed, calls to esp_ipc_isr_stall_other_cpu() and esp_ipc_isr_release_other_cpu() will have effect again.

### Macros

esp_ipc_isr_asm_call(func, arg)

  Execute an ISR callback on the other CPU See esp_ipc_isr_call().

esp_ipc_isr_asm_call_blocking(func, arg)

  Execute an ISR callback on the other CPU and busy-wait until it completes See esp_ipc_isr_call_blocking().

### Type Definitions

typedef void (*esp_ipc_isr_func_t)(void *arg)

  IPC ISR Callback.

  The callback must be written:

  - in assembly for XTENSA chips (such as ESP32, ESP32S3).

  - in C or assembly for RISCV chips (such as ESP32P4).

  A callback of this type should be provided as an argument when calling esp_ipc_isr_call() or esp_ipc_isr_call_blocking().

Was this page helpful?

- Thank you! We received your feedback.
- If you have any comments, fill in Espressif Documentation Feedback Form.

- We value your feedback.
- Let us know how we can improve this page by filling in Espressif Documentation Feedback Form.