综述:常见的 Panic & Exception 介绍

[English]

在进行代码调试前,了解常见的 Panic & Exception 也很有必要,常见的情况包括以下几种:

Watchdog Interrupt

此部分往往常见为两种情况:中断看门狗和任务看门狗,详细的文档请参考 看门狗,以下是这两个看门狗的简要说明:

中断看门狗

中断看门狗定时器的目的是,确保中断服务例程 (ISR) 运行不会受到长时间阻塞(即 IWDT 超时)。阻塞 ISR 及时运行会增加 ISR 延迟,也会阻止任务切换(因为任务切换是从 ISR 执行的)。阻止 ISR 运行的事项包括:

  • 禁用中断

  • 临界区(也会禁用中断)

  • 其他相同或更高优先级的 ISR,在完成前会阻止相同或较低优先级的 ISR

当 IWDT 超时后,默认操作是调用紧急处理程序 (Panic Handler),并显示出错原因( Interrupt wdt timeout on CPU0 或 Interrupt wdt timeout on CPU1,视情况而定)。

此时需要排查以上三点内容,尤其是要注意是否在 ISR 里放置了太多代码,导致 ISR 的时间过长导致中断看门狗触发。

任务看门狗

任务看门狗定时器用于监控 FreeRTOS 里的任务在规定的时间内是否得到调度。如果默认存在的优先级最低的 IDLE 任务没有在规定的时间内喂狗(即重置看门狗定时器),就会触发看门狗中断。这通常表示高优先级的任务因为无限循环且无延时的调用某个 API 等原因一直占用着 CPU。

此问题的调试方法简要举例如下,在 app_main 里刻意编写一个不带有延时的无限循环来打印 log :

void app_main(void)
{
    while (1) {
        printf("Hello world!----------------------------------------------------\n");
    }
}

在 ESP 上电运行 CONFIG_ESP_TASK_WDT_TIMEOUT_S 秒后,可以看到以下 log :

E (10303) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (10303) task_wdt:  - IDLE (CPU 0)
E (10303) task_wdt: Tasks currently running:
E (10303) task_wdt: CPU 0: main
E (10303) task_wdt: CPU 1: IDLE
E (10303) task_wdt: Print CPU 0 (current core) backtrace

上述 log 可简析为:

  • E (10303) task_wdt: - IDLE (CPU 0) –> CPU0 上的 IDLE0 没有及时喂狗

  • E (10303) task_wdt: CPU 0: main –> CPU0 上此时运行的任务是 main 任务(main 任务对应的即是 app_main 函数)

  • E (10303) task_wdt: CPU 1: IDLE –> CPU1 上此时运行的任务是 IDLE1 任务

此时则说明 IDLE0 任务因为在 CONFIG_ESP_TASK_WDT_TIMEOUT_S 秒内没有及时喂狗而触发了看门狗复位,然后通过 CPU0 上运行的任务为 main 任务可以推断应该是 main 任务一直在占用 CPU 导致任务看门狗复位,应重点排查此任务,添加适当的延时以让其他低优先级任务有得到调度的机会。

Brownout Interrupt

此部分为掉电中断,ESP 芯片内部集成掉电检测电路,并且会默认启用。当掉电检测器被触发时,会打印如下信息:

Brownout detector was triggered

芯片会在该打印信息结束后复位。此时应检查硬件供电电压是否满足设定的阈值。更多信息请参考 掉电 文档。

备注

在电池供电场景时,比如 2xAA 电池供电,电压为 3.1 V,此时在 Wi-Fi 建立连接等场景下会因较大的瞬时电流导致电压短时下降,触发掉电检测导致芯片重启。

此时建议使用峰值电流更大的稳压芯片。或更换能提供大电流的电池,或尝试增加电源的电容。

Assert / Abort

此部分为触发断言或中止,断言通常用于检查程序中的假设是否为真。如果某个断言失败(即假设不成立),就会触发中断。中断后,程序通常会中止(abort),并在日志中记录错误信息,以帮助开发人员调试。

因此可以查看 log 中此时哪个 API 触发了断言或中止,从这个 API 入手来调试代码,例如:

// Function to check if input is equal to 1
int check_input(int input) {
    if (input == 1) {
        return 1; // Return 1 if input is equal to 1
    } else {
        return 0; // Return 0 if input is not equal to 1
    }
}

void app_main(void)
{
    // Test case for input not equal to 1
    int input1 = 2;
    assert(check_input(input1) == 1); // Assert that the function returns 1 for input 1
}

对应的异常 log 为 :

assert failed: app_main hello_world_main.c:26 (check_input(input1) == 1)

可以看出原因是 assert(check_input(input1) == 1) 因为条件得不到满足而触发断言,此时可以重点排查此断言对应的代码。

备注

由于断言可以在 ESP-IDF menuconfig 中的 CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL 选项来禁用,故不建议在断言中做计算或进行函数调用。

Stack Overflow

此部分为栈溢出,往往实际使用的任务堆栈超出了预分配的任务堆栈时,会产生此报错,错误示例代码如下:

static void test_task(void *pvParameters)
{
    uint32_t large_array[4096] = {0};
    while (1) {
        // This task obstruct a setting tx_done_sem semaphore in the UART interrupt.
        // It leads to waiting the ticks_to_wait time in uart_wait_tx_done() function.
        vTaskDelay(200 / portTICK_PERIOD_MS);
    }
    vTaskDelete(NULL);
}


void app_main(void)
{
    xTaskCreate(test_task, "test_task", 1024, NULL,  5, NULL);
}

对应的错误 log 如下:

***ERROR*** A stack overflow in task test_task has been detected.

从 log 里可以看到是 test_task 任务发生了栈溢出,检查代码可以得知在 xTaskCreate 时仅仅分配了 1024 字节的任务堆栈,但是任务里却在使用一个大小为 4096 字节的数组,因此造成了堆栈溢出,此时需要减少任务函数里的数组大小,或者增加 xTaskCreate 分配的任务堆栈。

Cache Access Error

此部分为缓存访问错误,目前可以先参考 Cache disabled but cached memory region accessed 文档,在 利用 Guru Meditation 错误打印定位问题 中会进行对此错误进行更详细的讲解。

Invalid Memory / Instruction Address

当程序试图访问不存在的内存地址或者执行无效的指令时,会触发此类异常。这通常是由于指针错误或者内存损坏引起的。出现此类错误时,可以参考 使用 Backtrace & Coredump 定位问题定位内存问题 (踩内存、内存泄漏等) 章节来进一步代码调试。