Как я могу обработать опрос millis ()?

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

 void loop ()
{
    unsigned long currentMillis = millis ();

    //Прочитайте датчик при необходимости.
    if (currentMillis - previousMillis> интервал) {
        previousMillis = currentMillis;
        readSensor ();
    }

    //Делаем другие вещи ...
}

Проблема заключается в том, что millis () собирается перевернуться до нуля после примерно 49,7 дней. Поскольку мой эскиз предназначен для работы дольше чем это, мне нужно убедиться, что опрокидывание не делает мой эскиз потерпеть неудачу. Я могу легко обнаружить условие опрокидывания (currentMillis < previousMillis), но я не уверен, что делать дальше.

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

46 голосов | спросил Edgar Bonet 12 J0000006Europe/Moscow 2015, 14:16:30

4 ответа


60

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

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

Примечание по micros () : все сказанное здесь о millis () применяется одинаково для micros (), за исключением того, что micros () перекатывается каждые 71,6 минуты и функция setMillis (), приведенная ниже не влияет на micros ().

Инстанты, временные метки и длительность

Когда мы имеем дело со временем, мы должны провести различие между два разных понятия: мгновенные и длительности . Моментом является точка на оси времени. Длительность - это длина временного интервала, то есть расстояние во времени между моментами, которые определяют начало и конец интервала. Различие между этими понятиями не всегда очень резкий в повседневном языке. Например, если я скажу «, я буду вернитесь через пять минут , а затем â € œ пять минут - это оценка продолжительность моего отсутствия, тогда как через пять минут - это instant моего предсказанного возвращения. Имея в виду различие, важно, потому что это самый простой способ полностью избежать опрокидывания проблема.

Возвращаемое значение millis () может быть интерпретировано как продолжительность: время, прошедшее с начала программы до настоящего времени. Эта Интерпретация, однако, ломается, как только миллисы переполняются. это как правило, гораздо полезнее думать о millis () как о возврате timestamp , т. е. обозначение определенного момента. Это могло бы утверждают, что эта интерпретация страдает от этих неоднозначные, поскольку они повторно используются каждые 49,7 дней. Это, однако, Редко возникает проблема: в большинстве встроенных приложений все, что произошло 49,7 дня назад - это древняя история, о которой нас не волнует. Таким образом, утилизация старых ярлыков не должна быть проблемой.

Не сравнивать временные метки

Попытка выяснить, какая из двух временных меток больше другой не имеет смысла. Пример:

 unsigned long t1 = millis ();
Задержка (3000);
unsigned long t2 = millis ();
если (t2> t1) {...}

Наивно, можно было бы ожидать, что условие if () всегда будет правда. Но на самом деле это будет ложно, если миллисы переполняются во время Задержка (3000). Размышление о t1 и t2 в качестве подлежащих вторичной переработке меток простейший способ избежать ошибки: ярлык t1 явно назначен до момента t2, но в 49,7 дня он будет переназначен в будущем. Таким образом, t1 выполняется как до и после t2. Эта должен ясно показать, что выражение t2> t1 не имеет смысла.

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

  1. later_timestamp - early_timestamp дает продолжительность, а именно: количество времени, прошедшего между более ранним моментом и более поздним мгновенное. Это самая полезная арифметическая операция, включающая метки времени.
  2. 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; //завершено: timestamp
    для (;;) {
        unsigned long now = millis (); //now: timestamp
        if (now> = finished) //сравнение временных меток: BUG!
            вернуть;
    }
}

И вот правильный:

 void myDelay (unsigned long ms) {//ms: duration
    unsigned long start = millis (); //start: timestamp
    для (;;) {
        unsigned long now = millis (); //now: timestamp
        unsigned long elapsed = now - start; //Истек: продолжительность
        if (прошло> = ms) //сравнение продолжительности: OK
            вернуть;
    }
}

Большинство программистов на C должны писать вышеперечисленные петли в форме терминов, например

 while (millis () <start + ms); //версия BUGGY

и

 while (millis () - start <ms); //CORRECT версия

Хотя они выглядят обманчиво похожими, отметка времени /продолжительность должно быть ясно, какой из них глючит, а какой - правильный.

Что делать, если мне действительно нужно сравнивать временные метки?

Лучше постарайтесь избежать ситуации. Если это неизбежно, если известно, что соответствующие моменты достаточно близки: ближе к 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 до t2

Это можно упростить как:

 if (t1 - t2 + LONG_ENOUGH_DURATION <LONG_ENOUGH_DURATION)
    //t1 до t2

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

 if ((signed long) (t1 - t2) <0) //работает с gcc
    //t1 до t2

Ключевое слово signed выше избыточно (простой long всегда ), но это помогает сделать это ясно. Преобразование в подписанный длинный эквивалентно установке LONG_ENOUGH_DURATION, равной 24.85Â дней. Трюк не переносится, потому что, согласно C Стандарт, результат определяется реализация . Но так как gcc компилятор обещает сделать правильное вещь , он надежно работает на Arduino. Если мы хотим избежать реализации поведение, приведенное выше сравнение математически эквивалентно это:

 #include <limits.h>

if (t1 - t2> LONG_MAX) //слишком велик, чтобы верить
    //t1 до t2

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

 if ((t1 - t2) & 0x80000000) //проверить бит «знака»
    //t1 до 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 (назначения). Если вы хотитепройти миллионы переполнения снова и снова, как Фил Коннорс, переживающий День сурка, вы можете поместить это внутри цикла ():

 //6-секундный цикл времени, начиная с опрокидывания - 3 секунды
если (миллис () - (-3000)> = 6000)
    setMillis (-3000);

Отрицательная метка времени выше (-3000) неявно преобразуется компилятор в unsigned long, соответствующий 3000 миллисекундам до (он преобразуется в 4294964296).

Что делать, если мне действительно нужно отслеживать очень длительные длительности?

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

 uint64_t millis64 () {
    статический 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 дней.

Имейте в виду, что эта 64-разрядная арифметика стоит дорого Arduino. Возможно, стоит уменьшить временное разрешение, чтобы остаться на 32 бита.

ответил Edgar Bonet 12 J0000006Europe/Moscow 2015, 14:16:30
11

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

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

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

ответил Gerben 12 J0000006Europe/Moscow 2015, 16:14:02
1

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

Логика:

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

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

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

общественности:
    статический long last_stamp;
    статические длинные * штампы;
    статические int * развороты;
    static int count;
    static int reversal_count;

    static void setup_timer () {
        //Настройка переполнения таймера2 для запуска каждые 8 ​​мс (125 Гц)
        //period [sec] = (1 /f_clock [sec]) * prescale * (255-count)
        //(1/16000000) * 1024 * (255-130) = .008 с


        TCCR2B = 0x00; //Отключить Timer2, пока мы его настроили

        TCNT2 = 130; //Сброс счетчика таймера (255-130) = выполнение ev 125-го таймера T /C
        TIFR2 = 0x00; //Timer2 INT Flag Reg: сброс таймера переполнения таймера
        TIMSK2 = 0x01; //Timer2 INT Reg: включение прерывания переполнения Timer2
        TCCR2A = 0x00; //Timer2 Control Reg A: режим Wave Gen Normal
        TCCR2B = 0x07; //Timer2 Control Reg B: предустановитель таймера установлен на 1024

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

    static long get_stamp () {
        штампы [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 ++)
        если (обратные [j] <s2 & & revals [j]> s1)
            no_of_reversals ++;
        метки возврата [s2] -стопы [s1] + 49,7 * 86400 * 1000;
    }

};

long Timer :: last_stamp;
long * Timer :: штампы;
int * Таймер :: развороты;
int Timer :: count;
int Timer :: reversal_count;

ISR (TIMER2_OVF_vect) {

    long stamp = millis ();
    if (штамп <Timer :: last_stamp) //разворот
        Timer :: reversals [Timer :: reversal_count ++] = Timer :: count;
    еще
        ; //нет разворота
    Timer :: last_stamp = штамп;
    TCNT2 = 130; //сбросить таймер ct до 130 из 255
    TIFR2 = 0x00; //timer2 int flag reg: очистить флаг переполнения таймера
};

//Применение

void setup () {
    Таймер :: setup_timer ();

    long s1 = Timer :: get_stamp ();
    Задержка (3000);
    long s2 = Timer :: get_stamp ();

    Таймер :: compare_stamps_by_id (s1, s2); //правда

    Таймер :: get_stamp_difference (s1, s2); //возвращаем истинную разницу, принимая во внимание развороты
}

Таймерные кредиты .

ответил ps95 13 J0000006Europe/Moscow 2015, 00:02:24
0

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

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

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

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

volatile unsigned long long timer0_millis = 0; //BFB: требуется 64-битное разрешение

#if defined (__ AVR_ATtiny24__) || (__ AVR_ATtiny44__) || определен (__ AVR_ATtiny84__)
ИСР (TIM0_OVF_vect)
#else
ИСР (TIMER0_OVF_vect)
#endif
{
    //скопируем их в локальные переменные, чтобы они могли храниться в регистрах
    //(переменные volatile должны считываться из памяти при каждом доступе)
    unsigned long long m = timer0_millis; //BFB: требуется 64-битное разрешение
    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-разрядная версия
unsigned long long millis64 ()
{
    unsigned long long m;
    uint8_t oldSREG = SREG;

    //отключить прерывания, пока мы читаем timer0_millis, или мы можем получить
    //несогласованное значение (например, в середине записи на timer0_millis)
    кли ();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}
ответил brainbarker 8 J0000006Europe/Moscow 2018, 18:27:12

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132