Проверка кода утверждениями
В стандартной библиотеке периферии от ST есть специальный макрос,
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__)),
задача которого — проверять переданное в него выражение (expr)
. Если оно истинно, то макрос ничего не делает ((void)0)
, а если ложно, то вызывается функция assert_failed()
.
Для активации нужно определить макрос-метку
USE_FULL_ASSERT
, в противном случае проверка не выполнится.
void assert_failed(uint8_t* file, uint32_t line) {
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1) {
}
}
Такие штуки называют «утверждением» (англ. assert). Но зачем всё это нужно? Вам никто не мешает передать в качестве аргумента какую-нибудь ерунду или указатель на NULL
. Данный макрос позволяет исключить такую ситуацию (при условии, что код вы всё же отлаживаете). Посмотрите на реализацию функции инициализации модуля GPIO из стандартной библиотеки:
Может показаться, что использование утверждений не нужно и вы полностью понимаете поведение кода. Вот вам пример от Mozilla: добавление одного утверждения выявило 66 багов — FrameArena::~FrameArena should assert that it’s empty.
// stm32f10x_gpio.c
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) {
// ...
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
// ...
Вместо указателя на GPIO_InitDef
запросто можно передать указатель на GPIO_InitTypeDef
. С включенной проверкой такие фокусы не пройдут.
#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \
((PERIPH) == GPIOB) || \
((PERIPH) == GPIOC) || \
((PERIPH) == GPIOD) || \
((PERIPH) == GPIOE) || \
((PERIPH) == GPIOF) || \
((PERIPH) == GPIOG))
Использовать макрос можно и для своих задач. Допустим, имеется некоторый буфер, размер которого заранее известен. Также есть функция, принимающая индекс, которая меняет значение ячейки в массиве. Если индекс больше, чем размер массива, функция однозначно сделает что-то не то. С проверкой через макрос будет понятно, где искать баг.
void do_something(uint32_t index) {
assert_param(index < BUFFER_SIZE);
// ...
}
assert_param()
— это не выдумка создателей библиотеки, такая функциональность уже есть в стандартной библиотеке языка Си и подключается через заголовочный файл <assert.h>
.
Реализация стандартной библиотеки для микроконтроллера обычно отличается от реализации для универсального компьютера. В частности,
assert()
из стандартной библиотеки Си ведёт к вызову функцииabort()
и завершению программы. В любом случае лучше определить собственный макрос и пользоваться им.
Обратите внимание! Смысл макроса assert*()
в поиске багов, а не в обработке ошибок. Например, если вы используете динамическую память, то проверка работы malloc()
через макрос assert()
— неправильное его использование. Программа вылетит в обработчик вместо того, чтобы продолжить работу и пытаться как-то решить возникшую проблему.
uint8_t arr = malloc(/* ... */);
assert(arr != NULL); // missusing
Такие проверки эффективны на этапе разработки, но они не нужны в финальной версии прошивки. Макрос assert_param()
отключается удалением определения USE_FULL_ASSERT
, а assert()
из стандартной библиотеки Си — определением макроса NDEBUG
или добавлением к компилятору флага -DNDEBUG
.
Утверждения можно использовать для принудительной перезагрузки устройства, если во время выполнения программы оно столкнётся с неправильным состоянием системы. Для этого совершите программный сброс в функции-обработчике.
// assert.h
#ifdef NDEBUG /* required by ANSI standard */
# define assert(__e) ((void)0)
#else
# define assert(__e) ((__e) ? (void)0 : __assert_func (__FILE__, __LINE__, \
__ASSERT_FUNC, #__e))
// ...
Использование assert()
, как ни странно, может навредить. В качестве параметра expr
можно передавать что-то, что изменяет состояние системы, например функцию или инкремент переменной.
assert(f() < MAX_VALUE); // bad
assert(++i < MAX_VALUE); // bad ether
При отключении утверждений поведение программы изменится, так как нет необходимости действительно проверять передаваемое значение, а значит, его и не нужно вычислять. Посмотрите внимательно на то, как выглядит макрос assert()
при определении NDEBUG
. Вы сами должны следить за тем, что не передаёте в качестве аргумента что-либо изменяющее состояние системы.
В стандарте c11
появилось ещё одно ключевое слово, _Static_assert
. Оно, в отличие от библиотечного макроса assert()
, работает на этапе компиляции.
_Static_assert(expr, "Debug Message");
Если результат expr
равен нулю, то программа не скомпилируется.
static assertion failed: "Debug Message" main.c
Статическое утверждение в случае проверки аргумента функции (известного на этапе компиляции, конечно) подойдёт лучше, так как просто не позволит скомпилировать код.