Неожиданное поведение с расчетом Arduino Uno

У меня есть очень простой фрагмент кода, в котором одно и то же вычисление выводит разные значения в зависимости от того, как я выполняю вычисление. Платформой, на которой я запускаю этот код, является Arduino Uno с микроконтроллером Atmel MEGA328P. Вот код:

void setup() {
  
  Serial.begin(9600);
  unsigned long num = 100927;
  Serial.println(1000*60);
  Serial.println(num/(1000*60));
  Serial.println(num/(60000));
}

void loop() {}

Этот код выводит на консоль следующее:

-> -5536
-> 0
-> 1

Я не понимаю, почему num/(1000*60) и num/(60000) дают разные значения? Кроме того, 1000*60 оценивается как мусорное значение, которое выглядит так, как будто оно каким-то образом установило тип данных в int и привело к переполнению "int" диапазон.

У меня большой опыт программирования на Python. Я полагаю, что привык к тому, что python динамически интерпретирует переменные типы данных для меня, поэтому язык arduino меня немного смущает. Любая помощь приветствуется!

, 👍2


1 ответ


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

3

Это чисто программирование на C++, о котором я рассказываю на своем веб-сайте. р>

Однако, поскольку здесь не допускаются ответы только по ссылкам, я воспроизвожу этот пост ниже:


Как вы думаете, что здесь будет напечатано на платформе Arduino (Uno)?

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  
  Serial.println (30000 + 30000); // дважды 30000
  Serial.println (60 * 60 * 24);  // секунд в сутках
  Serial.println (50 / 100 * 1000); // половина от 1000
  }  // конец настройки

void loop () { }

Вы догадались:

60000
86400
500

Нет!

Он печатает:

-5536
20864
0

Это связано с целочисленной арифметикой. Если компилятор может, он обрабатывает числовой литерал (например, 60) как тип int, что означает, что он имеет диапазон от -32768 до +32767. Это может быть неожиданным для пользователей современных компиляторов, поскольку в настоящее время int обычно имеет длину 32 бита. Однако на 8-битных процессорах Arduino, таких как Atmega328P, компилятор обрабатывает целое число как 16-битное. Стандарт C++ допускает размер минимум 16 бит, но не гарантируется, что он будет больше.

И арифметика выполняется с использованием типа наибольшего аргумента, что означает, что арифметика в каждом случае выполняется как 16-битная арифметика, и, таким образом, она переполняется, когда достигает 32767.

Например, 30000 + 30000 = 60000, что равно 0xEA60 в шестнадцатеричном формате. К сожалению, 0xEA60 — это именно то, как -5536 хранится в типе int, поэтому он печатает -5536.

При этом 60 * 60 * 24 = 86400, что равно 0x15180 в шестнадцатеричном формате. Поскольку это не помещается в 16 бит, оно усекается до 0x5180, что равно 20864 в десятичном виде (как напечатано).

Наконец, в целочисленной арифметике 50/100 равно нулю, умножьте ноль на 1000, и вы все равно получите ноль, поэтому окончательный результат равен нулю.


Можем ли мы "помочь" компилятору, сообщая ему, какой результат нам нужен, например, вот так?

-5536
20864
0

Это печатает:

-5536
20864
0,00

Так что нет, это не помогло.


Решение

Во-первых, вы можете добавить суффикс к числовым литералам (например, L для long или UL для unsigned long) и добавить десятичный разряд к числам с плавающей запятой, например:

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();

  long a = 30000 + 30000;
  long b = 60 * 60 * 24;
  float c = 50 / 100 * 1000;
  
  Serial.println (a);
  Serial.println (b);
  Serial.println (c);
  }  // end of setup

void loop () { }

Теперь мы получаем:

-5536
20864
0.00

Вам нужно помочь только с первым литералом, как только компилятор узнает, что мы используем длинные числа (или числа с плавающей запятой), он будет использовать их для выражения. *

Или мы можем "транслировать" их:

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  
  Serial.println (30000L + 30000); // twice 30000
  Serial.println (60L * 60 * 24);  // seconds in a day
  Serial.println (50.0 / 100 * 1000); // half of 1000
  }  // end of setup

void loop () { }

Результаты:

60000
86400
500.00

Приведение типов полезно для переменных, потому что вы не можете просто добавить "L" в конец имени переменной.

Альтернативный синтаксис — использовать такой конструктор:

void setup ()
  {
  Serial.begin (115200);
  Serial.println ();
  
  long a =  (long) 30000 + 30000;
  long b =  (long) 60 * 60 * 24;
  float c = (float) 50 / 100 * 1000;
  
  Serial.println (a);
  Serial.println (b);
  Serial.println (c);
  }  // конец настройки

void loop () { }

(Те же результаты).


* На самом деле это несколько сложнее, как объясняет эта ссылка: Понятие о правилах преобразования целых чисел

Компилятор "продвигает" значение в выражении, соответствующее другому "более высокому рангу" тип, при определенных обстоятельствах. Например, добавление int и long приведет к преобразованию int в long (независимо от независимо от того, появляется ли оно первым в выражении или нет). Однако если int добавляется к другому int, он не превращает их в long, даже если результат может не уместиться в int.


Дополнительные пояснения

Меня спросили в комментариях, почему 1000 * 60 не совпадает с 60000 с точки зрения компилятора.

Ответ заключается в том, что компилятор будет обрабатывать целочисленный литерал как int (если это возможно), а int на этой платформе составляет 16 бит.

Таким образом, 1000 (int) * 60 (еще одно int) умножаются с использованием 16-битной арифметики, что дает результат, который переполняется.

Однако литерал 60000 не может быть сохранен как int, поэтому компилятор продвигает его до long (он же "long целое"). Поскольку он был повышен до long, он правильно сохраняется, и дальнейшие операции с 60000 будут выполняться с long арифметикой.

Я видел людей, кодирующих 60000 как 60000L, но это необязательно. Компилятор не настолько глуп, чтобы хранить 60000 как -5536, так как это явно не намерение программиста.

,

Я думаю, что наиболее важным моментом является то, что sizeof(int) равен 2 на платформе AVR, что отличается от любой другой известной мне платформы., @PMF

Хорошая точка зрения. Современные компиляторы делают int 32-битным или даже 64-битным. Однако из-за нехватки места в микропроцессоре они решили сделать int шириной 16 бит. См. [int — справочник Arduino](https://www.arduino.cc/reference/en/language/variables/data-types/int/), @Nick Gammon

@PMF Ты кажешься молодым. Я общался с CP/M и DOS, даже с Atari TOS, а int целую вечность имел 16 бит. :-D С появлением 32-битной системы меня раздражало, что int стало таким же большим, как long. Урок, который я извлек: никогда не предполагайте размер, используйте типы «stdint.h» или «cstdint», если мне нужен определенный размер., @the busybee

@thebusybee Это было в основном до меня, да. Но я также начал использовать int32_t и его братьев и сестер, чтобы избежать ловушек. К счастью, для современных языков, таких как C# и Java, размер int зафиксирован в спецификации., @PMF

Хотя это не объясняет, почему "(1000*60)" отличается от "60000". Теперь я _догадываюсь_, что последнее принимается как беззнаковое, так что на самом деле 60000, в то время как первое выходит за пределы диапазона знакового целого числа и дает, гм, -5536? (Кроме того, это подписанное переполнение на самом деле не определено.) Решение явного приведения значений к long, конечно, работает, но объяснение может быть более подходящим для вопроса о, @ilkkachu

@ilkkachu Да, это так. 1000*60 оценивается в домене int. Однако 60000 обязательно нужно преобразовать в long. Было бы нелепо со стороны компилятора предположить, что каждый числовой литерал является целым числом. Предполагается, что это наименьший тип, который будет содержать литерал (начиная с int, а не с char)., @Nick Gammon

К сожалению, [ссылка, которую я дал](https://wiki.sei.cmu.edu/confluence/display/cplusplus/INT02-CPP.+Understand+integer+conversion+rules) не работает, я попытаюсь Найди другое., @Nick Gammon

[Вот оно](https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules) - я исправлю свой пост., @Nick Gammon

@NickGammon, да, это именно то объяснение, которое отсутствует в самом ответе., @ilkkachu

@ilkkachu Вы сказали * Теперь, я предполагаю, что последнее считается беззнаковым, так что на самом деле 60000 * - я не уверен насчет «неподписанной» части. См. ссылку, которую я дал выше, в которой говорится в пункте 4 «Обычные арифметические преобразования»: * Если тип операнда с целочисленным типом со знаком может представлять все значения типа операнда с целочисленным типом без знака, операнд с беззнаковым целым числом целочисленный тип преобразуется в тип операнда с целочисленным типом со знаком.* -- Таким образом, вместо этого он может считаться (со знаком) длинным., @Nick Gammon

@NickGammon, да, у меня было смутное воспоминание, что где-то может быть промежуточный шаг unsigned. Список здесь, кажется, говорит, что это только так для недесятичных оснований: https://en.cppreference.com/w/c/language/integer_constant так что действительно кажется, что здесь "длинный" случай, как вы говорите, @ilkkachu

@ilkkachu На этой странице странно умалчивается о начальном знаке минус (который не применяется в данном случае), однако кажется, что для десятичных литералов они обычно считаются знаковыми., @Nick Gammon

Возможно, ведущий знак считается унарным оператором после того, как литерал был оценен. Страницы, подобные приведенной выше, достаточно длинные и все же не полностью определяют ситуацию такого рода., @Nick Gammon

@NickGammon, да, это постоянный + унарный минус. Кажется, это упоминается там в разделе «Примечания»: «Нет отрицательных целочисленных констант. Выражения, такие как -1, применяют унарный оператор минус к значению, представленному константой»., @ilkkachu

В этом есть смысл. :), @Nick Gammon