Как сделать задержку в 1 секунду более точной?

Я написал две программы задержки (по 1 секунде).

Первая программа выглядит следующим образом:

void setup()
{
Serial.begin(9600);
TCCR1A = 0;
TCCR1B |= (1<<CS12);
TCCR1B &= ~(1<<CS11);
TCCR1B &= ~(1<<CS10);
TCNT1 = 3036;
TIMSK1 |= (1<<TOIE1);
}

ISR(TIMER1_OVF_vect)
{
  Serial.println(millis());
  TCNT1 = 3036;
}
void loop()
{
}

Вывод которого следующий:

999  // 1 сек
1999 // 2 сек
3000
3999
5000
6000
7001
8000
9000
10001
.
.
.
58012
59013
60013
61012
62013 //60 сек

Погрешность составляет около 13 мс в 1 минуту, а в 1 час она составит 780 мс, а в течение одного дня она составит около 19 секунд.

Вторая программа выглядит следующим образом:

void setup() {
Serial.begin(9600);
}

void loop() {
  Serial.println(millis());
  delay(1000);
}

Выходная мощность которого примерно такая же, как у первого.

Есть ли способ уменьшить погрешность и сделать его более точным? Чтобы я мог использовать его в своем секундомере на базе Arduino?

, 👍3


4 ответа


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

5

Маженко совершенно прав: от Arduino тактируется керамическим резонатором. Типичный дрейф порядка 1000 ppm, и зависит от температуры и старения. Вы можете, однако, получите задержку, которая довольно близка к идее ЦП об одном второе. Другими словами, вы можете получить что-то очень близкое к Идеальный период 16 000 000 циклов ЦП. Если это то, что вам нужно, Лучше всего использовать таймер.

В вашем переполнении ISR вы написали:

Serial.println(millis());

Здесь есть две проблемы. Первая — использование Serial.println() в ISR. Обычно этого следует избегать: поскольку Serial полагается на прерывания, использование его с отключенными прерываниями может заблокировать вашу программу, если вы когда-нибудь заполняете выходной буфер. В данном конкретном случае это происходит чтобы быть в безопасности, потому что при таком раскладе вы никогда не заполните выход буфер в любом случае. Однако, хотя это может быть нормально сделать в небольшом тесте программу, подобную вашей, я бы не советовал вам делать ее в продакшене код.

Другая проблема — использование millis(). Эта функция не предоставляет истинное разрешение в миллисекундах: это счетчик, который обновляется каждые 1024 мкс. Время от времени он обновляется на 2 мс сразу в для компенсации дрейфа. Таким образом, вы должны думать о millis() как хорошо в пределах ±2 мс. Если вам нужно что-то лучшее, используйте микрос().

TCNT1 = 3036;

Этого следует избегать. Отправка прерывания занимает время, и это время зависит от того, есть ли уже обработчик прерываний (или другой критическая секция) работает. Если таймер увеличивается после прерывание срабатывает, но прежде чем вы успеете выполнить строку выше, то Вы пропускаете тик таймера. Вам было предложено переместить эту строку перед Serial.println(). Это, конечно, уменьшило бы риск, но это не предотвратит его полностью.

Правильнее всего никогда не сбрасывать таймер, если вы хотите, чтобы он работал. непрерывность времени. Вместо этого установите его в режим CTC (сброс таймера при сравнении (соответствует) и позвольте ему сбросить себя, когда он достигнет выбранного вами значения. В этом случае вам придется многократно считать от 0 до 62 499. вместо 3036 до 65535.

Вот модифицированная версия вашей программы, иллюстрирующая пункты выше:

void setup()
{
    Serial.begin(9600);
    TCCR1A = 0;            // отменить конфигурацию таймера Arduino
    TCCR1B = 0;            // то же самое
    TCNT1  = 0;            // сбросить таймер
    OCR1A  = 62500 - 1;    // период = 62500 тактов
    TCCR1B = _BV(WGM12)    // режим CTC, TOP = OCR1A
           | _BV(CS12);    // тактовая частота F_CPU/256
    TIMSK1 = _BV(OCIE1A);  // прерывание по выходу сравнения A
}

ISR(TIMER1_COMPA_vect)
{
    // Допустимо в тестовом коде, не делайте этого в рабочем коде.
    Serial.println(micros());
}

void loop(){}

Вывод следующий:

1000048
2000048
3000048
...
58000048
59000048
60000048  // 60 секунд
,

3

Ваша самая большая проблема в том, что вы не знаете, что такое секунда. У вас нет надежной временной базы для измерения времени.

millis() не является точным. Вы не можете полагаться на то, что это даст вам точное время - в противном случае, что-то такое простое, как:

uint32_t secs = 0;

void setup() {
    Serial.begin(115200);
}

void loop() {
    if (millis() / 1000 > secs) {
        secs = millis() / 1000;
        Serial.println(millis());
    }
}

даст вам точную "задержку" в 1000 миллисекунд. Однако все, что вам это дает, это точная задержка "Arduino 1000 миллисекунд". То есть - он срабатывает при переключении между блоками в 1000 миллисекунд, где 1 миллисекунда - это то, что функция Arduino millis() считает 1 миллисекундой.

Хотя это может казаться точным, поскольку выдает круглые числа «1000, 2000, 3000, 4000» и т. д., сравните это с внешними часами, и вы обнаружите, что они на самом деле дрейфуют так же сильно (если не хуже), как ваши существующие методы.

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

Если вам нужен точный 1-секундный импульс с почти нулевым дрейфом, вам придется использовать внешний источник часов. Самым точным будет GPS. Многие модули GPS имеют возможность выводить 1-секундный импульс, синхронизированный с часами GPS.

Модуль RTC также немного улучшит точность — опять же, большинство из них имеют выход прямоугольной волны 1 Гц, которую можно использовать для синхронизации вещей — однако даже они не на 100% точны и зависят от точности подключенного кристалла. Хотя они точнее керамического резонатора Arduino.

,

0

Я никогда бы не подумал, что этот день настанет, но я не согласен с @Majenko и @EdgarBonet.

Источник ваших часов важен. Если резонатор Arduino достаточно точен, то вы можете использовать millis(). Если вам нужна большая точность, то вы можете использовать RTC, например ds3231. ds3231 имеет температурную компенсацию для большей точности. Если вам нужно что-то еще лучшее, то вы можете использовать GPS или сервер времени в Интернете или даже сервер времени длинноволнового радиосигнала.

Если вам нужен 1-секундный таймер в качестве основы для часов или секундомера, то вы можете начать с использования millis(), который основан на резонаторе 16 МГц. Millis() компенсируется смещением, он не будет дрейфовать. Он работает на 100% идеально с той же точностью, что и резонатор. Используйте пример мигания без задержки и измените приращение previousMillis на это:

if (currentMillis - previousMillis >= interval) {
  // увеличиваем previousMillis на интервал
  previousMillis += interval;

Таким образом, интервал в одну секунду будет оставаться синхронизированным со временем, даже если в скетче время от времени возникает большая задержка.

Вы не получите большей точности, используя внутренний таймер.

Некоторые клоны Arduino имеют кристалл вместо резонатора. Если вы используете один из этих клонов с кристаллом и используете миллис, как я упоминал выше, то ваши часы будут такими же точными, как кристалл. Когда вы создаете часы с этим, они могут отставать от реального времени на несколько минут в год.

,

-2

Используйте micros() вместо millis() и мигайте без отслеживания времени задержки, как описал Jot. Результаты будут иметь гораздо меньший дрейф. А для лучших результатов используйте плату, которая использует кристалл вместо резонатора, как упоминалось.

,