引言

ESP32 集成 Xtensa 双核处理器,FreeRTOS 默认支持 SMP(对称多处理)。当多个任务或中断同时访问 FreeRTOS 队列时,传统做法是使用 taskENTER_CRITICAL() 关闭中断保护临界区。但在多核环境下,关中断只屏蔽当前核的中断,无法阻止另一核的并发访问,且会阻塞本核所有中断,严重影响实时性。因此,ESP-IDF 提供了基于自旋锁的 portMUX_TYPE 和原子操作,作为更精细的同步手段。

原理:为什么关中断在多核下失效?

  • 单核模型:关中断后,当前核不再响应中断,其他任务无法抢占,临界区安全。
  • 双核模型:每个核独立运行,关中断仅作用于当前核,另一核仍可执行任务并访问同一队列,导致数据竞争。
  • FreeRTOS 队列内部:队列的读写操作本身有临界区保护(使用 portMUX_TYPE),但若用户在外层再包一层关中断,反而可能造成死锁或性能下降。

ESP-IDF 的 portMUX_TYPE 是自旋锁,在双核上通过原子指令(如 xchg)实现互斥。当锁被占用时,等待核会自旋,不阻塞中断,适合短临界区。

边界场景分析

场景一:任务间队列读写(无中断)

两个任务(TaskA、TaskB)分别向队列写和读。若使用关中断保护,TaskA 关中断期间,TaskB 在另一核可能同时操作队列,导致未定义行为。正确做法:直接使用 FreeRTOS 队列 API(内部已用 portMUX_TYPE 保护),无需额外临界区。

场景二:中断服务程序(ISR)与任务并发

ISR 中调用 xQueueSendFromISR,任务中调用 xQueueReceive。ISR 运行在某个核上,任务可能在另一核。此时,队列 API 内部使用 portMUX_TYPE 保证跨核安全。但若用户自定义共享变量(如标志位),则需用原子操作或自旋锁。

场景三:多核同时写队列(高并发)

两个任务同时向同一队列发送数据。队列 API 内部自旋锁会串行化操作,但若在发送前需要读取一个全局计数器,则计数器更新必须用原子操作,否则可能丢失计数。

配置步骤

  1. 启用多核支持:在 menuconfig 中确保 CONFIG_FREERTOS_UNICORE 未启用。
  2. 包含头文件
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/queue.h"
    #include "esp_attr.h"
    #include "esp_spinlock.h"
    
  3. 定义全局原子变量:使用 portMUX_TYPE 或 C11 atomic

代码示例:原子操作保护队列计数

以下示例展示两个任务并发写队列,并用原子操作维护发送计数。

#include <stdatomic.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

QueueHandle_t q;
atomic_int send_count = 0;  // 原子计数

void writer_task(void *arg) {
    int id = (int)arg;
    int data = id * 100;
    while (1) {
        if (xQueueSend(q, &data, 0) == pdTRUE) {
            atomic_fetch_add(&send_count, 1);  // 原子自增
            printf("Task %d sent %d\n", id, data);
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main() {
    q = xQueueCreate(10, sizeof(int));
    xTaskCreatePinnedToCore(writer_task, "W1", 2048, (void*)1, 1, NULL, 0);
    xTaskCreatePinnedToCore(writer_task, "W2", 2048, (void*)2, 1, NULL, 1);
}

若使用 portMUX_TYPE 实现相同功能:

portMUX_TYPE my_mux = portMUX_INITIALIZER_UNLOCKED;
int send_count = 0;

// 在写队列后
portENTER_CRITICAL(&my_mux);
send_count++;
portEXIT_CRITICAL(&my_mux);

注意:portENTER_CRITICAL 在 ESP-IDF 中接受 portMUX_TYPE* 参数,用于多核互斥,而非关中断。

注意事项

  • 不要混用关中断和自旋锁:在持有自旋锁时调用 taskENTER_CRITICAL 可能导致死锁,因为另一核可能等待锁。
  • 临界区要短:自旋锁等待会浪费 CPU,长时间持有会拖慢系统。
  • ISR 中只能使用 FromISR 结尾的 API,且不能调用阻塞函数。
  • 原子操作适用于简单变量:对于复杂结构,仍需互斥锁。
  • 测试边界:使用 portASSERT_IF_IN_ISR() 检查上下文,避免在 ISR 中调用非 FromISR 函数。

总结

在 ESP32 多核环境下,关中断不再是可靠的互斥手段。通过使用 portMUX_TYPE 自旋锁或 C11 原子操作,可以在保证数据一致性的同时,避免中断延迟,提升系统实时性。理解边界场景,合理选择同步机制,是嵌入式多核编程的关键。