Как сделать millis() на AVR без Arduino?
Обновлено Редакция GAW
Содержание
Arduino считает время в библиотеке ядра, но на голом avr-gcc функцию можно написать за десяток строк. Нужны три вещи: таймер, который раз в миллисекунду вызывает прерывание, счётчик в памяти и безопасное чтение этого счётчика.
Расчёт таймера
В режиме CTC таймер считает от нуля до OCR0A включительно, поэтому период — N · (1 + OCR0A) тактов:
Наш расчёт для частых кварцев:
| F_CPU | Делитель N | OCR0A | Период |
|---|---|---|---|
| 16 МГц | 64 | 249 | ровно 1 мс |
| 8 МГц | 64 | 124 | ровно 1 мс |
| 1 МГц | 8 | 124 | ровно 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 мс.
Источники
- 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
- AVR-LibC 2.3.2: <util/atomic.h>, <avr/interrupt.h> — ATOMIC_BLOCK(ATOMIC_RESTORESTATE) — запрещает прерывания на время блока и восстанавливает SREG на выходе; ISR(TIMER0_COMPA_vect), sei()
- ISO/IEC 9899:201x (C11), Committee Draft N1570, April 2011 — 6.2.5p9 — беззнаковая арифметика по модулю 2^N: разность uint32_t корректна и после переполнения счётчика
- Arduino. ArduinoCore-avr, коммит 11b9130: cores/arduino/wiring.c, variants/standard/pins_arduino.h — millis() считается в обработчике ISR(TIMER0_OVF_vect) — по переполнению Timer0; цифровые выводы 5 и 6 — каналы TIMER0B и TIMER0A
Нашли ошибку?
Смотрите также
- Таймеры и прерыванияCTC на Timer0 и Timer1, ISR, sei, volatile, ATOMIC_BLOCK
- Режим CTC (сброс при совпадении)Счётчик таймера обнуляется при совпадении с регистром сравнения
- Атомарная операцияОперация, которую прерывание не застанет наполовину
- Таймер AVR и STM32Предделитель, OCRnA, PSC и ARR под частоту, мёртвое время, энкодер