没志青年
发布于 2025-07-20 / 22 阅读
0

ESP32 IDF-FreeRTOS

ESP-IDF 框架是根据FreeRTOS改过来的。

为什么和原生的 FreeRTOS 有区别呢?

因为 ESP32 单片机为双核,一般的单片机为单核,。

ESP-IDF中的FreeRTOS与原生FreeRTOS的区别分析_esp-idf支持freertos吗-CSDN博客

FreeRTOS (IDF) - ESP32 - — ESP-IDF 编程指南 v5.5 文档

SMP 多核系统

特点:

  • 多个核独立运行。每个核都有自己的寄存器文件、中断和中断处理。

  • 每个核看到的内存是一样的,无论是哪个核,相同地址指向的是同一块内存。

任务调度机制

单核中仅看任务优先级,优先级高的任务先执行。

多核中不仅看任务优先级,还看任务对核心的亲和度,若任务 A 指定了核心 0,那它只能在核心 0 上运行,不能在核心 1 上运行

(一)抢占

(1)指定核心

每个核心有单独的调度器,优先级高的任务抢占。

核 0 会调度在创建时核心指定为 0 的任务,核 1 会调度在创建时核心指定为 1 的任务。

(2)不指定核心

两个核心有自己独立的时钟,调度器不是同时调度的,当一个核心的调度器调度时,如果当前核心上有未分配核心的任务就绪了,且优先级比当前任务高,那就在当前核心上运行,因此运行在哪个核心上是不固定的。

比如:

  • 优先级为 8 的任务 A 当前在核 0 上运行

  • 优先级为 9 的任务 B 当前在核 1 上运行

  • 优先级为 10 的任务 C 未分配,并由任务 B 解除了阻塞

任务 C 就绪且优先级比任务 B 高,那就在当前的核 1 上运行。

(二)时间片

在原生 FreeRTOS 的时间片调度中,多个最高优先级就绪任务,会按时间片轮流执行。

ESP32是双核,每个核心都有自己的时间片调度。区别在于,原生FreeRTOS是按照最高的、相同的优先级,来让任务时间片执行。

ESP-IDF的,时间片可能发生在不同优先级之间,因为相同优先级的任务可以指定在两个不同的核,或者不指定核心,那既可能在同一核心上,又可能不在同一核心上。

为了实现理想的时间片调度,应保证特定的相同优先级的任务都分配在同一个核心上。

举例:

X:未指定;0:核0;1:核1

就绪队列头部 [ AX , B0 , C1 , D0 ] 就绪队列尾部


// 核0调度,运行AX,然后将其放在尾部
就绪队列头部 [ B0 , C1 , D0 , AX ] 就绪队列尾部

// 核1调度,不亲和任务B0,跳过B0去执行C1,并将其放在尾部
就绪队列头部 [ B0 , D0 , AX , C1 ] 就绪队列尾部

// 核0调度,运行B0,然后将其放在尾部
就绪队列头部 [ D0 , AX , C1 , B0 ] 就绪队列尾部

// 核1调度,不亲和任务D0,跳过D0去执行AX,并将其放在尾部
就绪队列头部 [ D0 , C1 , B0 , AX ] 就绪队列尾部

// 核0调度,运行D0,然后将其放在尾部
就绪队列头部 [ C1 , B0 , AX , D0 ] 就绪队列尾部


......

与原生 FreeRTOS 的区别

系统时钟中断

每个核上都有滴答定时器,每个核上的滴答中断,周期是相同的,但可能不是同步的。

ESP-IDF 的 FreeRTOS 使用核 0 来负责时间计数。

暂停核 0 上的调度器,会导致整个系统暂停,也就是核 0 暂停了,核 1 也会暂停。

创建任务函数不同

原生动态创建:

变为xTaskCreatePinnedToCore()

BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pxTaskCode,   // 任务执行函数
                                    const char *const pcName,    // 任务名
                                    const uint32_t ulStackDepth,    // 栈大小
                                    void *const pvParameters,     // 传给执行函数的参数
                                    UBaseType_t uxPriority,     // 优先级
                                    TaskHandle_t *const pxCreatedTask,  // 任务句柄
                                    const BaseType_t xCoreID )    // 指定核心



// 显式绑定到Core 0
xTaskCreatePinnedToCore(task_function, "Task", 4096, NULL, 2, NULL, 0);

// 不绑定核心(由调度器自动分配)
xTaskCreatePinnedToCore(task_function, "Task", 4096, NULL, 2, NULL, tskNO_AFFINITY);
  • 0:核心0

  • 1:核心1

  • tskNO_AFFINITY :执行时会根据调度切换核心

也可以以静态方式创建。

任务堆栈单位不同

  • FreeRTOS:字(4字节)

  • ESP-IDF:字节

浮点运算需指定核心

如果任务中用到浮点运算,则创建任务的时候必须指定具体运行在哪个核上,不能由系统自动安排。

ESP32只有一个浮点运算单元 FPU,两个核共享。

FreeRTOS中,当任务上下文切换时:

  • 切出:核寄存器的当前状态保存到要切出的任务栈中

  • 切入:核寄存器的先前保存状态从要切入的任务栈中加载

FPU 实现了延迟上下文切换,同一个核中任务切换:

  • 切出的任务A使用了FPU,切入的任务B也使用了FPU:将触发异常,FPU 寄存器将保存到任务 A 的堆栈中。

  • 切出的任务A使用了FPU,切入的任务B不使用FPU:没有任何动作,数据仍保存在 FPU 寄存器中。

如果任务A未指定核心,在核0上使用了FPU,将它从核 0 调度到核 1 时,如果核0新切入的任务B也用到了FPU,那FPU的数据不能跨核保存到任务A的堆栈中。那就造成数据丢失,系统错乱。因此必须指定核心。

你代码中用到了float的运算,那编译的时候,编译器会自动生成对应的FPU指令。

FPU不能在中断上下文中使用。

还有其他的:。。。

禁止删除另一个核心上的任务

在删除时,与原生 FreeRTOS 存在差异:

  • 若删除的任务没有在任一核心上运行,会立即释放其内存。

  • 核 A 删除核 B 的任务时,会触发核 B 的任务调度器进行任务切换,目的是让目标删除任务退出,之后由核 B 的空闲任务释放内存。

注意,不要删除另一个核上正在运行的任务,否则可能会导致:

  • 任务中申请的内存还没释放,造成内存泄漏。

  • 任务持有互斥锁,造成需要该临界资源的任务被永久阻塞。

上面两个情况在单核中也有可能发生,任务 A 申请了内存还没释放时、获得了互斥锁还没释放时,被高优先级任务 B 抢占,B 把 A 删除了。

虽然有这种可能,但是一般的程序没有机会出现这种情况,毕竟删除任务的需求很小。

删除任务时,要明确被删除任务的状态。

被删除的任务要先释放自己申请的内存,然后最好使用 vTaskSuspend() 将自己挂起。

最安全的办法还是,任务自己删除自己。

中断和临界区保护

在原生 FreeRTOS 单核处理器中,使用taskDISABLE_INTERRUPTStaskENABLE_INTERRUPTS 禁用和开启中断,IDF FreeRTOS也支持。

由于 ESP32 的每个核都有单独的中断,调用上面的函数,关闭或开启的是当前核上的中断

原生:

  • taskENTER_CRITICAL() 通过禁用中断进入临界区

  • taskEXIT_CRITICAL() 通过重新启用中断退出临界区

  • taskENTER_CRITICAL_FROM_ISR() 通过禁用中断嵌套从 ISR 进入临界区

  • taskEXIT_CRITICAL_FROM_ISR() 通过重新启用中断嵌套从 ISR 退出临界区

ESP32:

  • taskENTER_CRITICAL(&spinlock) 从任务上下文进入临界区

  • taskEXIT_CRITICAL(&spinlock) 从任务上下文退出临界区

  • taskENTER_CRITICAL_ISR(&spinlock) 从中断上下文进入临界区

  • taskEXIT_CRITICAL_ISR(&spinlock) 从中断上下文退出临界区

仅关闭中断,不能实现多核任务对共享资源的互斥,例如核0的任务A进入临界区,核1的任务B也可以进入临界区,造成了冲突。

IDF-FreeRTOS的临界区,是使用自旋锁的临界区。

自旋锁:任务获取不到临界资源,就会一直检查锁的状态,直到成功获取。

自旋锁的判断,是在关闭中断之后的,因此,如果自旋锁要等待,是在关中断的情况下等待的。

写代码和之前一样写就行了,无非多传入了一个变量。

静态分配自旋锁:

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

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

动态分配自旋锁:

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

...

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

临界区API递归调用?

自动创建的后台任务

空闲任务

和 FreeRTOS 的空闲任务作用一样(包括低功耗处理吗?),每个核都有各自的空闲任务。

FreeRTOS 定时器任务

主任务

IPC 任务

ESP 定时器任务

FreeRTOS 单核模式

配置 IDF-FreeRTOS

在 ESP-IDF 的终端中,输入:

idf.py menuconfig

运行失败的话,清理构建,重新执行,加载挺慢的。

要把终端拉大一点才好操作: