Таймеры, прерывания и volatile
Пустой цикл _delay_ms занимает процессор целиком. Таймер считает такты сам, а в нужный момент вызывает прерывание: программа получает точный отсчёт времени и свободна в остальное время.
Обновлено Редакция GAW
AVR с нуляурок 5 из 830 мин
Цель урока: Настроить таймер ATmega328P в режиме CTC на заданный период, написать обработчик прерывания и безопасно обмениваться данными между ним и основной программой.
В уроке 3 шаг опроса кнопки задавал _delay_ms(1): процессор всё это время крутил пустой цикл. Таймер-счётчик делает ту же работу аппаратно. В ATmega328P их три: 8-битные Timer0 и Timer2 и 16-битный Timer1.
Как считает таймер
Счётчик TCNTn увеличивается на единицу на каждом такте таймера. Такт таймера — это системная частота, поделённая предделителем N. Для Timer0 его выбирают биты CS02…CS00 регистра TCCR0B (раздел 15.9.2):
| CS02…CS00 | Такт таймера |
|---|---|
| 000 | таймер остановлен |
| 001 | clk/1 |
| 010 | clk/8 |
| 011 | clk/64 |
| 100 | clk/256 |
| 101 | clk/1024 |
| 110, 111 | внешний вход T0, спад или фронт |
В режиме CTC (Clear Timer on Compare Match, WGM02:0 = 2) счётчик сравнивается с регистром OCR0A и при совпадении сбрасывается в ноль (раздел 15.7.2). Одновременно поднимается флаг OCF0A, и если прерывание разрешено — вызывается обработчик. Период прерываний:
Формула периода выведена нами из формулы частоты выхода в разделе 15.7.2, где переключение вывода на каждом совпадении даёт f = f_clk / (2 · N · (1 + OCR0A)).
Прерывание: что нужно разрешить
Чтобы прерывание сработало, нужны два разрешения (раздел 7.7): свой бит источника — для совпадения Timer0 это OCIE0A в регистре TIMSK0 — и общий бит I в регистре SREG, который ставит функция sei(). При входе в обработчик аппаратура сбрасывает бит I, поэтому обработчики не прерывают друг друга, а команда RETI в конце обработчика ставит его снова. Реакция на прерывание — не меньше четырёх тактов, возврат — ещё четыре (раздел 7.7.1).
Обработчик в avr-libc объявляется макросом ISR с именем вектора из заголовка кристалла. Имена векторов ATmega328P повторяют таблицу 12-6 даташита с суффиксом _vect: TIMER0_COMPA_vect, TIMER1_COMPA_vect, USART_RX_vect, ADC_vect.
Пример: системный тик 1 мс на Timer0
Посчитаем: при 16 МГц и делителе 64 такт таймера — 250 кГц, значит, на миллисекунду приходится 250 тактов, и OCR0A = 250 − 1 = 249. Значение укладывается в 8 бит.
#define F_CPU 16000000UL
#include <avr/interrupt.h>
#include <avr/io.h>
#include <stdint.h>
#include <util/atomic.h>
static volatile uint32_t ms_ticks; /* changed in the ISR: must be volatile */
ISR(TIMER0_COMPA_vect)
{
ms_ticks++; /* called every 1 ms */
}
static uint32_t millis_now(void)
{
uint32_t t;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
t = ms_ticks; /* 4 bytes read with interrupts off */
}
return t;
}
int main(void)
{
DDRB |= _BV(DDB5); /* LED on PB5 */
TCCR0A = _BV(WGM01); /* CTC mode, TOP = OCR0A */
OCR0A = 249; /* 16 MHz / 64 / 250 = 1 kHz */
TIMSK0 = _BV(OCIE0A); /* compare match A interrupt */
TCCR0B = _BV(CS01) | _BV(CS00); /* clk/64, the timer starts */
sei(); /* global interrupt enable */
uint32_t last = 0;
for (;;) {
uint32_t now = millis_now();
if (now - last >= 500) { /* every 500 ms */
last += 500;
PINB = _BV(PINB5); /* toggle the LED */
}
/* free time for other work */
}
}
Две детали этого кода важнее самого таймера.
volatile. Переменная ms_ticks меняется в обработчике, о котором компилятор при разборе цикла main ничего не знает. Без volatile оптимизатор вправе прочитать её один раз и больше не обращаться к памяти — этот случай разобран в FAQ avr-libc. Подробнее — термин volatile.
Атомарность. Процессор AVR 8-битный, и 32-битную переменную он читает четырьмя командами. Если прерывание случится между ними, получится смесь старого и нового значения. Макрос ATOMIC_BLOCK(ATOMIC_RESTORESTATE) из util/atomic.h запрещает прерывания на время чтения и затем возвращает бит I в прежнее состояние. Вычитание now - last в беззнаковых числах правильно работает и при переполнении счётчика. Об этих проблемах — статья «Прерывания и конкурентный доступ».
Пример: Timer1 и период в секунду
На 8-битном таймере длинный период не получить: даже при делителе 1024 и OCR0A = 255 это 1024 · 256 / 16 МГц ≈ 16,4 мс. У Timer1 счётчик 16-битный. Его режим CTC с вершиной OCR1A — режим 4, бит WGM12 в регистре TCCR1B (разделы 16.9.2 и 16.11.2); делитель выбирают биты CS12…CS10 с теми же значениями, что у Timer0.
При делителе 256 такт таймера — 62,5 кГц, и секунда — это 62 500 тактов, OCR1A = 62 499:
#include <avr/interrupt.h>
#include <avr/io.h>
ISR(TIMER1_COMPA_vect)
{
PINB = _BV(PINB5); /* toggle the LED once a second */
}
int main(void)
{
DDRB |= _BV(DDB5);
TCCR1A = 0; /* no output compare pins, WGM11:10 = 0 */
OCR1A = 62499; /* 16 MHz / 256 / 62500 = 1 Hz */
TIMSK1 = _BV(OCIE1A);
TCCR1B = _BV(WGM12) | _BV(CS12);/* mode 4 (CTC), clk/256 */
sei();
for (;;) {
}
}
Запись 16-битного OCR1A компилятор разбивает на две 8-битные в правильном порядке — сначала старший байт во временный регистр, потом младший (раздел 16.3). Но если 16-битный регистр таймера пишут и в основной программе, и в обработчике, запись тоже надо защищать от прерываний.
Подобрать предделитель и OCRnA под любую частоту поможет калькулятор таймера, а готовый код инициализации — генератор кода AVR.
Правила для обработчиков
- Обработчик должен быть коротким: пока он выполняется, остальные прерывания ждут.
- Никаких
_delay_msи долгих циклов внутри обработчика. - Всё, что обработчик меняет, а
mainчитает, —volatile; всё, что длиннее байта, — читать вATOMIC_BLOCK. - Не разрешайте прерывание без обработчика: по умолчанию avr-libc отправляет такие прерывания на адрес 0, и программа начнётся заново с адреса 0 — без аппаратного сброса, с прежними значениями регистров ввода-вывода.
Задание
- Нужно прерывание каждые 0,5 с на Timer1 при 16 МГц. Подберите делитель из ряда 1, 8, 64, 256, 1024 и значение OCR1A.
- Почему в первом примере
ms_ticksнельзя читать вmainпросто какuint32_t now = ms_ticks;, даже если переменная объявленаvolatile?
Ответ и решение
-
Нужно 16 000 000 · 0,5 = 8 000 000 тактов процессора. С делителем 64 это 125 000 тактов таймера — больше 65 536, не помещается в 16 бит. С делителем 256 — 31 250 тактов: OCR1A = 31 249, делитель 256 (
_BV(CS12)). С делителем 1024 вышло бы 7812,5 — дробное число, период был бы неточным. -
volatileгарантирует, что значение будет прочитано из памяти, но не то, что оно будет прочитано целиком за один раз. 32-битное число 8-битный процессор читает четырьмя командами, и прерывание между ними может изменить старшие байты после того, как младшие уже прочитаны. НуженATOMIC_BLOCKили запрет прерываний на время чтения.
Что дальше
В уроке 6 таймеры начнут сами управлять выводами и выдавать ШИМ. Все режимы таймеров, таблицы WGM и захват — в статье «Таймеры AVR»; регистры с битами — на страницах Timer0 и Timer1 атласа. Коротко о понятиях урока — термины «Прерывание», «Предделитель», «Режим CTC» и «Таблица векторов прерываний».
Частые вопросы
Зачем переменной из прерывания volatile?
С оптимизацией компилятор считает, что внутри цикла переменную никто не меняет, читает её один раз и держит в регистре. Квалификатор volatile заставляет читать переменную из памяти при каждом обращении, и основная программа видит изменения, сделанные в обработчике. Этот пример разобран в FAQ avr-libc.
Почему OCR0A = 249, а не 250?
В режиме CTC таймер считает от 0 до значения OCR0A включительно и только потом сбрасывается, то есть период равен OCR0A + 1 тактам таймера. Для 250 тактов на миллисекунду при 16 МГц и делителе 64 в регистр записывают 249.
Что будет, если разрешить прерывание без обработчика?
По умолчанию avr-libc ставит на все незанятые векторы переход к адресу 0, поэтому программа начнётся заново с адреса 0 при каждом срабатывании. Это не аппаратный сброс: регистры ввода-вывода сохраняют свои значения, и программа стартует с неожиданными настройками периферии. Если это мешает отладке, можно написать обработчик ISR(BADISR_vect), который поймает любое неожиданное прерывание.
Источники
- Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061B) — разд. 7.7: прерывание разрешают свой бит и бит I в SREG, приоритет по адресу вектора; разд. 7.7.1: реакция не меньше 4 тактов, возврат — 4 такта, RETI снова ставит I; табл. 12-6: векторы ATmega328P; разд. 15.7.2: режим CTC Timer0 (WGM02:0 = 2), TOP = OCR0A, частота выхода fclk/(2·N·(1 + OCR0A)), N = 1, 8, 64, 256, 1024; разд. 15.9.1–15.9.6: TCCR0A, TCCR0B, OCR0A, TIMSK0; разд. 16.9.2 и 16.11: Timer1, режим 4 (WGM12), TCCR1B, OCR1A, TIMSK1; разд. 16.3: доступ к 16-битным регистрам через временный регистр
- AVR-LibC 2.3.2: avr/interrupt.h, util/atomic.h, FAQ (github.com/avrdudes/avr-libc) — макрос ISR, имена векторов TIMER0_COMPA_vect и TIMER1_COMPA_vect (iom328p.h), sei и cli; аппаратура сбрасывает флаг прерываний при входе в обработчик, обработчики не вложены; прерывание без обработчика по умолчанию переходит на вектор сброса — программа начинается с адреса 0; FAQ: переменная, изменяемая в прерывании, должна быть volatile; ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
Нашли ошибку?
Смотрите также
- Таймеры AVRTimer0/1/2, режимы WGM, предделители, формулы ШИМ, захват, примеры
- Таймер AVR и STM32Предделитель, OCRnA, PSC и ARR под частоту, мёртвое время, энкодер
- Прерывания и общие данныеISR на AVR и Cortex-M, атомарность, критические секции, кольцевой буфер
- volatileКвалификатор C для регистров и переменных прерываний
- Как сделать millis() на AVR без Arduino?Timer0 CTC на 1 кГц, volatile uint32_t и чтение в ATOMIC_BLOCK