FreeRTOS (IDF)
==============

:link_to_translation:`en:[English]`

本文档介绍了 ESP-IDF 框架内的 FreeRTOS 双核 SMP 实现，包含以下小节：

.. contents:: 目录
  :depth: 2

.. ---------------------------------------------------- Overview -------------------------------------------------------

概述
----

原始 FreeRTOS（下文称 Vanilla FreeRTOS）是一款小巧高效的实时操作系统，适用于许多单核 MCU 和 SoC。但为了支持双核 ESP 芯片，如 ESP32、ESP32-S3、ESP32-P4，ESP-IDF 特别提供了支持双核对称多处理 (SMP) 的 FreeRTOS 实现（下文称 IDF FreeRTOS）。

IDF FreeRTOS 源代码基于 Vanilla FreeRTOS v10.5.1，但内核行为和 API 都有重大修改，以支持双核 SMP。不过用户也可以启用 :ref:`CONFIG_FREERTOS_UNICORE` 选项，将 IDF FreeRTOS 配置为支持单核，详情请参阅 :ref:`freertos-idf-single-core`。

.. note::

  本文档假定读者已具备 Vanilla FreeRTOS 的必要背景知识，即了解其特性、行为和 API 用法。如需了解背景知识，请参阅 `Vanilla FreeRTOS 文档 <https://www.freertos.org/index.html>`_。

.. -------------------------------------------- Symmetric Multiprocessing ----------------------------------------------

对称多处理
----------

基本概念
^^^^^^^^

对称多处理是一种计算架构，其中，两个及以上相同的 CPU 核连接到单个共享的主内存，并由单个操作系统控制。SMP 系统通常具有以下特点：

- 多个核独立运行。每个核都有自己的寄存器文件、中断和中断处理。
- 对每个核呈现相同的内存视图。因此，无论在哪个核上运行，访问特定内存地址的代码都会产生相同的效果。

与单核或非对称多处理系统相比，SMP 系统的主要优势在于：

- 存在多个核，支持多个硬件线程，从而提高整体处理吞吐量。
- 对称内存支持线程在执行期间切换核，从而提高 CPU 利用率。

尽管 SMP 系统支持线程切换核，但在某些情况下，线程必须或应该仅在特定核上运行。因此，在 SMP 系统中，线程也具备核亲和性，指定线程在哪个特定核上运行。

- 分配给特定核的线程只能在该核上运行。
- 未分配给特定核的线程支持在执行期间切换核。

ESP 芯片上的 SMP
^^^^^^^^^^^^^^^^

ESP32、ESP32-S3、ESP32-P4 和 ESP32-H4 等 ESP 芯片是双核 SMP SoC，具有以下硬件特性以支持 SMP：

- 具有两个完全相同的核，分别称为核 0 和核 1。代码段无论在哪个核上运行，都有相同的执行效果。
- 具有对称内存（除了少数例外情况）。

  - 如果多个核同时访问相同的内存地址，它们的访问会被内存总线串行化。
  - 通过 ISA 提供的原子比较和交换指令，可以实现对同一内存地址的真正原子访问。

- 跨核中断支持由一个核触发另一个核上的中断，这使得核间可以互相发送信号，如请求在另一个核上进行上下文切换。

.. note::

    在 ESP-IDF 中，核 0 和核 1 有时分别又被称为 ``PRO_CPU`` 和 ``APP_CPU``。别名 ``PRO_CPU`` 和 ``APP_CPU`` 反映了典型 ESP-IDF 应用程序使用这两个 CPU 的方式。负责处理 Wi-Fi 或蓝牙等协议相关处理程序的任务通常会分配给核 0，因此称核 0 为 ``PRO_CPU``；而处理应用程序其余部分的任务会分配给核 1，因此称核 1 为 ``APP_CPU``。

.. ------------------------------------------------------ Tasks --------------------------------------------------------

任务
----

创建任务
^^^^^^^^

Vanilla FreeRTOS 提供以下用于创建任务的函数：

- 使用 :cpp:func:`xTaskCreate` 创建任务时，任务内存动态分配。
- 使用 :cpp:func:`xTaskCreateStatic` 创建任务时，任务内存静态分配，即由用户提供。

然而，在 SMP 系统中，任务需要分配到特定核。因此，ESP-IDF 提供了 Vanilla FreeRTOS 任务创建函数的 ``...PinnedToCore()`` 版本：

- 使用 :cpp:func:`xTaskCreatePinnedToCore` 可以创建具有特定核亲和性的任务，任务内存动态分配。
- 使用 :cpp:func:`xTaskCreateStaticPinnedToCore` 可以创建具有特定核亲和性的任务，任务内存静态分配，即由用户提供。

不同于普通的任务创建函数 API，``...PinnedToCore()`` 版本的任务创建函数 API 有额外的 ``xCoreID`` 参数，用于指定所创建任务的核亲和性。核亲和性的有效值包括：

- ``0``：将创建的任务分配给核 0
- ``1``：将创建的任务分配给核 1
- ``tskNO_AFFINITY``：支持任务在两个核上运行

注意，IDF FreeRTOS 仍支持普通的任务创建函数，但这些标准函数已经过调整，会内部调用其 ``...PinnedToCore()`` 版本，同时将核亲和性设置为 ``tskNO_AFFINITY``。

.. note::

  IDF FreeRTOS 还更改了任务创建函数中的 ``ulStackDepth`` 参数。在 Vanilla FreeRTOS 中，任务堆栈的大小以字为单位指定，而在 IDF FreeRTOS 中，任务堆栈的大小以字节为单位指定。

执行任务
^^^^^^^^

IDF FreeRTOS 中任务的结构与 Vanilla FreeRTOS 相同。具体而言，IDF FreeRTOS 任务：

- 只能处于以下任一状态：运行中、就绪、阻塞或挂起。
- 任务函数通常为无限循环。
- 任务函数不应返回。

删除任务
^^^^^^^^

调用 :cpp:func:`vTaskDelete` 可以在 Vanilla FreeRTOS 中删除任务。该函数可用于删除其他任务，若任务句柄为 ``NULL`` 则删除当前运行任务。如果删除的任务是当前正在运行的任务时，任务的内存释放有时会委托给空闲任务执行。

IDF FreeRTOS 提供了同样的 :cpp:func:`vTaskDelete` 函数。然而，IDF FreeRTOS 是一个双核系统，因此调用 :cpp:func:`vTaskDelete` 时，行为上会与 Vanilla FreeRTOS 有以下差异：

- 删除另一个核上运行的任务时，会在另一个核上触发一次让步，任务内存由其中一个空闲任务释放。
- 如果删除的任务没有在任一核上运行，则会立即释放其内存。

请避免删除正在另一个核上运行的任务，否则由于无法确定该任务正在执行的操作，可能会导致难以预料的行为，例如：

- 删除持有互斥锁的任务。
- 删除尚未释放其先前分配的内存的任务。

请尽可能自己设计应用程序，确保在调用 :cpp:func:`vTaskDelete` 时，删除的任务处于已知状态。例如：

- 当任务完成执行操作并清理了任务内使用的所有资源时，任务调用 ``vTaskDelete(NULL)`` 自行删除。
- 在被另一个任务删除前，任务调用 :cpp:func:`vTaskSuspend` 将自己置于挂起状态。


.. --------------------------------------------------- Scheduling ------------------------------------------------------

SMP 调度器
----------

对 Vanilla FreeRTOS 调度器最确切的描述是 **具有时间分片和固定优先级的抢占式调度器**，这意味着：

- 每个任务在创建时都赋予了固定优先级，调度器会执行具有最高优先级且处于就绪状态的任务。
- 调度器可以在不需要当前运行任务的协作下切换执行到另一个任务。
- 调度器会采用轮转方式，定期在具有相同优先级的就绪状态任务间切换执行，时间分片由时钟中断控制。

IDF FreeRTOS 调度器支持相同的调度特性，即固定优先级、抢占和时间分片，但也存在细微的行为差异。

固定优先级
^^^^^^^^^^

在 Vanilla FreeRTOS 中，当调度器选择要运行的新任务时，往往会选择当前优先级最高的就绪任务。而在 IDF FreeRTOS 中，每个核都独立地调度要运行的任务。当特定核选择一个任务时，该核会选择优先级最高且可以在该核上运行的就绪状态任务。满足以下条件时，任务可以在核上运行：

- 任务亲和性兼容，即已分配或未分配给当前核。
- 该任务当前没有在其他核上运行。

但是，两个具有最高优先级的就绪任务不一定始终由调度器运行，因为还需考虑到任务的核亲和性。例如，给定以下任务：

- 优先级为 10 的任务 A，分配给核 0
- 优先级为 9 的任务 B，分配给核 0
- 优先级为 8 的任务 C，分配给核 1

经过调度后，任务 A 将在核 0 上运行，任务 C 将在核 1 上运行。即使任务 B 是第二优先级任务，也不会被执行。

抢占
^^^^

在 Vanilla FreeRTOS 中，如果优先级更高的任务已准备好执行，调度器可以抢占当前正在运行的任务。同样，在 IDF FreeRTOS 任务中，如果调度器确定一个优先级更高的任务可以在某个核上运行，那么调度器可以单独抢占各个核。

但在某些情况下，一个优先级更高的就绪任务可以在多个核上运行。此时，调度器只会抢占一个核。即便当前有多个核可以抢占，调度器总是优先选择当前核。换句话说，如果优先级更高的就绪任务未分配，并且其优先级高于两个核的当前优先级，调度器将始终选择抢占当前核。例如，给定以下任务：

- 优先级为 8 的任务 A 当前在核 0 上运行
- 优先级为 9 的任务 B 当前在核 1 上运行
- 优先级为 10 的任务 C 未分配，并由任务 B 解除了阻塞

经过调度后，任务 A 将在核 0 上运行，任务 C 将抢占任务 B，因为调度器总是优先选择当前核。

时间分片
^^^^^^^^

Vanilla FreeRTOS 实现了时间分片，这意味着如果当前优先级最高的就绪任务包含多个就绪任务，调度器会在这些任务间轮转定期切换。

然而，在 IDF FreeRTOS 中，由于以下原因，特定任务可能无法在特定核上运行，因此无法实现完美的轮转时间分片：

- 任务分配给了另一个核。
- 任务未分配，但已经由其他核运行。

因此，当核在所有就绪状态任务中搜索寻找要运行的任务时，可能需要跳过同一优先级列表中的一些任务，或者降低优先级，以找到可以运行的就绪状态任务。

IDF FreeRTOS 调度器会确保已选择运行的任务置于列表末尾，为同一优先级的就绪状态任务实现最佳轮转时间分片。这样，在下一次调度迭代（即，下一个滴答中断或让步）中，未经选择的任务优先级会更高。

以下示例展示了最佳轮转时间分片的实操。假设：

- 有四个相同优先级的就绪状态任务 ``AX``、``B0``、``C1`` 和 ``D1``，其中：

  - 优先级是当前具有就绪状态任务的最高优先级。
  - 第一个字符代表任务名称，即 ``A``、``B``、``C``、``D``。
  - 第二个字符表示任务核的分配情况，``X`` 表示未分配。

- 任务列表始终从头开始搜索

1. 起始状态，尚未选择要运行的就绪状态任务。

    .. code-block:: none

        Head [ AX , B0 , C1 , D0 ] Tail

2. 核 0 有一个滴答中断，搜索要运行的任务。选择任务 A，并将其移至列表末尾。

    .. code-block:: none

        Core 0 ─┐
                ▼
        Head [ AX , B0 , C1 , D0 ] Tail

                              [0]
        Head [ B0 , C1 , D0 , AX ] Tail

3. 核 1 有一个滴答中断，搜索要运行的任务。由于亲和性不兼容，任务 B 无法运行，因此核 1 跳到任务 C。选择任务 C，并将其移至列表末尾。

    .. code-block:: none

        Core 1 ──────┐
                     ▼        [0]
        Head [ B0 , C1 , D0 , AX ] Tail

                         [0]  [1]
        Head [ B0 , D0 , AX , C1 ] Tail

4. 核 0 有另一个滴答中断，搜索要运行的任务。选择任务 B，并将其移至列表末尾。

    .. code-block:: none

        Core 0 ─┐
                ▼             [1]
        Head [ B0 , D0 , AX , C1 ] Tail

                         [1]  [0]
        Head [ D0 , AX , C1 , B0 ] Tail

5. 核 1 有另一个滴答中断，搜索要运行的任务。由于亲和性不兼容，任务 D 无法运行，因此核 1 跳到任务 A。选择任务 A，并将其移至列表末尾。

    .. code-block:: none

        Core 1 ──────┐
                     ▼        [0]
        Head [ D0 , AX , C1 , B0 ] Tail

                         [0]  [1]
        Head [ D0 , C1 , B0 , AX ] Tail

在使用最佳轮转时间分片时需注意：

- 相同优先级的多个就绪状态任务不一定可以像在 Vanilla FreeRTOS 中一样按顺序运行。如以上示例所示，核可能会跳过任务。
- 然而，经过足够的滴答次数，任务最终将获得一些处理时间。
- 如果核找不到优先级最高的可运行就绪任务，它将降低优先级来搜索任务。
- 为了实现理想的轮转时间分片，应确保特定优先级的所有任务都分配给同一个核。

时钟中断
^^^^^^^^

Vanilla FreeRTOS 要求定期发生滴答中断，滴答中断有以下作用：

- 增加调度器的滴答计数
- 为超时的阻塞任务解除阻塞
- 检查是否需要进行时间分片，即触发上下文切换
- 执行应用程序滴答函数

在 IDF FreeRTOS 中，每个核都会接收到定期中断，并独立运行滴答中断。每个核上的滴答中断周期相同，但可能不同步。然而，上述滴答中断任务不会由所有核同时执行，具体而言：

- 核 0 执行上述所有滴答中断任务
- 核 1 仅检查是否需要时间分片并执行应用程序滴答函数

.. note::

  在 IDF FreeRTOS 中，核 0 是负责时间计数的唯一核。因此，任何阻止核 0 增加滴答计数的情况，例如暂停核 0 上的调度器，都会导致整个调度器的时间计数滞后。

空闲任务
^^^^^^^^

启动调度器时，Vanilla FreeRTOS 会隐式创建一个优先级为 0 的空闲任务。当没有其他任务准备运行时，空闲任务运行并有以下作用：

- 释放已删除任务的内存
- 执行应用程序的空闲函数

而 IDF FreeRTOS 为每个核单独创建了一个固定的空闲任务。每个核上的空闲任务起到与其 Vanilla FreeRTOS 对应任务相同的作用。

调度器挂起
^^^^^^^^^^

Vanilla FreeRTOS 支持调用 :cpp:func:`vTaskSuspendAll` 挂起调度器，调用 :cpp:func:`xTaskResumeAll` 恢复调度器。调度器挂起时：

- 禁用任务切换，但仍启用中断。
- 禁止调用任何阻塞或让出函数，禁用时间分片。
- 时钟计数冻结，但仍会发生时钟中断以执行应用程序时钟函数。

调度器恢复时，:cpp:func:`xTaskResumeAll` 会补上所有丢失的时钟计数，并解除超时任务的阻塞。

在 IDF FreeRTOS 中，无法在多个核上同时挂起调度器。因此，在特定核上（如核 A）调用 :cpp:func:`vTaskSuspendAll` 时：

- 只在核 A 上禁用任务切换，但仍启用核 A 的中断。
- 禁止在核 A 上调用任何阻塞或让出函数，在核 A 上禁用时间分片。
- 核 A 上的中断解除任务阻塞时，对核 A 亲和的任务会进入核 A 的待执行任务列表。未分配的任务或对其他核亲和的任务可以在运行调度器的核上调度。
- 所有核上的调度器均挂起时，由中断解除阻塞的任务将进入它们分配的核的待执行任务列表。如果任务未分配，则进入调用中断的核的待执行任务列表。
- 如果核 A 是核 0，则时钟计数将被冻结，挂起的时钟计数将递增，但仍会发生时钟中断以执行应用程序时钟函数。

在特定核（如核 A）上调用 :cpp:func:`xTaskResumeAll` 时：

- 任何添加到核 A 的待执行任务列表中的任务将恢复执行。
- 如果核 A 是核 0，则挂起的时钟计数将回退，补上丢失的时钟计数。

.. warning::

  IDF FreeRTOS 上的调度器挂起仅暂停特定核上的调度，因此调度器挂起 **不能** 确保访问共享数据时任务互斥。如果需要互斥，请使用适当的锁定机制，如互斥锁或自旋锁。

.. ------------------------------------------------ Critical Sections --------------------------------------------------

临界区
------

禁用中断
^^^^^^^^

Vanilla FreeRTOS 支持通过调用 :c:macro:`taskDISABLE_INTERRUPTS` 和 :c:macro:`taskENABLE_INTERRUPTS` 分别禁用和启用中断。IDF FreeRTOS 提供了相同的 API，但中断只能在当前核上禁用或启用。

在 Vanilla FreeRTOS 以及其他普通单核系统中，禁用中断可以有效实现互斥，**但在 SMP 系统中，禁用中断并不能确保实现互斥**，而应使用有自旋锁的临界区以实现互斥。

API 变更
^^^^^^^^

Vanilla FreeRTOS 通过禁用中断实现临界区 (Critical Section)，以防止在临界区内发生抢占式上下文切换和中断服务，确保进入临界区的任务或 ISR 是访问共享资源的唯一实体。Vanilla FreeRTOS 中的临界区提供以下 API：

- ``taskENTER_CRITICAL()`` 通过禁用中断进入临界区
- ``taskEXIT_CRITICAL()`` 通过重新启用中断退出临界区
- ``taskENTER_CRITICAL_FROM_ISR()`` 通过禁用中断嵌套从 ISR 进入临界区
- ``taskEXIT_CRITICAL_FROM_ISR()`` 通过重新启用中断嵌套从 ISR 退出临界区

然而，在 SMP 系统中，仅禁用中断并不能构成临界区，因为存在其他核意味着共享资源仍可以同时访问。因此，IDF FreeRTOS 中的临界区是使用自旋锁实现的。为适应自旋锁，IDF FreeRTOS 中的临界区 API 包含一个额外的自旋锁参数，具体如下：

- 自旋锁为 ``portMUX_TYPE`` (**请勿与 FreeRTOS 互斥混淆**)
- ``taskENTER_CRITICAL(&spinlock)`` 从任务上下文进入临界区
- ``taskEXIT_CRITICAL(&spinlock)`` 从任务上下文退出临界区
- ``taskENTER_CRITICAL_ISR(&spinlock)`` 从中断上下文进入临界区
- ``taskEXIT_CRITICAL_ISR(&spinlock)`` 从中断上下文退出临界区

.. note::

    临界区 API 可以递归调用，即可以嵌套使用临界区。只要退出临界区的次数与进入的次数相同，多次递归进入临界区就是有效的。但是，由于临界区可以针对不同的自旋锁，因此在递归进入临界区时，应注意避免死锁。

自旋锁可以静态或动态分配。因此，提供了静态和动态初始化自旋锁的宏，如以下代码片段所示。

- 静态分配自旋锁并使用 ``portMUX_INITIALIZER_UNLOCKED`` 初始化：

  .. code:: c

      // 静态分配并初始化自旋锁
      static portMUX_TYPE my_spinlock = portMUX_INITIALIZER_UNLOCKED;

      void some_function(void)
      {
          taskENTER_CRITICAL(&my_spinlock);
          // 此时已处于临界区
          taskEXIT_CRITICAL(&my_spinlock);
      }

- 动态分配自旋锁并使用 ``portMUX_INITIALIZE()`` 初始化：

  .. code:: c

      // 动态分配自旋锁
      portMUX_TYPE *my_spinlock = malloc(sizeof(portMUX_TYPE));
      // 动态初始化自旋锁
      portMUX_INITIALIZE(my_spinlock);

      ...

      taskENTER_CRITICAL(my_spinlock);
      // 访问资源
      taskEXIT_CRITICAL(my_spinlock);

实现
^^^^

IDF FreeRTOS 中，特定核进入和退出临界区的过程如下：

- 对于 ``taskENTER_CRITICAL(&spinlock)`` 或 ``taskENTER_CRITICAL_ISR(&spinlock)``

  #. 核禁用其中断或中断嵌套，直到达到 ``configMAX_SYSCALL_INTERRUPT_PRIORITY``。
  #. 接着，核使用原子比较和设置指令在自旋锁上自旋，直到获取锁。当核能够将锁的所有者值设置为核的 ID 时，就获得了锁。
  #. 一旦获取了自旋锁，函数返回。剩余的临界区部分将在禁用中断或中断嵌套的情况下运行。

- 对于 ``taskEXIT_CRITICAL(&spinlock)`` 或 ``taskEXIT_CRITICAL_ISR(&spinlock)``

  #. 核通过清除自旋锁的所有者值释放自旋锁。
  #. 核重新启用中断或中断嵌套。

限制与注意事项
^^^^^^^^^^^^^^

由于在临界区内禁用了中断或中断嵌套，产生了多个关于在临界区内可执行操作的限制，请牢记以下操作限制和注意事项：

- 临界区应尽可能短

  - 临界区持续时间越长，越可能延迟挂起中断的执行。
  - 临界区通常应仅涉及少量数据结构和/或硬件寄存器。
  - 如果可以，尽可能将执行操作和/或事件处理程序推迟到临界区之外。

- 不应在临界区内调用 FreeRTOS API
- 不应在临界区内调用任何阻塞或让出函数


.. ------------------------------------------------------ Misc ---------------------------------------------------------

其他事项
--------

.. only:: SOC_CPU_HAS_FPU

    使用浮点
    ^^^^^^^^

    通常情况下，当发生上下文切换时：

    - 核寄存器的当前状态保存到要切出的任务栈中
    - 核寄存器的先前保存状态从要切入的任务栈中加载

    然而，IDF FreeRTOS 为核的浮点运算单元 (FPU) 寄存器实现了惰性上下文切换。换句话说，当在特定核上（如核 0）发生上下文切换时，核的 FPU 寄存器状态不会立即保存到要被切出的任务的堆栈中（如任务 A）。FPU 的寄存器在发生以下情况前将保持不变：

    - 另一个任务（如任务 B）在同一核上运行并使用 FPU，这将触发异常，将 FPU 寄存器保存到任务 A 的堆栈中。
    - 任务 A 重新调度到同一核并继续执行。在这种情况下，不需要保存和恢复 FPU 的寄存器。

    然而，由于任务并未分配给某一核，可以随意调度（如任务 A 切换到核 1），因此很难实现跨核复制和恢复 FPU 寄存器状态。因此，当任务在其执行流程中用 ``float`` 类型使用 FPU 时，IDF FreeRTOS 会自动将任务分配给当前正在运行的核，确保所有使用 FPU 的任务始终在特定核上运行。

    此外，请注意，由于 FPU 寄存器状态与特定任务相关联，IDF FreeRTOS 默认不支持在中断上下文中使用 FPU。

    .. only:: esp32

        .. note::

            如需在 ISR 例程中使用 ``float`` 类型，请参考配置选项:ref:`CONFIG_FREERTOS_FPU_IN_ISR`。

    .. note::

        包含 FPU 的乐鑫芯片不支持双精度浮点运算 ``double`` 的硬件加速。``double`` 通过软件实现，因此针对 ``float`` 类型的行为限制不适用于 ``double``。请注意，由于缺少硬件加速，``double`` 运算与 ``float`` 相比可能会消耗更多的 CPU 时间。


.. only:: SOC_CPU_HAS_PIE

    使用 PIE/AI 协处理器
    ^^^^^^^^^^^^^^^^^^^^

    与浮点运算单元 (FPU) 类似，IDF FreeRTOS 在 PIE 协处理器上也采用了 **惰性上下文切换** 策略。在上下文切换时，PIE 寄存器保持不变，直到某个任务执行 PIE 指令时才进行处理。一旦有任务使用了 PIE 协处理器，该任务将被 **固定到当前核心**。

    .. only:: esp32s31

        .. note::

            在 ESP32-S31 上，PIE 协处理器 **仅在核心 1 上可用**。如果任务在核心 0 上运行时执行了 PIE 指令，IDF FreeRTOS 会将该任务迁移至核心 1 并将其固定。此迁移会 **覆盖** 任何现有的核心亲和性设置。

            由于存在这种迁移机制，任务 **不得** 在临界区或 ISR 例程中使用 PIE 协处理器，否则将导致运行时中止。


.. only:: SOC_CPU_HAS_HWLOOP

    使用硬件循环 (HWLP)
    ^^^^^^^^^^^^^^^^^^^

    在 IDF FreeRTOS 中，硬件循环 (HWLP) 单元的处理方式与其他协处理器不同：它 **不** 使用惰性上下文切换。

    当使用 HWLP 的任务发生上下文切换时，HWLP 寄存器会在中断入口路径中立即保存。之后，如果同一任务被切换回来，所有 HWLP 寄存器会立即恢复。

    在实践中，这意味着任何曾经使用过 HWLP 的任务，在每次上下文切出和切入时都会产生额外的开销。

.. only:: SOC_CPU_HAS_DSP

    使用 DSP 协处理器
    ^^^^^^^^^^^^^^^^^

    在包含 DSP 协处理器的目标芯片上，上下文切换遵循与 FPU 相同的延迟方案：只有在同一核心上的另一个任务使用了该协处理器，或者该任务被切换到另一个核心时，协处理器的状态才会被保存。当任务使用 DSP 协处理器时，IDF FreeRTOS 会自动 **将该任务固定至其当前运行的核心**。DSP 协处理器不得在中断上下文中使用。


.. -------------------------------------------------- Single Core  -----------------------------------------------------

.. _freertos-idf-single-core:

单核模式
^^^^^^^^

尽管 IDF FreeRTOS 是为双核 SMP 专门设计的，但也可通过启用 :ref:`CONFIG_FREERTOS_UNICORE` 选项，将 IDF FreeRTOS 配置为支持单核。

对于 ESP32-S2 和 ESP32-C3 等单核芯片，:ref:`CONFIG_FREERTOS_UNICORE` 选项始终启用。对于 ESP32 和 ESP32-S3 等多核芯片也可以设置 :ref:`CONFIG_FREERTOS_UNICORE`，对于多核目标（如 ESP32 和 ESP32-S3），也可以设置 :ref:`CONFIG_FREERTOS_UNICORE`，但启用该选项后应用仅在核 0 上运行。

在单核模式下，IDF FreeRTOS 与 Vanilla FreeRTOS 完全相同，因此无需考虑前文提到的对内核行为的 SMP 更改。因此，在单核模式下构建 IDF FreeRTOS 具有以下特点：

- 内核在临界区内执行的所有操作都是确定的（即在临界区内不走链表）。
- 恢复了 Vanilla FreeRTOS 调度算法（包括完美的轮转时间分片）。
- 移除了所有单核构建中的 SMP 专用数据。

在单核模式下仍可调用 SMP API，这些 API 仍然保持公开，以便为单核和多核构建源代码，而无需调用不同的 API 集。不过，SMP API 在单核模式下不会展示任何 SMP 行为，因此实际上等同于其对应的单核模式 API。例如：

- 任何 ``...ForCore(..., BaseType_t xCoreID)`` SMP API 将只接受 ``0`` 为 ``xCoreID`` 的有效值。
- ``...PinnedToCore()`` 任务创建 API 将直接忽略 ``xCoreID`` 核亲和参数。
- 临界区 API 仍需要自旋锁参数，但不会使用自旋锁，临界区将恢复为仅用于禁用/启用中断。


.. ------------------------------------------------- API References ----------------------------------------------------

API 参考
--------

本节介绍了 FreeRTOS 类型、函数和宏，均从 FreeRTOS 头文件自动生成。

任务 API
^^^^^^^^

.. include-build-file:: inc/task.inc

队列 API
^^^^^^^^^

.. include-build-file:: inc/queue.inc

信号量 API
^^^^^^^^^^

.. include-build-file:: inc/semphr.inc

定时器 API
^^^^^^^^^^

.. include-build-file:: inc/timers.inc

事件组 API
^^^^^^^^^^

.. include-build-file:: inc/event_groups.inc

流缓冲区 API
^^^^^^^^^^^^

.. include-build-file:: inc/stream_buffer.inc

消息缓冲区 API
^^^^^^^^^^^^^^

.. include-build-file:: inc/message_buffer.inc
