引言
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 内部自旋锁会串行化操作,但若在发送前需要读取一个全局计数器,则计数器更新必须用原子操作,否则可能丢失计数。
配置步骤
-
启用多核支持:在
menuconfig中确保CONFIG_FREERTOS_UNICORE未启用。 -
包含头文件:
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" #include "esp_attr.h" #include "esp_spinlock.h" -
定义全局原子变量:使用
portMUX_TYPE或 C11atomic。
代码示例:原子操作保护队列计数
以下示例展示两个任务并发写队列,并用原子操作维护发送计数。
#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 原子操作,可以在保证数据一致性的同时,避免中断延迟,提升系统实时性。理解边界场景,合理选择同步机制,是嵌入式多核编程的关键。