Как справиться с rollover millis()?

Мне нужно считывать показания датчика каждые пять минут, но, поскольку у моего скетча есть и другие задачи, я не могу просто использовать delay() между показаниями. Существует учебник "Мигание без задержки ", в котором предлагается, чтобы я кодировал в следующих строках:

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 < Предыдущиймиллиметр), но я не уверен, что тогда делать.

Таким образом, мой вопрос: каков был бы правильный/самый простой способ обработки опрокидывания millis ()?

, 👍132

Обсуждение

Примечание редакции: Это не совсем мой вопрос, скорее учебник в формате вопрос/ответ. Я был свидетелем большой путаницы в Интернете (в том числе здесь) по этой теме, и этот сайт кажется очевидным местом для поиска ответа. Вот почему я предоставляю этот учебник здесь., @Edgar Bonet

Я бы сделал "Предыдущий миллиметр += интервал "вместо" Предыдущий миллиметр = текущий миллиметр", если бы мне нужна была определенная частота результатов., @Jasen

@Jasen: Совершенно верно! "Предыдущий миллиметр += интервал", если вам нужна постоянная частота и вы уверены, что ваша обработка занимает меньше, чем "интервал", но "Предыдущий миллиметр = текущий миллиметр" для гарантии минимальной задержки "интервала"., @Edgar Bonet

Один из "трюков", которые я использую, состоит в том, чтобы уменьшить нагрузку на arduino, используя наименьшее значение int, содержащее интервал. Например, для интервалов не более 1 минуты я пишу uint16_t предыдущий миллиметр; постоянный интервал uint16_t = 45000; ... uint16_t текущий миллиметр = (uint16_t) миллиметр(); если ((текущий миллиметр - предыдущий миллиметр) >= интервал) ..., @frarugi87

@frarugi87: Я иногда делаю то же самое. Обратите внимание, однако, что, если этот тип окажется меньше, чем " int " ("uint8_t", но также "uint16_t" на ARM), вы должны привести результат вычитания к соответствующему типу. В противном случае из-за [целочисленных рекламных акций](https://en.cppreference.com/w/c/language/conversion#Integer_promotions), вы в конечном итоге получаете неправильный тип. Кстати, актерский состав, который вы написали (кастинг millis()), является избыточным., @Edgar Bonet

@EdgarBonet Я никогда полностью не понимал автоматические рекламные акции, поэтому простите меня, если этот вопрос покажется глупым, но.. если все элементы (currentMillis, previousMillis, интервал) являются uint16_t, будет ли это "способствовать" вычитанию, скажем, int32_t (и таким образом нарушит то, что я хотел сделать)? Что касается приведения, то это просто по привычке (неявные приведения вызывают предупреждения MISRA, поэтому я стараюсь избегать неявных приведений), @frarugi87

@frarugi87: Для каждой двоичной операции, если оба аргумента меньше простого "int", то они оба неявно повышаются до "int". В противном случае меньшее преобразуется в тип большего, с предпочтением беззнаковым типам (я немного упрощаю, но это основная идея)., @Edgar Bonet

@EdgarBonet спасибо за объяснение; Мне нужно иметь это в виду при выполнении <250 мс циклов на 8-разрядных процессорах для экономии памяти... (8-разрядные операции на 16-разрядных процессорах, а также 8/16-разрядные операции на 32-разрядных процессорах не сохраняют память и флэш-память), @frarugi87

Я бы хотел, чтобы какой-нибудь ответ включал мнение о перезагрузке вашего устройства или решение, в котором вы вручную обнаруживаете переполнение (millis() < 1000) и сбрасываете временные метки переменных, которые используют nextUpdateMillis как if (currentMillis <= nextUpdateMillis) { }., @AgainPsychoX


4 ответа


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

178

Короткий ответ: не пытайтесь “обработать” опрокидывание миллиса, вместо этого напишите код, безопасный для опрокидывания. Ваш пример кода из учебника в порядке. Если вы попытаетесь обнаружить опрокидывание для реализации корректирующих мер, скорее всего, вы делаете что-то не так. Большинству программ Arduino приходится управлять только событиями относительно короткой продолжительности, такими как отключение кнопки на 50 мс или включение обогревателя на 12 часов... Тогда, и даже если программа предназначена для работы в течение многих лет, ролловер в миллис не должен вызывать беспокойства.

Правильный способ управления (или, скорее, избежать необходимости управления) проблемой опрокидывания состоит в том, чтобы рассматривать длинное число без знака, возвращаемое millis (), в терминах модульной арифметики. Для склонных к математике некоторое знакомство с этой концепцией очень полезно при программировании. Вы можете увидеть математику в действии в статье Ника Гэммона " Переполнение миллиса ()"... что-то плохое?. Для рассматриваемой проблемы важно знать, что в модульной арифметике числа “обернитесь” при достижении определенного значения – модуля – так, чтобы 1 − модуль-это не отрицательное число, а 1 (подумайте о 12 −часовых часах, где модуль равен 12: здесь 1 - 12 = 1).

Для тех, кто не хочет вдаваться в детали вычислений, я предлагаю здесь альтернативный (надеюсь, более простой) способ мышления об этом. Он основан на простом различии между мгновениями и длительностью. Пока ваши тесты включают только сравнение продолжительности, с вами все должно быть в порядке.

Примечание к micros(): Все, что здесь сказано о 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> не имеет смысла.

Но если это всего лишь ярлыки, то очевидный вопрос заключается в следующем: как мы можем с их помощью производить какие-либо полезные вычисления времени? Ответ таков: ограничившись только двумя вычислениями, которые имеют смысл для временных меток:

  1. later_timestamp - более ранняя метка времени дает длительность, а именно количество времени, прошедшее между более ранним моментом и более поздним моментом. Это наиболее полезная арифметическая операция, включающая временные метки.
  2. метка времени ± длительность дает метку времени, которая находится через некоторое время после (если используется +) или до (если −) начальной метки времени. Не так полезно, как кажется, поскольку полученная временная метка может использоваться только в двух видах вычислений...

Благодаря модульной арифметике, оба они гарантированно будут работать нормально во время ролловера millis, по крайней мере, до тех пор, пока связанные с этим задержки будут короче 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;
    }
}

Большинство программистов на C написали бы вышеуказанные циклы в более сжатой форме, например

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

Заманчиво еще больше упростить, если (t1 - t2 < 0). Очевидно, что это не работает, потому что t1 - t2, вычисляемый как число без знака, не может быть отрицательным. Это, однако, хотя и не портативно, но работает:

if ((signed long)(t1 - t2) < 0)  // works with gcc
    // t1 is before t2

Ключевое слово, подписанное выше, является избыточным (всегда подписывается простое длинное), но оно помогает прояснить намерение. Преобразование в длинное значение со знаком эквивалентно установке значения 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

с единственной проблемой, что сравнение выглядит в обратном направлении. Это также эквивалентно, если длина 32 бита, этому однобитовому тесту:

if ((t1 - t2) & 0x80000000)  // test the "sign" bit
    // t1 is before t2

Последние три теста фактически скомпилированы gcc в один и тот же машинный код.

Как мне проверить свой скетч на соответствие опрокидыванию millis

Если вы будете следовать вышеприведенным наставлениям, у вас все должно быть хорошо. Если вы все же хотите протестировать, добавьте эту функцию в свой скетч:

#include <util/atomic.h>

void setMillis(unsigned long ms)
{
    extern unsigned long timer0_millis;
    ATOMIC_BLOCK (ATOMIC_RESTORESTATE) {
        timer0_millis = ms;
    }
}

и теперь вы можете путешествовать во времени по своей программе, вызвав SetMillis(пункт назначения). Если вы хотите, чтобы это повторялось снова и снова, как Фил Коннорс, переживающий День сурка, вы можете поместить это в loop():

// 6-second time loop starting at rollover - 3 seconds
if (millis() - (-3000) >= 6000)
    setMillis(-3000);

Отрицательная временная метка выше (-3000) неявно преобразуется компилятором в unsigned long, соответствующую 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 бит.

,

Итак, вы хотите сказать, что код, написанный в вопросе, действительно будет работать правильно?, @Jasen

@Jasen: Вот именно! Мне не раз казалось, что люди пытаются “исправить” проблему, которой изначально не существовало., @Edgar Bonet

Я рад, что нашел это. У меня уже был этот вопрос раньше., @Sebastian Freeman

Один из лучших и самых полезных ответов на StackExchange! Большое спасибо! :), @Falko

Это такой удивительный ответ на вопрос. Я возвращаюсь к этому ответу в основном раз в год, потому что я параноик, боящийся испортить ролловеры., @Jeffrey Cash

Это фантастический ответ, за исключением того, что больше не представляется возможным "видеть" timer0_millis из скетча; вы можете объявить его внешним, но компоновщик его не видит. проводка.c не объявляет timer0_millis статическим, поэтому я не уверен, что именно они делают, но ответ может потребовать обновления., @Andrew Kohlsmith

@AndrewKohlsmith: Я только что протестировал "setMillis ()" на Uno с помощью Arduino IDE 1.8.10. Это работает так, как и ожидалось., @Edgar Bonet

+1. Такой глубокий ответ, что я думаю, что мой millis() перевернулся, прочитав его., @CharlieHanson

Что касается использования планирования будущих событий, то мне очень нравятся формы сравнения меток времени, а не принуждение их к общему шаблону сравнения всегда неотрицательных длительностей с момента прошлых событий. // Вы протестировали три кода сравнения временных меток на GCC, знаете ли вы компиляторы, которые делают неправильные вещи/ведут себя по-разному с разными кодами? // Будет ли `if ((t1 - t2) & 0x80000000) // тестировать бит "знак" на самом деле переносимым на машинах с более широкими размерами без знака?, @Dave X

@DaveX: Re: “_ знаете ли вы о компиляторах, которые делают неправильные вещи [при преобразовании целого числа без знака в знаковое]?_”: Я этого не делаю, и я не думаю, что такой компилятор будет легко найти. Сокращение n-битного целого числа по модулю 2^n-единственный вариант, который имеет смысл с дополнением two, а другие целочисленные представления со знаком практически исчезли. Обратите внимание, что начиная с C++20 такое поведение является обязательным. Несмотря на то, что Arduino, возможно, не использует C++20, я воспринимаю это как признак того, что большинство современных реализаций C++ уже делают это., @Edgar Bonet

@DaveX: Re: “_Would if ((t1 - t2) & 0x80000000) [...] быть переносимым на машинах с более широким unsigned long sizes_”: Это не будет проверять знаковый бит (который является 0x8000000000000000 на 64-битной длине), но он все равно будет работать, если t1 и t2 находятся на расстоянии менее 2^31 мс друг от друга., @Edgar Bonet

Спасибо за этот ответ! Если меня интересуют только очень короткие промежутки времени (скажем, только переключатели с устранением дребезга), кажется, что нет причин использовать тип «длинный без знака». Я мог бы использовать байт, если я не собираюсь иметь продолжительность более 255 мс, верно? (Я понимаю, что millis() возвращает "unsigned long", но в этом случае я мог бы написать свою собственную функцию, которая возвращает байт), @RobD

@RobD: В принципе, да. Однако вы должны знать о [неявных преобразованиях](https://en.cppreference.com/w/cpp/language/usual_arithmetic_conversions ), выполняемых C++. Если started и now являются байтами (тип uint8_t), то now-started является (потенциально отрицательным) int. Использование unsigned int было бы безопаснее (без интегрального расширения), если вы не пишете выражение типа millis()-started, которое неявно преобразует started в unsigned long., @Edgar Bonet


36

TL;DR Краткая версия:

unsigned long равна от 0 до 4 294 967 295 (2^32 - 1).

Итак, допустим, что предыдущее значение составляет 4 294 967 290 (5 мс до опрокидывания), а текущее значение равно 10 (10 мс после опрокидывания). Тогда currentMillis - previousMillis фактически равен 16 (не -4 294 967 280), так как результат будет рассчитан как unsigned long (который не может быть отрицательным, поэтому сам будет вращаться). Вы можете проверить это просто:

Serial.println( ( unsigned long ) ( 10 - 4294967290 ) ); // 16

Таким образом, приведенный выше код будет работать совершенно нормально. Хитрость в том, чтобы всегда вычислять разницу во времени, а не сравнивать два значения времени.

,

Как насчет **15 мс** до опрокидывания и **10 мс** после опрокидывания (т. е. 49,7 дней *после*). **15 > 10**, но штампу **15 мс** почти полтора месяца. *15-10 >> 0 и 10-15 >>> 0* логика "без знака", так что здесь это бесполезно!, @xyz

@prakharsingh95 10 мс-15 мс станут ~49,7 дней - 5 мс, что является правильной разницей. Математика работает до тех пор, пока millis() не перевернется дважды, но это вряд ли произойдет с рассматриваемым кодом., @BrettAM

Позвольте мне перефразировать. Предположим, у вас есть две временные метки 200 мс и 10 мс. Как вы определяете, какие из них(перевернуты)?, @xyz

@prakharsingh95 Тот, который хранится в "Предыдущем миллиметре", должен был быть измерен до "текущего миллиметра", поэтому, если "Текущий миллиметр" меньше, чем "Предыдущий миллиметр", произошел опрокидывание. Математика такова, что, если не произошло двух опрокидываний, вам даже не нужно об этом думать., @BrettAM

А, ладно. если вы сделаете "t2-t1`, и если вы можете гарантировать, что" t1 "измеряется до "t2", то это эквивалентно **подписанному** "(t2-t1)% 4,294,967,295", следовательно, автоматическое обтекание. Мило!. Но что, если есть два ролловера или "интервал" составляет > 4 294 967 295?, @xyz

Интервалы, превышающие 49 дней, будут немного сложнее, но в большинстве приложений не очень полезны, так как кристалл arduino не настолько точен. Более 49 эта неточность может привести к многочасовым ошибкам. Многократные ролловеры не могут произойти, так как предыдущий миллиметр изменяется по истечении интервала. PS формула на самом деле будет "(t2-t1+4,294,967,295) % 4,294,967,295", поскольку по модулю также будут получены отрицательные значения (например, "-3% 2==-1"), @Gerben

нужно ли приводить результат вычисления к значению без знака long? Или это будет автоматически, если вычисление выполняется с двумя переменными, уже объявленными как длинные без знака?, @Mausy5043

Принятый ответ очень хорош, но эта часть не объяснена., @NateS


1

Оберните millis() в класс!

Логика:

  1. Используйте идентификаторы вместо функции millis() напрямую.
  2. Сравните развороты, используя идентификаторы. Это чисто и не зависит от опрокидывания.
  3. Для конкретных приложений, чтобы рассчитать точную разницу между двумя идентификаторами, следите за разворотами и отметками. Рассчитайте разницу.

Отслеживание разворотов:

  1. Периодически обновляйте локальную метку быстрее, чем millis(). Это поможет вам узнать, не вылетела ли функция millis ().
  2. Период таймера определяет точность
class Timer {

public:
    static long last_stamp;
    static long *stamps;
    static int *reversals;
    static int count;
    static int reversal_count;

    static void setup_timer() {
        // Setup Timer2 overflow to fire every 8ms (125Hz)
        //   period [sec] = (1 / f_clock [sec]) * prescale * (255-count)
        //                  (1/16000000)  * 1024 * (255-130) = .008 sec


        TCCR2B = 0x00;        // Disable Timer2 while we set it up

        TCNT2  = 130;         // Reset Timer Count  (255-130) = execute ev 125-th T/C clock
        TIFR2  = 0x00;        // Timer2 INT Flag Reg: Clear Timer Overflow Flag
        TIMSK2 = 0x01;        // Timer2 INT Reg: Timer2 Overflow Interrupt Enable
        TCCR2A = 0x00;        // Timer2 Control Reg A: Wave Gen Mode normal
        TCCR2B = 0x07;        // Timer2 Control Reg B: Timer Prescaler set to 1024

        count = 0;
        stamps = new long[50];
        reversals = new int [10];
        reversal_count =0;
    }

    static long get_stamp () {
        stamps[count++] = millis();
        return count-1;
    }

    static bool compare_stamps_by_id(int s1, int s2) {
        return s1 > s2;
    }

    static long long get_stamp_difference(int s1, int s2) {
        int no_of_reversals = 0;
        for(int j=0; j < reversal_count; j++)
        if(reversals[j] < s2 && reversals[j] > s1)
            no_of_reversals++;
        return stamps[s2]-stamps[s1] + 49.7 * 86400 * 1000;       
    }

};

long Timer::last_stamp;
long *Timer::stamps;
int *Timer::reversals;
int Timer::count;
int Timer::reversal_count;

ISR(TIMER2_OVF_vect) {

    long stamp = millis();
    if(stamp < Timer::last_stamp) // reversal
        Timer::reversals[Timer::reversal_count++] = Timer::count;
    else 
        ; // no reversal
    Timer::last_stamp = stamp;    
    TCNT2 = 130;     // reset timer ct to 130 out of 255
    TIFR2 = 0x00;    // timer2 int flag reg: clear timer overflow flag
};

// Usage

void setup () {
    Timer::setup_timer();

    long s1 = Timer::get_stamp();
    delay(3000);
    long s2 = Timer::get_stamp();

    Timer::compare_stamps_by_id(s1, s2); // true

    Timer::get_stamp_difference(s1, s2); // return true difference, taking into account reversals
}

Кредиты по таймеру.

,

Я отредактировал код, чтобы удалить ошибки maaaaany, которые мешали его компиляции. Это обойдется вам примерно в 232 байта оперативной памяти и два канала ШИМ. Он также начнет повреждать память после того, как вы "get_stamp ()" 51 раз. Сравнение задержек вместо временных меток, безусловно, будет более эффективным., @Edgar Bonet

Разве этот полный статический класс не похож на пространство имен?, @AgainPsychoX


6

Мне понравился этот вопрос и отличные ответы, которые он дал. Сначала быстрый комментарий к предыдущему ответу (я знаю, я знаю, но у меня еще нет репутации, чтобы комментировать. :-).

Ответ Эдгара Бонета был удивительным. Я занимаюсь кодированием уже 35 лет, и сегодня я узнал кое-что новое. Спасибо. Тем не менее, я верю, что код "Что, если мне действительно нужно отслеживать очень длительные интервалы?" прерывается, если вы не вызываете millis64() по крайней мере один раз за период опрокидывания. Действительно придирчивый, и вряд ли это будет проблемой в реальной реализации, но вот и все.

Теперь, если вы действительно хотели, чтобы метки времени охватывали любой нормальный временной диапазон (64-разрядные миллисекунды составляют около полумиллиарда лет по моему подсчету), кажется простым расширить существующую реализацию millis() до 64 бит.

Эти изменения в attinycore/проводка.c (Я работаю с ATtiny85), похоже, работает (я предполагаю, что код для других AVR очень похож). Смотрите строки с комментариями //BFB и новой функцией millis64 (). Очевидно, что он будет как больше (98 байт кода, 4 байта данных), так и медленнее, и, как отметил Эдгар, вы почти наверняка сможете достичь своих целей, просто лучше разбираясь в математике беззнаковых целых чисел, но это было интересное упражнение.

volatile unsigned long long timer0_millis = 0;      // BFB: need 64-bit resolution

#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
ISR(TIM0_OVF_vect)
#else
ISR(TIMER0_OVF_vect)
#endif
{
    // copy these to local variables so they can be stored in registers
    // (volatile variables must be read from memory on every access)
    unsigned long long m = timer0_millis;       // BFB: need 64-bit resolution
    unsigned char f = timer0_fract;

    m += MILLIS_INC;
    f += FRACT_INC;
    if (f >= FRACT_MAX) {
        f -= FRACT_MAX;
        m += 1;
    }

    timer0_fract = f;
    timer0_millis = m;
    timer0_overflow_count++;
}

// BFB: 64-bit version
unsigned long long millis64()
{
    unsigned long long m;
    uint8_t oldSREG = SREG;

    // disable interrupts while we read timer0_millis or we might get an
    // inconsistent value (e.g. in the middle of a write to timer0_millis)
    cli();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}
,

Вы правы, мой "millis64 ()" работает только в том случае, если он вызывается чаще, чем период опрокидывания. Я отредактировал свой ответ, чтобы указать на это ограничение. В вашей версии этой проблемы нет, но она имеет еще один недостаток: она использует 64-разрядную арифметику _ в контексте прерывания_, что иногда увеличивает задержку при реагировании на другие прерывания., @Edgar Bonet