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

11

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

  • Организация Объединенных Наций
  • Мини
  • Nano
  • Pro Mini
  • Lilypad

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

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

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

Ник Гаммон
источник

Ответы:

25

TL; DR:

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

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

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

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


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

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

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Этот пример показывает, как, даже если основной цикл ничего не делает, вы можете включить или выключить светодиод на контакте 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.


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

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

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

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

interrupts ();  // or ...
sei ();         // set interrupts flag

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

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

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

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

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

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

Могут быть критические по времени фрагменты кода, которые вы не хотите прерывать, например, прерыванием по таймеру.

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

Например:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

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

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

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

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


подсказки

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

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

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

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

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


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

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

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


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

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


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

  • Вы пишете ISR (подпрограмма обработки прерываний). Это вызывается, когда происходит прерывание.
  • Вы сообщаете процессору, когда хотите, чтобы прерывание сработало.

Написание ISR

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

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

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

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

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

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

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

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

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

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

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

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

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

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

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

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

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

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

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

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

  • 0: низкий уровень INT0 генерирует запрос прерывания (НИЗКОЕ прерывание).
  • 1: Любое логическое изменение в INT0 генерирует запрос прерывания (CHANGE interrupt).
  • 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, если это абсолютно необходимо, например.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

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


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

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

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


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

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

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

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

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

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

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

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


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

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


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

Есть два вида прерываний:

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

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

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

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

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

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


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

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

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

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

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

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

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

Прерывание смены булавки

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

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

Пример кода:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to 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 ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

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

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

sei ();
sleep_cpu ();

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


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

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

EMPTY_INTERRUPT (PCINT1_vect);

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


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

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

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

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

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

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

например.

volatile int counter;

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

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

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

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

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

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

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Дальнейшие примеры

Из-за недостатка места (размер поста) в моем списке больше примеров кода. Для более примера кода см. Мою страницу о прерываниях .

Ник Гаммон
источник
Очень полезная ссылка - это был впечатляюще быстрый ответ.
Дат Хэн Бэг
Это был справочный вопрос. Я подготовил ответ, и он был бы еще быстрее, если бы ответ не был слишком длинным, поэтому мне пришлось его обрезать. Смотрите связанный сайт для более подробной информации.
Ник Гэммон
О "спящем режиме" эффективно ли заставить Arduino спать, скажем, 500 мс?
Дат Ха
@ Nick Gammon Я думаю, включение или выключение питания (с автоматизацией или нет) для процессора может быть определено как нетрадиционное прерывание - если вы хотите это сделать. «Я подготовил ответ» - вы просто убрали всю магию того момента, когда я думал, что это так.
Dat Han Bag
1
Боюсь, это не правда. У меня есть пример, который использует прерывания смены контактов, чтобы выйти из режима отключения питания. Также, как я упомянул на своей странице о прерываниях, Atmel подтвердила, что любое внешнее прерывание будет пробуждать процессор (т. Е. Повышение / падение / изменение и низкий уровень).
Ник Гэммон