ESP32S3

功能框图

s3gnkt

FreeRTOS(IDF)

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

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

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

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

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

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

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

ESP32 对 SMP 支持
ESP32、ESP32-S3、ESP32-P4 等 ESP 芯片是双核 SMP SoC,具有以下硬件特性以支持 SMP:

  • 具有两个完全相同的核,分别称为核 0(PRO_CPU) 和核 1(APP_CPU)。代码段无论在哪个核上运行,都有相同的执行效果。
  • 具有对称内存(除了少数例外情况)。
    • 如果多个核同时访问相同的内存地址,它们的访问会被内存总线串行化。
    • 通过 ISA 提供的原子比较和交换指令,可以实现对同一内存地址的真正原子访问。
  • 跨核中断支持由一个核触发另一个核上的中断,这使得核间可以互相发送信号,如请求在另一个核上进行上下文切换。

通常:负责处理 Wi-Fi 或蓝牙等协议相关处理程序的任务通常会分配给核 0,因此称核 0 为 PRO_CPU;而处理应用程序其余部分的任务会分配给核 1,因此称核 1 为 APP_CPU。

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

  • 使用 xTaskCreatePinnedToCore() 可以创建具有特定核亲和性的任务,任务内存动态分配。
  • 使用 xTaskCreateStaticPinnedToCore() 可以创建具有特定核亲和性的任务,任务内存静态分配,即由用户提供。

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

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

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

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

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

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

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

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

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

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

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

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

固定优先级调度

特性 FreeRTOS IDF FreeRTOS (多核)
调度目标 单核选择最高优先级就绪任务 每个核独立选择最高优先级且满足亲和性的任务
亲和性影响 无(单核无需亲和性) 任务需满足核亲和性

任务的核亲和性条件:

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

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

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

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

抢占机制

特性 Vanilla FreeRTOS IDF FreeRTOS (多核)
抢占逻辑 高优先级任务抢占当前任务 高优先级任务仅抢占当前核的任务(优先选择当前核)
多核抢占限制 不适用(单核) 高优先级任务可能仅在部分核上被抢占

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

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

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

“当前核”是触发调度事件的核,调度器优先在此核进行抢占,而非全局最优调度。这种策略平衡了实时性、多核开销和实现复杂度,是 SMP 系统中常见的权衡设计。

时间分片(轮转调度)

特性 Vanilla FreeRTOS IDF FreeRTOS (多核)
轮转方式 严格按优先级轮转同一优先级任务 受限轮转(跳过无法在当前核运行的任务)
任务移动策略 无特殊处理 已选任务移至列表末尾,优化后续轮转

多核(SMP)环境中,任务调度需考虑 核亲和性(即任务被允许运行的 CPU 核)。这导致以下差异:

  • 时间分片不完美
    同一优先级的任务可能因亲和性限制,无法在所有核上轮转执行。
  • 调度器行为
    • 搜索任务时,若当前核无法运行某个任务(因亲和性不匹配),需跳过该任务。
    • 若同一优先级无可用任务,调度器会降低优先级搜索可运行任务。

滴答时钟
FreeRTOS 要求定期发生滴答中断,滴答中断有以下作用:

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

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

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

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

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

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

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

临界区
禁用中断
FreeRTOS 支持通过调用 taskDISABLE_INTERRUPTS 和 taskENABLE_INTERRUPTS 分别禁用和启用中断。IDF FreeRTOS 提供了相同的 API,但中断只能在当前核上禁用或启用。
在 FreeRTOS 以及其他普通单核系统中,禁用中断可以有效实现互斥,但在 SMP 系统中,禁用中断并不能确保实现互斥,而应使用有自旋锁的临界区以实现互斥。

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

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

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

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

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

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

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

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

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

  • 临界区应尽可能短
    • 临界区持续时间越长,越可能延迟挂起中断的执行。
    • 临界区通常应仅涉及少量数据结构和/或硬件寄存器。
    • 如果可以,尽可能将执行操作和/或事件处理程序推迟到临界区之外。
  • 不应在临界区内调用 FreeRTOS API
  • 不应在临界区内调用任何阻塞或让出函数

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

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

简单总结

rtosjdzj