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

Как сделать millis() на AVR без Arduino?

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

Arduino считает время в библиотеке ядра, но на голом avr-gcc функцию можно написать за десяток строк. Нужны три вещи: таймер, который раз в миллисекунду вызывает прерывание, счётчик в памяти и безопасное чтение этого счётчика.

Расчёт таймера

В режиме CTC таймер считает от нуля до OCR0A включительно, поэтому период — N · (1 + OCR0A) тактов:

OCR0A=fCPUN⋅1000 Гц−1\text{OCR0A} = \frac{f_\text{CPU}}{N \cdot 1000\ \text{Гц}} - 1

Наш расчёт для частых кварцев:

F_CPUДелитель NOCR0AПериод
16 МГц64249ровно 1 мс
8 МГц64124ровно 1 мс
1 МГц8124ровно 1 мс

Код

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>

static volatile uint32_t ms_ticks;

ISR(TIMER0_COMPA_vect) { ms_ticks++; }

void millis_init(void) {
    TCCR0A = _BV(WGM01);              /* CTC, TOP = OCR0A */
    TCCR0B = _BV(CS01) | _BV(CS00);   /* clk/64 */
    OCR0A = 249;                      /* 16 MHz / 64 / 250 = 1 kHz */
    TIMSK0 = _BV(OCIE0A);
    sei();
}

uint32_t millis(void) {
    uint32_t t;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { t = ms_ticks; }
    return t;
}

Квалификатор volatile заставляет компилятор читать счётчик из памяти при каждом обращении, а ATOMIC_BLOCK из avr-libc запрещает прерывания на четыре команды чтения и восстанавливает прежнее состояние. Почему без этого число скачет, разобрано в статье об атомарных операциях.

Как пользоваться

Интервал проверяют разностью: if (millis() - start >= 500) { ... }. Беззнаковое вычитание по модулю 232 даёт верный результат и после перехода счётчика через ноль, который случается раз в 232 мс ≈ 49,7 суток.

Подробно про CTC, обработчики и volatile — в уроке «Таймеры, прерывания и volatile» и в статье о режиме CTC. Значения OCR и делителя для другой частоты подберёт калькулятор таймера.

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

Можно ли использовать Timer0, если на нём уже ШИМ?

В режиме CTC таймер считает до OCR0A и не даёт ШИМ на 256 шагов, поэтому для отсчёта времени удобнее отдать отдельный таймер, например Timer2: у него те же режимы и тот же расчёт. В Arduino millis() сделан иначе — на прерывании по переполнению Timer0, который одновременно выдаёт ШИМ на выводах 5 и 6.

Что будет через 49 дней?

Счётчик uint32_t перейдёт через ноль. Если интервалы считать разностью now − start в беззнаковой арифметике, переход не мешает: разность остаётся правильной, пока интервал меньше 49,7 суток. Ломаются только сравнения вида now > start + interval.

Почему без ATOMIC_BLOCK значение иногда скачет?

Четырёхбайтовая переменная читается несколькими командами. Если прерывание придёт между ними и счётчик перейдёт, например, от 0x000000FF к 0x00000100, основная программа соберёт младший байт от старого значения, а старшие — от нового, и получит число, отличающееся на 256 мс.

Источники

  1. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — 15.7.2 — режим CTC Timer0: счётчик сбрасывается при совпадении с OCR0A, флаг OCF0A и прерывание по совпадению; 15.9 — биты WGM01, CS01, CS00 (делитель 64), OCIE0A в TIMSK0
  2. AVR-LibC 2.3.2: <util/atomic.h>, <avr/interrupt.h> — ATOMIC_BLOCK(ATOMIC_RESTORESTATE) — запрещает прерывания на время блока и восстанавливает SREG на выходе; ISR(TIMER0_COMPA_vect), sei()
  3. ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 6.2.5p9 — беззнаковая арифметика по модулю 2^N: разность uint32_t корректна и после переполнения счётчика
  4. Arduino. ArduinoCore-avr, коммит 11b9130: cores/arduino/wiring.c, variants/standard/pins_arduino.h — millis() считается в обработчике ISR(TIMER0_OVF_vect) — по переполнению Timer0; цифровые выводы 5 и 6 — каналы TIMER0B и TIMER0A

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