Как обрабатываются ошибки (не связанные с синтаксисом) в 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.
@Coder_fox, 👍4
Обсуждение2 ответа
Лучший ответ:
Ответ простой: они вообще не обрабатываются.
Согласно стандартам 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
О номере 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
- Разные и самые быстрые способы вычисления синусов и косинусов в Arduino
- Поскольку double и float представляют один и тот же тип данных (обычно), что предпочтительнее?
- Почему IDE не может найти плату, несмотря на то, что она четко видна и выбрана?
- Восстановление всех fuse-битов на AtMega328P-PU
- Ошибка соединения Arduino Firebase с отказом
- Использование программной памяти в ESP8266 по сравнению с AVR, а также как обрабатывать большие динамические строки
- Использование функций в заголовочных файлах
- Что мне делать с StackOverflow при ошибке компиляции?
Я думаю, что стандартом является то, что он не определен для целочисленной математики. Плавающая точка соответствует стандарту 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