RTOS 内核控制

[English]

RTOS 内核控制是指通过调度器协调任务执行、管理系统资源并实现多任务并发运行的机制。本文档仅涵盖内核控制中部分常用宏定义,更多内容请参考 FreeRTOS 官方文档

任务切换

宏定义原型

void taskYIELD( void );

taskYIELD() 用于显式触发任务切换,当前任务主动让出处理器使用权,触发一次上下文切换,由调度器选择并运行优先级相同或更高的就绪任务。常用于协调同优先级任务的执行顺序。

备注

  • taskYIELD() 调用后,当前任务仍处于就绪状态,可能在下一调度周期继续运行。

  • 若没有其他任务的优先级高于或等于当前任务,调度器仍将继续选择该任务运行。

  • 在 ESP-IDF 环境下的对称多核系统中,每个核的调度独立,调用 taskYIELD() 只影响当前运行核。

  • configUSE_PREEMPTION 设置为 1 时,调度器始终运行最高优先级的就绪任务,调用 taskYIELD() 不会切换到更高优先级的任务,但会触发相同优先级任务之间的切换。

示例 1:任务间的显性切换

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void vTask1(void *pvParameters)
{
    int count = 0;
    while (1)
    {
        printf("Task1 count: %d\n", count++);
        taskYIELD();
    }
}

void vTask2(void *pvParameters)
{
    int count = 0;
    while (1)
    {
        printf("Task2 count: %d\n", count++);
        taskYIELD();
    }
}

void app_main(void)
{
    // 创建两个同优先级任务,且绑定同一核心
    xTaskCreatePinnedToCore(vTask1, "Task1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(vTask2, "Task2", 2048, NULL, 1, NULL, 0);
}

以上代码执行结果如下:

Task1 count: 0
Task1 count: 1
Task2 count: 0
Task1 count: 2
Task2 count: 1
Task1 count: 3
Task2 count: 2

本示例展示了两个同优先级任务通过 taskYIELD() 显式让出 CPU,从而实现基本的任务切换效果。执行结果显示两个任务交替运行。

该示例运行在 ESP-IDF 环境下,具备如下特性和注意事项:

  • 任务需绑定在同一核心执行。taskYIELD() 仅影响当前运行核心上的任务队列,若任务绑定至不同核心,则无法通过 taskYIELD() 实现交替切换。

  • 在 ESP-IDF 中,app_main() 本身是一个 FreeRTOS 任务,默认优先级为 1。若创建的第一个任务优先级高于 app_main(),该任务进入就绪态后会抢占 app_main(),导致后续任务尚未创建且永远无法创建。因此,创建多个任务时应确保任务创建逻辑在被抢占前完成,或由任务内部逐步创建。

相较于 vTaskDelay()taskYIELD() 是一种非阻塞的显式让出 CPU 操作,调用后任务仍保持就绪态,调度器会尝试切换到其他同优先级任务,但不保证一定切换。 而 vTaskDelay() 会使任务进入阻塞态,明确挂起一段时间,调度器必然切换到其他任务执行。

因此,taskYIELD() 更适合任务间快速协作切换,vTaskDelay() 更适合周期性或延时挂起任务。

临界区控制

宏定义原型

// 原生 FreeRTOS
void taskENTER_CRITICAL( void );
void taskEXIT_CRITICAL( void );

//  ESP-IDF 环境下
void portENTER_CRITICAL(portMUX_TYPE *mux);
void portEXIT_CRITICAL(portMUX_TYPE *mux);

taskENTER_CRITICAL()taskEXIT_CRITICAL() 用于进入和退出临界区,保护临界区内的代码不被中断或任务切换打断,确保操作的原子性和数据一致性。

  • taskENTER_CRITICAL():关闭中断(或提升中断优先级),禁止任务切换,进入临界区。

  • taskEXIT_CRITICAL():恢复中断状态,允许任务切换,退出临界区。

这两个函数常用于多任务环境中,防止多个任务同时访问共享资源,从而避免数据冲突或执行错误。

备注

操作的原子性指的是一组操作在执行过程中不可被中断或分割,要么全部完成,要么完全不执行,系统中不会看到中间状态。

在原生 FreeRTOS 和 ESP-IDF 环境下,这两个宏都用于进入和退出临界区,但在使用方式和内部实现上存在差异。

原生 FreeRTOS 通常运行于单核系统中,taskENTER_CRITICAL()taskEXIT_CRITICAL() 是无参数函数,主要通过禁用和恢复中断来防止当前核被中断,从而保护共享资源。 而在 ESP-IDF 的多核架构中,这两个宏实际封装为带参数的函数,需要传入跨核互斥锁 portMUX_TYPE,结合中断屏蔽和自旋锁机制,实现多核间的同步和保护,避免竞态条件。

造成这种差异的原因在于单核系统仅需屏蔽本核中断即可保证临界区安全,而多核系统必须通过跨核互斥锁确保多个核心访问共享资源时的原子性和数据一致性。

更进一步的相关信息请查阅 ESP-IDF 官方文档中关于 FreeRTOS 临界区 的章节。

备注

互斥锁是一种用于多任务或多线程环境中的同步机制,用来保护共享资源,防止多个任务同时访问导致数据冲突或不一致。

它确保同一时间只有一个任务能够进入临界区访问共享资源,从而避免竞态条件。

示例 1:ESP-IDF 环境下临界区的进入与退出

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

volatile int shared_var = 0;

// 定义互斥锁
static portMUX_TYPE my_mux = portMUX_INITIALIZER_UNLOCKED;

void vTask1(void *pvParameters)
{
    while (1)
    {
        portENTER_CRITICAL(&my_mux);
        shared_var++;
        portEXIT_CRITICAL(&my_mux);
        printf("Task 1: shared_var = %d\n", shared_var);

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vTask2(void *pvParameters)
{
    while (1)
    {
        portENTER_CRITICAL(&my_mux);
        shared_var++;
        portEXIT_CRITICAL(&my_mux);
        printf("Task 2: shared_var = %d\n", shared_var);

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreatePinnedToCore(vTask1, "Task1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(vTask2, "Task2", 2048, NULL, 1, NULL, 1);
}

以上代码执行结果如下:

Task 1: shared_var = 1
Task 2: shared_var = 2
Task 1: shared_var = 3
Task 2: shared_var = 4
Task 1: shared_var = 5
Task 2: shared_var = 6
Task 1: shared_var = 7
Task 2: shared_var = 8
Task 1: shared_var = 9
Task 2: shared_var = 10

该示例清晰展示了 ESP 多核架构下多任务安全访问共享资源的基本原则和实践方法。

通过两个分别绑定在不同 CPU 核心上的任务并发操作共享变量 shared_var,利用 portMUX_TYPE 自旋锁配合 portENTER_CRITICAL()portEXIT_CRITICAL() 宏,实现了对共享变量的互斥保护,避免了竞态条件和数据错乱。 临界区内仅包含自增操作,保证代码简短高效,将打印放在临界区外执行以避免因阻塞函数引发死锁。 两个任务分别固定核心运行,充分利用多核并行,输出结果中 shared_var 顺序递增,验证了临界区保护的有效性和多核环境下安全访问共享资源的实践。

调用临界区时应注意:

  • 自旋锁变量通过 portMUX_INITIALIZER_UNLOCKED 宏进行静态初始化,确保互斥锁初始状态为“未锁定”,便于安全使用。

  • 死锁指任务在等待资源释放时无法切换或继续执行,导致系统卡死。因此,临界区内应避免调用可能阻塞的函数,确保代码简洁快速执行。