您现在的位置是:首页 >技术教程 >Peripheral Drivers in ESP-IDF(3)——WatchDog Timer网站首页技术教程

Peripheral Drivers in ESP-IDF(3)——WatchDog Timer

Zheyuan Zou 2025-09-16 00:01:03
简介Peripheral Drivers in ESP-IDF(3)——WatchDog Timer

0.briefly speaking

本篇文章准备对ESP-IDF中的看门狗计时器(WatchDog Timer,c WDT)的用法进行研究与分析,主要涉及到的参考资料与源代码文件如下:

0.1 一些必要的背景

看门狗定时器负责在芯片整个工作期间内监视系统和软件的运行状态,一旦系统陷入长期无响应的状态而使看门狗定时器中的计时值超过了设定的上限时,看门狗定时器就可以触发特定的动作,使芯片陷入中断或复位状态,帮助芯片错误状态中恢复。

ESP-IDF中包含4个看门狗定时器,三个数字看门狗和一个模拟看门狗,它们各自可以触发的超时动作是有差异的,下图中列出了主系统看门狗(MWDT)RTC-看门狗(RWDT)各自可以触发的超时动作,以及各个超时动作可以复位的芯片组件范围,可以看到RWDT相比于MWDT的复位能力更强,可以支持System Reset级别的复位。这意味可以将包含低功耗系统在内的整个数字系统全部复位。所以请注意,主看门狗定时器不可以复位低功耗系统,其中寄存器的值在复位之后还是会被保存下来。

在这里插入图片描述
ESP-IDF中封装了上述两个硬件看门狗定时器,并在默认情况下将MWDT0用作任务看门狗定时器(监视FreeRTOS中注册的任务是否超时),MWDT1作为中断看门狗定时器(监视中断处理函数ISR是否超时),下面我们分门别类地对这两种WDT进行介绍。

A 任务看门狗

A.1 任务看门狗使用示例

以下代码节选于examples/system/task_watchdog/main/task_watchdog_example_main.c,它展示了如何在任务中注册和使用一个任务看门狗来保护任务执行时间不至于过长,这个示例分为以下几段:

A.1.1 app_main

这段代码主要作用是使用指定的配置项初始化一个任务看门狗硬件(esp_task_wdt_init),并启动要执行的主任务task_func,task_func中将会完成更多具体配置任务看门狗的动作,见下一小节。

void app_main(void)
{
// 如果任务看门狗在启动时没有初始化,则在使用时初始化
#if !CONFIG_ESP_TASK_WDT_INIT
    // If the TWDT was not initialized automatically on startup, manually intialize it now
    // 指定任务看门狗的配置
    esp_task_wdt_config_t twdt_config = {
        .timeout_ms = TWDT_TIMEOUT_MS,
        .idle_core_mask = (1 << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1,    // Bitmask of all cores
        .trigger_panic = false,
    };
	
	// 使用指定配置初始化看门狗
    ESP_ERROR_CHECK(esp_task_wdt_init(&twdt_config));
    printf("TWDT initialized
");
#endif // CONFIG_ESP_TASK_WDT_INIT

    // 将全局的循环标志置为1,此后task_func将在任务中执行循环
    run_loop = true;
    
    // 将task_func绑定在核0上运行,此后task_func将开始执行
    // task_func的实现见A.1.2节
    xTaskCreatePinnedToCore(task_func, "task", 2048, xTaskGetCurrentTaskHandle(), 10, NULL, 0);

    // 执行一段时间的task_func函数
    printf("Delay for %d seconds
", MAIN_DELAY_MS/1000);
    vTaskDelay(pdMS_TO_TICKS(MAIN_DELAY_MS));

    // 停止执行task_func中的循环
    run_loop = false;
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

#if !CONFIG_ESP_TASK_WDT_INIT
    // 释放任务看门狗
    ESP_ERROR_CHECK(esp_task_wdt_deinit());
    printf("TWDT deinitialized
");
#endif // CONFIG_ESP_TASK_WDT_INIT
    printf("Example complete
");
}

A.1.2 task_func

这是app_main中注册的任务主函数,此函数的实现如下,它向看门狗注册了3个任务(esp_task_wdt_add,esp_task_wdt_add_user),并在一个循环中周期性喂狗(esp_task_wdt_reset, esp_task_wdt_reset_user),以此避免看门狗被触发

void task_func(void *arg)
{
    // Subscribe this task to TWDT, then check if it is subscribed
    // 将此任务(task_func)注册到任务看门狗中,并检测其是否注册成功
    ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
    ESP_ERROR_CHECK(esp_task_wdt_status(NULL));

    // Subscribe func_a and func_b as users of the the TWDT
    // 将func_a和func_b注册为任务看门狗的用户
    ESP_ERROR_CHECK(esp_task_wdt_add_user("func_a", &func_a_twdt_user_hdl));
    ESP_ERROR_CHECK(esp_task_wdt_add_user("func_b", &func_b_twdt_user_hdl));

    printf("Subscribed to TWDT
");
	
	// 开始执行循环,run_loop是一个全局变量,直到app_main将其置为false时退出
    while (run_loop) {
        // Reset the task and each user periodically
        /*
        Note: Comment out any one of the calls below to trigger the TWDT
        */
        // 周期性喂狗,保证看门狗不被触发
        // 为当前任务喂狗
        esp_task_wdt_reset();
        // 为任务a和b喂狗
        func_a();	// i.e esp_task_wdt_reset_user(func_a_twdt_user_hdl);
        func_b();	// i.e esp_task_wdt_reset_user(func_b_twdt_user_hdl);
		
		// 延时2S进入下一次循环
        vTaskDelay(pdMS_TO_TICKS(TASK_RESET_PERIOD_MS));
    }

    // Unsubscribe this task, func_a, and func_b
    // 解除以上三个任务的注册
    ESP_ERROR_CHECK(esp_task_wdt_delete_user(func_a_twdt_user_hdl));
    ESP_ERROR_CHECK(esp_task_wdt_delete_user(func_b_twdt_user_hdl));
    ESP_ERROR_CHECK(esp_task_wdt_delete(NULL));

    printf("Unsubscribed from TWDT
");

    // Notify main task of deletion
    xTaskNotifyGive((TaskHandle_t)arg);
    vTaskDelete(NULL);
}

A.1.3 小结

综合这个使用示例,可以看出使用一个任务看门狗的基本步骤如下:

// 初始化一个看门狗定时器
esp_task_wdt_init(const esp_task_wdt_config_t*);
// 注册一个任务到看门狗中
esp_task_wdt_add(TaskHandle_t)/esp_task_wdt_add_user(const char*, esp_task_wdt_user_handle_t*);
// 任务定期喂狗,防止看门狗被触发
esp_task_wdt_reset/esp_task_wdt_reset_user();
// 关闭并回收任务看门狗
esp_task_wdt_delete(TaskHandle_t)/esp_task_wdt_delete_user(esp_task_wdt_user_handle_t);

接下来将会深入研究一下这些函数的实现细节,以下是相关目录:

A.2 看门狗的初始化

A.2.1 看门狗的用户配置

IDF中使用esp_task_wdt_config_t结构体抽象出了一个任务看门狗的用户可指定配置,它的定义如下:

typedef struct {
	// 超时时间(ms计)
    uint32_t timeout_ms;        
    // 标记哪些核心的空闲任务应在初始化时被TWDT监视
    uint32_t idle_core_mask;
    // 超时发生时是否触发CPU panic    
    bool trigger_panic;        
} esp_task_wdt_config_t;

A.2.2 esp_task_wdt_init——看门狗的初始化

首先,按照惯例,给出函数esp_task_wdt_init的函数调用层次关系图

/* esp_task_wdt_init的函数调用关系图 */
esp_task_wdt_init(const esp_task_wdt_config_t*);
	// 分配一个看门狗定时器硬件实例
	esp_task_wdt_impl_timer_allocate(const esp_task_wdt_config_t*, twdt_isr_callback, twdt_ctx_t*);
	// 根据传入的掩码为指定的CPU核心注册空闲任务
	subscribe_idle(uint32_t);
	// 喂狗,并重新开启看门狗计时
	esp_task_wdt_impl_timer_restart(twdt_ctx_t);

接下来是esp_task_wdt_init的完整实现代码和详细注解,它完成了以下几件事:

  • 完成硬件的初始化与超时动作配置,以及ISR的注册
  • 为指定的CPU核心注册空闲任务
  • 根据当前是否正在监视任务,开启看门狗定时器
esp_err_t esp_task_wdt_init(const esp_task_wdt_config_t *config)
{
	// 入口参数合法性检查
    ESP_RETURN_ON_FALSE((config != NULL && config->idle_core_mask < (1 << CONFIG_FREERTOS_NUMBER_OF_CORES)), ESP_ERR_INVALID_ARG, TAG, "Invalid arguments");
    ESP_RETURN_ON_FALSE(p_twdt_obj == NULL, ESP_ERR_INVALID_STATE, TAG, "TWDT already initialized");
    esp_err_t ret = ESP_OK;
    twdt_obj_t *obj = NULL;

    /* Allocate and initialize the global object */
    // 分配一个twdt_obj_t对象,它的定义如下:
    // Structure used to hold run time configuration of the TWDT
	// typedef struct twdt_obj twdt_obj_t;
	// struct twdt_obj {
    //		twdt_ctx_t impl_ctx;									/* 任务看门狗的实现上下文,因实现而异*/
    //		SLIST_HEAD(entry_list_head, twdt_entry) entries_slist;	/* entry的链表头,每一个entry都是一个需要被监视的对象 */
    //		uint32_t idle_core_mask;    							/* 和用户传入配置中的idle_core_mask含义一致 */
    //		bool panic; 											/* 和用户传入配置中的trigger_panic含义一致 */
    //		bool waiting_for_task; 									/* 是否在添加任务时立即开启定时器的标志 */
	// };
    obj = calloc(1, sizeof(twdt_obj_t));
    ESP_GOTO_ON_FALSE((obj != NULL), ESP_ERR_NO_MEM, err, TAG, "insufficient memory");
	
	// 将链表头置为空,并将panic标志传入obj
    SLIST_INIT(&obj->entries_slist);
    obj->panic = config->trigger_panic;

    /* Allocate the timer itself, NOT STARTED */
    // 分配一个定时器,传入的task_wdt_isr是将要注册的中断处理程序
    ret = esp_task_wdt_impl_timer_allocate(config, task_wdt_isr, &obj->impl_ctx);
    if (ret != ESP_OK) {
        goto err;
    }

    /* No error so far, we can assign it to the driver object */
    // p_twdt_obj是一个全局的指针,指向分配并初始化好的任务看门狗实例
    p_twdt_obj = obj;

    /* Update which core's idle tasks are subscribed */
    // 将配置的idle_core_mask记录在p_twdt_obj中,并注册空闲任务
    // subscribe_idle中调用了esp_task_wdt_add/esp_task_wdt_add_user函数来完成任务注册
    p_twdt_obj->idle_core_mask = config->idle_core_mask;
    if (config->idle_core_mask) {
        /* Subscribe the new cores idle tasks */
        subscribe_idle(config->idle_core_mask);
    }

    /* Start the timer only if we are watching some tasks */
    // 检查p_twdt_obj->entries_slist是否为空
    // 不为空则说明当前有任务在被监视,立即开启中断看门狗
    if (!SLIST_EMPTY(&p_twdt_obj->entries_slist)) {
        p_twdt_obj->waiting_for_task = false;
        // 此函数不仅开启了定时器,还顺带完成了一次喂狗动作
        esp_task_wdt_impl_timer_restart(p_twdt_obj->impl_ctx);
    } else {
        p_twdt_obj->waiting_for_task = true;
    }

    return ESP_OK;
err:
    free(obj);
    return ret;
}

A.2.2.1 esp_task_wdt_impl_timer_allocate——分配一个任务看门狗定时器

IDF中以esp_task_wdt_impl_开头的函数意味着有着多种实现方案,对于看门狗定时器来说,有着基于timergroup的实现方案,也有着基于esp_timer的实现方案。由于大部分ESP32芯片采用的都是基于timergroup的看门狗实现方案,因此这里以timergroup为例来分析对应的实现代码。在Digression 1中更详细地介绍了IDF中的这种条件编译方法。

此函数用于分配一个看门狗定时器作为任务看门狗,这个函数中主要完成了以下两件事情:

  • 开启timergroup对应的总线时钟,并为其分配中断
  • 初始化timergroup中的看门狗定时器(wdt_hal_init),并按需配置任务看门狗的超时动作(中断+内核复位)

以下是完整的代码实现和注释:

esp_err_t esp_task_wdt_impl_timer_allocate(const esp_task_wdt_config_t *config,
                                           twdt_isr_callback callback,
                                           twdt_ctx_t *obj)
{
    esp_err_t ret = ESP_OK;
    // init_context是一个全局静态变量,专门存储在初始化过程中需要的中间变量配置
    // typedef struct {
    // 		wdt_hal_context_t hal;			/* 看门狗定时器的硬件实例 */
    //		intr_handle_t intr_handle;		/* 中断信息 */
	//	} twdt_ctx_hard_t;
    twdt_ctx_hard_t *ctx = &init_context;
	
	// 入口参数合法性检查
    if (config == NULL || obj == NULL) {
        ret = ESP_ERR_INVALID_STATE;
    }
	
	// 为看门狗分配中断,并注册中断处理函数
	// 注册完毕的中断信息会保留在ctx->intr_handle中
	// 此函数详解请见Peripheral Drivers in ESP-IDF(1)——GPIO
    if (ret == ESP_OK) {
        esp_intr_alloc(TWDT_INTR_SOURCE, 0, callback, NULL, &ctx->intr_handle);
    }

    if (ret == ESP_OK) {
        // enable bus clock for the timer group registers
        // 使能timergroup的时钟信号
        // 关于PERIPH_RCC_ACQUIRE_ATOMIC如何保证访问的原子性
        // 请见Peripheral Drivers in ESP-IDF(2)——GP Timer
        PERIPH_RCC_ACQUIRE_ATOMIC(TWDT_PERIPH_MODULE, ref_count) {
            if (ref_count == 0) {
                timer_ll_enable_bus_clock(TWDT_TIMER_GROUP, true);
                timer_ll_reset_register(TWDT_TIMER_GROUP);
            }
        }
		
		/* 以下是一些具体的看门狗硬件初始化过程 */
	
		// 任务看门狗初始化函数,此函数默认情况下调用的是ROM中的函数
		// IDF中可参考components/hal/wdt_hal_iram.c中的实现代码
		// 请注意:
		// #define TWDT_INSTANCE WDT_MWDT0,即默认情况下任务看门狗用MWDT0来实现
        wdt_hal_init(&ctx->hal, TWDT_INSTANCE, TWDT_PRESCALER, true);
		
		// 关闭看门狗的写保护
        wdt_hal_write_protect_disable(&ctx->hal);
		
        // Configure 1st stage timeout and behavior
        // 配置第一阶段的超时时长和动作,超时触发中断
        // config->timeout_ms * (1000 / TWDT_TICKS_PER_US)计算出来的是以看门狗时钟周期数计算的timeout时长
        wdt_hal_config_stage(	&ctx->hal, 
        						WDT_STAGE0, 
        						config->timeout_ms * (1000 / TWDT_TICKS_PER_US), 
        						WDT_STAGE_ACTION_INT);
        						
        // 配置第二阶段的超时时长和动作,超时触发内核复位
        wdt_hal_config_stage(	&ctx->hal, 
        						WDT_STAGE1, 
        						config->timeout_ms * (2 * 1000 / TWDT_TICKS_PER_US), 
        						WDT_STAGE_ACTION_RESET_SYSTEM);
        						
        // No need to enable to enable the WDT here, it will be enabled with `esp_task_wdt_impl_timer_restart`
        // <! 注意在这里不需要立即开启看门狗定时器>
        // 再次开启写保护
        wdt_hal_write_protect_enable(&ctx->hal);

        /* Return the implementation context to the caller */
        // 将初始化的上下文传入obj->impl_ctx
        *obj = (twdt_ctx_t) ctx;

#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP && SOC_TIMER_SUPPORT_SLEEP_RETENTION
        esp_task_wdt_retention_enable(TWDT_TIMER_GROUP);
#endif
    }
    return ret;
}

A.2.2.2 subscribe_idle——注册CPU核心的空闲任务

这个函数接收用户传入的core_mask,为需要的CPU注册空闲任务及其钩子函数,其中核心是esp_task_wdt_add/esp_task_wdt_add_user这两个函数,它们将空闲任务注册到看门狗中,通过监视空闲任务,进而保证了对其他正在运行任务长时间没有退出的情况的监视,这两个函数将会在A.3 注册一个任务到看门狗中进行更详细的介绍。

空闲任务是FreeRTOS中的一种特殊任务,它的优先级很低,因此任何其他任务都会抢占属于空闲任务的执行机会,因此监视空闲任务没有被调用的时间本质上就是在监视其他任务的连续占用CPU的时间。空闲任务在执行循环中会调用钩子函数,而在空闲任务钩子(hook)中会喂狗,这保证了正常情况下任务看门狗不会被误触发

static void subscribe_idle(uint32_t core_mask)
{
    int core_num = 0;
    // 通过循环方式来轮询传入的掩码
    while (core_mask != 0) {
    	// 如果当前核心需要注册空闲任务
        if (core_mask & 0x1) {
    
// 如果当前使用的是多核版本的FreeRTOS
#if CONFIG_FREERTOS_SMP
			// 填充core_user_names数组
            snprintf(core_user_names[core_num], CORE_USER_NAME_LEN, "CPU %d", (uint8_t)core_num);
			
			// 使用esp_task_wdt_add_user函数添加当前核心的空闲任务的entry
            ESP_ERROR_CHECK(esp_task_wdt_add_user((const char *)core_user_names[core_num], 
            				&core_user_handles[core_num]));
            				
            // 注册空闲任务的钩子函数idle_hook_cb,在这个钩子函数中会喂狗,以防任务超时
            // 关于空闲任务及其钩子函数的介绍,可参考:
            // https://www.cnblogs.com/Liu-Jing/p/7105182.html
            ESP_ERROR_CHECK(esp_register_freertos_idle_hook_for_cpu(idle_hook_cb, core_num));
#else // CONFIG_FREERTOS_SMP

			// 单核情况下直接获取空闲任务的句柄
            TaskHandle_t idle_task_handle = xTaskGetIdleTaskHandleForCore(core_num);
            assert(idle_task_handle);
            
			// 调用esp_task_wdt_add完成空闲任务的注册
            ESP_ERROR_CHECK(esp_task_wdt_add(idle_task_handle));
            ESP_ERROR_CHECK(esp_register_freertos_idle_hook_for_cpu(idle_hook_cb, core_num));
#endif // CONFIG_FREERTOS_SMP
        }
        // 处理下一个核心
        core_mask >>= 1;
        core_num++;
    }
}

下面是空闲任务钩子函数idle_hook_cb的实现,可以看到它的本质就是调用esp_task_wdt_reset/esp_task_wdt_reset_user函数完成喂狗动作,防止了看门狗定时器超时,在喂狗一节将会更深入地介绍这两个函数。

static bool idle_hook_cb(void)
{
#if CONFIG_FREERTOS_SMP
	// 多核情况下使用esp_task_wdt_reset_user函数喂狗
    esp_task_wdt_reset_user(core_user_handles[xPortGetCoreID()]);
#else // CONFIG_FREERTOS_SMP
	// 单核情况下使用esp_task_wdt_reset函数喂狗
    esp_task_wdt_reset();
#endif // CONFIG_FREERTOS_SMP
    return true;
}

A.2.2.3 esp_task_wdt_impl_timer_restart——开启看门狗定时器

esp_task_wdt_impl_timer_restart也是一个因实现而异的函数,这里首先以timergroup中的实现方案为例进行介绍,这个函数主要是一些写寄存器的HAL层动作:即喂狗+开启计时

esp_err_t esp_task_wdt_impl_timer_restart(twdt_ctx_t obj)
{
    esp_err_t ret = ESP_OK;
    twdt_ctx_hard_t* ctx = (twdt_ctx_hard_t*) obj;

    if (ctx == NULL) {
        ret = ESP_ERR_INVALID_STATE;
    }

    if (ret == ESP_OK) {
        wdt_hal_write_protect_disable(&ctx->hal);
        wdt_hal_enable(&ctx->hal);
        wdt_hal_feed(&ctx->hal);
        wdt_hal_write_protect_enable(&ctx->hal);
    }

    return ret;
}

Digression 1:ESP-IDF中的任务看门狗的多重软件实现(条件编译)


值得展开一说的是,IDF中为任务看门狗实现了两套方案,分别位于task_wdt/task_wdt_impl_timergroup.ctask_wdt/task_wdt_impl_esp_timer.c。顾名思义,任务看门狗定时器可以借助于不同的底层硬件来实现,可以是timergroup中真实的硬件看门狗定时器,也可以是esp_timer这种“软定时器”(事实上,esp_timer也是有多种基于硬件的实现方案的,这意味着任务看门狗最终也可以基于多种不同的硬件单元来实现)。

在menuconfig中,用户可在esp_system目录下对任务看门狗的实现方案进行配置,这一点也可以从esp_system的Kconfig配置文件中看到。当前芯片目标是ESP32-C2时,此选项为yes,其他芯片目标均为no。

# defined in components/esp_system/Kconfig
config ESP_TASK_WDT_USE_ESP_TIMER
    # Software implementation of Task Watchdog, handy for targets with only a single
    # Timer Group, such as the ESP32-C2
    bool
    depends on ESP_TASK_WDT_EN
    default y if IDF_TARGET_ESP32C2
    default n if !IDF_TARGET_ESP32C2
    select ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD

可以看到,除了ESP32-C2默认使用ESP-Timer作为任务看门狗的底层软件实现之外,其他芯片目标应当均以timergroup作为硬件基础来实现任务看门狗。此选项一旦被勾选,CONFIG_ESP_TASK_WDT_USE_ESP_TIMER这个宏就会被启用,进而会影响到CMake中添加的源代码文件,如下CMake构建规则所示,这是条件编译的一种具体实施方案

# defined in components/esp_hw_support/CMakeLists.txt
if(CONFIG_ESP_TASK_WDT_EN)
    list(APPEND srcs "task_wdt/task_wdt.c")
	# 根据配置的不同,添加不同的源代码文件进入构建系统
    if(CONFIG_ESP_TASK_WDT_USE_ESP_TIMER)
        list(APPEND srcs "task_wdt/task_wdt_impl_esp_timer.c")
	# 除了ESP32-C2,大部分芯片都用的是基于timergroup的任务看门狗实现方案
    else()
        list(APPEND srcs "task_wdt/task_wdt_impl_timergroup.c")
    endif()
endif()


A.3 注册一个任务到看门狗中

在上述的代码中,已经不止一次地(应用程序中、subscribe_idle函数中)看到使用esp_task_wdt_addesp_task_wdt_add_user函数来向任务看门狗注册需要监视的任务,这一小节对这两个函数的异同展开分析。

A.3.1 esp_task_wdt_add——添加一个任务entry

在上述代码中,subscribe_idle函数使用esp_task_wdt_add在单核版本下注册了空闲任务的句柄,这个函数的主要功能还是通过调用add_entry函数来完成的,此函数的实现如下:

esp_err_t esp_task_wdt_add(TaskHandle_t task_handle)
{
    ESP_RETURN_ON_FALSE(p_twdt_obj != NULL, ESP_ERR_INVALID_STATE, TAG, "TWDT was never initialized");
    esp_err_t ret;
	
	// 如果传入的任务句柄为空,则获取当前任务句柄并准备注册
    if (task_handle == NULL) {   // Get handle of current task if none is provided
        task_handle = xTaskGetCurrentTaskHandle();
    }	
	
	// 调用add_entry完成任务到看门狗的添加,twdt_entry_t结构体的定义如下
	// struct twdt_entry {
    // SLIST_ENTRY(twdt_entry) slist_entry;		// 一个链表节点,里面隐藏着一个指向下一节点的指针
    // 	TaskHandle_t task_handle;   			// 如果是用户entry,则此项为空
    // 	const char *user_name;      			// 如果是任务entry,则此项为空
    // 	bool has_reset;							// 是否完成软喂狗的标志
	// };
    twdt_entry_t *entry;
    ret = add_entry(true, (void *)task_handle, &entry);
	
	// 防编译报错,这里假装引用一下entry变量
    (void) entry; // Returned entry pointer not used
    return ret;
}

A.3.2 add_entry——注册一个entry到看门狗中(*)


这个函数是完成任务注册到看门狗的精髓,IDF中将所有需要被看门狗监视的任务串成了一个链表,链表的头部位于p_twdt_obj->entries_slist中,在这个链表中串接了两种类型的节点,一种称为用户entry,另一种称为任务entry。其实它们本质是一样的,只是表面上监视的对象不一样而已,任务entry监视的对象是FreeRTOS的任务句柄TaskHandle_t,而用户entry监视的对象是用户名字符串,但无论如何,它们对应的entry都不可以超时。

在这里插入图片描述

add_entry函数除了会将新的entry头插到链表之外,还会检查当前监视的所有任务的状态,如果发现它们都已经正常reset了,则会执行一次硬件喂狗动作。事实上,由于一个看门狗可能要同时监视很多任务,所以单个任务想要喂狗时,事实上它们只需要将twdt_entry对应的has_reset标志置位即可,而当所有任务的has_reset标志都已经置位时,才会真正在硬件上触发喂狗(task_wdt_timer_feed)

在下面的文章中,用软喂狗表示置位entry中的has_reset标志,而将真正的硬件喂狗动作称之为硬喂狗…

static esp_err_t add_entry(bool is_task, void *entry_data, twdt_entry_t **entry_ret)
{
    esp_err_t ret;

    // 分配一个entry的存储空间
    twdt_entry_t *entry = calloc(1, sizeof(twdt_entry_t));
    if (entry == NULL) {
        return ESP_ERR_NO_MEM;
    }
	
	// 第一个入口参数,标明了是任务entry还是用户entry
	// 任务entry传入的是任务句柄,用户entry传入的是用户名
	// 将其设置到entry的相应字段中
    if (is_task) {
        entry->task_handle = (TaskHandle_t)entry_data;
    } else {
        entry->user_name = (const char *)entry_data;
    }

    portENTER_CRITICAL(&spinlock);
    // 检查当前p_twdt_obj是否不为空
    // 回忆:esp_task_wdt_init会初始化p_twdt_obj
    ESP_GOTO_ON_FALSE_ISR((p_twdt_obj != NULL), ESP_ERR_INVALID_STATE, state_err, TAG, "task watchdog was never initialized");
    // Check if the task is an entry, and if all entries have been reset
    bool all_reset;
    if (is_task) {
        twdt_entry_t *entry_found = find_entry_from_task_handle_and_check_all_reset(entry->task_handle, &all_reset);
        ESP_GOTO_ON_FALSE_ISR((entry_found == NULL), ESP_ERR_INVALID_ARG, state_err, TAG, "task is already subscribed");
    } else {
    	// find_entry_and_check_all_reset函数会遍历entry链表,检查两件事:
    	// 1.是否当前entry已经被注册过(通过返回值传回)
    	// 2.是否所有的entry都被reset过了(通过入口参数传回)
        bool entry_found = find_entry_and_check_all_reset(entry, &all_reset);
        ESP_GOTO_ON_FALSE_ISR(!entry_found, ESP_ERR_INVALID_ARG, state_err, TAG, "user is already subscribed");
    }
    
    // Add entry to list
    // 将entry头插到p_twdt_obj所管理的链表中去
    SLIST_INSERT_HEAD(&p_twdt_obj->entries_slist, entry, slist_entry);
    
    // Start the timer if it has not been started yet and was waiting on a task to registered
    // 如果当前看门狗还在等待任务到来,而没有开始计时
    // 则立即开始计时,因为本函数已经添加了一个新的任务
    if (p_twdt_obj->waiting_for_task) {
        esp_task_wdt_impl_timer_restart(p_twdt_obj->impl_ctx);
        p_twdt_obj->waiting_for_task = false;
    }
	
	// 如果所有任务都已经reset,则在此处喂一次狗
	// <! 添加任务入口时,若检测到其他任务均已正常退出,则会执行一次喂狗 >
    if (all_reset) {   //Reset hardware timer if all other tasks in list have reset in
        task_wdt_timer_feed();
    }
	
	// 填充返回参数
    portEXIT_CRITICAL(&spinlock);
    *entry_ret = entry;
    return ESP_OK;

state_err:
    portEXIT_CRITICAL(&spinlock);
    free(entry);
    return ret;
}

A.3.3 esp_task_wdt_add_user——添加一个用户entry

在详细解读完add_entry函数之后,esp_task_wdt_add_user完成的功能就非常好理解了,它和esp_task_wdt_add最大的区别就是传入add_entry的参数不同,它传入add_entry的第一个参数is_task为false,表明这是一个用户entry,而非任务entry。

esp_err_t esp_task_wdt_add_user(const char *user_name, esp_task_wdt_user_handle_t *user_handle_ret)
{
    ESP_RETURN_ON_FALSE((user_name != NULL && user_handle_ret != NULL), ESP_ERR_INVALID_ARG, TAG, "Invalid arguments");
    ESP_RETURN_ON_FALSE(p_twdt_obj != NULL, ESP_ERR_INVALID_STATE, TAG, "TWDT was never initialized");
    esp_err_t ret;
    twdt_entry_t *entry;
	// 添加一个用户entry,注意is_task标志是false
    ret = add_entry(false, (void *)user_name, &entry);
    if (ret == ESP_OK) {
        *user_handle_ret = (esp_task_wdt_user_handle_t)entry;
    }
    return ret;
}

点此跳回子目录

A.4 喂狗

在进入这一节之前,请保留一个印象,IDF的喂狗动作是分为软喂狗和硬喂狗两个阶段的,详情请参考上面

A.4.1 esp_task_wdt_reset——针对任务entry的喂狗动作

在上述的应用程序以及空闲任务钩子函数中,均有调用esp_task_wdt_reset的动作,这个函数就是用来完成喂狗动作的。具体来说,它首先执行软喂狗动作,即将当前任务对应的entry中的has_reset置位,并同时检查其他entry是否也已经reset,若是,则进一步触发硬喂狗动作,这是通过调用task_wdt_timer_feed函数实现的。

esp_err_t esp_task_wdt_reset(void)
{
    ESP_RETURN_ON_FALSE(p_twdt_obj != NULL, ESP_ERR_INVALID_STATE, TAG, "TWDT was never initialized");
    esp_err_t ret;
    // 获取当前任务句柄
    TaskHandle_t handle = xTaskGetCurrentTaskHandle();

    portENTER_CRITICAL(&spinlock);
    // Find entry from task handle
    bool all_reset;
    twdt_entry_t *entry;
	
	// 和上面一样,扫描entry链表找到对应的entry,并同时检查是否所有其他entry已经被reset
    entry = find_entry_from_task_handle_and_check_all_reset(handle, &all_reset);
    ESP_GOTO_ON_FALSE_ISR((entry != NULL), ESP_ERR_NOT_FOUND, err, TAG, "task not found");
    
    // Mark entry as reset and issue timer reset if all entries have been reset
 	// 软复位当前的entry,并在所有其他entry都reset时,触发真正的硬喂狗动作
    entry->has_reset = true;    // Reset the task if it's on the task list
    if (all_reset) {    		
    	// 如果所有entry均已reset,则执行硬喂狗动作
        task_wdt_timer_feed();
    }
    ret = ESP_OK;
err:
    portEXIT_CRITICAL(&spinlock);
    return ret;
}

A.4.2 task_wdt_timer_feed——硬喂狗,并复原所有软喂狗标记(*)

此函数在硬件上完成了喂狗动作,并将当前看门狗监视的所有entry的reset标记全部还原,自此开启了下一轮的监控

static void task_wdt_timer_feed(void)
{
	// 硬喂狗动作,这个操作将直接使硬件重新开始计数
    esp_task_wdt_impl_timer_feed(p_twdt_obj->impl_ctx);

    /* Clear the has_reset flag in each entry */
    // 将所有软喂狗标记清空,为下一轮计数做准备 
    twdt_entry_t *entry;
    SLIST_FOREACH(entry, &p_twdt_obj->entries_slist, slist_entry) {
        entry->has_reset = false;
    }
}

A.4.3 esp_task_wdt_reset_user——针对用户entry的喂狗动作

这个函数和esp_task_wdt_reset函数完全一致,只是查询链表的方式不再是FreeRTOS的任务句柄,而是具体的entry地址,这里不再展开过多解释。

esp_err_t esp_task_wdt_reset_user(esp_task_wdt_user_handle_t user_handle)
{
    ESP_RETURN_ON_FALSE(user_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "Invalid arguments");
    ESP_RETURN_ON_FALSE(p_twdt_obj != NULL, ESP_ERR_INVALID_STATE, TAG, "TWDT was never initialized");
    esp_err_t ret;

    portENTER_CRITICAL(&spinlock);
    // Check if entry exists
    bool all_reset;
    twdt_entry_t *entry = (twdt_entry_t *)user_handle;
	
	// 查找对应的entry,同时查询其他entry是否已经reset
    bool entry_found = find_entry_and_check_all_reset(entry, &all_reset);
    ESP_GOTO_ON_FALSE_ISR(entry_found, ESP_ERR_NOT_FOUND, err, TAG, "user handle not found");
    
    // Mark entry as reset and issue timer reset if all entries have been reset
    entry->has_reset = true;    // Reset the task if it's on the task list
    if (all_reset) {    
        task_wdt_timer_feed();
    }
    ret = ESP_OK;
err:
    portEXIT_CRITICAL(&spinlock);

    return ret;
}

综上所述,硬喂狗动作发生在所有被监视的任务的reset标志全部置位时,因此共有4个地方可能会触发硬复位动作:

  • 添加一个entry时(add_entry)
  • 删除一个entry时(delete_entry)
  • reset任务entry时(esp_task_wdt_reset)
  • reset用户entry时(esp_task_wdt_reset_user)

以上的4个函数均会扫描看门狗正在监视的entry列表,并在发现监视任务链表中的所有entry均被复位(has_reset标志为1)之后立即触发硬喂狗动作,并重置所有软复位标志(has_reset)到未复位状态。

点此跳回子目录

A.5 task_wdt_isr——发生超时动作时的中断处理函数

在上述的esp_task_wdt_init函数中调用了esp_task_wdt_impl_timer_allocate,其中存在将task_wdt_isr设置为中断处理函数的动作。出于篇幅上的精简,这里不再深入解释每一个子函数的细节,而只是阐述在超时事件发生时,整个中断处理函数完成了什么事情:

static void task_wdt_isr(void *arg)
{
    portENTER_CRITICAL_ISR(&spinlock);
    // 喂狗,并清空中断标记,这里防止了看门狗进入第二阶段
    esp_task_wdt_impl_timeout_triggered(p_twdt_obj->impl_ctx);

    /* Keep a bitmap of CPU cores having tasks that have not reset TWDT.
     * Bit 0 represents core 0, bit 1 represents core 1, and so on. */
    int cpus_fail = 0;
    bool panic = p_twdt_obj->panic;
	
	// 打印超时函数的信息
    if (esp_task_wdt_print_triggered_tasks(NULL, NULL, &cpus_fail) != ESP_OK) {
        // If there are no entries, there's nothing to do.
        portEXIT_CRITICAL_ISR(&spinlock);
        return;
    }
	
	// 打印超时事件发生时,每个CPU正在执行的任务
    ESP_EARLY_LOGE(TAG, "%s", DRAM_STR("Tasks currently running:"));
    for (int x = 0; x < CONFIG_FREERTOS_NUMBER_OF_CORES; x++) {
        ESP_EARLY_LOGE(TAG, "CPU %d: %s", x, pcTaskGetName(xTaskGetCurrentTaskHandleForCore(x)));
    }
    portEXIT_CRITICAL_ISR(&spinlock);

    /* Run user ISR handler.
     * This function has been declared as weak, thus, it may be possible that it was not defines.
     * to check this, we can directly test its address. In any case, the linker will get rid of
     * this `if` when linking, this means that if the function was not defined, the whole `if`
     * block will be discarded (zero runtime overhead), else only the function call will be kept.
     */
    // 调用用户自定义的中断处理子函数
    if (esp_task_wdt_isr_user_handler != NULL) {
        esp_task_wdt_isr_user_handler();
    }

    // Trigger configured timeout behavior (e.g., panic or print backtrace)
    assert(cpus_fail != 0);
    // 执行具体的超时处理动作,panic/backtrace,具体看传入看门狗的panic配置
    task_wdt_timeout_handling(cpus_fail, panic);
}

点此跳回子目录

B 中断看门狗

相比于任务看门狗,中断看门狗更加简单。上面,我们看到任务看门狗是通过监视FreeRTOS中的空闲任务(IDLE TASK)来避免任务长期占据CPU的非法情况的,空闲任务钩子函数会对任务看门狗进行喂狗。无独有偶,中断看门狗是在FreeRTOS的tick中断钩子函数中完成喂狗的,进而防止tick中断过长时间没有被触发

tick中断是FreeRTOS的“心跳”,管理着FreeRTOS中所有与时间相关的节律,例如何时应该解除任务阻塞,何时又应该执行任务切换,这些都需要参考FreeRTOS内核中的tick计数器,而tick中断就会不断增加tick计数器,为系统运行提供时间参考。IDF编程指南中明确列出了中断看门狗防范的几种非法情形:
在这里插入图片描述
对于RTOS,长时间地禁用中断,或者长期陷入一个中断处理函数不退出,都会导致任务无法调度,所以这些情况都是非法的。类似于任务看门狗的监视目标——IDLE TASK具有低调度优先级一样,IWDT监视的对象——tick中断也应该具有低优先级。这一点在ESP32系列芯片启动时就已经完成了,对应的代码片段如下所示

void vSystimerSetup(void)
{
    unsigned cpuid = xPortGetCoreID();
#ifdef CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3
    const unsigned level = ESP_INTR_FLAG_LEVEL3;
// 除非显式配置,否则tick中断的优先级为ESP_INTR_FLAG_LEVEL1(最低级别)
#else
    const unsigned level = ESP_INTR_FLAG_LEVEL1;
#endif
    /* Systimer HAL layer object */
    static systimer_hal_context_t systimer_hal;
    /* set system timer interrupt vector */
	// 在这里完成tick中断的分配,优先级为最低的1
    ESP_ERROR_CHECK(esp_intr_alloc(ETS_SYSTIMER_TARGET0_INTR_SOURCE + cpuid, ESP_INTR_FLAG_IRAM | level, SysTickIsrHandler, &systimer_hal, NULL));

// 以下代码从略
}

与任务看门狗不同的是,中断看门狗对用户是透明的,因此它的接口并不多,且均集中在components/esp_system/int_wdt.c源代码文件中,守护着系统的执行状态,下面简单对其进行解读:

B.1 esp_int_wdt_init——中断看门狗的硬件初始化

这部分代码主要用于在硬件上使能timergroup中的看门狗定时器,因此大部分动作都是与写寄存器相关的,和上面的任务看门狗初始化流程完全一致,这里不再过多展开。

void esp_int_wdt_init(void)
{

	// 原子操作:打开对应timergroup的总线时钟
    PERIPH_RCC_ACQUIRE_ATOMIC(IWDT_PERIPH, ref_count) {
        if (ref_count == 0) {
            timer_ll_enable_bus_clock(IWDT_TIMER_GROUP, true);
            timer_ll_reset_register(IWDT_TIMER_GROUP);
        }
    }
    
    // 这里也是一系列面向硬件寄存器的配置动作,和上面的任务看门狗没有区别
    // 但是当timergroup不止一个时,默认使用MWDT1作为中断看门狗
    // #define IWDT_INSTANCE WDT_MWDT1 (#if SOC_TIMER_GROUPS > 1)
    wdt_hal_init(&iwdt_context, IWDT_INSTANCE, IWDT_PRESCALER, true);
    wdt_hal_write_protect_disable(&iwdt_context);
	
	// 第一阶段超时动作
    wdt_hal_config_stage(&iwdt_context, WDT_STAGE0, IWDT_INITIAL_TIMEOUT_S * 1000000 / IWDT_TICKS_PER_US, WDT_STAGE_ACTION_INT);
    // 第二阶段超时动作
    wdt_hal_config_stage(&iwdt_context, WDT_STAGE1, IWDT_INITIAL_TIMEOUT_S * 1000000 / IWDT_TICKS_PER_US, WDT_STAGE_ACTION_RESET_SYSTEM);
	// 开启中断看门狗
    wdt_hal_enable(&iwdt_context);
    wdt_hal_write_protect_enable(&iwdt_context);

#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP && SOC_TIMER_SUPPORT_SLEEP_RETENTION
    // 低功耗相关的代码,从略
#endif

#if (CONFIG_ESP32_ECO3_CACHE_LOCK_FIX && CONFIG_BTDM_CTRL_HLI)
	// 从略
#endif // (CONFIG_ESP32_ECO3_CACHE_LOCK_FIX && CONFIG_BTDM_CTRL_HLI)
}

B.2 esp_int_wdt_cpu_init——中断看门狗CPU相关的初始化

中断看门狗中与CPU相关的初始化动作也非常简单,主要是完成了注册tick中断钩子函数打开看门狗中断两件事情。这样,tick中断就可以连续不断地完成对中断看门狗的喂狗操作,从而实现了对其他ISR的监视作用。

void esp_int_wdt_cpu_init(void)
{
    assert((CONFIG_ESP_INT_WDT_TIMEOUT_MS >= (portTICK_PERIOD_MS << 1)) && "Interrupt watchdog timeout needs to be at least twice the RTOS tick period!");
    // Register tick hook for current CPU to feed the INT WDT
    // 为当前的CPU核心注册tick中断钩子函数,此tick钩子函数用来在tick中断中喂狗
    esp_register_freertos_tick_hook_for_cpu(tick_hook, esp_cpu_get_core_id());
    /*
     * Register INT WDT interrupt for current CPU. We do this manually as the timeout interrupt should call an assembly
     * panic handler (see riscv/vector.S and xtensa_vectors.S).
     */
    // 将看门狗中断(TG1中断)路由到指定的中断号(ETS_INT_WDT_INUM),并开启中断
    // #define WDT_LEVEL_INTR_SOURCE SYS_TG1_WDT_INTR_SOURCE
    // IWDT没有指定的中断处理程序,而是会直接跳入panic_handler,并在其中完成对IWDT超时的处理
    esp_intr_disable_source(ETS_INT_WDT_INUM);
    esp_rom_route_intr_matrix(esp_cpu_get_core_id(), WDT_LEVEL_INTR_SOURCE, ETS_INT_WDT_INUM);
	
	// 设置IWDT中断的触发方式和优先级(4)
	// 这意味着更高级别的ISR可以压制IWDT的中断,进而迫使IWDT进入第二阶段超时
#if SOC_CPU_HAS_FLEXIBLE_INTC
    esp_cpu_intr_set_type(ETS_INT_WDT_INUM, INTR_TYPE_LEVEL);
    esp_cpu_intr_set_priority(ETS_INT_WDT_INUM, SOC_INTERRUPT_LEVEL_MEDIUM);
#endif
#if CONFIG_ESP32_ECO3_CACHE_LOCK_FIX
    // 从略
#endif
	
	// 开中断
    esp_intr_enable_source(ETS_INT_WDT_INUM);
}

进一步地,当IWDT发生超时事件时,ESP32系列MCU的CPU会跳入中断向量表(components/riscv/vectors_intc.S(vectors_clic.S)),如下,可以看到24号入口是预留给IWDT中断的,而它指向_panic_handler。这意味着,中断看门狗超时会进入一个panic_handler。

global _vector_table
    .type _vector_table, @function
_vector_table:
    j _panic_handler            /* 0: Exception entry */
    # ...省略一部分向量表入口
    j _interrupt_handler        /* 22: Free interrupt number */
    j _interrupt_handler        /* 23: Free interrupt number */
    j _panic_handler            /* 24: ETS_INT_WDT_INUM panic-interrupt (soc-level panic) */

我在手头的ESP32-C6上进行了中断看门狗超时尝试,现象如下,panic_handler会打印错误原因,并转储超时发生时所有的CPU寄存器值,并在最后打印backtrace,帮助设计人员确定超时发生的代码位置。
在这里插入图片描述

C 总结

本文介绍了ESP32系列MCU中看门狗定时器相关的内容,以及在ESP-IDF中对看门狗的软件抽象以及封装。具体来说,IDF中利用硬件上的两个定时器组中的看门狗定时器,分别作为了任务看门狗(MWDT0)和中断看门狗(MWDT1),并分别通过监视FreeRTOS中的两大基本机制——IDLE TASK和tick中断,实现了对非法超时情况的监视与警告。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。