# 引言 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. **包含头文件**: ```c #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`。 # 代码示例:原子操作保护队列计数 以下示例展示两个任务并发写队列,并用原子操作维护发送计数。 ```c #include #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` 实现相同功能: ```c 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 原子操作,可以在保证数据一致性的同时,避免中断延迟,提升系统实时性。理解边界场景,合理选择同步机制,是嵌入式多核编程的关键。