Как обрабатываются ошибки (не связанные с синтаксисом) в Arduino и в архитектуре AVR в целом?

Мне просто было любопытно, как архитектура AVR справляется с ошибками, которые могут привести к сбою обычной настольной программы. Я говорю о логических ошибках, например, о математических задачах, которые не определены. Например, деление на 0 и извлечение квадратного корня из отрицательного числа. Сначала я ожидал, что выдав ошибку чипу AVR, он просто вернет 0. Но я запустил вот эту программу:

void setup() {
Serial.begin(9600);
}
void loop(){
int x = 0;
int p = 50;
int result;
result = p/x;
Serial.println(result);
result = sqrt(-10);
delay(1000);
Serial.println(result);
delay(10000);
}

и получил такой вывод:

4294967295
0

После этого я совсем запутался. Почему переменная result принимает максимальное значение unsigned long? Поскольку я объявил int, микроконтроллер должен был выделить для этой переменной 2 байта динамической памяти, но, видимо, ему каким-то образом удалось получить дополнительные 2 байта для хранения данных. Может ли это означать, что чип AVR может повредить данные в других ячейках памяти, одновременно выделяя новые данные для переменной, которой только что было введено неопределенное значение? Ладно, возможно, чип AVR просто устанавливает какую-то математическую ерунду на 4294967295. Но нет, в примере получения квадратного корня из -10 мы видим, что значение стало 0. Хотя, вероятно, это функция, обработанная какой-то библиотекой, так что, возможно, есть какая-то защиты от этих ошибок. Кроме того, я попытался запустить приведенную выше программу, но с переменной byte для result вместо int.

void setup() {
Serial.begin(9600);
}
void loop(){
int x = 0;
int p = 50;
int result;
result = p/x;
Serial.println(result);
result = sqrt(-10);
delay(1000);
Serial.println(result);
delay(10000);
}

И результат был тот же. Итак, есть ли какая-то документация по управлению ошибками в AVR, поскольку это важно знать во время разработки.

PS Все запускалось на настоящем чипе Atmega 328p на Arduino Nano.

, 👍4

Обсуждение

Я думаю, что стандартом является то, что он не определен для целочисленной математики. Плавающая точка соответствует стандарту IEEE и возвращает значение nan, но не является исключением. Перехват исключения в Arduino невозможен (я думаю): https://stackoverflow.com/questions/10095591/enable-Exceptions-in-the-arduino-environment. Когда вы обнаружите странные вещи с байтом, дайте полную информацию набросок, который мы можем попробовать, и сказать нам, какую плату Arduino вы используете., @Jot

@Jot Это интересный момент, но зачем заменять nan на 4294967295. Очевидно, у них разные двоичные значения. Почему Atmel не включила значение nan (которое, возможно, можно было бы отключить с помощью предохранителей) в свою архитектуру процессора. Это полезная функция устранения неполадок, которая широко используется во всех основных языках программирования., @Coder_fox

@Jot Я использовал Arduino nano с Atmega 328p на борту., @Coder_fox

Целое число не может быть значением «nan», указанным для числа с плавающей запятой. Пожалуйста, обновите свой вопрос, указав плату и микроконтроллер и, возможно, полный эскиз., @Jot

Я попробовал сделать эскиз сам, и у меня разные результаты. Пожалуйста, покажите свой эскиз и сообщите нам результаты эскиза, который вы показываете. Вы запускаете это в симуляторе?, @Jot

@Jot Нет, все работает на настоящем Arduino., @Coder_fox

Спасибо за полную схему. Я добавил ответ. Вы столкнулись с очень странной ошибкой компилятора, которая приводит к появлению 4294967295. Теперь я понимаю, почему мой код не показывает это число, а ваш код показывает., @Jot


2 ответа


Лучший ответ:

8

Ответ простой: они вообще не обрабатываются.

Согласно стандартам C и C++, то, что вы вызываете, называется неопределенное поведение, что означает, что может произойти все, что угодно. . В практике В терминах это означает, что компилятор может выполнять оптимизацию при условии вы никогда не будете вызывать неопределенное поведение и полностью игнорировать то, что может произойти, если предположение не выполняется. Сгенерированный код может будет полностью сломан, это не будет вина компилятора.

Архитектура AVR не поддерживает деление и математические операции с плавающей запятой. Таким образом все ошибки, которые у вас есть, происходят на уровне программного обеспечения. Обратите внимание, что sqrt(-10) не является ошибкой (это NaN, которая правильно обрабатывается avr-libc), но преобразуя его в int (что вы и делаете, когда присвоить его result) является ошибкой.

Если вы действительно хотите узнать подробности того, что происходит с каждым из этих ошибок, вариант только один: придется разбирать исполняемую программу и прочитайте ассемблерный листинг.

,

_"... неопределенное поведение, то есть может случиться что угодно."_ - Стоит подчеркнуть, что это полностью фишка C/C++, не имеющая отношения к чипам AVR. Точно такое же предупреждение относится и к коду, работающему, скажем, на Intel i5., @marcelm

Также стоит отметить, что хотя стандарт C++ утверждает, что компилятор может делать *что угодно*, компиляторы C++ обычно определяют дополнительное поведение, выходящее за рамки спецификации C++. Например, компилятор Visual Studio определяет поведение x/0, которое не было определено в спецификации C++, основываясь на том, что он знает об архитектуре. Это действительно полезно, когда вы хотите сделать что-то, что запрещено спецификацией, но разрешено вашим конкретным компилятором, например расширения gcc., @Cort Ammon


1

О номере 4294967295

К моему удивлению, скетч действительно выдает число 4294967295. Это неправильно.
Оказывается, компилятор знает числа и пытается выполнить математические вычисления самостоятельно (а не во время выполнения).

Во время выполнения 50 / 0 приводит к целому числу со знаком -1.
При использовании байта результат 50 / 0 равен 255.
Результатом этого расчета является переменная, содержащая только 1. Это 0xFFFF для 16-битной переменной. В результате получается 65535 для беззнакового целого числа, -1 для знакового целого числа и 255 для беззнакового байта.

Пока все в порядке. Это имеет смысл.

Однако когда компилятор пытается выполнить математические вычисления самостоятельно с помощью 50 / 0, он превращает результат в 32-битную переменную 0xFFFFFFFF. Компилятор не помещает математические вычисления в двоичный результат, а вызывает Serial.println() напрямую с 32-битным беззнаковым длинным значением 0xFFFFFFFF, что равно 4294967295.

Причина не в типе переменной 'result', а в разделении на 'p/x'. Компилятор считает, что лучше получить 32-битное целое число, если математические вычисления выполняются с двумя 16-битными целыми числами.

Я думаю, что при оптимизации компилятора результат должен быть таким же, как при расчете во время выполнения.
Под этим ответом есть интересное обсуждение. @EdgarBonet называет поведение компилятора странным, но не ошибкой.

Ниже приведен тестовый скетч, который создает 4294967295 с Arduino 1.8.7 для Arduino Uno:

void setup() {
Serial.begin(9600);
}

void loop(){
  int x = 0;
  int p = 50;
  int result;
  result = p/x;
  Serial.println(result);
  delay(5000);
}

Код выводит нормальные результаты, когда вычисляется «x» или «p», например результат функции, или когда одна из трех переменных становится изменчивой.

,

Обработка компилятором неопределенного поведения _никогда_ не является ошибкой, поскольку по определению все, что делает компилятор, приемлемо в соответствии со стандартом языка., @Edgar Bonet

@EdgarBonet Я думаю, что он должен делать все одинаково. По вашему мнению, компилятор может вставить код, который будет насвистывать мелодию при делении на ноль? Я так не думаю. На мой взгляд, менять int на unsigned long слишком странно., @Jot

Хотя компилятор не может _намеренно_ делать глупости, странные вещи случаются, когда вы вызываете неопределенное поведение. См., например, начало [этого сообщения в блоге](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html) (вся серия сообщений обязательна к прочтению). для любого программиста C или C++). Странный? Да. Ошибка? Нет., @Edgar Bonet

@EdgarBonet, спасибо, интересно это читать. Вещи, о которых они говорят, просты. Здесь происходит следующее: во время выполнения вызывается определенная функция, а когда компилятор ее оптимизирует, вызывается другая (перегруженная) функция. Это еще один шаг к странностям., @Jot

_"По вашему мнению, компилятор может вставить код, который будет насвистывать мелодию при делении на ноль? Я так не думаю."_ - Когда вы вызываете Undefine Behavior, все происходит, включая неожиданно большие переменные, свист мелодии или форматирование вашего жесткого диска. водить машину. Добро пожаловать в чудесный мир C и C++. Однако было бы интересно посмотреть, происходит ли такое же «переменное продвижение» без UB., @marcelm

Это происходит только тогда, когда x равен нулю, а p не равен нулю. Когда напечатано 4294967295, sizeof(result) по-прежнему равен 2., @Jot