# 引言 ESP32 作为一款双核 MCU,其 FreeRTOS 调度器支持对称多处理(SMP),任务可分配到 Core 0 或 Core 1。然而,多核环境下的资源竞争、优先级反转或任务长时间占用 CPU 都可能导致系统级死锁。任务看门狗(TWDT)正是用于检测这类问题的硬件辅助机制,但若配置不当,反而会引发误复位或掩盖真正故障。本文将从原理到实践,教你正确使用 TWDT。 # 一、双核调度与死锁的本质 ## 1.1 双核调度模型 - ESP32 的 FreeRTOS 使用 `xTaskCreatePinnedToCore()` 将任务绑定到指定核心,未绑定任务由调度器动态分配。 - 每个核心独立运行调度器,但共享全局就绪队列和互斥量。 - 死锁常见场景: - 任务 A(Core 0)持有锁,等待任务 B(Core 1)释放另一把锁,而 B 又在等 A 释放锁,形成循环等待。 - 一个任务在临界区中执行耗时操作(如 `vTaskDelay` 或长循环),阻塞其他核心任务。 ## 1.2 为什么需要 TWDT? - 软件看门狗(如 `vTaskDelay` 超时)无法检测 CPU 占用型死锁,因为任务可能仍在运行但未让出 CPU。 - TWDT 由硬件定时器驱动,周期性检查每个已订阅任务是否被调度执行。若任务在超时时间内未获得 CPU 时间,则触发中断或复位。 # 二、ESP32 任务看门狗的工作原理 ## 2.1 硬件基础 - ESP32 使用 TIMG0 定时器作为 TWDT 时钟源,默认超时时间为 5 秒(可配置)。 - 每个核心有一个独立的 TWDT 实例,但共享同一中断源。 ## 2.2 订阅机制 - 任务必须显式调用 `esp_task_wdt_add()` 订阅看门狗,否则不被监控。 - 订阅后,任务需要在每次执行循环中调用 `esp_task_wdt_reset()` 喂狗,否则超时触发。 - 空闲任务(IDLE)默认被订阅,用于检测调度器是否停滞。 ## 2.3 触发与处理 - 超时后,TWDT 会先触发中断,默认处理函数打印任务栈回溯并复位系统。 - 可通过 `esp_task_wdt_init()` 配置超时时间和是否立即复位。 # 三、配置步骤与代码示例 ## 3.1 初始化 TWDT ```c #include "esp_task_wdt.h" void app_main() { // 初始化 TWDT,超时 3 秒,不立即复位(便于调试) esp_task_wdt_config_t twdt_config = { .timeout_ms = 3000, .idle_core_mask = (1 << 0) | (1 << 1), // 监控两个核心的空闲任务 .trigger_panic = false, // 触发 panic 而非复位 }; esp_task_wdt_init(&twdt_config); // 创建任务并订阅 xTaskCreatePinnedToCore(task_worker, "worker", 4096, NULL, 5, NULL, 1); } ``` ## 3.2 任务内订阅与喂狗 ```c void task_worker(void *arg) { // 订阅当前任务 esp_task_wdt_add(NULL); // NULL 表示当前任务 while (1) { // 执行工作 do_some_work(); // 喂狗,告知 TWDT 本任务仍活跃 esp_task_wdt_reset(); // 适当让出 CPU,避免独占核心 vTaskDelay(pdMS_TO_TICKS(10)); } } ``` ## 3.3 处理超时事件(自定义回调) ```c void twdt_timeout_handler(void *arg) { // 打印所有任务状态,便于定位死锁 vTaskList((char *)pcTaskListBuffer); ESP_LOGE("TWDT", "Timeout! Tasks:\n%s", pcTaskListBuffer); // 可在此处尝试恢复,如强制删除死锁任务 } // 在初始化时注册回调 esp_task_wdt_isr_handler_t handler = twdt_timeout_handler; esp_task_wdt_isr_register(handler, NULL); ``` # 四、避免死锁的实战策略 ## 4.1 合理分配任务优先级和核心 - 将高实时性任务(如传感器读取)绑定到 Core 0,将通信任务(如 WiFi)绑定到 Core 1,避免竞争。 - 避免两个核心上的任务互相等待对方释放锁,使用 `xSemaphoreGive` 前检查返回值。 ## 4.2 使用互斥量而非二值信号量 - 互斥量支持优先级继承,可减少优先级反转导致的死锁。 - 示例: ```c SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); void task_a(void *arg) { while (1) { if (xSemaphoreTake(mutex, pdMS_TO_TICKS(100)) == pdTRUE) { // 临界区操作 xSemaphoreGive(mutex); } else { ESP_LOGW("TASK", "Failed to take mutex"); } vTaskDelay(pdMS_TO_TICKS(10)); } } ``` ## 4.3 避免在临界区中阻塞 - 临界区内不要调用 `vTaskDelay` 或 `vTaskSuspend`,否则会阻塞其他任务。 - 如果必须等待,使用 `xSemaphoreTake` 带超时参数。 ## 4.4 监控任务状态 - 定期调用 `vTaskList` 或 `vTaskGetRunTimeStats` 查看任务运行时间,发现异常占用。 # 五、注意事项与常见误区 - **误区 1:所有任务都订阅 TWDT**。实际上,只有关键任务需要订阅,过多订阅会增加开销。 - **误区 2:喂狗频率越高越好**。过于频繁的喂狗会掩盖任务长时间阻塞问题,建议在任务主循环末尾喂狗。 - **误区 3:忽略空闲任务监控**。空闲任务若超时,说明调度器被阻塞,这是最严重的死锁。 - **注意**:`trigger_panic` 设为 `false` 时,超时只触发中断,不会自动复位,便于调试;生产环境建议设为 `true` 以自动恢复。 - **注意**:在中断服务函数中不要调用 `esp_task_wdt_reset()`,因为中断上下文不关联任务。 # 六、总结 正确使用 TWDT 是 ESP32 多任务开发的关键技能。通过理解双核调度原理、合理配置看门狗、遵循避免死锁的编程规范,你可以显著提高系统的稳定性。当死锁发生时,TWDT 提供的任务栈回溯能快速定位问题,减少调试时间。记住:看门狗是最后防线,良好的设计才是根本。