Иногда уже после сборки и отладки очередного устройства, а затем и установки его “на место”, вдруг начинают проявляться самопроизвольные перезагрузки из-за паники. Это означает, что в прошивке где-то присутствует ранее незамеченная проблема. Возникает задача: определить место возникновения сбоя. Зная файл исходного кода и номер строки, можно с большой степенью вероятности предположить причину проблемы и устранить её. При локальной отладке в случае сбоя паники в 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 и расшифровать место возникновения ошибки. Задача решена.
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
Здорово! А откуда берется APP_VERSION?
#define APP_VERSION “20240417.001” в project_config.h
Вручную чтоли или генерируется автоматом?
Чаще вручную, но при массовой компиляции проектов автоматом
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
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???
.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
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
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) {…}
Does your method work in Arduino framework also?
As far as I know – yes. But you need to use PlatformIO, not Arduino IDE. How to do it in Arduino IDE – I don’t know.
I could catch the exception using your code. But unfortunately there is no heap data, only backtrace: (I stored it in a json file on my on site SD card):
{“Year”:24,”Month”:8,”Day”:20,”Hour”:8,”Minute”:4,”Second”:50,”Boot reason”:4,”heap_total”:0,”heap_free”:0,”heap_free_min”:0,”heap_min_time”:0,”backtrace0″:”0x400D68AF”,”backtrace1″:”0x400F7237″,”backtrace2″:”0x40083675″,”backtrace3″:”0x4008513C”,”backtrace4″:”0x400DEBF8″,”backtrace5″:”0x400DF51F”,”backtrace6″:”0x400DF537″,”backtrace7″:”0x400DF555″,”backtrace8″:null}
And this is the decoded version (I am searching a bug in AsyncWebServer because it crashes randomly):
d:\ZMonitorV4\SW\ZMonitor\zmonitor\Panic>xtensa-esp32-elf-addr2line.exe -pfiaC -e firmware.elf 0x400D68AF 0x400F7237 0x40083675 0x4008513C 0x400DEBF8 0x400DF51F 0x400DF537 0x400DF555
0x400d68af: __wrap_esp_panic_handler at C:\B+N\ZMonitorV4\SW\Munka\zmonitor/src/ZMonitor.cpp:1567
0x400f7237: panic_handler at /home/runner/work/esp32-arduino-1ib-builder/esp32-arduino-lib-builder/esp-idf/components/esp_system/port/panic_handler.c:188
0x40083675: xt_unhandled_exception at /home/runner /work/esp32-arduino-lib-builder /esp32-arduino-1ib-builder/esp-idf/components/esp_system/port/panic_handler.c:219
0x4008513c: _xt_user_exc at /home/runner/work/esp32-arduino-1ib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port/xtensa/xtensa_vectors.S:703
0x400debf8: AsyncWebSocket: :_cleanBuffers() at C:\B+N\ZMonitorV4\SW\Munka\zmonitor/.pio/libdeps/zmon/ESP Async WebServer/src/AsyncWebSocket.cpp:1229 (discriminator 2)
0x400df51f: AsyncWebSocket: :textAll (AsyncWebSocketMessageBuffer*) at C:\B+N\ZMonitorV4\SW\Munka\zmonitor/.pio/libdeps/zmon/ESP Async WebServer/src/AsynchlebSocket.cpp:962
0x400df537: AsyncWebSocket: :textAll(char const*, unsigned int) at C:\B+N\ZMonitorV4\sW\Munka\zmonitor/.pio/libdeps/zmon/ESP Async WebServer/src/AsyncebSocket.cpp:968
0x400df555: AsyncWebSocket: :textAll (String const&) at C:\B+N\ZMonitorV4\SW\Munka\zmonitor/.pio/libdeps/zmon/ESP Async WebServer/src/AsyncWebSocket.cpp:1109
Could you help me to get heap information also?
Hi,
Everything is ok now.
It is a good solution!
Thanks!
Добрый день. Добавьте пример пожалуйста, я не могу заставить это работать.