Краткий ответ: не пытайтесь «обрабатывать» опрокидывание миллисервера, вместо этого пишите код, безопасный для опрокидывания. Ваш пример кода из учебника в порядке. Если вы попытаетесь обнаружить опрокидывание для принятия корректирующих мер, скорее всего, вы делаете что-то не так. Большинству программ Arduino приходится управлять событиями, которые охватывают относительно короткие промежутки времени, например, отмена кнопки на 50 мс или включение нагревателя на 12 часов ... Затем, и даже если программа рассчитана на несколько лет, опрокидывание миллис не должно быть проблемой.
Правильный способ управлять (или, скорее, избежать необходимости управлять) проблемой опрокидывания - это думать о unsigned long
числе, возвращаемом
millis()
в терминах модульной арифметики . Для математически склонных некоторое знакомство с этой концепцией очень полезно при программировании. Вы можете увидеть математику в действии в статье переполнения миллиметра () Nick Gammon ... плохо? , Для тех, кто не хочет разбираться в вычислительных деталях, я предлагаю здесь альтернативный (надеюсь, более простой) способ мышления об этом. Он основан на простом различии между моментами и продолжительностью . Пока ваши тесты включают только сравнение длительностей, у вас все будет хорошо.
Обратите внимание на тысячные () : здесь сказано все о millis()
одинаково относится и к micros()
, за исключением того , что кроме micros()
переворачивается каждые 71,6 минут, а setMillis()
функция , обеспечиваемая ниже не влияет micros()
.
Моменты, временные метки и длительности
Имея дело со временем, мы должны различать, по крайней мере, два разных понятия: моменты и длительности . Момент - это точка на оси времени. Длительность - это длина временного интервала, то есть расстояние во времени между моментами, которые определяют начало и конец интервала. Различие между этими понятиями не всегда очень резкое в повседневном языке. Например, если я скажу « я вернусь через пять минут », то « пять минут » - это предполагаемая
продолжительность моего отсутствия, тогда как « через пять минут » - это момент
моего предсказанного возвращения. Важно помнить о различии, поскольку это самый простой способ полностью избежать проблемы опрокидывания.
Возвращаемое значение millis()
можно интерпретировать как длительность: время, прошедшее с начала программы до настоящего времени. Эта интерпретация, однако, разрушается, как только миллис переполняется. Как правило, гораздо полезнее думать о том, чтобы millis()
возвратить
временную метку , то есть «метку», обозначающую конкретный момент. Можно утверждать, что эта интерпретация страдает от неоднозначности этих ярлыков, так как они повторно используются каждые 49,7 дней. Это, однако, проблема редко: в большинстве встроенных приложений все, что произошло 49,7 дней назад, является древней историей, нас не волнует. Таким образом, утилизация старых этикеток не должна быть проблемой.
Не сравнивайте временные метки
Попытка выяснить, какая из двух временных меток больше другой, не имеет смысла. Пример:
unsigned long t1 = millis();
delay(3000);
unsigned long t2 = millis();
if (t2 > t1) { ... }
Наивно можно ожидать, что условие if ()
будет всегда истинным. Но это на самом деле будет ложным, если во время переполнения миллисекунд переполняется
delay(3000)
. Думать о t1 и t2 как о метках, пригодных для повторного использования, - это самый простой способ избежать ошибки: метка t1 была явно назначена на момент до t2, но через 49,7 дня она будет переназначена на следующий момент. Таким образом, t1 происходит как до, так и после t2. Это должно прояснить, что выражение t2 > t1
не имеет смысла.
Но, если это просто ярлыки, очевидный вопрос: как мы можем делать с ними полезные вычисления времени? Ответ таков: ограничив себя только двумя вычислениями, которые имеют смысл для отметок времени:
later_timestamp - earlier_timestamp
дает продолжительность, а именно количество времени, прошедшего между более ранним моментом и более поздним моментом. Это наиболее полезная арифметическая операция с использованием меток времени.
timestamp ± duration
возвращает временную метку, которая через некоторое время после (если используется +) или до (если -) начальной временной метки. Не так полезно, как кажется, поскольку полученная временная метка может использоваться только в двух видах вычислений ...
Благодаря модульной арифметике, обе эти функции гарантированно будут работать на протяжении всего цикла пролонгации по крайней мере до тех пор, пока соответствующие задержки не превышают 49,7 дня.
Сравнение продолжительности в порядке
Длительность - это количество миллисекунд, прошедших в течение некоторого интервала времени. Пока нам не нужно обрабатывать длительности, превышающие 49,7 дня, любая физическая операция также должна иметь смысл в вычислительном отношении. Мы можем, например, умножить длительность на частоту, чтобы получить количество периодов. Или мы можем сравнить две длительности, чтобы узнать, какая из них длиннее. Например, вот две альтернативные реализации delay()
. Сначала глючный:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
unsigned long finished = start + ms; // finished: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
if (now >= finished) // comparing timestamps: BUG!
return;
}
}
И вот правильный:
void myDelay(unsigned long ms) { // ms: duration
unsigned long start = millis(); // start: timestamp
for (;;) {
unsigned long now = millis(); // now: timestamp
unsigned long elapsed = now - start; // elapsed: duration
if (elapsed >= ms) // comparing durations: OK
return;
}
}
Большинство программистов на С написали вышеуказанные циклы в более краткой форме, например:
while (millis() < start + ms) ; // BUGGY version
а также
while (millis() - start < ms) ; // CORRECT version
Хотя они выглядят обманчиво похожими, различие между меткой времени и продолжительностью должно ясно указывать, какая из них глючная, а какая - правильная.
Что если мне действительно нужно сравнить метки времени?
Лучше постарайся избежать ситуации. Если это неизбежно, остается надежда, если известно, что соответствующие моменты достаточно близки: ближе, чем 24,85 дня. Да, наша максимальная задержка в 49,7 дня сократилась вдвое.
Очевидное решение состоит в том, чтобы преобразовать нашу проблему сравнения временных меток в проблему сравнения продолжительности. Скажем, нам нужно знать, наступил ли момент t1 до или после t2. Выберем некоторое эталонное мгновение в их общем прошлом, и сравнить длительности этой ссылки до как t1 и t2. Эталонный момент получается путем вычитания достаточно большой продолжительности из t1 или t2:
unsigned long reference_instant = t2 - LONG_ENOUGH_DURATION;
unsigned long from_reference_until_t1 = t1 - reference_instant;
unsigned long from_reference_until_t2 = t2 - reference_instant;
if (from_reference_until_t1 < from_reference_until_t2)
// t1 is before t2
Это можно упростить как:
if (t1 - t2 + LONG_ENOUGH_DURATION < LONG_ENOUGH_DURATION)
// t1 is before t2
Заманчиво упрощать дальше if (t1 - t2 < 0)
. Очевидно, что это не работает, потому что t1 - t2
, будучи вычисленным как число без знака, не может быть отрицательным. Это, однако, хотя и не переносимо, работает:
if ((signed long)(t1 - t2) < 0) // works with gcc
// t1 is before t2
Ключевое слово, signed
приведенное выше, является избыточным (просто long
подписывается всегда), но оно помогает прояснить цель. Преобразование в подписанный длинный эквивалентно установке, LONG_ENOUGH_DURATION
равной 24,85 дням. Уловка не переносима, потому что, согласно стандарту C, результат определяется реализацией . Но поскольку компилятор gcc обещает поступить правильно , он надежно работает на Arduino. Если мы хотим избежать поведения, определенного реализацией, приведенное выше сравнение со знаком математически эквивалентно этому:
#include <limits.h>
if (t1 - t2 > LONG_MAX) // too big to be believed
// t1 is before t2
с единственной проблемой, что сравнение выглядит в обратном направлении. Это также эквивалентно, пока longs 32-битные, для этого однобитового теста:
if ((t1 - t2) & 0x80000000) // test the "sign" bit
// t1 is before t2
Последние три теста на самом деле скомпилированы gcc в один и тот же машинный код.
Как мне проверить мой эскиз против опрокидывания миллис
Если вы будете следовать вышеизложенным заповедям, у вас все должно быть хорошо. Если вы все же хотите протестировать, добавьте эту функцию в ваш эскиз:
#include <util/atomic.h>
void setMillis(unsigned long ms)
{
extern unsigned long timer0_millis;
ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
timer0_millis = ms;
}
}
и теперь вы можете путешествовать во времени по вашей программе, позвонив по телефону
setMillis(destination)
. Если вы хотите, чтобы он снова и снова проходил через переполнение Миллиса, как Фил Коннорс переживает День сурка, вы можете поместить это внутрь loop()
:
// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
setMillis(-3000);
Вышеуказанная отрицательная метка времени (-3000) неявно преобразуется компилятором в длинную без знака, соответствующую 3000 миллисекундам до ролловера (она преобразуется в 4294964296).
Что, если мне действительно нужно отслеживать очень длительные периоды?
Если вам нужно включить реле и выключить его через три месяца, то вам действительно необходимо отслеживать переполнение миллис. Есть много способов сделать это. Наиболее простым решением может быть расширение millis()
до 64 бит:
uint64_t millis64() {
static uint32_t low32, high32;
uint32_t new_low32 = millis();
if (new_low32 < low32) high32++;
low32 = new_low32;
return (uint64_t) high32 << 32 | low32;
}
По сути, это подсчет событий опрокидывания и использование этого количества в качестве 32 старших значащих бит 64-битного миллисекунды. Чтобы этот подсчет работал правильно, функцию необходимо вызывать не реже одного раза в 49,7 дня. Однако, если он вызывается только один раз в 49,7 дней, в некоторых случаях возможно, что проверка (new_low32 < low32)
не пройдена и код пропустит счетчик high32
. Использование millis () для определения того, когда совершать единственный вызов этого кода в одной «оболочке» из millis (конкретное 49,7-дневное окно), может быть очень опасным, в зависимости от того, как выстроены временные рамки. В целях безопасности, если с помощью функции millis () определить, когда совершать единственные вызовы функции millis64 (), должно быть не менее двух вызовов в каждом 49,7-дневном окне.
Имейте в виду, однако, что 64-битная арифметика стоит дорого на Arduino. Возможно, стоит уменьшить временное разрешение, чтобы остаться на 32 битах.