Идентификация и обработка ошибок времени выполнения кода (run-time errors) важна для разработки надежных приложений. Существует несколько видов подобных ошибок.
Восстановимые (recoverable errors):
- Ошибки, о которых было сообщено кодом возврата из функции (error codes). - Исключения C++ (exceptions), выбрасываемые оператором throw.
Не восстановимые (fatal errors):
- Ошибки assert (при использовании макроса assert и эквивалентных ему методов, см. раздел "Assertions" документации [3]) и вызовов abort(). - Исключения процессора (CPU exceptions): доступ к защищенным регионам памяти, недопустимая инструкция, и т. п. - Проверки системного уровня: watchdog timeout, cache access error, stack overflow, stack smashing, heap corruption, и т. д.
В этом руководстве (перевод [1]) объясняется использование механизмов обработки ошибок ESP-IDF (error handling), относящихся к восстанавливаемым ошибкам (recoverable errors), и предоставляются некоторые общие шаблоны обработки ошибок.
Для руководства по диагностике не восстановимых ошибок (unrecoverable errors или fatal errors) см. [2].
[Коды ошибок]
Основная масса функций ESP-IDF возвращают тип esp_err_t, который представляет коды ошибок (error codes). Тип esp_err_t это целое число со знаком. Успешный возврат (отсутствие ошибки) обозначается кодом ESP_OK, который определен как 0.
Различные заголовочные файлы ESP-IDF определяют возможные коды ошибок, используя директивы препроцессора #define. Обычно эти определения дают имена кодом ошибок, начинающиеся с префикса ESP_ERR_. Общие коды ошибок для традиционных отказов (out of memory, timeout, invalid argument, и т. п.) определены в файле esp_err.h file. Различные компоненты в ESP-IDF могут определять дополнительные коды ошибок для отдельных ситуаций.
Полный список ошибок см. в справочнике Error Code Reference [4].
[Преобразование кода ошибки в сообщение ошибки]
Для каждого кода ошибки, определенного в компонентах ESP-IDF, значение esp_err_t может быть преобразовано в имя кода ошибки с помощью функций esp_err_to_name() или esp_err_to_name_r(). Например, если передать 0x101 в esp_err_to_name(), то она возвратит строку "ESP_ERR_NO_MEM". Такие строки можно использовать в выводе лога, чтобы упростить понимание, какая произошла ошибка.
Дополнительно функция esp_err_to_name_r() будет пытаться интерпретировать код ошибки как стандартный код ошибки POSIX, если не было найдено подходящего значения ESP_ERR_. Это делается с помощью функции strerror_r. Коды ошибок POSIX (такие как ENOENT, ENOMEM) определены в errno.h, и они обычно извлекаются из переменной errno. В ESP-IDF эта переменная является локальной для каждого потока (задачи): в приложении работает несколько задач FreeRTOS, и у каждой из них есть своя собственная копия errno. Функции, которые устанавливают переменную errno, модифицируют её только у той задачи, в которой функция была запущена.
Эта фича по умолчанию разрешена, однако её можно запретить для уменьшения размера бинарника приложения, см. опцию CONFIG_ESP_ERR_TO_NAME_LOOKUP. Когда эта фича запрещена, esp_err_to_name() и esp_err_to_name_r() все еще определены, и могут быть вызваны. В этом случае esp_err_to_name() вернет UNKNOWN ERROR, а esp_err_to_name_r() вернет Unknown error 0xXXXX(YYYYY), где 0xXXXX и YYYYY соответственно шестнадцатеричное и десятичное представление кода ошибки.
[Макросы для обработки ошибок]
ESP_ERROR_CHECK. Макрос ESP_ERROR_CHECK используется для тех же целей, что и assert, за исключением того, что ESP_ERROR_CHECK проверяет свое значение как esp_err_t, а не bool. Если аргумент ESP_ERROR_CHECK не равен ESP_OK, то в консоль печатается сообщение об ошибке, и вызывается abort().
Выводимое сообщение об ошибке будет выглядеть примерно так:
ESP_ERROR_CHECK failed: esp_err_t 0x107 (ESP_ERR_TIMEOUT) at 0x400d1fdf
file: "/Users/user/esp/example/main/main.c" line 20
func: app_main
expression: sdmmc_card_init(host, &card)
Backtrace: 0x40086e7c:0x3ffb4ff0 0x40087328:0x3ffb5010 0x400d1fdf:0x3ffb5030 0x400d0816:0x3ffb5050
Замечание: если используется IDF monitor (idf.py monitor [5]), то адреса в backtrace будут преобразованы в имена файлов и номера строк исходного кода.
Первая строка сообщения об ошибке выводит код ошибки в шестнадцатеричном формате, и в круглых скобках идентификатор для этого кода ошибки, который используется в исходном коде. Последнее зависит от установленной опции CONFIG_ESP_ERR_TO_NAME_LOOKUP. Также в конце строки печатается адрес в программе, где произошла ошибка.
Последующие строки показывают место в программе, где был вызван макрос ESP_ERROR_CHECK, и выражение, которое было передано в макрос как аргумент.
В завершение печатается backtrace. Это часть вывода panic handler, общий для всех фатальных ошибок. Более подробно про backtrace см. в документации [2].
ESP_ERROR_CHECK_WITHOUT_ABORT. Макрос ESP_ERROR_CHECK_WITHOUT_ABORT работает так же, как и ESP_ERROR_CHECK, за исключением того, что не вызывает abort().
ESP_RETURN_ON_ERROR. Макрос ESP_RETURN_ON_ERROR проверяет код ошибки, и если он не равен ESP_OK, то печатает сообщение об ошибке и выполняет возврат из вызвавшей макрос функции.
ESP_GOTO_ON_ERROR. Макрос ESP_GOTO_ON_ERROR проверяет код ошибки, и если он не равен ESP_OK, то печатает сообщение, установит локальную переменную ret в err_code, и затем выполнит переход по метки goto_tag.
ESP_RETURN_ON_FALSE. Макрос ESP_RETURN_ON_FALSE проверяет условие, и если оно не равно true, то печатает сообщение и делает возврат с предоставленным err_code.
ESP_GOTO_ON_FALSE. Макрос ESP_GOTO_ON_FALSE проверяет условие, и если оно не равно true, то печатает сообщение, установит локальную переменную ret в предоставленный err_code, и затем выполнит переход по метке goto_tag.
Несколько примеров использования этих макросов:
static const char* TAG = "Test";
esp_err_t test_func(void)
{
esp_err_t ret = ESP_OK;
// Если x не равен ESP_OK, то печатается сообщение об ошибке и затем вызывается abort():
ESP_ERROR_CHECK(x);
// Если x не равен ESP_OK, то печатается сообщение об ошибке без вызова abort():
ESP_ERROR_CHECK_WITHOUT_ABORT(x);
// Если x не равен ESP_OK, то печатается сообщение, и функция выполнит возврат с кодом x:
ESP_RETURN_ON_ERROR(x, TAG, "fail reason 1");
// Если x не равен ESP_OK, то печатается сообщение об ошибке, ret устанавливается в x,
// и затем делается переход на метку err:
ESP_GOTO_ON_ERROR(x, err, TAG, "fail reason 2");
// Если a не равно true, то печатается сообщение об ошибке, и функция выполнит возврат
// с кодом err_code:
ESP_RETURN_ON_FALSE(a, err_code, TAG, "fail reason 3");
// Если a не равно true, то печатается сообщение об ошибке, ret устанавливается в err_code,
// и затем делается переход на метку err:
ESP_GOTO_ON_FALSE(a, err_code, err, TAG, "fail reason 4");
err:
// Очистка ...
return ret;
}
Замечание: если разрешена опция CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT в Kconfig, то сообщение об ошибке отбрасывается, но остальные действия работают как и прежде.
Макросы ESP_RETURN_XX и ESP_GOTO_xx не могут вызываться из ISR. Однако существуют их версии xx_ISR, например ESP_RETURN_ON_ERROR_ISR, которые можно использовать в ISR.
[Шаблоны обработки ошибок]
Здесь приведены типовые методы реализации обработки ошибок.
1. Попытка восстановления из состояния ошибки. В зависимости от ситуации мы можем попробовать следующие методы:
- Сделать повторную попытку через некоторое время. - Попытаться деинициализировать драйвер, и заново его инициализировать. - Исправить ошибку, используя специальный механизм (наподобие сброса внешнего периферийного устройства, которое не отвечает).
Пример:
esp_err_t err;
do
{
err = sdio_slave_send_queue(addr, len, arg, timeout);
// Повторные попытки, пока очередь не переполнится.
}while (err == ESP_ERR_TIMEOUT);
if (err != ESP_OK)
{
// Обработка других ошибок.
}
2. Передать ошибку "наверх", в вызывающий код. В некоторых промежуточных компонентах это означает, что функция должна выполнить выход с тем же самым кодом ошибки, и должна гарантировать, что любые выделенные ресурсы возвращены в исходное состояние.
Пример:
sdmmc_card_t* card = calloc(1, sizeof(sdmmc_card_t));if (card == NULL)
{
return ESP_ERR_NO_MEM;
}
esp_err_t err = sdmmc_card_init(host, &card);
if (err != ESP_OK)
{
// Очистка:
free(card);
// Передача ошибки на верхний уровень (например для оповещения пользователя).
// Альтернативно приложение может определить и возвратить пользовательский код ошибки.
return err;
}
3. Преобразовать ситуацию в невосстановимую ошибку, например с использованием макроса ESP_ERROR_CHECK.
Прерывание работы приложения в случае обнаружения нежелательного поведения обычно нежелательно для промежуточных компонентов, однако это иногда допустимо на уровне приложения.
Многие их примеров ESP-IDF используют ESP_ERROR_CHECK для обработки ошибок от различных API-функций. Это не лучшая практика для приложений, и делается для того, чтобы сделать код примера более лаконичным.
Пример:
ESP_ERROR_CHECK(spi_bus_initialize(host, bus_config, dma_chan));
[C++ Exceptions]
Обработку исключений C++ см. в документации [6].
[Ссылки]
1. ESP-IDF Error Handling site:docs.espressif.com. 2. ESP-IDF: диагностика не восстановимых ошибок. 3. Espressif IoT Development Framework Style Guide site:docs.espressif.com. 4. ESP-IDF Error Codes Reference site:docs.espressif.com. 5. ESP-IDF Monitor site:docs.espressif.com. 6. ESP-IDF C++ Support site:docs.espressif.com. |