attachInterrupt изнутри: INT0, INT1 и свой ISR
attachInterrupt прячет три регистра и таблицу указателей на функции. Это удобно, но обработчик вызывается косвенно, а флаг фронта, пришедшего после detachInterrupt, никто не сбрасывает. Разберём WInterrupts.c и напишем свой ISR(INT0_vect).
Обновлено Редакция GAW
От Arduino к регистрамурок 6 из 830 мин
Цель урока: Понимать, что attachInterrupt делает с регистрами EICRA и EIMSK, оценивать цену косвенного вызова в обработчике, писать свой ISR(INT0_vect) со сбросом старого флага и подключать прерывание PCINT к выводу без INT0 и INT1.
У ATmega328P два внешних прерывания с собственными векторами — INT0 на выводе 2 (PD2) и INT1 на выводе 3 (PD3), — и прерывания по изменению на любом выводе (PCINT). Ядро Arduino даёт доступ к первым двум через attachInterrupt(). Что такое вектор, ISR и volatile, разобрано в уроке о прерываниях трека «AVR с нуля». Здесь — файл WInterrupts.c ядра ArduinoCore-avr 1.8.8.
Что делает attachInterrupt
// ArduinoCore-avr 1.8.8, cores/arduino/WInterrupts.c (ATmega328P branch, abridged)
static volatile voidFuncPtr intFunc[EXTERNAL_NUM_INTERRUPTS] = { nothing, nothing };
void attachInterrupt(uint8_t interruptNum, void (*userFunc)(void), int mode) {
if (interruptNum < EXTERNAL_NUM_INTERRUPTS) {
intFunc[interruptNum] = userFunc;
switch (interruptNum) {
case 0:
EICRA = (EICRA & ~((1 << ISC00) | (1 << ISC01))) | (mode << ISC00);
EIMSK |= (1 << INT0);
break;
case 1:
EICRA = (EICRA & ~((1 << ISC10) | (1 << ISC11))) | (mode << ISC10);
EIMSK |= (1 << INT1);
break;
}
}
}
Первым аргументом идёт номер прерывания, а не вывода: digitalPinToInterrupt(2) = 0, digitalPinToInterrupt(3) = 1, для остальных выводов — NOT_AN_INTERRUPT (pins_arduino.h, строка 79). Режим ядро не переводит, а просто сдвигает в биты ISC: константы Arduino подобраны так, что совпадают с таблицей 13-2 даташита.
| Режим Arduino | Значение | ISC01:00 | Когда срабатывает |
|---|---|---|---|
LOW | 0 | 00 | пока на выводе низкий уровень |
CHANGE | 1 | 01 | при любом изменении |
FALLING | 2 | 10 | по спаду |
RISING | 3 | 11 | по фронту |
Отсюда курьёз: HIGH тоже равна 1, и attachInterrupt(0, f, HIGH) на AVR включит прерывание по любому изменению. Режим LOW опасен по-своему: прерывание по уровню срабатывает всё время, пока вывод в нуле (раздел 13), а после каждого выхода из обработчика процессор успевает выполнить ровно одну команду основной программы (раздел 7.7). Пока кнопка нажата, loop() почти стоит.
Чего attachInterrupt не делает — не трогает регистр флагов EIFR. По разделу 7.7 флаг прерывания, пришедшего, пока оно запрещено, запоминается до разрешения. До первого вызова это не опасно: после сброса EICRA = 0, это прерывание по низкому уровню (раздел 13.2.1), а в этом режиме флаг INTF0 всегда сброшен (раздел 13.2.3). Опасен повторный вызов: detachInterrupt сбрасывает только бит в EIMSK, режим фронта в EICRA остаётся, и фронт на выводе 2 — дребезг кнопки, наводка — ставит флаг. Следующий attachInterrupt разрешит прерывание, и обработчик выполнится сразу, хотя нового события не было. То же бывает после своей записи режима фронта в EICRA.
Как ядро вызывает вашу функцию
// ArduinoCore-avr 1.8.8, cores/arduino/WInterrupts.c, lines 335-338 and 372-373
#define IMPLEMENT_ISR(vect, interrupt) \
ISR(vect) { \
intFunc[interrupt](); \
}
IMPLEMENT_ISR(INT0_vect, EXTERNAL_INT_0)
IMPLEMENT_ISR(INT1_vect, EXTERNAL_INT_1)
Настоящий обработчик прерывания — в ядре, а ваша функция вызывается из него по указателю. Компилятор не знает, какую функцию он вызовет, и по правилам avr-gcc считает, что она может испортить все call-used регистры — r18–r27, r30 и r31 (FAQ avr-libc). Прерванная программа этого не ожидает, поэтому обработчик ядра сохраняет все двенадцать в стеке, а заодно r0, r1 и SREG — регистр состояния аппаратура не сохраняет (раздел 7.7).
Оценка по таблице команд (раздел 37) и разделу 7.7.1, без тела вашей функции:
| Этап | Тактов |
|---|---|
| реакция на прерывание и переход по вектору | 4 + 3 |
| сохранить r1, r0, SREG и 12 call-used регистров (15 × PUSH, IN, CLR) | 32 |
загрузить указатель из intFunc (2 × LDS) и ICALL | 4 + 3 |
| возврат из вашей функции (RET) | 4 |
| восстановить регистры (15 × POP, OUT) | 31 |
| RETI | 4 |
Около 85 тактов — 5,3 мкс при 16 МГц (расчёт наш по ABI avr-gcc, точное число зависит от компилятора и видно в листинге avr-objdump -d). Свой ISR() с коротким телом сохраняет только те регистры, которые использует, и обходится без косвенного вызова. Для кнопки разница не важна, для энкодера на 10 000 импульсов в секунду — это 5 % процессорного времени только на накладные расходы (расчёт наш: 10 000 · 5,3 мкс = 53 мс в секунду). Термин о времени реакции — «Задержка прерывания».
То же самое регистрами
volatile uint16_t edges;
ISR(INT0_vect)
{
edges++;
}
void setup() {
Serial.begin(115200);
DDRD &= ~_BV(DDD2); // pin 2 (PD2, INT0) input
PORTD |= _BV(PORTD2); // pull-up on
EICRA = (EICRA & ~(_BV(ISC01) | _BV(ISC00))) | _BV(ISC01); // falling edge
EIFR = _BV(INTF0); // drop an edge latched so far
EIMSK |= _BV(INT0); // enable INT0
}
void loop() {
noInterrupts(); // 16-bit counter: read it in one piece
uint16_t n = edges;
interrupts();
Serial.println(n);
delay(500);
}
Порядок важен: флаг сбрасывают после записи режима фронта в EICRA — с этого момента фронты уже ставят флаг — и до разрешения в EIMSK. Сбрасывают его записью единицы (раздел 13.2.3), и именно присваиванием EIFR = _BV(INTF0), а не |=: FAQ avr-libc объясняет, что запись нулей в остальные биты флагов их не меняет, а чтение-изменение-запись сбросило бы заодно и другие установленные флаги. В этом скетче нельзя вызывать attachInterrupt: тогда в программу попадёт WInterrupts.c со своим ISR(INT0_vect), и компоновщик сообщит о повторном определении вектора.
Ещё две детали из даташита. Импульс короче периода такта процессора не гарантированно вызовет прерывание по фронту (раздел 13.2.1). И прерывание срабатывает даже тогда, когда вывод настроен на выход (раздел 13) — так можно вызвать обработчик программно.
Внутри обработчика
millis()внутри обработчика не растёт: прерывания запрещены (раздел 7.7), и обработчик Timer0 ждёт своей очереди.delay()там работает неверно — подробности в уроке 5.Serial.print()в обработчике не зависнет: при запрещённых прерыванияхHardwareSerial::write()сама опрашивает флаг UDRE0 (строки 255–266HardwareSerial.cpp). Но обработчик будет ждать отправки каждого байта сверх буфера — на 9600 бит/с это около 1 мс на символ.- Переменные, общие с
loop(), объявляютvolatile, а многобайтовые читают при запрещённых прерываниях, как в примере. Подробно о гонках — в статье «Прерывания и параллельность».
PCINT: прерывание на любом выводе
Кроме INT0 и INT1, у каждого вывода есть прерывание по изменению. Выводы собраны в три группы со своим вектором: PCINT0–7 (порт B, вектор PCINT0_vect), PCINT8–14 (порт C, PCINT1_vect) и PCINT16–23 (порт D, PCINT2_vect). Группу разрешают битом в PCICR, выводы внутри группы — маской PCMSK0–2 (разделы 13.2.4–13.2.8). В pins_arduino.h ядра есть макросы digitalPinToPCICR() и digitalPinToPCMSK(), но функции подключения и обработчиков PCINT ядро не даёт — только регистры. Прерывание PCINT срабатывает на любое изменение любого разрешённого вывода группы, и какой вывод изменился, обработчик выясняет сам, сравнивая PINx с прошлым значением.
Задание
- Какие биты каких регистров выставит
attachInterrupt(digitalPinToInterrupt(3), onEdge, RISING)? - Напишите регистровую настройку прерывания по изменению на выводе 8 Uno и пустой обработчик для него.
- Энкодер выдаёт 20 000 импульсов в секунду на выводе 2, обработчик через
attachInterruptв режимеCHANGE. Сколько процентов времени процессора уйдёт только на накладные расходы вызова по оценке из урока?
Ответ и решение
-
Вывод 3 — прерывание 1 (INT1).
RISING= 3 → ISC11:10 = 11 в регистре EICRA, затем бит INT1 в EIMSK. Флаг INTF1 в EIFR не сбрасывается. -
Вывод 8 — PB0, это PCINT0 в группе 0:
ISR(PCINT0_vect)
{
// compare PINB with the previous value to see which pin changed
}
void setup() {
DDRB &= ~_BV(DDB0); // pin 8 input
PORTB |= _BV(PORTB0); // pull-up
PCMSK0 |= _BV(PCINT0); // PB0 contributes to PCINT0_vect
PCIFR = _BV(PCIF0); // drop an old flag
PCICR |= _BV(PCIE0); // enable pin change group 0
}
CHANGEсрабатывает на оба фронта: 40 000 прерываний в секунду. 40 000 · 5,3 мкс ≈ 0,21 с в секунду — около 21 % времени процессора (расчёт наш, без тела функции). СвойISR(INT0_vect)с коротким телом заметно дешевле.
Что дальше
В уроке 7 разберём Serial: два кольцевых буфера по 64 байта, прерывания приёма и передачи и почему на 57 600 бит/с ядро выбирает режим с большей ошибкой. Регистры внешних прерываний EICRA, EIMSK, EIFR, PCICR и PCMSK — на странице регистров EXINT ATmega328P, а список векторов с приоритетами — в термине «Таблица векторов прерываний». Как подавить дребезг кнопки, которая висит на прерывании, рассказывает термин «Дребезг контактов».
Частые вопросы
Почему после повторного attachInterrupt обработчик срабатывает сразу?
detachInterrupt сбрасывает только бит в EIMSK, а режим фронта в EICRA остаётся. Флаг INTF0 или INTF1 ставится по фронту и при запрещённом прерывании и хранится, пока его не сбросят, а attachInterrupt флаг не трогает: фронт, пришедший между detachInterrupt и новым attachInterrupt, вызовет обработчик, как только бит в EIMSK встанет. До первого attachInterrupt этого не бывает: после сброса EICRA = 0 — режим низкого уровня, а в нём флаг всегда сброшен. Своё прерывание включайте так: EICRA, затем EIFR = _BV(INTF0), затем EIMSK.
Можно ли написать свой ISR(INT0_vect) в скетче Arduino?
Можно, если в скетче нет вызовов attachInterrupt и detachInterrupt. Обработчики INT0_vect и INT1_vect определены в файле ядра WInterrupts.c, и компоновщик подключает этот файл, только когда нужна одна из его функций. Если вызвать attachInterrupt и написать свой ISR(INT0_vect), сборка остановится с ошибкой о повторном определении вектора.
Что будет, если передать в attachInterrupt режим HIGH?
На ATmega328P прерывания по высокому уровню нет, а константа HIGH равна 1 — столько же, сколько CHANGE. Ядро просто сдвигает число в биты ISC, поэтому attachInterrupt(0, f, HIGH) включит прерывание по любому изменению уровня. Режим HIGH есть у некоторых других архитектур Arduino, но не у AVR.
Источники
- Arduino. ArduinoCore-avr 1.8.8: cores/arduino/WInterrupts.c, wiring_private.h, Arduino.h, variants/standard/pins_arduino.h — тег 1.8.8 — коммит 86df345 от 21 мая 2026 (GitHub API, tags и commits); WInterrupts.c, стр. 38–67: таблица intFunc указателей на функции, по умолчанию пустая nothing(); стр. 69–217: attachInterrupt запоминает функцию и для ATmega328P (ветка #else, стр. 171–214) пишет EICRA = (EICRA & ~(ISCn1 | ISCn0)) | (mode << ISCn0) и ставит бит в EIMSK, EIFR не трогает; стр. 219–331: detachInterrupt сбрасывает бит EIMSK и возвращает nothing(); стр. 335–338 и 370–374: IMPLEMENT_ISR — ISR(INT0_vect) и ISR(INT1_vect) вызывают intFunc[n](); wiring_private.h, стр. 62–64: для ATmega328P EXTERNAL_NUM_INTERRUPTS = 2; Arduino.h, стр. 40–41 и 60–62: LOW = 0, HIGH = 1, CHANGE = 1, FALLING = 2, RISING = 3; pins_arduino.h, стр. 74–79: digitalPinToInterrupt — вывод 2 → 0, вывод 3 → 1, макросы digitalPinToPCICR, digitalPinToPCMSK; HardwareSerial.cpp, стр. 255–266: при запрещённых прерываниях write() сам опрашивает флаг UDRE0; platform.txt, стр. 66: файлы ядра собираются в архив, из которого компоновщик берёт только нужные; HardwareSerial0.cpp, стр. 28–34: компоновщик подключает файл целиком, когда нужен любой его элемент
- Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet (DS40002061B) — разд. 7.7: при входе в прерывание бит I сбрасывается, RETI ставит его снова; флаг, пришедший при запрещённом прерывании, запоминается до разрешения; после выхода из прерывания всегда выполняется ещё одна команда основной программы; SREG не сохраняется аппаратно; приоритет — по адресу вектора, после RESET — INT0; разд. 7.7.1: реакция не меньше 4 тактов, переход по вектору — 3 такта, RETI — 4 такта; разд. 13: прерывание по уровню срабатывает, пока вывод в низком уровне, INT0 и INT1 работают и на выходе; разд. 13.2.1, табл. 13-1 и 13-2: EICRA, ISC01:00 — 00 низкий уровень, 01 любое изменение, 10 спад, 11 фронт; импульс длиннее периода такта гарантированно вызывает прерывание; разд. 13.2.2: EIMSK; разд. 13.2.1: начальное значение EICRA — 0, то есть прерывание по низкому уровню; разд. 13.2.3: EIFR, флаг INTF0 сбрасывается при входе в обработчик или записью единицы, а пока INT0 настроен на уровень, флаг всегда сброшен; разд. 13.2.4–13.2.8: PCICR, PCIFR, PCMSK0–2; разд. 14.3.1 и 14.3.3: INT0 — PD2, INT1 — PD3, PCINT0 — PB0; разд. 37: PUSH и POP — по 2 такта, ICALL — 3
- AVR-LibC 2.3.2: avr/interrupt.h, iom328p.h, FAQ (github.com/avrdudes/avr-libc) — макрос ISR, векторы INT0_vect (1) и PCINT0_vect (3); FAQ: регистры r18–r27, r30, r31 — call-used, вызванная функция может их испортить, сохраняет их вызывающая сторона; r0 и r1 обработчики прерываний сохраняют; флаги прерываний сбрасывают записью единицы без чтения-изменения-записи
Нашли ошибку?
Смотрите также
- Прерывания и общие данныеISR на AVR и Cortex-M, атомарность, критические секции, кольцевой буфер
- Задержка прерыванияВремя от запроса до обработчика: Cortex-M3 и AVR
- Прерывание по фронту и по уровнюДва способа запуска внешнего прерывания: перепадом сигнала или его уровнем
- Прерывание по изменению вывода (PCINT)Прерывание AVR при любом изменении уровня на выводе
- Таймеры и прерыванияCTC на Timer0 и Timer1, ISR, sei, volatile, ATOMIC_BLOCK
- Почему не срабатывает attachInterrupt на Arduino?Номер прерывания вместо номера вывода, только D2 и D3 на Uno, подтяжка, volatile