Коротка відповідь: не намагайтеся "впоратися" з переворотом мільйонів, замість цього запишіть безпечний код. Ваш приклад код з підручника прекрасно. Якщо ви спробуєте виявити перекидання, щоб здійснити коригувальні заходи, швидше за все, ви робите щось не так. Більшість програм Arduino мають керувати лише подіями, що тривають відносно невеликі тривалості, як-от відключення кнопки протягом 50 мс або включення нагрівача протягом 12 годин ... Потім, і навіть якщо програма призначена для роботи за один раз, перекидання міліс не повинно викликати занепокоєння.
Правильний спосіб управління (а точніше, уникнути необхідності управління) проблемою перекидання - це думка про unsigned long
число, повернене
millis()
в термінах модульної арифметики . Для математично схильних деяке ознайомлення з цією концепцією дуже корисне при програмуванні. Ви можете бачити математику в дії в статті Ніка Гамона millis () overflow ... погана справа? . Для тих, хто не хоче проходити обчислювальні деталі, пропоную тут альтернативний (сподіваюсь, простіший) спосіб його роздуму. Він заснований на простому розмежуванні між моментами та тривалістю . Поки ваші тести включають лише порівняння тривалості, вам слід добре.
Примітка про мікросистему () : Все, про що йдеться тут, 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;
}
}
Більшість програмістів на 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
Далі спростити спрощення 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
з єдиною проблемою, що порівняння дивиться назад. Це також еквівалентно, поки довгі 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 () для вирішення, коли здійснювати єдиний дзвінок до цього коду в єдиному "обгортанні" міліс (конкретне вікно 49,7 дня), може бути дуже небезпечним, залежно від того, як вирівнюються часові рамки. З метою безпеки, якщо використовувати millis (), щоб визначити, коли робити єдині дзвінки на millis64 (), повинно бути щонайменше два виклики кожні 49,7-денне вікно.
Майте на увазі, що 64-бітна арифметика дорога на Arduino. Можливо, варто зменшити роздільну здатність часу, щоб залишитися на 32 біті.