(не)переполнение millis()

Как, наверное, любой начинающий ардуинщик, когда я узнал о том, как можно реализовать всякое с использованием функции millis(), возвращающей 32-разрядное беззнаковое целое число миллисекунд, прошедших с момента запуска микроконтроллера, я задался вопросом: «а что будет, когда счётчик миллисекунд переполнится?»

Функция millis() для ардуинщика — это альфа и омега всей «многозадачности» ардуино. Если что-то нужно регулярно выполнять через заданный интервал времени, то обычно пишут какой-то такой код:

uint32_t  my_timer;    // где-то объявляем переменную "таймер"
#define MY_PERIOD 1000 // "раз в секунду"

...

my_timer = millis();    // "запускаем" таймер, запоминая в
                        // переменной текущее значение millis()

...

// и где-то в основном рабочем цикле пишем примерно такое:
if ( millis() - my_timer > MY_PERIOD ){
    // Заданное время ожидания прошло
    // делаем здесь, что хотели

    my_timer = millis(); // снова взводим таймер для следующего
                         // срабатывания
}

Так вот, предположим, что в «таймере» мы сохранили число 0xFFFFFFFE. Естественно, 32-разрядный счётчик переполняется через 1 миллисекунду и millis() начинает возвращать 0, 1, 2, и т.д.

И куча новичков-ардуинщиков в интернете пугается, когда понимает, что при таком раскладе «всё пропало», потому что в этом случае при проверке срабатывания таймера из 1 будет вычитаться 0xFFFFFFFE и, вроде бы, так нельзя. Поиск по клчевым словам «переполнение millis()» выдаёт кучу ссылок на страницы, где «знатоки» со знанием делом говорят «не ссыте, всё будет нормально, потому что вычитание беззнаковое», не объясняя при этом ничего.

А я объясню, мне не жалко. 🙂

Для того, чтобы убедиться, что «так можно», нужно всего лишь понимать, как в процессорах общего назначения происходит вычитание целых чисел. А происходит оно через сложение.

В этом месте, может, стоило бы рассказать про комбинационную схему «сумматор» и объяснить, как она работает. Для того, чтобы на низком уровне показать, как же, собственно, компьютер складывает числа. И чтобы стало понятно, что вычитать (подобно тому, как это делает человек) процессор не умеет. Он умеет только складывать. Но про (полный) сумматор читайте в другом месте. А здесь и сейчас достаточно понять, что кто-то очень умный придумал, как можно вычитать одно число из другого, имея в качестве инструмента комбинационную схему, которая умеет только складывать (суммировать).

Он придумал представлять отрицательные числа в т.н. «дополнительных кодах«. То есть, если нам надо вычесть из положительного числа А положительное число Б, нужно преобразовать число Б в дополнительный код (получив тем самым число -Б), и после этого спокойно складывать числа «А» и «-Б». Здесь важно то, что беззнаковые числа тоже преобразуются в дополнительный код при вычитании.

Т.е. если наше А = 3 и Б = 1 (00000011 и 00000001 в двоичном представлении), то при вычитании Б из А будут складываться числа 00000011 и 11111111. В результате такого сложения мы получим 00000010, а точнее (1)00000010, где единица, образующаяся при сложении старших разрядов улетает в переполнение (за пределы нашего 8-разрядного целого). То есть, при сложении числа 3 и представленного в дополнительном коде числа -1 мы получили число 2.

В случае с millis(), нам надо было из беззнакового числа 1 вычесть беззнаковое число 0xFFFFFFFE (т.е. двоичное 1111111111111110). Ну, хорошо, преобразуем вычитаемое в дополнительный код и получаем двоичное 0000000000000010. В результате мы складываем 1 и двоичное 10, получая двоичное 11, т.е 3. Т.е. результатом операции «1 — 0xFFFFFFFE» будет 3. И это именно то, что мы рассчитывали получить, проверяя таймер:

1111111111111110 <-- здесь "засекли" таймер
1111111111111111
0000000000000000
0000000000000001 <-- а здесь "1 - 0xFFFFFFFE = 3"

Т.е. всё, что надо, успешно вычитается, и получается именно то, что надо. Вот такая магия.

P.S. Только что осознал, что двоичное представление 32-разрядных чисел у меня получилось 16-разрядным :). Но не буду исправлять, т.к. будет хуже читаться. А на суть явления размерность чисел не влияет. Можно было хоть на 3-разрядных числах это показывать.

(не)переполнение millis(): 3 комментария

  1. Прекрасная статья!

    Хотелось бы только заметить, что критически важно, какая именно конструкция используется для замера временных интервалов, а именно:
    1. millis() — last_time > interval — добро, не подвержено переполнению.

    2. millis() > last_time + interval вполне себе подвержены и не будут корректно работать при прохождении через середину беззнакового int-a…

    Так что важно ещё правильную арифметику не забывать…

Добавить комментарий