Мне нужно читать датчик каждые пять минут, но, поскольку у моего эскиза есть и другие задачи, я не могу просто delay()
между показаниями. Существует обучающее руководство по Blink без промедления, предлагающее кодировать по следующим направлениям:
void loop()
{
unsigned long currentMillis = millis();
// Read the sensor when needed.
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
readSensor();
}
// Do other stuff...
}
Проблема заключается в том, что millis()
после 49,7 дней он снова обернется к нулю. Поскольку мой эскиз предназначен для работы дольше, я должен убедиться, что при опрокидывании мой эскиз не потерпит неудачу. Я легко могу определить условие опрокидывания ( currentMillis < previousMillis
), но я не уверен, что делать дальше.
Таким образом, мой вопрос: каков был бы правильный / самый простой способ справиться с
millis()
опрокидыванием?
programming
time
millis
Эдгар Бонет
источник
источник
previousMillis += interval
вместо того,previousMillis = currentMillis
чтобы хотеть определенную частоту результатов.previousMillis += interval
если вы хотите постоянную частоту и уверены, что ваша обработка занимает меньшеinterval
, ноpreviousMillis = currentMillis
для обеспечения минимальной задержкиinterval
.uint16_t previousMillis; const uint16_t interval = 45000; ... uint16_t currentMillis = (uint16_t) millis(); if ((currentMillis - previousMillis) >= interval) ...
Ответы:
Краткий ответ: не пытайтесь «обрабатывать» опрокидывание миллисервера, вместо этого пишите код, безопасный для опрокидывания. Ваш пример кода из учебника в порядке. Если вы попытаетесь обнаружить опрокидывание для принятия корректирующих мер, скорее всего, вы делаете что-то не так. Большинству программ Arduino приходится управлять событиями, которые охватывают относительно короткие промежутки времени, например, отмена кнопки на 50 мс или включение нагревателя на 12 часов ... Затем, и даже если программа рассчитана на несколько лет, опрокидывание миллис не должно быть проблемой.
Правильный способ управлять (или, скорее, избежать необходимости управлять) проблемой опрокидывания - это думать о
unsigned long
числе, возвращаемомmillis()
в терминах модульной арифметики . Для математически склонных некоторое знакомство с этой концепцией очень полезно при программировании. Вы можете увидеть математику в действии в статье переполнения миллиметра () Nick Gammon ... плохо? , Для тех, кто не хочет разбираться в вычислительных деталях, я предлагаю здесь альтернативный (надеюсь, более простой) способ мышления об этом. Он основан на простом различии между моментами и продолжительностью . Пока ваши тесты включают только сравнение длительностей, у вас все будет хорошо.Обратите внимание на тысячные () : здесь сказано все о
millis()
одинаково относится и кmicros()
, за исключением того , что кромеmicros()
переворачивается каждые 71,6 минут, аsetMillis()
функция , обеспечиваемая ниже не влияетmicros()
.Моменты, временные метки и длительности
Имея дело со временем, мы должны различать, по крайней мере, два разных понятия: моменты и длительности . Момент - это точка на оси времени. Длительность - это длина временного интервала, то есть расстояние во времени между моментами, которые определяют начало и конец интервала. Различие между этими понятиями не всегда очень резкое в повседневном языке. Например, если я скажу « я вернусь через пять минут », то « пять минут » - это предполагаемая продолжительность моего отсутствия, тогда как « через пять минут » - это момент моего предсказанного возвращения. Важно помнить о различии, поскольку это самый простой способ полностью избежать проблемы опрокидывания.
Возвращаемое значение
millis()
можно интерпретировать как длительность: время, прошедшее с начала программы до настоящего времени. Эта интерпретация, однако, разрушается, как только миллис переполняется. Как правило, гораздо полезнее думать о том, чтобыmillis()
возвратить временную метку , то есть «метку», обозначающую конкретный момент. Можно утверждать, что эта интерпретация страдает от неоднозначности этих ярлыков, так как они повторно используются каждые 49,7 дней. Это, однако, проблема редко: в большинстве встроенных приложений все, что произошло 49,7 дней назад, является древней историей, нас не волнует. Таким образом, утилизация старых этикеток не должна быть проблемой.Не сравнивайте временные метки
Попытка выяснить, какая из двух временных меток больше другой, не имеет смысла. Пример:
Наивно можно ожидать, что условие
if ()
будет всегда истинным. Но это на самом деле будет ложным, если во время переполнения миллисекунд переполняетсяdelay(3000)
. Думать о t1 и t2 как о метках, пригодных для повторного использования, - это самый простой способ избежать ошибки: метка t1 была явно назначена на момент до t2, но через 49,7 дня она будет переназначена на следующий момент. Таким образом, t1 происходит как до, так и после t2. Это должно прояснить, что выражениеt2 > t1
не имеет смысла.Но, если это просто ярлыки, очевидный вопрос: как мы можем делать с ними полезные вычисления времени? Ответ таков: ограничив себя только двумя вычислениями, которые имеют смысл для отметок времени:
later_timestamp - earlier_timestamp
дает продолжительность, а именно количество времени, прошедшего между более ранним моментом и более поздним моментом. Это наиболее полезная арифметическая операция с использованием меток времени.timestamp ± duration
возвращает временную метку, которая через некоторое время после (если используется +) или до (если -) начальной временной метки. Не так полезно, как кажется, поскольку полученная временная метка может использоваться только в двух видах вычислений ...Благодаря модульной арифметике, обе эти функции гарантированно будут работать на протяжении всего цикла пролонгации по крайней мере до тех пор, пока соответствующие задержки не превышают 49,7 дня.
Сравнение продолжительности в порядке
Длительность - это количество миллисекунд, прошедших в течение некоторого интервала времени. Пока нам не нужно обрабатывать длительности, превышающие 49,7 дня, любая физическая операция также должна иметь смысл в вычислительном отношении. Мы можем, например, умножить длительность на частоту, чтобы получить количество периодов. Или мы можем сравнить две длительности, чтобы узнать, какая из них длиннее. Например, вот две альтернативные реализации
delay()
. Сначала глючный:И вот правильный:
Большинство программистов на С написали вышеуказанные циклы в более краткой форме, например:
а также
Хотя они выглядят обманчиво похожими, различие между меткой времени и продолжительностью должно ясно указывать, какая из них глючная, а какая - правильная.
Что если мне действительно нужно сравнить метки времени?
Лучше постарайся избежать ситуации. Если это неизбежно, остается надежда, если известно, что соответствующие моменты достаточно близки: ближе, чем 24,85 дня. Да, наша максимальная задержка в 49,7 дня сократилась вдвое.
Очевидное решение состоит в том, чтобы преобразовать нашу проблему сравнения временных меток в проблему сравнения продолжительности. Скажем, нам нужно знать, наступил ли момент t1 до или после t2. Выберем некоторое эталонное мгновение в их общем прошлом, и сравнить длительности этой ссылки до как t1 и t2. Эталонный момент получается путем вычитания достаточно большой продолжительности из t1 или t2:
Это можно упростить как:
Заманчиво упрощать дальше
if (t1 - t2 < 0)
. Очевидно, что это не работает, потому чтоt1 - t2
, будучи вычисленным как число без знака, не может быть отрицательным. Это, однако, хотя и не переносимо, работает:Ключевое слово,
signed
приведенное выше, является избыточным (простоlong
подписывается всегда), но оно помогает прояснить цель. Преобразование в подписанный длинный эквивалентно установке,LONG_ENOUGH_DURATION
равной 24,85 дням. Уловка не переносима, потому что, согласно стандарту C, результат определяется реализацией . Но поскольку компилятор gcc обещает поступить правильно , он надежно работает на Arduino. Если мы хотим избежать поведения, определенного реализацией, приведенное выше сравнение со знаком математически эквивалентно этому:с единственной проблемой, что сравнение выглядит в обратном направлении. Это также эквивалентно, пока longs 32-битные, для этого однобитового теста:
Последние три теста на самом деле скомпилированы gcc в один и тот же машинный код.
Как мне проверить мой эскиз против опрокидывания миллис
Если вы будете следовать вышеизложенным заповедям, у вас все должно быть хорошо. Если вы все же хотите протестировать, добавьте эту функцию в ваш эскиз:
и теперь вы можете путешествовать во времени по вашей программе, позвонив по телефону
setMillis(destination)
. Если вы хотите, чтобы он снова и снова проходил через переполнение Миллиса, как Фил Коннорс переживает День сурка, вы можете поместить это внутрьloop()
:Вышеуказанная отрицательная метка времени (-3000) неявно преобразуется компилятором в длинную без знака, соответствующую 3000 миллисекундам до ролловера (она преобразуется в 4294964296).
Что, если мне действительно нужно отслеживать очень длительные периоды?
Если вам нужно включить реле и выключить его через три месяца, то вам действительно необходимо отслеживать переполнение миллис. Есть много способов сделать это. Наиболее простым решением может быть расширение
millis()
до 64 бит:По сути, это подсчет событий опрокидывания и использование этого количества в качестве 32 старших значащих бит 64-битного миллисекунды. Чтобы этот подсчет работал правильно, функцию необходимо вызывать не реже одного раза в 49,7 дня. Однако, если он вызывается только один раз в 49,7 дней, в некоторых случаях возможно, что проверка
(new_low32 < low32)
не пройдена и код пропустит счетчикhigh32
. Использование millis () для определения того, когда совершать единственный вызов этого кода в одной «оболочке» из millis (конкретное 49,7-дневное окно), может быть очень опасным, в зависимости от того, как выстроены временные рамки. В целях безопасности, если с помощью функции millis () определить, когда совершать единственные вызовы функции millis64 (), должно быть не менее двух вызовов в каждом 49,7-дневном окне.Имейте в виду, однако, что 64-битная арифметика стоит дорого на Arduino. Возможно, стоит уменьшить временное разрешение, чтобы остаться на 32 битах.
источник
TL; DR Короткая версия:
An
unsigned long
составляет от 0 до 4 294 967 295 (2 ^ 32 - 1).Допустим, что
previousMillis
это 4 294 967 290 (5 мс до ролловера) иcurrentMillis
10 (10 мс после ролловера). ТогдаcurrentMillis - previousMillis
фактическое значение 16 (не -4,294,967,280), так как результат будет рассчитан как длинная без знака (которая не может быть отрицательной, поэтому сама будет катиться). Вы можете проверить это просто:Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16
Таким образом, приведенный выше код будет работать отлично. Хитрость заключается в том, чтобы всегда вычислять разницу во времени, а не сравнивать два значения времени.
источник
unsigned
логика, так что здесь нет смысла!millis()
перевернется дважды, но это вряд ли произойдет с рассматриваемым кодом.previousMillis
, должен быть измерен ранееcurrentMillis
, поэтому, еслиcurrentMillis
он меньше, чемpreviousMillis
произошло опрокидывание. Математика работает так: если не произошло двух опрокидываний, вам даже не нужно об этом думать.t2-t1
, и если вы можете гарантировать,t1
что измеряется раньше,t2
то это эквивалентно знаку(t2-t1)% 4,294,967,295
, отсюда и автоматическое завершение. Приятно!. Но что, если есть два ролловера, илиinterval
> 4 294 967 295?Заворачивай
millis()
в класс!Логика:
millis()
напрямую.Отслеживание разворотов:
millis()
. Это поможет вам узнать,millis()
переполнен ли он.Таймер кредитов .
источник
get_stamp()
51 раз. Сравнение задержек вместо временных меток, безусловно, будет более эффективным.Мне очень понравился этот вопрос и прекрасные ответы, которые он породил. Сначала быстрый комментарий к предыдущему ответу (я знаю, я знаю, но у меня пока нет представителя, чтобы комментировать. :-).
Ответ Эдгара Бонета был удивительным. Я занимаюсь программированием 35 лет, и сегодня я узнал что-то новое. Спасибо. Тем не менее, я считаю, что код "Что если мне действительно нужно отслеживать очень длинные периоды?" перерывы, если вы не вызываете millis64 () хотя бы один раз за период ролловера. Действительно придирчивый, и вряд ли будет проблемой в реальной реализации, но все готово.
Теперь, если вы действительно хотите, чтобы временные метки охватывали любой разумный временной диапазон (по моим подсчетам, 64-битные миллисекунды - это примерно полмиллиарда лет), то выглядит просто расширить существующую реализацию millis () до 64-битных.
Эти изменения в attinycore / wiring.c (я работаю с ATTiny85), кажется, работают (я предполагаю, что код для других AVR очень похож). Смотрите строки с комментариями // BFB и новой функцией millis64 (). Очевидно, что он будет и больше (98 байт кода, 4 байта данных) и медленнее, и, как отметил Эдгар, вы почти наверняка сможете достичь своих целей, просто лучше понимая математику без знака, но это было интересное упражнение. ,
источник
millis64()
работает только в том случае, если он вызывается чаще, чем период пролонгации. Я отредактировал свой ответ, чтобы указать на это ограничение. Ваша версия не имеет этой проблемы, но имеет другой недостаток: она выполняет 64-битную арифметику в контексте прерываний , что иногда увеличивает задержку при ответе на другие прерывания.