Перейти к содержимому

Удаленный перехват backtrace на ESP32

Иногда уже после сборки и отладки очередного устройства, а затем и установки его “на место”, вдруг начинают проявляться самопроизвольные перезагрузки из-за паники. Это означает, что в прошивке где-то присутствует ранее незамеченная проблема. Возникает задача: определить место возникновения сбоя. Зная файл исходного кода и номер строки, можно с большой степенью вероятности предположить причину проблемы и устранить её. При локальной отладке в случае сбоя паники в COM-порт обычно печатается backtrace, который можно “расшифровать” и выяснить место точку возникновения проблемы. Как это сделать, я писал в предыдущей статье.

Core 1 register dump:
PC      : 0x400e14ed  PS      : 0x00060030  A0      : 0x800d0805  A1      : 0x3ffb5030
A2      : 0x00000000  A3      : 0x00000001  A4      : 0x00000001  A5      : 0x3ffb50dc
A6      : 0x00000000  A7      : 0x00000001  A8      : 0x00000000  A9      : 0x3ffb5000
A10     : 0x00000000  A11     : 0x3ffb2bac  A12     : 0x40082d1c  A13     : 0x06ff1ff8
A14     : 0x3ffb7078  A15     : 0x00000000  SAR     : 0x00000014  EXCCAUSE: 0x0000001d
EXCVADDR: 0x00000000  LBEG    : 0x4000c46c  LEND    : 0x4000c477  LCOUNT  : 0xffffffff

Backtrace: 0x400e14ed:0x3ffb5030 0x400d0802:0x3ffb5050

 

Осталось получить этот самый backtrace после сбоя. Если доступ к устройству имеется, можно подключиться к USB / COM-порту и проследить вывод логов. Но и при доступе к устройству так же могут быть некоторые сложности – ошибка может возникать раз в неделю или реже, в этом случае “мониторить” устройство через COM-порт становится проблематично. В PlatformIO имеется возможность сохранять отладочный вывод в COM-порт в файл, можно использовать этот способ.

Гораздо хуже, если физического доступа к устройству нет – например, если оно физически расположено в другом населенном пункте или в труднодоступном месте. В таком случае отладка возможна, только если устройство самостоятельно отправит какие-либо данные для отладки в telegram, на mqtt-брокер или куда-либо ещё. Проблема в том, что на текущий момент ни framework-espidf, ни framework-arduinoespressif32 не предусматривают сохранение backtrace куда-либо, кроме COM-порта. Придется “колхозить” перехват и сохранение backtrace самостоятельно.

 


Массив для сохранения backtrace

Под сохранение backtrace достаточно выделить сравнительно небольшой массив uint32_t чисел. Размер массива зависит от того, сколько адресов нужно там хранить. Обычно достаточно 10-16 ячеек, но в некоторых особо тяжелых случаях нужно и больше (например если перезагрузка возникает при ошибке выделения памяти, когда куча исчерпана – в этом случае в начале backtrace будут вызовы системных функций выделения памяти).

Я определил этот массив так:

#if CONFIG_RESTART_DEBUG_INFO

typedef struct {
  size_t heap_total;
  size_t heap_free;
  size_t heap_free_min;
  time_t heap_min_time;
  #if CONFIG_RESTART_DEBUG_STACK_DEPTH > 0
  uint32_t backtrace[CONFIG_RESTART_DEBUG_STACK_DEPTH];
  #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH
} re_restart_debug_t;

#endif // CONFIG_RESTART_DEBUG_INFO

С помощью макроса CONFIG_RESTART_DEBUG_INFO можно включать или отключать сохранение отладочной информации, с помощью другого макроса CONFIG_RESTART_DEBUG_STACK_DEPTH можно устанавливать размер буфера под сохранение backtrace. Кроме того, сохраняются данные о размере свободной кучи.

// EN: Preserve debugging information across device software restarts
// RU: Сохранять отладочную информацию при программном перезапуске устройства
#define CONFIG_RESTART_DEBUG_INFO 1
// EN: Allow heap information to be saved periodically, with subsequent output on restart
// RU: Разрешить периодическое сохранение информации о куче / памяти с последующем выводом при перезапуске
#define CONFIG_RESTART_DEBUG_HEAP_SIZE_SCHEDULE 1
// EN: Depth to save the processor stack on restart. 0 - do not save
// RU: Глубина сохранения стека процессора при перезапуске. 0 - не сохранять
#define CONFIG_RESTART_DEBUG_STACK_DEPTH 28

Примечание: здесь и далее все управляющие макросы определены в файле project_config.h, который есть в любом моем проекте ESP-IDF. Пример файла можно посмотреть по ссылке: github.com/kotyara12/dzen_autowatering/blob/master/include/project_config.h. Но вы можете задать их любым другим способом, например в CMakeLists.txt

 

Возникает вопрос в том, где сохранить эти данные при перезагрузке.

Первая мысль была сохранить необходимые данные во flash памяти, например в NVS-разделе. Но, увы, это не всегда прокатывает, так как не удается выполнить запись. Поэтому пришлось найти другое решение: пометил переменную, хранящую данные для отладки модификатором __NOINIT_ATTR. Это заставит компилятор поместить данный блок данных в область, которая не очищается при перезагрузке. Конечно, при выключении и включении устройства данные будут утеряны в любом случае, но в этом случае они не требуются.

#if CONFIG_RESTART_DEBUG_INFO

__NOINIT_ATTR static re_restart_debug_t _debug_info;

#endif // CONFIG_RESTART_DEBUG_INFO

 


Заполнение данных для backtrace

Как я уже упомянул выше, в ESP-IDF не предусмотрен вывод backtrace куда-либо ещё, кроме com-порта. Что ж, придется сделать это самостоятельно. Для этого я воспользовался функцией esp_backtrace_print из файла \.platformio\packages\framework-espidf\components\xtensa\debug_helpers.c, и написал похожий вариант:

#if CONFIG_RESTART_DEBUG_INFO && (CONFIG_RESTART_DEBUG_STACK_DEPTH > 0)
#include "esp_types.h"
#include "esp_attr.h"
#include "esp_err.h"
#include "esp_debug_helpers.h"
#include "soc/soc_memory_layout.h"
#include "soc/cpu.h" 
#endif // CONFIG_RESTART_STACK_DEPTH

#if CONFIG_RESTART_DEBUG_INFO

void IRAM_ATTR debugHeapUpdate()
{
  _debug_info.heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
  _debug_info.heap_free = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
  size_t _new_free_min = heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT);
  if ((_debug_info.heap_free_min == 0) || (_new_free_min < _debug_info.heap_free_min)) {
    _debug_info.heap_free_min = _new_free_min;
    _debug_info.heap_min_time = time(nullptr);
  };
}

#if CONFIG_RESTART_DEBUG_STACK_DEPTH > 0

void IRAM_ATTR debugBacktraceUpdate()
{
  esp_backtrace_frame_t stk_frame;
  esp_backtrace_get_start(&(stk_frame.pc), &(stk_frame.sp), &(stk_frame.next_pc)); 
  _debug_info.backtrace[0] = esp_cpu_process_stack_pc(stk_frame.pc);

  bool corrupted = (esp_stack_ptr_is_sane(stk_frame.sp) &&
                    esp_ptr_executable((void*)esp_cpu_process_stack_pc(stk_frame.pc))) ?
                    false : true; 

  #if CONFIG_RESTART_DEBUG_STACK_DEPTH > 1
    uint8_t i = CONFIG_RESTART_DEBUG_STACK_DEPTH;
    while (i-- > 0 && stk_frame.next_pc != 0 && !corrupted) {
      if (!esp_backtrace_get_next_frame(&stk_frame)) {
        corrupted = true;
      };
      _debug_info.backtrace[CONFIG_RESTART_DEBUG_STACK_DEPTH - i] = esp_cpu_process_stack_pc(stk_frame.pc);
    };
  #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH > 1
}

#endif // CONFIG_RESTART_DEBUG_STACK_DEPTH

void IRAM_ATTR debugUpdate()
{
  debugHeapUpdate();
  #if CONFIG_RESTART_DEBUG_STACK_DEPTH > 0
  debugBacktraceUpdate();
  #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH
}

#endif // CONFIG_RESTART_DEBUG_INFO

Для сохранения отладочной информации теперь достаточно вызывать debugUpdate(). При сохранении данных stk_frame.sp не сохраняется, так как это особо не влияет на расшифровку адресов. Осталось обеспечить вызов этой функции перед перезагрузкой и в случае паники.

 


Сохранение backtrace перед перезагрузкой устройства

Реализовать сохранение данных при “штатной” перезагрузке устройства командой esp_restart очень просто. Для этого достаточно зарегистрировать обработчик “штатной” перезагрузки. Всего можно использовать от 1 до 5 обработчиков, и это число, увы, изменить нельзя.

#if CONFIG_RESTART_DEBUG_INFO

esp_register_shutdown_handler(debugUpdate);

#endif // CONFIG_RESTART_DEBUG_INFO

Это обеспечит сохранение отладочной информации в зарезервированной области памяти при перезагрузке из-за обновления OTA или в других “нормальных” ситуациях.

Но, увы, при “панике” из-за ошибке метод не сработает. Решить эту задачу для ESP-IDF можно, создав wrapper для системной функции esp_panic_handler с помощью опции -Wl. Для этого создаем обертку для esp_panic_handler:

// This function will be considered the esp_panic_handler to call in case a panic occurs
void __wrap_esp_panic_handler(void* info) 
{
  #if CONFIG_RESTART_DEBUG_INFO && (CONFIG_RESTART_DEBUG_STACK_DEPTH > 0)
    debugBacktraceUpdate();
  #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH
    
  // Call the original panic handler function to finish processing this error (creating a core dump for example...)
  __real_esp_panic_handler(info);
}

 

Добавление обертки в ESP-IDF

Для того, чтобы заставить наш обработчик паники работать, необходимо в файл CMakeLists.txt добавить строку idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND):

cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
project(village_garage)

Это заставит компилятор вызвать нашу функцию esp_panic_handler вместо “штатной”, а затем уже наша функция вызовет встроенную. Вуаля!

 

Добавление обертки в Arduino

Решение для фреймворка Arduino32 подсказал один из читателей, но работает оно только в PlatformIO. Достаточно добавить в platformio.ini строчку:

[env:esp32dev] 
platform = espressif32
board = esp32dev
framework = arduino
build_flags = -Wl,--wrap=esp_panic_handler

 


Отправка backtrace после перезагрузки устройства

Осталось только отправить эти данные после перезагрузки куда-нибудь на сервер. Для этого необходимо считать из сохраненной ранее области и из системных функций:

re_restart_debug_t debugGet()
{
  re_restart_debug_t ret;
  memset(&ret, 0, sizeof(re_restart_debug_t));
  esp_reset_reason_t esp_reason = esp_reset_reason();
  if ((esp_reason != ESP_RST_UNKNOWN) && (esp_reason != ESP_RST_POWERON)) {
    ret = _debug_info;
    if (_debug_info.heap_total > heap_caps_get_total_size(MALLOC_CAP_DEFAULT)) {
      memset(&ret, 0, sizeof(re_restart_debug_t));
    };
  };
  memset(&_debug_info, 0, sizeof(re_restart_debug_t));
  return ret;
}

Теперь можно отправить эти данные разработчику. Например можно это можно сделать, опубликовав эти данные через mqtt-брокер с флагом retained. А можно отправить в telegram, добавив backtrace к информации о запуске устройства, вот мои шаблоны сообщений при “нормальном” и “аварийном” перезапуске:

#define CONFIG_MESSAGE_TG_VERSION_DEF "🔸 <b>Устройство запущено</b>\n\n<code>Версия прошивки:     </code><b>%s</b>\n<code>Причина перезапуска: </code><b>%s</b>\n<code>CPU0: </code><i>%s</i>\n<code>CPU1: </code><i>%s</i>"

#define CONFIG_MESSAGE_TG_VERSION_TRACE "🔸 <b>Устройство запущено</b>\n\n<code>Версия прошивки:     </code><b>%s</b>\n<code>Причина перезапуска: </code><b>%s</b>\n<code>CPU0:  </code><i>%s</i>\n<code>CPU1:  </code><i>%s</i>\n<code>HEAP:  </code><i>%s</i>\n<code>TRACE: </code><i>%s</i>"

Заливаем прошивку в устройство. Теперь при “программной перезагрузке” или сбое получим примерно такое сообщение:

🔸 Устройство запущено

Версия прошивки:     20220320.039
Причина перезапуска: EXCEPTION / PANIC
CPU0:  Software reset CPU
CPU1:  Software reset CPU
HEAP:  272800 : 104648 (38.4%) : 57644 (21.1%) 21.03.2022 07:30:00
TRACE: 0x400d39bf 0x40089792 0x40089855 0x40082d16 0x400014fa 0x4014fb51 0x40156bde 0x40156c1a 0x400d34e3 0x400da8b0 0x400d44b1 0x400dbb23

Теперь можно скопировать эти данные в attr2line и расшифровать место возникновения ошибки. Задача решена.

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 1 оценок ]

9 комментариев для “Удаленный перехват backtrace на ESP32”

  1. very useful guide, I tried to use your idea in my project. I created a person file composed of the code presented on the blog. However, when trying to panic (divide by 0) I still get ESP-IDF’s built-in panic handling. Do you have any idea what else is wrong? Of course, I added idf_build_set_property(LINK_OPTIONS “-Wl,–wrap=esp_panic_handler” APPEND) to CmakeLists

    1. The built-in panic handler is not disabled. The custom handler acts only as an additional tool.

      Additionally, you must ensure that the data remains in memory between reboots. Perhaps you forgot to change the buffer for storing data as __NOINIT_ATTR static???

  2. .C file:
    #include “backtrace_stats.h”

    #if CONFIG_RESTART_DEBUG_INFO && (CONFIG_RESTART_DEBUG_STACK_DEPTH > 0)
    #include “esp_types.h”
    #include “esp_attr.h”
    #include “esp_err.h”
    #include “esp_debug_helpers.h”
    #include “soc/soc_memory_layout.h”
    //#include “soc/cpu.h”
    #endif // CONFIG_RESTART_STACK_DEPTH

    #include “esp_system.h”
    #include “esp_heap_caps.h”
    #include “esp_cpu_utils.h”

    #if CONFIG_RESTART_DEBUG_INFO
    __NOINIT_ATTR static re_restart_debug_t _debug_info;
    #endif // CONFIG_RESTART_DEBUG_INFO

    #if CONFIG_RESTART_DEBUG_INFO

    void IRAM_ATTR debugHeapUpdate()
    {
    _debug_info.heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
    _debug_info.heap_free = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
    size_t _new_free_min = heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT);
    if ((_debug_info.heap_free_min == 0) || (_new_free_min 0

    void IRAM_ATTR debugBacktraceUpdate()
    {
    esp_backtrace_frame_t stk_frame;
    esp_backtrace_get_start(&(stk_frame.pc), &(stk_frame.sp), &(stk_frame.next_pc));
    _debug_info.backtrace[0] = esp_cpu_process_stack_pc(stk_frame.pc);

    bool corrupted = (esp_stack_ptr_is_sane(stk_frame.sp) &&
    esp_ptr_executable((void*)esp_cpu_process_stack_pc(stk_frame.pc))) ?
    false : true;

    #if CONFIG_RESTART_DEBUG_STACK_DEPTH > 1
    uint8_t i = CONFIG_RESTART_DEBUG_STACK_DEPTH;
    while (i– > 0 && stk_frame.next_pc != 0 && !corrupted) {
    if (!esp_backtrace_get_next_frame(&stk_frame)) {
    corrupted = true;
    };
    _debug_info.backtrace[CONFIG_RESTART_DEBUG_STACK_DEPTH – i] = esp_cpu_process_stack_pc(stk_frame.pc);
    };
    #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH > 1
    }

    #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH

    void IRAM_ATTR debugUpdate()
    {
    debugHeapUpdate();
    #if CONFIG_RESTART_DEBUG_STACK_DEPTH > 0
    debugBacktraceUpdate();
    #endif // CONFIG_RESTART_DEBUG_STACK_DEPTH
    }

    #endif // CONFIG_RESTART_DEBUG_INFO

    /* Declare the real panic handler function. We’ll be able to call it after executing our custom code */
    void __real_esp_panic_handler(void*);

    // /* This function will be considered the esp_panic_handler to call in case a panic occurs */
    void __wrap_esp_panic_handler (void* info) {
    // /* Custom code, count the number of panics or simply print a message */
    printf(“FROM OWN HANDLER\n”);

    /* Call the original panic handler function to finish processing this error (creating a core dump for example…) */
    __real_esp_panic_handler(info);
    }

    CMakeLists:

    cmake_minimum_required(VERSION 3.5)

    set(V_MAJOR 0)
    set(V_MINOR 11)
    set(V_REVISION 3)
    add_definitions(-DCODE_NAME=”Added own panic handler”)
    add_definitions(-DCODE_PREFIX=”feat”)
    add_definitions(-DCODE_DATE=”10/05/2024″)

    add_definitions(-DSW_VERSION_MAJOR=${V_MAJOR})
    add_definitions(-DSW_VERSION_MINOR=${V_MINOR})
    add_definitions(-DSW_VERSION_REVISION=${V_REVISION})

    set(PROJECT_VER “${V_MAJOR}.${V_MINOR}.${V_REVISION}”)

    include($ENV{IDF_PATH}/tools/cmake/project.cmake)
    project(my_project)

    idf_build_set_property(LINK_OPTIONS “-Wl,–wrap=esp_panic_handler” APPEND)

    That’s look my code. If I thinking right now on my terminal I should find test from printf in function __wrap_esp_panic_handler, but this doesn’t happend

  3. Where are these macros defined???

    // EN: Preserve debugging information across device software restarts
    // RU: Сохранять отладочную информацию при программном перезапуске устройства
    #define CONFIG_RESTART_DEBUG_INFO 1
    // EN: Allow heap information to be saved periodically, with subsequent output on restart
    // RU: Разрешить периодическое сохранение информации о куче / памяти с последующем выводом при перезапуске
    #define CONFIG_RESTART_DEBUG_HEAP_SIZE_SCHEDULE 1
    // EN: Depth to save the processor stack on restart. 0 – do not save
    // RU: Глубина сохранения стека процессора при перезапуске. 0 – не сохранять
    #define CONFIG_RESTART_DEBUG_STACK_DEPTH 28

  4. Note for future people that will use this tutorial using C++: you need to declare __real_esp_panic_handler and __wrap_esp_panic_handler as extern “C”. i.e.
    extern “C” void __real_esp_panic_handler(void* info);

    extern “C” void __wrap_esp_panic_handler(void* info) {…}

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *