Что такое прерывание и зачем оно
Прерывание — механизм, при котором периферия (таймер, 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 — там прерывания и кольцевой буфер дают неблокирующий обмен с ПК.