Перейти к содержанию

millis, micros и delay изнутри

millis() не считает миллисекунды: она считает переполнения Timer0, которые приходят каждые 1024 мкс, и примерно раз в 43 мс доплачивает накопленную дробь. Разберём, как это устроено в wiring.c, чем грозит и как получить ровный тик регистрами.

Обновлено Редакция GAW

От Arduino к регистрамурок 5 из 835 мин

Цель урока: Разобрать по исходнику ядра, как millis, micros, delay и delayMicroseconds получают время от Timer0, правильно сравнивать время с учётом переполнения и сделать ровный миллисекундный тик на Timer2.

millis() — самая популярная функция времени в скетчах, и самая непонятная: она прыгает через значения, перестаёт расти внутри обработчиков прерываний и обнуляется через 49,7 суток. Всё это следует из 60 строк файла wiring.c ядра ArduinoCore-avr 1.8.8. Как работают таймер, прерывание и volatile, разобрано в уроке о таймерах трека «AVR с нуля»; здесь — что из них собрало ядро.

Источник времени — переполнение Timer0

init() запускает Timer0 в режиме Fast PWM с делителем 64 и разрешает прерывание по переполнению (строки 247–282). В Fast PWM флаг TOV0 ставится, когда счётчик доходит до TOP = 0xFF (раздел 15.7.3), то есть раз в 64 · 256 = 16 384 такта. При 16 МГц это 1024 мкс. Ядро считает константы из этого числа (строки 25–36):

// ArduinoCore-avr 1.8.8, cores/arduino/wiring.c, lines 27-36
#define MICROSECONDS_PER_TIMER0_OVERFLOW (clockCyclesToMicroseconds(64 * 256))
#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)
#define FRACT_INC ((MICROSECONDS_PER_TIMER0_OVERFLOW % 1000) >> 3)
#define FRACT_MAX (1000 >> 3)
КонстантаФормула16 МГц8 МГц
мкс на переполнение64 · 256 / (F_CPU / 10⁶)10242048
MILLIS_INCцелые мс12
FRACT_INCостаток мкс, делённый на 824 >> 3 = 348 >> 3 = 6
FRACT_MAX1000 >> 3125125

Сдвиг на 3 нужен, чтобы дробь поместилась в один байт: остаток копится в единицах по 8 мкс. Комментарий ядра оговаривает, что для 8 и 16 МГц точность при этом не теряется — 24 и 48 делятся на 8 (расчёт наш).

Обработчик и дробная поправка

// ArduinoCore-avr 1.8.8, cores/arduino/wiring.c, lines 42-63
ISR(TIMER0_OVF_vect)
{
    unsigned long m = timer0_millis;
    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++;
}

Каждое переполнение прибавляет 1 мс и 3 единицы дроби. Когда дробь доходит до 125, к счёту прибавляется ещё одна миллисекунда. За 125 переполнений (128 мс) дробь трижды переходит через 125, и счёт вырастает на 125 + 3 = 128 — время в среднем точное, но три значения из 128 пропущены. Это примерно 23 пропуска в секунду: 976,6 переполнения · 3 / 125 ≈ 23,4 (расчёт наш). Проверка if (millis() == 1000) может не сработать никогда.

millis() (строки 65–77) читает четырёхбайтовую timer0_millis при запрещённых прерываниях. Без этого прерывание могло бы изменить переменную между чтением младших и старших байтов — классическая неатомарная операция, термин «Атомарная операция».

micros: 4 мкс на шаг

// ArduinoCore-avr 1.8.8, cores/arduino/wiring.c, lines 79-104 (ATmega328P branch)
unsigned long micros() {
    unsigned long m;
    uint8_t oldSREG = SREG, t;

    cli();
    m = timer0_overflow_count;
    t = TCNT0;
    if ((TIFR0 & _BV(TOV0)) && (t < 255))
        m++;
    SREG = oldSREG;

    return ((m << 8) + t) * (64 / clockCyclesPerMicrosecond());
}

Число переполнений, умноженное на 256, плюс текущий TCNT0 — это время в тиках таймера, а тик при делителе 64 и 16 МГц длится 4 мкс. Поэтому шаг micros() — 4 мкс, и меньшего интервала она не различит.

Строка с TIFR0 закрывает гонку: если TCNT0 уже перешёл через 0xFF, а обработчик ещё не успел выполниться (прерывания запрещены — мы внутри cli()), флаг TOV0 стоит, а timer0_overflow_count ещё старый. Тогда micros() сама прибавляет пропущенное переполнение. Условие t < 255 отсекает случай, когда TCNT0 прочитан ещё до перехода.

delay и delayMicroseconds

delay() (строки 106–117) — цикл, который вызывает yield() и сравнивает micros() с моментом старта, отнимая по 1000 мкс. yield() в hooks.c — слабый псевдоним пустой функции, её можно переопределить в скетче. Процессор всё время задержки занят этим циклом, а прерывания работают.

delayMicroseconds() (строки 120–239) устроена иначе — это подсчёт тактов. Для 16 МГц:

// ArduinoCore-avr 1.8.8, cores/arduino/wiring.c (16 MHz branch, abridged)
if (us <= 1) return;   // the call itself takes about 1 us
us <<= 2;              // 4 loop passes per microsecond
us -= 5;               // overhead of the preceding code
__asm__ __volatile__ (
    "1: sbiw %0,1" "\n\t"   // 2 cycles
    "brne 1b" : "=w" (us) : "0" (us) // 2 cycles
);

SBIW — 2 такта, BRNE с переходом — 2 (раздел 37): один проход — 4 такта, 0,25 мкс. Прерывания функция не запрещает, и обработчик Timer0, пришедший посреди цикла, удлинит задержку на своё время.

Переполнение счётчиков

millis() возвращает 32-битное число миллисекунд и обнуляется через 2³² мс ≈ 49,7 суток. Выражение (m << 8) + t в micros() тоже 32-битное, и micros() обнуляется через 2³² мкс ≈ 71,6 минуты (расчёт наш). Правильное сравнение — через разность беззнаковых чисел:

static unsigned long last;

void loop() {
  if (millis() - last >= 500) {   // correct across the 49.7-day wrap
    last += 500;
    // ...
  }
}

Беззнаковое вычитание даёт правильный интервал и тогда, когда millis() уже перешла через ноль, а last — ещё нет. Сравнение millis() >= last + 500 в этот момент ломается: last + 500 переполнится раньше и станет маленьким числом.

Ровный тик регистрами

Если нужен тик без пропусков и дрожания — например, для опроса датчика ровно раз в миллисекунду, — его даёт таймер в режиме CTC. Timer0 трогать нельзя: на нём держатся millis() и delay(). Возьмём Timer2 — потеряем ШИМ на выводах 3 и 11 и функцию tone() (урок 3), а остальное ядро продолжит работать:

volatile uint32_t tickMs;

ISR(TIMER2_COMPA_vect)
{
  tickMs++;
}

uint32_t ticks(void)
{
  uint8_t sreg = SREG;
  cli();                       // 4-byte read must not be split by the ISR
  uint32_t t = tickMs;
  SREG = sreg;
  return t;
}

void setup() {
  Serial.begin(115200);
  TCCR2A = _BV(WGM21);         // CTC, TOP = OCR2A; clears the core's WGM20
  TCCR2B = _BV(CS22);          // clk/64
  OCR2A = 249;                 // 16 MHz / (64 * (1 + 249)) = 1000 Hz
  TIMSK2 = _BV(OCIE2A);        // compare match A interrupt
}

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

Режим CTC Timer2 — WGM22:0 = 010 (табл. 18-8), делитель 64 — CS22:0 = 100 (табл. 18-9). Период — 64 · 250 = 16 000 тактов, ровно 1 мс без дробей (расчёт наш). Своя millis() на голом AVR без ядра разобрана в ответе «Как сделать millis() без Arduino», а делитель и OCR под другой период подберёт калькулятор таймера.

Для коротких интервалов точнее 4 мкс подходит Timer1 без делителя — по 62,5 нс на отсчёт, как в скетче-измерителе из урока 1.

Задание

  1. Плата Pro Mini на 8 МГц. Чему равны MILLIS_INC, FRACT_INC и FRACT_MAX, на сколько вырастает millis() за одно переполнение и как часто она прыгает сразу на 3?
  2. Какой шаг будет у micros() на 8 МГц?
  3. Для опроса раз в 2 мс на Timer2 в CTC с делителем 128 (CS22:0 = 101) какое значение нужно записать в OCR2A?
Ответ и решение
  1. Переполнение — 64 · 256 / 8 = 2048 мкс. MILLIS_INC = 2, FRACT_INC = 48 >> 3 = 6, FRACT_MAX = 125. Обычно счёт растёт на 2, а когда дробь переходит через 125 — на 3. Это случается каждые 125 / 6 ≈ 20,8 переполнения, то есть примерно раз в 43 мс (расчёт наш).

  2. 64 / clockCyclesPerMicrosecond() = 64 / 8 = 8 мкс.

  3. Тик таймера — 128 / 16 МГц = 8 мкс, на 2 мс нужно 250 тиков: OCR2A = 250 − 1 = 249 (расчёт наш). Делитель 128 у Timer2 есть — CS22:0 = 101 (табл. 18-9).

Что дальше

В уроке 6 разберём attachInterrupt(): почему обработчик через ядро медленнее своего ISR() и что делать с флагом, который ждёт с прошлого раза. Регистры таймеров — на страницах Timer0 и Timer2 атласа, обзор всех таймеров AVR — в статье «Таймеры AVR». Как меняется время задержек от частоты кварца, покажет калькулятор задержек AVR.

Частые вопросы

Почему соседние значения millis() иногда отличаются на 2?

Прерывание Timer0 приходит каждые 1024 мкс при 16 МГц. Обработчик прибавляет к счёту 1 мс, а остаток 24 мкс копит в переменной timer0_fract в единицах по 8 мкс. Когда остатки набирают целую миллисекунду, счёт растёт сразу на 2. За каждые 128 мс так пропускаются 3 значения, около 23 в секунду, поэтому проверка millis() == x может никогда не сработать — сравнивайте через разность.

Почему delay() внутри обработчика прерывания не работает?

В обработчике прерывания запрещены, и прерывание по переполнению Timer0 не может выполниться: счётчик timer0_overflow_count стоит на месте. micros() умеет учесть только одно пропущенное переполнение, а дальше её значения начинают возвращаться назад, и цикл delay() отсчитывает время неверно. Обработчик должен быть коротким: выставить флаг и выйти.

Насколько точна delayMicroseconds?

Функция отсчитывает такты пустым циклом по 4 такта на проход, поправленным на время вызова, — при 16 МГц четыре прохода на микросекунду. Значения 0 и 1 возвращаются сразу. Прерывания функция не запрещает, поэтому прерывание Timer0 раз в 1024 мкс или приём байта по Serial могут удлинить задержку. Для точных коротких интервалов прерывания на это время отключают.

Источники

  1. Arduino. ArduinoCore-avr 1.8.8: cores/arduino/wiring.c и hooks.c — тег 1.8.8 — коммит 86df345 от 21 мая 2026 (GitHub API, tags и commits); wiring.c, стр. 25–36: MICROSECONDS_PER_TIMER0_OVERFLOW = clockCyclesToMicroseconds(64 · 256), MILLIS_INC, FRACT_INC с сдвигом на 3, FRACT_MAX = 1000 >> 3; стр. 42–63: ISR(TIMER0_OVF_vect) — прибавка MILLIS_INC, дробная часть, timer0_overflow_count; стр. 65–77: millis читает timer0_millis при cli(); стр. 79–104: micros — счёт переполнений и TCNT0, поправка по флагу TOV0 при t < 255, умножение на 64 / clockCyclesPerMicrosecond(); стр. 106–117: delay — цикл по micros() с вызовом yield(); стр. 120–239: delayMicroseconds — для 16 МГц возврат при us ≤ 1, us <<= 2, us −= 5, цикл sbiw/brne по 4 такта; стр. 247–282: Timer0 — Fast PWM, делитель 64, TOIE0; hooks.c, стр. 20–31: yield() — слабый псевдоним пустой функции
  2. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061B) — разд. 7.7.1: реакция на прерывание не меньше 4 тактов, возврат — 4 такта; разд. 15.7.3: в Fast PWM флаг TOV0 ставится, когда счётчик достигает TOP; разд. 15.9.7: TOV0 сбрасывается аппаратно при входе в обработчик или записью единицы; табл. 15-9: делитель 64 — CS02:0 = 011; разд. 18.7.2 и табл. 18-8: режим CTC Timer2 (WGM22:0 = 2), TOP = OCR2A; табл. 18-9: CS22:0 = 100 — делитель 64; разд. 18.11.6: TIMSK2, бит OCIE2A; разд. 37: SBIW — 2 такта, BRNE — 1 или 2 такта
  3. AVR-LibC 2.3.2: avr/interrupt.h, util/atomic.h, iom328p.h (github.com/avrdudes/avr-libc) — векторы TIMER0_OVF_vect (16) и TIMER2_COMPA_vect (7); при входе в обработчик прерывания запрещены, обработчики не вложены; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) для чтения многобайтовых переменных

Нашли ошибку?