Добавить в избранное

Прерывания AVR на C: INT0/INT1, Pin Change Interrupts и правила написания ISR

Чтение ≈ 4 мин

Всё о прерываниях на AVR с примерами на чистом C: макрос ISR и векторы, внешние прерывания INT0/INT1 по фронту и уровню, Pin Change Interrupts на любой ножке, volatile и атомарный доступ, антидребезг кнопки в ISR, подсчёт импульсов энкодера и типичные грабли.

Что такое прерывание и зачем оно

Прерывание — механизм, при котором периферия (таймер, UART, ножка порта) останавливает выполнение основной программы и вызывает функцию-обработчик ISR (Interrupt Service Routine). После завершения ISR процессор возвращается туда, где был.

Без прерываний пришлось бы постоянно опрашивать флаги в цикле: кнопка «промаргивалась» бы, байты в UART терялись, а точные интервалы разъезжались. С прерываниями реакция — 3–4 такта после события, а main свободен.

Макрос ISR и векторы

В avr-libc обработчик объявляется макросом ISR() с именем вектора из даташита:

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

ISR(INT0_vect)            // внешнее прерывание 0
{
    // код обработчика
}

Полезные векторы ATmega328P:

  • INT0_vect, INT1_vect — внешние прерывания на PD2/PD3;
  • PCINT0_vect..PCINT2_vect — pin change на портах B, C, D;
  • TIMER0_COMPA_vect, TIMER1_OVF_vect и т.д. — таймеры;
  • USART_RX_vect, USART_UDRE_vect — UART;
  • ADC_vect, EE_READY_vect, WDT_vect.

Глобальный разрешатель — бит I в SREG: sei() включает прерывания, cli() выключает. После сброса I=0: пока не вызвали sei(), ни одно прерывание не сработает.

Внешние прерывания INT0/INT1

ATmega328P имеет два «настоящих» внешних прерывания: INT0 на PD2 (Arduino D2) и INT1 на PD3. Умеют срабатывать по спаду, фронту, любому изменению и низкому уровню — настраивается битами ISC в EICRA:

static void int0_init(void)
{
    DDRD  &= ~(1 << PD2);      // PD2 — вход
    PORTD |=  (1 << PD2);      // pull-up

    EICRA |= (1 << ISC01);     // ISC01=1, ISC00=0 -> спад (falling edge)
    EICRA &= ~(1 << ISC00);
    EIFR   = (1 << INTF0);     // сбросить возможный «застрявший» флаг
    EIMSK |= (1 << INT0);      // разрешить INT0
}

Кнопка между PD2 и GND:

volatile uint8_t g_button_flag = 0;

ISR(INT0_vect)
{
    g_button_flag = 1;         // только ставим флаг — и выходим
}

int main(void)
{
    int0_init();
    sei();

    while (1) {
        if (g_button_flag) {
            g_button_flag = 0;
            // реакция на нажатие — в main, не в ISR
        }
    }
}

Режимы ISC: 00 — низкий уровень, 01 — любое изменение, 10 — спад, 11 — фронт. Уровень 00 срабатывает, пока ножка в нуле — пользуйтесь аккуратно.

Антидребезг кнопки в прерывании

Дребезг порождает серию прерываний за миллисекунды. Простейшая защита — игнорировать повторные срабатывания ближе 50 мс по системному тику:

volatile uint8_t  g_button_flag = 0;
volatile uint32_t g_ms = 0;    // инкремент в ISR(TIMER0_COMPA_vect)

ISR(INT0_vect)
{
    static uint32_t last = 0;
    uint32_t now = g_ms;
    if (now - last >= 50) {
        g_button_flag = 1;
        last = now;
    }
}

Pin Change Interrupts: прерывание на любой ножке

PCINT покрывает все порты, но вектор один на группу из 8 ножек: PCINT0_vect — PB0..PB5, PCINT1_vect — PC0..PC6, PCINT2_vect — PD0..PD7. Срабатывает по любому изменению, поэтому внутри нужно самим определять, что и как изменилось.

Энкодер на PC0/PC1 (фазы A и B):

volatile int8_t g_enc_delta = 0;
static  uint8_t g_prev_ab = 0;

ISR(PCINT1_vect)
{
    uint8_t ab = PINC & 0x03;          // текущее состояние A,B
    // таблица переходов квадратурного энкодера
    static const int8_t lut[16] = {
         0,-1, 1, 0,
         1, 0, 0,-1,
        -1, 0, 0, 1,
         0, 1,-1, 0
    };
    g_enc_delta += lut[(g_prev_ab << 2) | ab];
    g_prev_ab = ab;
}

static void pcint_init(void)
{
    DDRC  &= ~((1 << PC0) | (1 << PC1));
    PORTC |=  (1 << PC0) | (1 << PC1); // pull-ups

    PCMSK1 |= (1 << PCINT8) | (1 << PCINT9); // следим за PC0, PC1
    PCICR  |= (1 << PCIE1);                  // разрешить группу 1
}

В main читаете g_enc_delta и обнуляете — энкодер не теряет шагов даже при быстром вращении.

Правила написания ISR

  • Короче — лучше. Идеальный ISR ставит флаг или кладёт байт в буфер и выходит. Вся тяжёлая логика — в main.
  • volatile обязателен для всех переменных, которые ISR пишет/читает совместно с main. Иначе компилятор закеширует значение в регистре и «не увидит» обновления.
  • Атомарность многобайтовых данных. Чтение/запись uint16_t/uint32_t — не одна инструкция. Оборачивайте в cli()/sei() или ATOMIC_BLOCK.
  • Никаких _delay_ms в ISR — задержки используют счётчики, которые могут остановиться или дать непредсказуемый код.
  • Serial/printf внутри ISR — путь к зависаниям: эти функции сами ждут прерываний.
  • Пропущенные прерывания: пока выполняется ISR (или I=0), новые прерывания ждут — но флаг хранит только одно событие. Длинный ISR теряет события.

Атомарный доступ: ATOMIC_BLOCK

Удобнее ручных cli/sei — восстанавливает прежнее состояние I и компилируется в компактный код:

#include <util/atomic.h>

uint32_t copy;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    copy = g_ms;
}

То же для записи нескольких полей структуры, которую читает ISR.

Пример: точный расходомер (подсчёт импульсов)

Датчик расхода воды выдаёт ~450 импульсов на литр. Считаем импульсы на INT0, раз в секунду по таймеру пересчитываем в литры/мин:

volatile uint16_t g_pulses = 0;
volatile uint8_t  g_second = 0;

ISR(INT0_vect)  { g_pulses++; }

ISR(TIMER1_COMPA_vect) { g_second = 1; }   // 1 Гц, настройка CTC как в статье про таймеры

int main(void)
{
    // ... int0_init(), timer1_1hz_init() ...
    sei();

    uint16_t flow_ml_min = 0;
    while (1) {
        if (g_second) {
            g_second = 0;
            uint16_t p;
            ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { p = g_pulses; g_pulses = 0; }
            flow_ml_min = (uint32_t)p * 60000 / 450;  // мл/мин
            // вывести/передать flow_ml_min
        }
    }
}

Заметьте: подсчёт и пересчёт разнесены — ISR только инкремент, математика в main по секундному флагу.

Пробуждение из сна прерыванием

Прерывания — единственный способ разбудить спящий AVR (кроме сброса и WDT):

set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_cpu();          // спим до прерывания INT0
sleep_disable();      // проснулись — INT0 уже отработал

В режиме power-down работают только INT0/INT1 по уровню, PCINT и WDT — важно для автономных устройств на батарейках.

Типичные грабли

  • Забытый sei() — «прерывания не работают»: настроили маску, а глобальный бит I=0.
  • Имя вектора с опечаткой — компилятор не ругается, но вектор пуст: программа молча улетает на __vector_default и сбрасывается.
  • Флаг не сброшен перед разрешением — входите в ISR сразу после sei из-за старого события; чистите EIFR.
  • Общие буферы без защиты — классика race condition: ISR дописал байт, пока main пересчитывал указатель.
  • Использование прерывания по уровню там, где нужен фронт — ISR крутится, пока низкий уровень, и душит всю программу.

Итог

Прерывания превращают AVR из «последовательного исполнителя» в отзывчивую систему: аппаратура сообщает о событиях сама, а программа занимается делом. Дальше логично разобрать UART — там прерывания и кольцевой буфер дают неблокирующий обмен с ПК.

Поделиться
Понравился материал?

Комментарии

Свернуть форму

Необходимо Войти или Зарегистрироваться, чтобы оставлять комментарии