Закрыть
Прерывания на Arduino

Прерывания на Arduino

Я уже касался темы прерываний в статье Многозадачность и прерывания на Arduino. Сегодня я постараюсь раскрыть тему прерываний более подробно.

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

 

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

  • Прерывание (англ. interrupt) — сигнал, сообщающий процессору о наступлении какого-либо события. При этом выполнение текущей последовательности команд приостанавливается, и управление передаётся обработчику прерывания.
  • Обработчик прерывания (функция обработки прерывания, процедура обработки прерывания) по английски называется Interrupt Service Routine, или сокращенно ISR, реагирует на событие и обслуживает его, после чего возвращает управление в прерванный код.
  • Вектор прерывания — это указатель на адрес расположения инструкций, которые должны быть выполнены при вызове данного прерывания. То есть это адрес программы обработки данного прерывания. Векторы прерываний объединяются в таблицу векторов прерываний, содержащую адреса обработчиков прерываний. Местоположение таблицы зависит от типа и режима работы процессора.

 

Что не является прерыванием?

Прерывания — это не просто когда вы передумываете и делаете что-то другое. Например:

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

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

 

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

Показать/скрыть код
const byte LED = 13;
const byte BUTTON = 2;

// Функция обработки прерывания (ISR)
void pinChange ()
{
 if (digitalRead (BUTTON) == HIGH)
  digitalWrite (LED, HIGH);
 else
  digitalWrite (LED, LOW);
}

void setup ()
{
  pinMode (LED, OUTPUT);  // устанавливаем пин в режим вывода
  digitalWrite (BUTTON, HIGH);  // встроенный подтягивающий резистор
  attachInterrupt (0, pinChange, CHANGE);  // подключаем обработчик прерывания
}
void loop ()
{
  // не делаем здесь ничего
}

Этот пример показывает, что, несмотря на то, что в основном цикле loop не выполняется никаких действий, вы можете включать и выключать светодиод на 13 пине, нажимая кнопку, подключенную к D2.

Чтобы проверить это, просто нажмите кнопку, подключенную между 2 выводом и землей. Внутренний подтягивающий резистор (подключается в функции setup) в нормальном состоянии (когда нет внешнего сигнала) поддерживает этот вывод в состоянии HIGH. Когда вы замыкаете его на замлю, он переходит в состояние LOW. Изменение состояния пина определяется при помощи прерывания CHANGE, которое является причиной вызова функции обработки прерывания.

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

 

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

Ниже приведен список прерываний для микроконтроллера ATmega328 в порядке их приоритетности

Показать/скрыть таблицу
Пр.ПрерываниеОбработчик
1СбросRESET_vect
2Внешнее прерывание 0 (вывод D2)INT0_vect
3Внешнее прерывание 1 (вывод D3)INT1_vect
4Изменение состояния вывода 1 (выводы с D8 по D13)PCINT0_vect
5Изменение состояния вывода 2 (выводы с A0 по A5)PCINT1_vect
6Изменение состояния вывода 3 (выводы с D0 по D7)PCINT2_vect
7Переполнение сторожевого таймераWDT_vect
8Прерывание по сравнению, канал A таймера/счетчика 2TIMER2_COMPA_vect
9Прерывание по сравнению, канал B таймера/счетчика 2TIMER2_COMPB_vect
10Переполнение таймера/счетчика 2TIMER2_OVF_vect
11Прерывание таймера/счетчика 1 по захвату событияTIMER1_CAPT_vect
12Прерывание по сравнению, канал A таймера/счетчика 1TIMER1_COMPA_vect
13Прерывание по сравнению, канал A таймера/счетчика 1TIMER1_COMPB_vect
14Переполнение таймера/счетчика 2TIMER1_OVF_vect
15Прерывание по сравнению, канал A таймера/счетчика 0TIMER0_COMPA_vect
16Прерывание по сравнению, канал B таймера/счетчика 0TIMER0_COMPB_vect
17Переполнение таймера/счетчика 0TIMER0_OVF_vect
18Завершение передачи по SPISPI_STC_vect
19Завершение приема по каналу USARTUSART_RX_vect
20Регистр данных USART пустUSART_UDRE_vect
21Завершение передачи по каналу USARTUSART_TX_vect
22Преобразование АЦП завершеноADC_vect
23EEPROM готоваEE_READY_vect
24Аналоговый компаратор переключилсяANALOG_COMP_vect
25Событие двухпроводного интерфейса (I2C)TWI_vect
26Готовность флеш-памятиSPM_READY_vect

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

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

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

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

Различные варианты для передачи данных используются, чтобы позволить программу делать что-то еще пока данные будут отправляться или получаться через Serial- порт, SPI или I2C.

 

Вывод процессора из спящего режима

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

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

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

 

Разрешение и запрещение прерываний

Прерывание RESET не может быть запрещено, но другие прерывания могут временно запрещаться сбросом флага прерывания.

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

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

Если вам нужно запретить прерывания, можно воспользоваться функцией noInterrupts или сбросить флаг прерывания:

noInterrupts(); // или ...
cli(); // сбрасываем флаг прерывания

И тот и другой метод дают один и тот же результат. Использование функций interrupts и noInterrupts немного проще запомнить.

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

 

Зачем запрещать прерывания?

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

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

Например:

noInterrupts();
long myCounter = isrCounter; // получаем значение, выданное ISR
interrupts();

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

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

Например, это то, что делает код внутри функции millis:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;

  // запрещаем прерывания, пока мы читаем timer0_millis или же мы можем получить
  // несостоятельное значение (т.е. в середине записи в timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;
  return m;
}

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

 

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

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

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

 

Может прерывание произойти пока прерывания запрещены?

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

 

Что такое переменные volatile?

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

Показать/скрыть пример кода
volatile boolean flag;

// Процедура обработки прерывания (ISR)
void isr ()
{
 flag = true;
}

void setup ()
{
 attachInterrupt (0, isr, CHANGE);  // подключаем обработчик прерывания
}
void loop ()
{
 if (flag)
 {
  // произошло прерывание
 }
}

 

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

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

Написание функции обработки прерывания

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

// Функция обработки прерывания (ISR)
void pinChange ()
{
 flag = true;
}

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

volatile char buf [100];
volatile byte pos;

// функция обработки прерывания SPI
ISR (SPI_STC_vect)
{
byte c = SPDR;  // захватываем байт из регистра данных SPI
  // добавляем в буфер, если возможно
  if (pos < sizeof buf)
  {
   buf [pos++] = c;
  }
}

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

Соединение обработчика с прерыванием

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

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

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

Другой пример представляет внешнее прерывание на пине:

// Процедура обработки прерывания (ISR)
void pinChange ()
{
 // здесь обработка изменения состояния пина
}
void setup ()
{
  attachInterrupt (0, pinChange, CHANGE);  // подключаем обработчик прерывания
}

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

Конфигурирование процессора для обработки прерывания

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

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

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

В более читабельном варианте используются предопределенные имена:

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

EICRA (External Interrupt Control Register A) должен быть установлен в соответствии с таблицей из технического руководства ATmega328 (см. на стр.71). Это значение определяет точный тип желаемого прерывания:

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

EIMSK (External Interrupt Mask Register), на самом деле, разрешает прерывание.

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

 

Может ли обработчик прерывания быть прерван?

Если коротко — то нет, если вы хотите, чтобы прерывания были.

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

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

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

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

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

 

Как долго может выполняться обработчик прерывания?

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

Затем функция ISR делает что-то вроде этого:

// процедура обработки SPI
ISR (SPI_STC_vect)
 118: 1f 92         push  r1   (2)    // сохраняем R1 - нулевой регистр
 11a: 0f 92         push  r0   (2)    // сохраняем регистр R0
 11c: 0f b6         in  r0, 0x3f (1)  // берем SREG (статусный регистр)
 11e: 0f 92         push  r0  (2)     // сохраняем SREG
 120: 11 24         eor r1, r1 (1)    // гарантируем, что R1 - ноль
 122: 8f 93         push  r24  (2)
 124: 9f 93         push  r25  (2)
 126: ef 93         push  r30  (2)
 128: ff 93         push  r31  (2)
{

Это еще 16 тактов (указаны в круглых скобках). Итак, с момента, как произошло прерывание до того, как будет выполнена первая строка кода пройдет 16+7=23 такта, по 62 нс на такт, итого 1.4375 мкс. Это для частоты 16 МГц.

При выходе из обработчика прерывания, мы имеем следующий код:

}  // конец процедуры обработки прерывания SPI_STC_vect
 152: ff 91         pop r31 (2)
 154: ef 91         pop r30 (2)
 156: 9f 91         pop r25 (2)
 158: 8f 91         pop r24 (2)
 15a: 0f 90         pop r0 (2)    // получаем старый SREG
 15c: 0f be         out 0x3f, r0 (1)  // возвращаем SREG
 15e: 0f 90         pop r0 (2)    // теперь помещаем старый регистр R0 обратно
 160: 1f 90         pop r1 (2)    // возвращаем старое значение R1
 162: 18 95         reti (4)      // возвращаемся из прерывания, включаем прерывания

Это еще 19 тактов (1.1875 мкс). Таким образом, процедура обработки прерывания, используя предопределенную функцию ISR займет 2.626 мкс плюс код, который будет внутри.

Однако, внешние прерывания (где используется attachInterrupts) выполняются немного дольше:

Показать/скрыть код
SIGNAL(INT0_vect) {
 182: 1f 92         push  r1  (2)
 184: 0f 92         push  r0 (2)
 186: 0f b6         in  r0, 0x3f (1)
 188: 0f 92         push  r0 (2)
 18a: 11 24         eor r1, r1 (1)
 18c: 2f 93         push  r18 (2)
 18e: 3f 93         push  r19 (2)
 190: 4f 93         push  r20 (2)
 192: 5f 93         push  r21 (2)
 194: 6f 93         push  r22 (2)
 196: 7f 93         push  r23 (2)
 198: 8f 93         push  r24 (2)
 19a: 9f 93         push  r25 (2)
 19c: af 93         push  r26 (2)
 19e: bf 93         push  r27 (2)
 1a0: ef 93         push  r30 (2)
 1a2: ff 93         push  r31 (2)
  if(intFunc[EXTERNAL_INT_0])
 1a4: 80 91 00 01   lds r24, 0x0100 (2)
 1a8: 90 91 01 01   lds r25, 0x0101 (2)
 1ac: 89 2b         or  r24, r25 (2)
 1ae: 29 f0         breq  .+10   (2)
    intFunc[EXTERNAL_INT_0]();
 1b0: e0 91 00 01   lds r30, 0x0100 (2)
 1b4: f0 91 01 01   lds r31, 0x0101 (2)
 1b8: 09 95         icall (3)
}
 1ba: ff 91         pop r31 (2)
 1bc: ef 91         pop r30 (2)
 1be: bf 91         pop r27 (2)
 1c0: af 91         pop r26 (2)
 1c2: 9f 91         pop r25 (2)
 1c4: 8f 91         pop r24 (2)
 1c6: 7f 91         pop r23 (2)
 1c8: 6f 91         pop r22 (2)
 1ca: 5f 91         pop r21 (2)
 1cc: 4f 91         pop r20 (2)
 1ce: 3f 91         pop r19 (2)
 1d0: 2f 91         pop r18 (2)
 1d2: 0f 90         pop r0 (2)
 1d4: 0f be         out 0x3f, r0 (1)
 1d6: 0f 90         pop r0 (2)
 1d8: 1f 90         pop r1 (2)
 1da: 18 95         reti (4)

Я насчитал здесь 82 такта (итого 5.125 мкс на 16 Мгц) и плюс еще время, которое требуется указанной функции обработчика, то есть 2.9375 мкс перед входом в обработчик и 2.1875 при возвращении.
 

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

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

  • Если процессор в режиме сна, то существует время пробуждения, которое может занять до нескольких миллисекунд пока тактирование опять наберет скорость. Это время будет зависеть от установок фьюзов (конфигурационных регистров) и того, насколько глубоко процессор ушел в сон.
  • Если процедура обработки прерывания уже выполняется, то следующие прерывания не могут быть введены либо до ее окончания, либо пока они не будут разрешены в самой этой процедуре. Вот почему нужно делать процедуру обработки прерывания как можно короче, каждая микросекунда, которая будет здесь потрачена, затянет выполнение другой обработки.
  • Иногда код выключает прерывания. Например, вызов millis ненадолго выключает прерывания. Поэтому, время обработки прерывания увеличится на то время, пока прерывания были запрещены.
  • Прерывания могут обрабатываться только в конце инструкции процессора. Так, если конкретная команда занимает три такта, и она только началась, то прерывание будет отложено, по крайней мере, на пару тактов.
  • Событие, которое возвращает прерывание обратно (например, возвращение из процедуры обработки прерывания) гарантированно выполнит, по крайней мере, еще одну инструкцию. Таким образом, даже если процедура обработки прерывания уже завершилась и ожидает следующее прерывание, то ему придется ожидать не менее одной инструкции, прежде чем оно будет обработано.
  • Так как прерывания имеют приоритет, то прерывания с более высоким приоритетом могут быть обработаны до нужного вам прерывания.

 

Производительность

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

 

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

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

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

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

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

EIFR = 1;  // сбрасываем флаг для прерывания 0
EIFR = 2;  // сбрасываем флаг для прерывания 1

или в более читабельной форме:

EIFR = bit (INTF0);  // сбрасываем флаг прерывания 0
EIFR = bit (INTF1);  // сбрасываем флаг прерывания 1

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

Показать/скрыть код
#include <avr/sleep.h>

// процедура обработуи прерывания в спящем режиме
void wake ()
{
 sleep_disable ();         // первая вещь после выхода из сна
}
void sleepNow ()
{
 set_sleep_mode (SLEEP_MODE_PWR_DOWN);
 sleep_enable ();          // разрешаем бит sleep в регистре mcucr
  attachInterrupt (0, wake, LOW);  // пробуждение по уровню low
  sleep_mode ();            // здесь устройство действительно переходит в сон!!
  detachInterrupt (0);      // останавливаем прерывание по LOW
}

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

Улучшенная версия кода, представленная ниже решает эту проблему путем отключения прерываний перед вызовом attachInterrupt. Таким образом, одна инструкция будет выполнена после повторного разрешения прерываний и мы уверены, что вызов sleep_cpu будет сделан до возникновения прерывания.

Показать/скрыть код
#include <avr/sleep.h>

// процедура обработки прерывания в режиме сна
void wake ()
{
 sleep_disable ();         // первое, что мы делаем после пробуждения
 detachInterrupt (0);      // останавливаем прерывания по LOW
}
void sleepNow ()
{
  set_sleep_mode (SLEEP_MODE_PWR_DOWN);
  noInterrupts ();          // мы не получим прерывание до того, как спим
  sleep_enable ();          // рарешаем бит sleep в регистре mcucr
  attachInterrupt (0, wake, LOW);  // пробуждаемся по уровню low
  interrupts ();           // теперь прерывания разрешены, следующая инструкция БУДЕТ выполнена
  sleep_cpu ();            // здесь устройство помещается в сон
}  // end of sleepNow

Эти примеры иллюстрируют каким образом можно снизить энергопотребление, используя прерывание LOW. Хотя можно использовать и любой другой тип прерываний (RISING, FALLING, CHANGE) — все они выведут процессор из состояния сна.
 

Советы по написанию функции обработки прерывания

Делайте обработчик прерывания как можно короче! Пока выполняется обработчик прерывания, вы не можете обрабатывать другие прерывания. Таким образом, можно легко пропустить нажатие кнопки или входящее сообщение по Serial-порту если сделать его слишком большим. В частности, в обработчик не нужно помещать отладочные операторы print. Это, скорее, создаст проблем больше, чем поможет решить.

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

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

Тест показал, что для микроконтроллера ATmega328, работающего на частоте 16 МГц, вызов функции micros занимает 3.5625 мкс, вызов millis занимает 1.9375 мкс. Запись (сохранение) текущего значения таймера разумно сделать в обработчике прерывания. Определение числа прошедших миллисекунд быстрее, чем прошедших микросекунд (количество миллисекунд просто извлекается из переменной). Однако количество микросекунд определяется путем добавления текущего значения таймера 0 (хранит увеличение) к сохраненному «Timer 0 overflow count».

Так как прерывания запрещены внутри обработчика и из-за того, что Arduino IDE использует прерывания для чтения и записи данных через Serial порт, а также для увеличения счетчика, использующего millis и delay — не пытайтесь использовать эти функции внутри обработчика прерываний.

Иначе говоря:

  • Не пытайтесь использовать функцию delay
  • Вы можете получить время, вызвав millis, однако оно не будет увеличиватся
  • Не используйте вывод в Serial-порт (например: Serial.println(«ISR entered»);)
  • Не пытайтесь читать данные с Serial-порта.

 

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

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

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

ISR (PCINT0_vect)
 {
  // состояние одного из выводов D8-D13 изменилось
 }

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

Каждое прерывание по изменению состояния пина имеет связанный байт «маски» в процессоре, так что возможно сконфигурировать их реагировать только, например, на D8, D10 и D12, а не на изменения любого из D8-D13. Однако, по-прежнему нужны дополнительные операции, чтобы выяснить, состояние каких именно выводов изменилось.

 

Пример прерывания сторожевого таймера

Показать/скрыть код
#include <avr/sleep.h>
#include <avr/wdt.h>

#define LED 13

// процедура обработки прерывания по нажатию кнопки
void wake ()
{
 wdt_disable();  // отключаем сторожевой таймер
}

// прерывание сторожевого таймера
ISR (WDT_vect)
{
 wake ();
}

void myWatchdogEnable (const byte interval)
{
 noInterrupts (); 

 MCUSR = 0;                          // сбрасываем различные флаги
 WDTCSR |= 0b00011000;               // устанавливаем WDCE, WDE
 WDTCSR =  0b01000000 | interval;    // устанавливаем WDIE, и соответсвующую задержку
 wdt_reset();

 byte adcsra_save = ADCSRA;
 ADCSRA = 0;  // запрещаем работу АЦП
 power_all_disable ();   // выключаем все модули
 set_sleep_mode (SLEEP_MODE_PWR_DOWN);   // устанавливаем режим сна
 sleep_enable();
 attachInterrupt (0, wake, LOW);   // позволяем заземлить pin 2 для выхода из сна
 interrupts ();
 sleep_cpu ();            // переходим в сон и ожидаем прерывание
 detachInterrupt (0);     // останавливаем прерывание LOW

 ADCSRA = adcsra_save;  // останавливаем понижение питания
 power_all_enable ();   // включаем все модули
}

void setup ()
{
 digitalWrite (2, HIGH);    // кнопка с подтягивающим резистором
}

void loop()
{
 pinMode (LED, OUTPUT);
 digitalWrite (LED, HIGH);
 delay (5000);
 digitalWrite (LED, LOW);
 delay (5000);

 // шаблон битов sleep:
 //  1 секунда:  0b000110
 //  2 секунды: 0b000111
 //  4 секунды: 0b100000
 //  8 секунд: 0b100001
  // засыпаем на 8 секунд
  myWatchdogEnable (0b100001);  // 8 секунд
}

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

  • Со светящимся светодиодом — 19.5 мА
  • При включенном светодиоде — 16.5 мА
  • В режиме сна — 6 мкА

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

Это также иллюстрирует, что можно выйти из режима сна двумя различными способами. Один из них — нажатие клавиши (то есть заземление пина D2), другой — это периодическое просыпание (каждые 8 секунд), хотя вы можете сделать это каждые 1, 2, 4 или 8 секунд (возможно, даже короче, но для этого нужно подробнее изучить техническое руководство на микроконтроллер).

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

Замечание по-поводу определения пониженного напряжения: полученные выше цифры получены с отключенным определением пониженного напряжения (brownout detection). Для создания опорного напряжения для определения пониженного напряжения требуется небольшой ток. Если его включить, то в режиме сна будет потребляться около 70 мкА (вместо 6 мкА). Одним из способов выключить определение пониженного напряжения является использование компилятора avrdude из командной строки:

avrdude -c usbtiny -p m328p -U efuse:w:0x07:m

При помощи параметров устанавливается efuse (дополнительные фьюзы) равным 7, что отключает определение пониженного напряжения. Здесь предполагается, что используется программатор USBtinyISP.

 

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

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

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

 

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

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

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

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

Это позволяет написать следующий код:

sei ();
sleep_cpu ();

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

 

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

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

EMPTY_INTERRUPT (PCINT1_vect);

Она просто сгенерирует инструкцию reti (вернуться из прерывания). Так как она не пытается сохранить или восстановить регистры, то это будет самым быстрым способом вызвать прерывание, чтобы разбудить процессор.

[add_ratings]

7 thoughts on “Прерывания на Arduino

    1. Одним прерыванием не обойдется — нужно 2:

      1 — считаем импульсы с датчика

      2 — (по таймеру) считаем количество импульсов за период времени

  1. Здравствуйте. Помогите пожалуйста. Переделал дешёвый вентилятор за 300 руб. Сделал и и отладил код отвечающий за включение любого из 3-х режимов скорости и авто отключения по таймеру через ИК пульт. Изменил механизм поворота вентилятора, сделал повороты через servo с ИК пульта. Реализовал функционал: при коротком нажатии на кнопку №1 пульта серво будет непрерывно медленно вращаться влево-вправо, при нажатии другой кнопки №2 servo остановится (Ваш код выше отлично справляется с этим). А вот добавить в него ещё одну возможность не получается: при долгом зажатии кнопки №3 (влево) servo должен медленно вращаться до упора Влево или пока не отпустишь кнопку пульта, тоже самое в правую сторону...

    Вот сам полурабочий код:

    #include // Библиотека для Servo-Мотора с регулировкой скорости #include // Библиотека для ИК приёмника byte RECV_PIN = 11; // вход ИК приемника IRrecv irrecv(RECV_PIN); // Объект ИК decode_results results; // переменные для контроля за процессом поворотов Servo bool Right_Pin_1 = false; //для ручного поворота вправо при зажатии кнопки bool Left_Pin_1 = false; //для ручного поворота влево при зажатии кнопки bool Auto_Povorot = false; //для Автоповорота при кратковременном нажатии class Sweeper { Servo servoAutoPovorot; // сервопривод int pos; // текущее положение сервы int increment; // увеличиваем перемещение на каждом шаге int updateInterval; // промежуток времени между обновлениями unsigned long lastUpdate; // последнее обновление положения public: Sweeper (int interval) { updateInterval = interval; increment = 2; // шаг приращения положения сервы pos = 90; // устанавливаем серву в среднее положение servoAutoPovorot.write(pos); } void Attach(int pin) { servoAutoPovorot.attach(pin); } void Detach() { servoAutoPovorot.detach(); } void Update() { if (((millis() - lastUpdate) > updateInterval) && (Right_Pin_1 == true)) { Left_Pin_1 = false; Auto_Povorot = false; // если время обновлять и нажата кнопка на пине 2 - увеличиваем положение сервы lastUpdate = millis(); if (pos updateInterval) && (Left_Pin_1 == true)) { Right_Pin_1 = false; Auto_Povorot = false; // если время обновлять и нажата кнопка на пине 3 - уменьшаем положение сервы lastUpdate = millis(); if (pos >= 0) { pos -= increment; servoAutoPovorot.write(pos); } } else if ((millis() - lastUpdate) > updateInterval && (Auto_Povorot == true)) // время обновлять { Left_Pin_1 = false; Right_Pin_1 = false; lastUpdate = millis(); pos += increment; servoAutoPovorot.write(pos); if ((pos >= 130) || (pos <= 0)) // конец вращения { // обратное направление increment = -increment; } } } }; Sweeper servoAutoPovorot(35); // скорость Авто-вращения серво void setup() { irrecv.enableIRIn(); // включить ИК приёмник servoAutoPovorot.Attach(9); // для автоповоротов класса Sweeper } void loop() { servoAutoPovorot.Update(); if (irrecv.decode(&results)) { // 2 Команды с пульта отвечающие за ручные повороты Серво. if (results.value == 0xFFC23D) Right_Pin_1 = true; // Поворот Вправо if (results.value == 0xFF22DD) Left_Pin_1 = true; // Поворот Влуво if (results.value == 0xFF629D) Auto_Povorot = true; //Автоповорот вкл. if (results.value == 0xFFA857) Auto_Povorot = false; //Автоповорот откл. irrecv.resume(); // получаем следующие значения } }

Оставить ответ

Ваш email не будет опубликован.Обязательны поля помечены *