Как работают прерывания на Arduino Uno и аналогичных платах?

Пожалуйста, объясните, как работают прерывания на Arduino Uno и подобных платах с процессором ATmega328P. Например, на таких платах:

  • Уно
  • Мини
  • Нано
  • Про Мини
  • Кувшинка

В частности, пожалуйста, обсудите:

  • Для чего использовать прерывания
  • Как написать процедуру обработки прерываний (ISR)
  • Проблемы со временем
  • Критические разделы
  • Атомарный доступ к данным

Примечание: это справочный вопрос.

, 👍18


1 ответ


Лучший ответ:

40

Кратко:

При написании процедуры обработки прерываний (ISR):

  • Пишите коротко
  • Не используйте delay()
  • Не делайте серийных отпечатков
  • Сделать переменные, общие с основным кодом, нестабильными
  • Переменные, используемые совместно с основным кодом, могут нуждаться в защите с помощью «критических секций» (см. ниже)
  • Не пытайтесь включать или выключать прерывания

Что такое прерывания?

У большинства процессоров есть прерывания. Прерывания позволяют вам реагировать на «внешние» события, одновременно занимаясь другими делами. Например, если вы готовите ужин, вы можете поставить картофель вариться на 20 минут. Вместо того, чтобы смотреть на часы 20 минут, вы можете установить таймер, а затем пойти посмотреть телевизор. Когда таймер звонит, вы «прерываете» просмотр телевизора, чтобы что-то сделать с картофелем.


Пример прерываний

const byte LED = 13;
const byte SWITCH = 2;

// Процедура обслуживания прерываний (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // конец switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // чтобы мы могли обновить светодиод
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // присоединить обработчик прерываний
}  // конец настройки

void loop ()
{
  // цикл ничего не делает
}

В этом примере показано, как, даже если основной цикл ничего не делает, можно включить или выключить светодиод на выводе 13, если нажат переключатель на выводе D2.

Чтобы проверить это, просто подключите провод (или переключатель) между D2 и землёй. Внутренняя подтяжка (включена при настройке) обычно переводит вывод в состояние ВЫСОКОГО уровня. При подаче на землю он становится НИЗКИМ. Изменение состояния вывода обнаруживается прерыванием CHANGE, которое вызывает процедуру обработки прерываний (ISR).

В более сложном примере основной цикл может выполнять что-то полезное, например, снимать показания температуры, и позволять обработчику прерываний обнаруживать нажатие кнопки.


Преобразование номеров выводов в номера прерываний

Для упрощения преобразования номеров векторов прерываний в номера выводов можно вызвать функцию digitalPinToInterrupt(), передав ей номер вывода. Она возвращает соответствующий номер прерывания или NOT_AN_INTERRUPT (-1).

Например, на Uno вывод D2 на плате — это прерывание 0 (INT0_vect из таблицы ниже).

Таким образом, эти две строки имеют одинаковый эффект:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Однако второй вариант легче читать и он более переносим на различные типы Arduino.


Доступные прерывания

Ниже приведен список прерываний в порядке приоритета для Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Внутренние имена (которые можно использовать для настройки обратных вызовов ISR) указаны в скобках.

Предупреждение: если вы неправильно напишете имя вектора прерывания, даже просто ошибившись с заглавными буквами (что легко сделать), подпрограмма прерывания не будет вызвана, и вы не получите ошибку компиляции.


Причины использования прерываний

Основные причины, по которым вы можете использовать прерывания:

  • Для обнаружения изменений положения выводов (например, поворотных энкодеров, нажатий кнопок)
  • Сторожевой таймер (например, если ничего не происходит в течение 8 секунд, прервите меня)
  • Прерывания таймера — используются для сравнения/переполнения таймеров
  • Передача данных SPI
  • Передача данных I2C
  • Передача данных USART
  • АЦП-преобразования (аналогово-цифровые)
  • EEPROM готов к использованию
  • Флэш-память готова

«Передача данных» может использоваться, чтобы позволить программе выполнять что-то еще во время отправки или получения данных через последовательный порт, порт SPI или порт I2C.

Разбудить процессор

Внешние прерывания, прерывания по изменению состояния контактов и прерывания от сторожевого таймера также могут использоваться для пробуждения процессора. Это может быть очень удобно, поскольку в спящем режиме процессор можно настроить на значительно меньшее потребление энергии (например, около 10 микроампер). Прерывание по нарастанию, спаду или низкому уровню может использоваться для пробуждения устройства (например, при нажатии кнопки), а прерывание по «сторожевому таймеру» может периодически пробуждать его (например, для проверки времени или температуры).

Прерывания по изменению состояния контакта можно использовать для пробуждения процессора при нажатии клавиши на клавиатуре или в аналогичной ситуации.

Процессор также может быть разбужен прерыванием таймера (например, достижением таймером определенного значения или переполнением) и некоторыми другими событиями, такими как входящее сообщение I2C.


Включение/отключение прерываний

Прерывание «сброс» отключить нельзя. Однако другие прерывания можно временно отключить, сбросив глобальный флаг прерывания.

Включить прерывания

Вы можете включить прерывания с помощью вызова функции «interrupts» или «sei», например:

interrupts ();  // или ...
sei ();         // установить флаг прерываний

Отключить прерывания

Если вам нужно отключить прерывания, вы можете «очистить» глобальный флаг прерывания следующим образом:

noInterrupts ();  // или ...
cli ();           // очистить флаг прерываний

Оба метода имеют одинаковый эффект, но использование interrupts / noInterrupts немного облегчает запоминание их расположения.

По умолчанию прерывания в Arduino включены. Не отключайте их надолго, иначе, например, таймеры, будут работать неправильно.

Зачем отключать прерывания?

Возможно, существуют фрагменты кода, выполнение которых критически важно и которые вы не хотите прерывать, например, прерыванием по таймеру.

Кроме того, если обработчик прерывания (ISR) обновляет многобайтовые поля, вам может потребоваться отключить прерывания, чтобы данные получались «атомарно». В противном случае обработчик прерывания (ISR) может обновить один байт, пока вы читаете другой.

Например:

noInterrupts ();
long myCounter = isrCounter;  // получить значение, установленное ISR
interrupts ();

Временное отключение прерываний гарантирует, что isrCounter (счетчик, установленный внутри ISR) не изменится, пока мы получаем его значение.

Внимание: если вы не уверены, включены ли уже прерывания, вам необходимо сохранить текущее состояние и восстановить его позже. Например, код из функции millis() делает следующее:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- сохранить регистр состояния

  // отключаем прерывания, пока читаем timer0_millis, иначе можем получить
  // несогласованное значение (например, в середине записи в timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- восстановить регистр состояния, включая флаг прерывания

  return m;
}

Обратите внимание, что указанные строки сохраняют текущий регистр SREG (регистр состояния), включая флаг прерывания. После получения значения таймера (длиной 4 байта) мы возвращаем регистр состояния в исходное состояние.


Советы

Имена функций

Функции cli / sei и регистр SREG специфичны для процессоров AVR. При использовании других процессоров, например, ARM, функции могут немного отличаться.

Глобальное отключение против отключения одного прерывания

При использовании cli() вы отключаете все прерывания (включая прерывания по таймеру, последовательные прерывания и т. д.).

Однако, если вы хотите просто отключить конкретное прерывание, следует снять флаг разрешения прерываний для этого источника. Например, для внешних прерываний вызовите функцию detachInterrupt().


Что такое приоритет прерывания?

Поскольку имеется 25 прерываний (не считая сброса), возможно возникновение нескольких событий прерывания одновременно или, по крайней мере, до обработки предыдущего. Кроме того, событие прерывания может произойти, когда прерывания отключены.

Порядок приоритетов — это последовательность, в которой процессор проверяет события прерываний. Чем выше в списке, тем выше приоритет. Например , внешний запрос прерывания 0 (вывод D2) будет обработан раньше внешнего запроса прерывания 1 (вывод D3).


Могут ли возникать прерывания, если прерывания отключены?

События прерываний (то есть, обнаружение события) могут произойти в любой момент, и большинство из них запоминаются установкой флага «событие прерывания» внутри процессора. Если прерывания отключены, то они будут обработаны при повторном включении в порядке приоритета.


Как использовать прерывания?

  • Вы пишете процедуру обработки прерывания (ISR). Она вызывается при возникновении прерывания.
  • Вы сообщаете процессору, когда нужно вызвать прерывание.

Написание ISR

Процедуры обработки прерываний — это функции без аргументов. Некоторые библиотеки Arduino разработаны для вызова ваших собственных функций, поэтому вы просто указываете обычную функцию (как в примерах выше), например.

// Процедура обслуживания прерываний (ISR)
void switchPressed ()
{
 flag = true;
}  // конец switchPressed

Однако, если библиотека еще не предоставила «крючок» для ISR, вы можете создать свой собственный, например:

volatile char buf [100];
volatile byte pos;

// Процедура прерывания SPI
ISR (SPI_STC_vect)
{
byte c = SPDR;  // захватить байт из регистра данных SPI

  // добавить в буфер, если есть место
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // конец комнаты доступен
}  // конец процедуры прерывания SPI_STC_vect

В этом случае вы используете макрос «ISR» и указываете имя соответствующего вектора прерывания (из таблицы выше). В этом случае обработчик прерывания обрабатывает завершение передачи по SPI. (Обратите внимание, что в некоторых старых кодах вместо обработчика прерывания используется SIGNAL, однако SIGNAL устарел).

Подключение ISR к прерыванию

Для прерываний, уже обрабатываемых библиотеками, достаточно использовать документированный интерфейс. Например:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // сделать что-нибудь с входящим байтом
    }
}  // конец receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

В этом случае библиотека I2C предназначена для внутренней обработки входящих байтов I2C с последующим вызовом предоставленной вами функции в конце входящего потока данных. В этом случае receiveEvent не является строго обработчиком прерывания (у него есть аргумент), но вызывается встроенным обработчиком прерывания.

Другим примером является прерывание от «внешнего контакта».

// Процедура обслуживания прерываний (ISR)
void switchPressed ()
{
  // обрабатывать изменение пина здесь
}  // конец switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // прикрепить обработчик прерываний для D2
}  // конец настройки

В этом случае функция attachInterrupt добавляет функцию switchPressed во внутреннюю таблицу и, кроме того, настраивает соответствующие флаги прерываний в процессоре.

Настройка процессора для обработки прерывания

Следующий шаг после получения ISR — сообщить процессору, что вы хотите, чтобы это конкретное условие вызвало прерывание.

Например, для внешнего прерывания 0 (прерывание D2) вы можете сделать что-то вроде этого:

EICRA &= ~3;  // очистить существующие флаги
EICRA |= 2;   // установить требуемые флаги (прерывание по падению уровня)
EIMSK |= 1;   // включить его

Более читабельным было бы использование определенных имен, например:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // очистить существующие флаги
EICRA |= bit (ISC01);    // установить требуемые флаги (прерывание по падающему уровню)
EIMSK |= bit (INT0);     // включить его

EICRA (регистр управления внешним прерыванием A) будет настроен в соответствии с этой таблицей из технического описания Atmega328. Это определяет точный тип прерывания, который вам нужен:

  • 0: Низкий уровень INT0 генерирует запрос прерывания (прерывание LOW).
  • 1: Любое логическое изменение на INT0 генерирует запрос прерывания (прерывание CHANGE).
  • 2: Задний фронт INT0 генерирует запрос на прерывание (прерывание FALLING).
  • 3: Передний фронт INT0 генерирует запрос прерывания (прерывание RISING).

EIMSK (регистр внешней маски прерываний) фактически разрешает прерывание.

К счастью, вам не нужно запоминать эти номера, потому что attachInterrupt делает это за вас. Однако именно это и происходит на самом деле, а для других прерываний вам, возможно, придётся «вручную» устанавливать флаги прерываний.


Низкоуровневые ISR против библиотечных ISR

Для упрощения работы некоторые распространённые обработчики прерываний фактически находятся внутри библиотечного кода (например, INT0_vect и INT1_vect), а для них предоставляется более удобный интерфейс (например, attachInterrupt). Функция attachInterrupt фактически сохраняет адрес нужного обработчика прерываний в переменную, а затем вызывает её из INT0_vect/INT1_vect при необходимости. Она также устанавливает соответствующие флаги регистров для вызова обработчика при необходимости.


Можно ли прерывать ISR?

Короче говоря, нет, если только вы этого не хотите.

При запуске ISR прерывания отключаются. Естественно, они должны быть включены изначально, иначе ISR не был бы запущен. Однако, чтобы избежать прерывания самого ISR, процессор отключает прерывания.

После завершения работы ISR прерывания снова включаются. Компилятор также генерирует код внутри ISR для сохранения регистров и флагов состояния, так что ваши действия в момент возникновения прерывания не будут затронуты.

Однако вы можете включить прерывания внутри ISR, если это абсолютно необходимо, например.

// Процедура обслуживания прерываний (ISR)
void switchPressed ()
{
  // обрабатывать изменение пина здесь
  interrupts ();  // разрешить больше прерываний

}  // конец switchPressed

Обычно для этого нужна очень веская причина, поскольку другое прерывание теперь может привести к рекурсивному вызову pinChange, что может привести к нежелательным результатам.


Сколько времени занимает выполнение ISR?

Согласно техническому описанию, минимальное время обработки прерывания составляет 4 такта (для помещения текущего счётчика команд в стек), после чего следует выполнение кода, находящегося в позиции вектора прерывания. Обычно это включает переход к месту, где фактически находится процедура обработки прерывания, что занимает ещё 3 такта. Анализ кода, полученного компилятором, показывает, что выполнение ISR, созданного с объявлением «ISR», может занять около 2,625 мкс, плюс время выполнения самого кода. Точное время зависит от того, сколько регистров необходимо сохранить и восстановить. Минимальное время составляет 1,1875 мкс.

Внешние прерывания (где используется attachInterrupt) выполняют немного больше работы и занимают в общей сложности около 5,125 мкс (при работе с тактовой частотой 16 МГц).


Сколько времени пройдет до того, как процессор начнет входить в ISR?

Этот показатель может немного варьироваться. Приведённые выше значения являются идеальными, при которых прерывание обрабатывается немедленно. Несколько факторов могут задержать обработку:

  • Если процессор находится в спящем режиме, предусмотрены определённые интервалы времени «пробуждения», которые могут составлять несколько миллисекунд, пока тактовая частота возвращается к прежней частоте. Это время зависит от настроек предохранителей и глубины сна.

  • Если процедура обработки прерываний уже выполняется, то дальнейшие прерывания не могут быть введены, пока она не завершится или не разрешит прерывания сама. Именно поэтому следует делать каждую процедуру обработки прерываний короткой, поскольку каждая микросекунда, проводимая в одной, потенциально задерживает выполнение другой.

  • Некоторые коды отключают прерывания. Например, вызов millis() кратковременно отключает прерывания. Следовательно, время обработки прерывания увеличится на время, в течение которого прерывания были отключены.

  • Прерывания могут быть обработаны только в конце инструкции, поэтому, если конкретная инструкция занимает три такта и только что началась, то прерывание будет задержано как минимум на пару тактов.

  • Событие, которое снова включает прерывания (например, возврат из процедуры обработки прерывания), гарантированно приведёт к выполнению как минимум одной новой инструкции. Поэтому даже если обработчик прерывания завершится, а ваше прерывание будет ожидать обработки, ему всё равно придётся ждать ещё одну инструкцию, прежде чем оно будет обработано.

  • Поскольку прерывания имеют приоритет, прерывание с более высоким приоритетом может быть обслужено раньше интересующего вас прерывания.


Вопросы производительности

Прерывания могут повысить производительность во многих ситуациях, поскольку вы можете продолжать выполнять «основную работу» программы, не проверяя постоянно, нажаты ли кнопки. При этом накладные расходы на обслуживание прерывания, как обсуждалось выше, фактически превышают затраты на «короткий цикл» опроса одного входного порта. Вы едва ли сможете отреагировать на событие в течение, скажем, микросекунды. В этом случае можно отключить прерывания (например, таймеры) и просто циклически искать изменение состояния контакта.


Как прерывания выстраиваются в очередь?

Существует два вида прерываний:

  • Некоторые события устанавливают флаг и обрабатываются в порядке приоритета, даже если событие, вызвавшее их, уже завершилось. Например, прерывание по повышению, понижению или изменению уровня на выводе D2.

  • Другие события проверяются только в том случае, если они происходят «прямо сейчас». Например, низкоуровневое прерывание на выводе D2.

Те, что устанавливают флаг, можно считать поставленными в очередь, поскольку флаг прерывания остаётся установленным до тех пор, пока не будет запущена процедура обработки прерывания, после чего процессор сбрасывает флаг. Конечно, поскольку флаг только один, если то же самое прерывание возникнет снова до обработки первого, оно не будет обработано дважды.

Следует помнить, что эти флаги можно установить до подключения обработчика прерываний. Например, можно «пометить» прерывание по нарастанию или спаду уровня на выводе D2, и тогда, как только вы выполните команду attachInterrupt, прерывание сработает немедленно, даже если событие произошло час назад. Чтобы избежать этого, можно вручную сбросить флаг. Например:

EIFR = bit (INTF0);  // очистить флаг для прерывания 0
EIFR = bit (INTF1);  // очистить флаг для прерывания 1

Однако «низкоуровневые» прерывания постоянно проверяются, поэтому, если не соблюдать осторожность, они будут продолжать срабатывать даже после вызова прерывания. То есть, обработчик прерывания завершится, и прерывание немедленно сработает снова. Чтобы избежать этого, следует выполнить отсоединение Interrupt сразу после того, как станет известно о срабатывании прерывания.


Советы по написанию ISR

Короче говоря, делайте их короткими! Пока выполняется обработчик прерываний (ISR), другие прерывания не могут быть обработаны. Поэтому вы можете легко пропустить нажатия кнопок или входящие последовательные данные, если попытаетесь сделать слишком много. В частности, не стоит пытаться выполнять отладочные «печатные» операции внутри обработчика прерываний (ISR). Время, затрачиваемое на это, скорее всего, создаст больше проблем, чем решит.

Разумным решением будет установить однобайтовый флаг, а затем проверить его в основном цикле. Или сохранить входящий байт из последовательного порта в буфер. Встроенные прерывания таймера отслеживают прошедшее время, срабатывая каждый раз при переполнении внутреннего таймера, и таким образом можно определить прошедшее время, зная, сколько раз таймер переполнялся.

Помните, что внутри ISR прерывания отключены. Поэтому надежда на то, что время, возвращаемое вызовами функции millis(), изменится, приведёт к разочарованию. Получить время таким способом вполне допустимо, но учтите, что таймер не увеличивается. Если вы задержитесь в ISR слишком долго, таймер может пропустить событие переполнения, что приведёт к тому, что возвращаемое функцией millis() время станет неверным.

Тест показывает, что на процессоре Atmega328 с частотой 16 МГц вызов micros() занимает 3,5625 мкс. Вызов millis() занимает 1,9375 мкс. Запись (сохранение) текущего значения таймера — разумное решение для обработчика прерывания (ISR). Определение прошедших миллисекунд выполняется быстрее, чем прошедших микросекунд (счётчик миллисекунд просто извлекается из переменной). Однако счётчик микросекунд получается путём сложения текущего значения таймера Timer 0 (которое будет продолжать увеличиваться) с сохранённым значением «Счётчик переполнений таймера 0».

Предупреждение: Поскольку прерывания внутри ISR отключены, а последняя версия Arduino IDE использует прерывания для последовательного чтения и записи, а также для увеличения счётчика, используемого «millis» и «delay», не следует пытаться использовать эти функции внутри ISR. Другими словами:

  • Не пытайтесь задерживать, например: delay (100);
  • Вы можете получить время, вызвав millis, однако оно не увеличится, поэтому не пытайтесь задержать его, ожидая его увеличения.
  • Не делайте последовательную печать (например, Serial.println ("ISR filled"); )
  • Не пытайтесь читать последовательно.

Прерывания по изменению контакта

Существует два способа обнаружения внешних событий на контактах. Первый — это специальные контакты «внешнего прерывания» D2 и D3. Это общие дискретные события прерывания, по одному на контакт. Вы можете получить их, используя функцию attachInterrupt для каждого контакта. Для прерывания можно указать условие нарастания, спада, изменения или низкого уровня.

Однако существуют также прерывания по смене состояния выводов для всех выводов (в Atmega328, но не обязательно для всех выводов в других процессорах). Они действуют на группы выводов (D0–D7, D8–D13 и A0–A5). Они также имеют более низкий приоритет, чем прерывания по внешним событиям. Однако их использование немного сложнее, чем внешних прерываний, поскольку они сгруппированы в пакеты. Поэтому, если прерывание срабатывает, вам придётся самостоятельно определить в своём коде, какой именно вывод вызвал прерывание.

Пример кода:

ISR (PCINT0_vect)
 {
 // обрабатывать прерывание по смене контакта для D8-D13 здесь
 }  // конец PCINT0_vect

ISR (PCINT1_vect)
 {
 // обрабатывать прерывание по смене контакта для A0-A5 здесь
 }  // конец PCINT1_vect

ISR (PCINT2_vect)
 {
 // обрабатывать прерывание по смене контакта для D0-D7 здесь
 }  // конец PCINT2_vect


void setup ()
  {
  // прерывание по смене контакта (пример для D9)
  PCMSK0 |= bit (PCINT1);  // нужен вывод 9
  PCIFR  |= bit (PCIF0);   // очистить все невыполненные прерывания
  PCICR  |= bit (PCIE0);   // разрешить прерывания по смене контакта для D8-D13
  }

Чтобы обработать прерывание по изменению контакта, вам необходимо:

  • Укажите, какой вывод входит в группу. Это переменная PCMSKn (где n может быть 0, 1 или 2 согласно таблице ниже). Прерывания могут быть на нескольких выводах.
  • Включить соответствующую группу прерываний (0, 1 или 2)
  • Предоставьте обработчик прерываний, как показано выше

Таблица пинов -> Имена/маски изменения пинов

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Обработка обработчика прерываний

Обработчику прерываний потребуется определить, какой вывод вызвал прерывание, если маска указывает более одного (например, если вы хотите прерывания по D8/D9/D10). Для этого потребуется сохранить предыдущее состояние этого вывода и определить (выполнив digitalRead или аналогичный метод), изменилось ли состояние этого вывода.


Вы, вероятно, все равно используете прерывания...

«Обычная» среда Arduino уже использует прерывания, даже если вы лично не пытаетесь это сделать. Вызовы функций millis() и micros() используют функцию «переполнения таймера». Один из внутренних таймеров (таймер 0) настроен на прерывание примерно 1000 раз в секунду и увеличение внутреннего счётчика, который фактически становится счётчиком millis(). Это ещё не всё, поскольку настройка выполняется для точной тактовой частоты.

Кроме того, аппаратная библиотека последовательного порта использует прерывания для обработки входящих и исходящих последовательных данных. Это очень полезно, поскольку ваша программа может заниматься другими делами, пока срабатывают прерывания, и заполнять внутренний буфер. Затем, проверяя Serial.available(), вы можете узнать, что было помещено в этот буфер, если таковое имелось.


Выполнение следующей инструкции после разрешения прерываний

После небольшого обсуждения и исследования на форуме Arduino мы прояснили, что именно происходит после включения прерываний. Я думаю, есть три основных способа включения прерываний, которые раньше были недоступны:

  sei ();  // установить флаг разрешения прерывания
  SREG |= 0x80;  // установить старший бит в регистре состояния
  reti  ;   // инструкция ассемблера "возврат из прерывания"

Во всех случаях процессор гарантирует, что следующая инструкция после включения прерываний (если они ранее были отключены) всегда будет выполнена, даже если ожидается событие прерывания. (Под «следующей» я подразумеваю следующую в последовательности программы, а не обязательно ту, которая физически следует за ней. Например, инструкция RETI возвращается к месту возникновения прерывания, а затем выполняет ещё одну инструкцию).

Это позволяет вам писать такой код:

sei ();
sleep_cpu ();

Если бы не эта гарантия, прерывание могло бы произойти до того, как процессор перешел в спящий режим, и тогда он мог бы никогда не проснуться.


Пустые прерывания

Если вам нужно просто прерывание для пробуждения процессора, но не нужно выполнять какие-либо конкретные действия, вы можете использовать определение EMPTY_INTERRUPT, например.

EMPTY_INTERRUPT (PCINT1_vect);

Это просто генерирует инструкцию «reti» (возврат из прерывания). Поскольку она не пытается сохранять или восстанавливать регистры, это был бы самый быстрый способ получить прерывание для её пробуждения.


Критические секции (атомарный доступ к переменным)

Существуют некоторые тонкие проблемы, касающиеся переменных, которые являются общими для процедур обработки прерываний (ISR) и основного кода (то есть кода, не находящегося в ISR).

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

Во-первых... когда используются «изменчивые» переменные?

Переменная должна быть помечена как volatile только в том случае, если она используется как внутри ISR, так и вне его.

  • Переменные, используемые только вне ISR, не должны быть изменчивыми.
  • Переменные, используемые только внутри ISR, не должны быть изменчивыми.
  • Переменные, используемые как внутри, так и вне ISR, должны быть изменчивыми.

напр.

volatile int counter;

Обозначение переменной как volatile указывает компилятору не «кэшировать» её содержимое в регистре процессора, а всегда считывать его из памяти при необходимости. Это может замедлить обработку, поэтому не стоит делать каждую переменную volatile, когда она не нужна.

Отключить прерывания при доступе к изменчивой переменной

Например, чтобы сравнить count с каким-то числом, отключите прерывания во время сравнения на случай, если один байт count был обновлен обработчиком прерывания, а другой — нет.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // конец TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // конец настройки

void loop ()
  {
  noInterrupts ();    // <------ критическая секция
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ конец критической секции
  } // конец цикла

Прочитайте техническое описание!

Более подробную информацию о прерываниях, таймерах и т. д. можно получить из технического описания процессора.

https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf


Дополнительные примеры

Из-за ограничений по размеру публикации я не могу привести больше примеров кода. Больше примеров кода можно найти на моей странице о прерываниях.

,

Очень полезная ссылка. Это был поразительно быстрый ответ., @Dat Han Bag

Это был справочный вопрос. У меня был заготовлен ответ, и он был бы ещё быстрее, если бы не был слишком длинным, поэтому мне пришлось его сократить. Подробнее см. на сайте по ссылке., @Nick Gammon

Что касается «спящего режима», эффективно ли переводить Arduino в спящий режим, скажем, на 500 мс?, @Dat Ha

@Nick Gammon Полагаю, включение или выключение питания (с автоматизацией или без) процессора можно определить как нестандартное прерывание, если вы этого хотите. «У меня был готов ответ» — вы только что разрушили всю магию того момента, которая, по моему мнению, в нём была., @Dat Han Bag

«Эффективно ли переводить Arduino в спящий режим, скажем, на 500 мс?» — Конечно, да. У меня есть проект [который мигает светодиодом](http://www.gammon.com.au/forum/?id=12769) примерно каждую секунду, а в промежутках между ними засыпает. Значительно меньшее энергопотребление в спящем режиме значительно экономит заряд батареи., @Nick Gammon

«Я полагаю, что включение или выключение питания (с автоматизацией или без) для ЦП можно определить как нестандартное прерывание» - выключение и включение питания вызовет «прерывание» Сброс., @Nick Gammon

Несколько замечаний: - В примере прерываний следует упомянуть механический дребезг. - Не каждый источник прерывания можно использовать для пробуждения ЦП из большинства спящих режимов. - Вы можете пропустить прерывание, если во время обработки другого ISR или критической секции произошло более одного события (см. раздел «Может ли прерывание произойти ...»). - Если вам нужен только флаг, но вы не знаете, сколько раз, вы можете использовать соответствующий флаг, не включая прерывание. Флаг необходимо сбросить вручную, записав логическую 1 (см. раздел «Советы»). - Максимальная частота прерываний по изменению состояния вывода и их рабочий цикл (F_CPU/2 для 50:50), @KIIV

Меня ограничивает ограничение на размер сообщения. Изначально мой пост был размером около 36 000 байт, что превысило лимит в 30 000 байт. Я сократил его до 29 990 байт и нажал «Отправить», но увидел, что мой пост весит 36 000 байт. Затем я удалил 6000 байт, чтобы его приняли. Так что, если вы хотите предложить что-то добавить, вам придётся предложить и что-то удалить. Извините. :), @Nick Gammon

Да, что-то вроде [Arduino "Documentation"](http://stackoverflow.com/documentation/arduino/topics) от SO подошло бы больше. Жаль, что здесь этого нет., @KIIV

Фантастическая ссылка! Несколько комментариев: В разделе «Пробуждение процессора»: любое прерывание может разбудить процессор, если только он не находится в режиме глубокого сна (спящего режима, отличного от режима ожидания). В разделе «Включение/отключение прерываний»: поскольку многие флаги называются «флагами прерываний», я бы предпочёл последовать примеру Atmel и использовать «глобальный флаг прерываний» для SREG(I). В разделе «Зачем отключать прерывания?»: заменить «сохранение/восстановление регистров процессора» на «сохранение/восстановление регистра состояния». Или, возможно, стоит задокументировать ATOMIC_BLOCK() вместо явного сохранения и восстановления SREG... В разделе «Написание ISR»: определение «ISR» → макрос «ISR»., @Edgar Bonet

В разделе _Сколько времени занимает выполнение ISR?_: Длина пролога и эпилога зависит от количества регистров, необходимых ISR. Минимум — 8 (пролог) и 11 (эпилог) тактов ЦП. В разделе _Сколько времени процессор должен пройти до начала выполнения ISR?_: несколько → несколько варьируется. В разделе _Вопросы производительности_: Вы можете получить ответ едва ли за 1 мкс, если не включены другие прерывания и не используется cli(). В разделе _Вы, вероятно, всё равно используете прерывания ..._: удалите «в более поздних версиях библиотеки», так было с версии 1.0-beta2 (выпущенной в августе 2011 г.)., @Edgar Bonet

@EdgarBonet - добавил большинство ваших предложений (я думаю)., @Nick Gammon

@KIIV - Не каждый источник прерывания можно использовать для пробуждения ЦП из большинства спящих режимов - какие из них вы имели в виду? Если вам нужен только флаг, но вы не знаете, сколько раз его нужно подать, вы можете использовать соответствующий флаг, не включая прерывания - о чём здесь идёт речь?, @Nick Gammon

@NickGammon **1.** Например, режим сна при отключении питания может быть пробуждён только по сигналу сторожевого таймера (WDT), совпадению адреса TWI и внешнему прерыванию (только обнаружение низкого уровня). Прерывания по изменению состояния контакта могут пробуждать процессор только в режиме ожидания (только обнаружение низкого уровня внешних прерываний). И так далее. **2.** Это относится ко второму абзацу раздела **Советы по написанию обработчиков прерываний (ISR)**, @KIIV

Боюсь, это неправда. У меня есть [пример](http://www.gammon.com.au/forum/?id=11497&reply=4#reply4), который использует прерывания по изменению состояния пина для пробуждения из режима пониженного энергопотребления. Кроме того, как я упоминал на [моей странице о прерываниях](http://www.gammon.com.au/interrupts), Atmel подтвердила, что любое внешнее прерывание (например, повышение/понижение/изменение **и** низкого уровня) пробуждает процессор., @Nick Gammon

Это ссылка на второй абзац в разделе «Советы по написанию ISR» — да, но ведь в том, что я написал, нет ничего явно неправильного, не так ли? Конечно, проверка флага может дать знать о том, что был подан сигнал прерывания (как это делается в micros`)., @Nick Gammon

Ну что ж, тогда он выполнил свою задачу, и я только что узнал что-то действительно интересное (мне просто нужно это перепроверить). И да, в этом абзаце нет ничего неправильного. Просто можно использовать флаги прерываний напрямую, вместо вызова ISR, и установить другую переменную. (Если это возможно и вам не нужен немедленный ответ/действие. Если вам нужно знать, сколько раз событие произошло между проверками, вам в любом случае придётся использовать ISR и переменную-счётчик.), @KIIV