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

Таймеры, прерывания и 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таймер остановлен
001clk/1
010clk/8
011clk/64
100clk/256
101clk/1024
110, 111внешний вход T0, спад или фронт

В режиме CTC (Clear Timer on Compare Match, WGM02:0 = 2) счётчик сравнивается с регистром OCR0A и при совпадении сбрасывается в ноль (раздел 15.7.2). Одновременно поднимается флаг OCF0A, и если прерывание разрешено — вызывается обработчик. Период прерываний:

T=N⋅(1+OCR0A)fclkT = \frac{N \cdot (1 + \text{OCR0A})}{f_\text{clk}}

Формула периода выведена нами из формулы частоты выхода в разделе 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 — без аппаратного сброса, с прежними значениями регистров ввода-вывода.

Задание

  1. Нужно прерывание каждые 0,5 с на Timer1 при 16 МГц. Подберите делитель из ряда 1, 8, 64, 256, 1024 и значение OCR1A.
  2. Почему в первом примере ms_ticks нельзя читать в main просто как uint32_t now = ms_ticks;, даже если переменная объявлена volatile?
Ответ и решение
  1. Нужно 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 — дробное число, период был бы неточным.

  2. 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), который поймает любое неожиданное прерывание.

Источники

  1. 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-битным регистрам через временный регистр
  2. 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)

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