Использование прерываний на arduino

Синтаксис define ардуино

Синтаксис использования инструкции достаточно прост:

#define <что меняем>  <на что меняем>

  • Знак # означает начало инструкции препроцессора.
  • define – название инструкции.
  • <что меняем>  – имя макроса: словосочетание, которое будет находить препроцессор
  • <на что меняем> – тело макроса: строка, которая будет подставлена в то место, где будет найдено <что меняем>

Обратите внимание, что в конце строки не нужно ставить знак точки с запятой

Примеры использования define:

  • #define PIN_LED 13
  • #define RED 1
  • #define BUTTON_LEFT   5
  • #define test Serial.println(“test”)
  • #define test(a) Serial.println(a)

Большой интерес вызывает  последний пример. Мы можем попросить ардуино подставить во фрагмент кода тот аргумент, который мы указали в качестве аргумента для параметра <что меняем>. Мы поговорим об этом в статье ниже.

Тело макроса должно заканчиваться в той же строке. Но если мы хотим сделать многострочный блок, то добавляем символ “/” в конце. Например:

#define LONG_STRING "Очень длинный текст \
Который мы смогли разбить на две части"

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

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

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

Например:

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

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

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

Внутри ATmega328

В основе данного подраздела лежит техническое описание на ATmega328 версии Rev. 8271C – 08/10. Приводимые мной страницы могут немного отличаться от текущей версии.

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

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

Микроконтроллер включает в себя аппаратный модуль TWI, который обрабатывает связь через шину I2C. … Интересно, что это означает, что связь не обрабатывается библиотекой исключительно программно, как вы могли бы подумать! Другими словами, библиотека сама в программе не создает битовый поток. Библиотека взаимодействует с аппаратным компонентом, который выполняет тяжелую работу. Смотрите страницу 222 технического описания.
«AVR TWI работает с байтами и основывается на прерываниях…» (раздел 21.6 на странице 224)

Это ключевой момент; это означает, что
вы настраиваете регистры;
вы позволяете TWI модулю осуществлять связь;
вы можете делать в это время что-то еще; ваш микроконтроллер с тактовой частотой 16 МГц не занят управлением последовательной связью на 100 кГц;
TWI модуль вызывает прерывание, когда заканчивает работу, чтобы уведомить процессор об изменениях состояния (включая успешность операций и/или ошибки).

Однако обратите внимание, что библиотека блокирует ввод/вывод. Это означает, что он переходит в цикл ожидания и ждет завершения связи I2C

Ваше приложение не может ничего делать, пока модуль TWI общается по шине I2C. Обратите внимание, что это может быть не то, чего бы вы хотели: если ваша программа критична ко времени, то ваш 16-мегагерцовый процессор, застрявший в цикле ожидания и ждущий 100-килогерцового потока связи, будет не эффективен. Возможно, вам лучше написать собственный код I2C. В исходном коде avr-libc есть пример в ./doc/examples/twitest/twitest.c (смотрите http://www.nongnu.org/avr-libc/). Вы можете найти версию avr-libc, используемую вашей конкретной IDE Arduino, посмотрев файл versions.txt в каталоге установки Arduino IDE. Это будет где-то в Java/hardware/tools/avr. На Mac полный путь будет следующим /Applications/Arduino.app/Contents/Resources/Java/hardware/tools/avr/versions.txt; путь у вас будет другим, но схожим.

Функция millis вместо delay

Функция millis() позволит выполнить задержку без delay на ардуино, тем самым обойти недостатки предыдущих способов. Максимальное значение параметра millis такое же, как и у функции delay (4294967295мс или 50 суток). При переполнении значение просто сбрасывается в 0, не забывайте об этом.

С помощью millis мы не останавливаем выполнение всего скетча, а просто указываем, сколько времени ардуино должна просто “обходить” именно тот блок кода, который мы хотим приостановить. В отличие от delay millis сама по себе ничего не останавливает. Данная команда просто возвращает нам от встроенного таймера микроконтроллера количество миллисекунд, прошедших с момента запуска. При каждом вызове loop Мы сами измеряем время, прошедшее с последнего вызова нашего кода и если разница времени меньше желаемой паузы, то игнорируем код. Как только разница станет больше нужной паузы, мы выполняем код, получаем текущее время с помощью той же millis и запоминаем его – это время будет новой точкой отсчета. В следующем цикле отсчет уже будет от новой точки и мы опять будем игнорировать код, пока новая разница millis и нашего сохраненного прежде значения не достигнет вновь желаемой паузы.

Вот пример, наглядно иллюстрирующий работу команды:

Сначала мы вводим переменную timing, в ней будет храниться количество миллисекунд. По умолчанию значение переменной равно 0. В основной части программы проверяем условие: если количество миллисекунд с запуска микроконтроллера минус число, записанное в переменную timing больше, чем 10000, то выполняется действие по выводу сообщения в монитор порта и в переменную записывается текущее значение времени. В результате работы программы каждые 10 секунд в монитор порта будет выводиться надпись 10 seconds. Данный способ позволяет моргать светодиодом без delay.

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

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

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

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

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

Программирование таймеров в плате Arduino UNO

В этом проекте мы будем использовать прерывание переполнения таймера (Timer Overflow Interrupt) и использовать его для включения и выключения светодиода на определенные интервалы времени при помощи установки заранее определяемого значения (preloader value) регистра TCNT1 с помощью кнопок. Полный код программы будет приведен в конце статьи, здесь же рассмотрим его основные части.

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

Анод светодиода подключен к контакту 7 платы Arduino, поэтому определим (инициализируем) его как ledPin.

Затем сообщим плате Arduino к каким ее контактам подключен ЖК дисплей.

Установим заранее определенное значение (preloader value) равное 3035 – это будет соответствовать интервалу времени в 4 секунды. Формула для расчета этого значения приведена выше в статье.

Затем в функции void setup() установим режим работы ЖК дисплея 16х2 и высветим приветственное сообщение на нем на несколько секунд.

Затем контакт, к которому подключен светодиод, установим в режим вывода данных, а контакты, к которым подключены кнопки – в режим ввода данных.

После этого отключим все прерывания.

Далее инициализируем Timer1.

Загрузим заранее определенное значение (3035) в TCNT1.

Затем установим коэффициент деления предделителя равный 1024 при помощи конфигурирования битов CS в регистре TCCR1B.

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

Теперь разрешим все прерывания.

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

В функции void loop() предварительно загружаемое значение увеличивается и уменьшается на 10 (инкрементируется и декрементируется) при помощи кнопок в схеме. Также это значение отображается на экране ЖК дисплея 16х2.

Примеры использования attachInterrupt

Давайте приступим к практике и рассмотрим простейший пример использования прерываний. В примере мы определяем функцию-обработчик, которая при изменении сигнала на 2 пине Arduino Uno переключит состояние пина 13, к которому мы традиционно подключим светодиод.


#define PIN_LED 13

volatile boolean actionState = LOW;
void setup() {
pinMode(PIN_LED, OUTPUT);
// Устанавливаем прерывание
// Функция myEventListener вызовется тогда, когда
// на 2 пине (прерываниие 0 связано с пином 2)
// изменится сигнал (не важно, в какую сторону)
attachInterrupt(0, myEventListener, CHANGE);
}

void loop() {
// В функции loop мы ничего не делаем, т.к. весь код обработки событий будет в функции myEventListener
}

void myEventListener() {
actionState != actionState; //
// Выполняем другие действия, например, включаем или выключаем светодиод
digitalWrite(PIN_LED, actionState);
}. Давайте рассмотрим несколько примеров более сложных прерываний и их обработчиков: для таймера и кнопок

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

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

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

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

if(digitalRead(2)==HIGH) { //при нажатии кнопки
//Если от предыдущего нажатия прошло больше 100 миллисекунд
if (millis() - previousMillis >= 100) {
//Запоминается время первого срабатывания
previousMillis = millis();

if (led==oldled) { //происходит проверка того, что состояние кнопки не изменилось
led=!led;
}

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

Прерывания по таймеру

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

Таймер и прерывание по таймеру позволяет выполнять прерывание один раз в миллисекунду. В Ардуино имеется 3 таймера – Timer0, Timer1 и Timer2. Timer0 используется для генерации прерываний один раз в миллисекунду, при этом происходит обновление счетчика, который передается в функцию millis (). Этот таймер является восьмибитным и считает от 0 до 255. Прерывание генерируется при достижении значения 255. По умолчанию используется тактовый делитель на 65, чтобы получить частоту, близкую к 1 кГц.

Для сравнения состояния на таймере и сохраненных данных используются регистры сравнения. В данном примере код будет генерировать прерывание при достижении значения 0xAF на счетчике.

OCR0A = 0xAF;

TIMSK0 |= _BV(OCIE0A);

Требуется определить обработчик прерывания для вектора прерывания по таймеру. Вектором прерывания называется указатель на адрес расположения команды, которая будет выполняться при вызове прерывания. Несколько векторов прерывания объединяются в таблицу векторов прерываний.  Таймер в данном случае будет иметь название TIMER0_COMPA_vect. В этом обработчике будут производиться те же действия, что и в loop ().

SIGNAL(TIMER0_COMPA_vect) {
unsigned long currentMillis = millis();

sweeper1.Update(currentMillis);

if(digitalRead(2) == HIGH) {
sweeper2.Update(currentMillis);
led1.Update(currentMillis);
}

led2.Update(currentMillis);
led3.Update(currentMillis);

}

//Функция loop () останется пустой.

void loop()

{

}

Подведение итогов

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

attachInterrupt()

Описание

Задает функцию, которую необходимо вызвать при возникновении внешнего прерывания. Заменяет предыдущую функцию, если таковая была ранее ассоциирована с прерыванием. В большинстве плат Ардуино существует два внешних прерывания: номер 0 (цифровой вывод 2) и 1 (цифровой вывод 3). Номера выводов для внешних прерываний, доступные в тех или иных платах Ардуино, приведены в таблице ниже:

Плата int.0 int.1 int.2 int.3 int.4 int.5
Uno, Ethernet 2 3
Mega2560 2 3 21 20 19 18
Leonardo 3 2 1 7
Due (см. ниже)

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

Параметры

interrupt: номер прерывания (int)
pin: номер вывода (только для Arduino Due)
function: функция, которую необходимо вызвать при возникновении прерывания; эта функция должна быть без параметров и не возвращать никаких значений. Такую функцию иногда называют обработчиком прерывания.
mode:

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

  • LOW — прерывание будет срабатывать всякий раз, когда на выводе присутствует низкий уровень сигнала
  • CHANGE — прерывание будет срабатывать всякий раз, когда меняется состояние вывода
  • RISING — прерывание сработает, когда состояние вывода изменится с низкого уровня на высокий
  • FALLING — прерывание сработает, когда состояние вывода изменится с высокого уровня на низкий.

В Arduino Due доступно еще одно значение:

HIGH — прерывание будет срабатывать всякий раз, когда на выводе присутствует высокий уровень сигнала (только для Arduino Due).

Примечание

Внутри функции-обработчика прерывания функция delay() не будет работать; значения, возвращаемые функцией millis(), не будут увеличиваться. Также будут потеряны данные, полученные по последовательному интерфейсу во время выполнения обработчика прерывания. Любые переменные, которые изменяются внутри функции обработчика должны быть объявлены как volatile.

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

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

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

Пример

int pin = 13;
volatile int state = LOW;

void setup()
{
  pinMode(pin, OUTPUT);
  attachInterrupt(0, blink, CHANGE);
}

void loop()
{
  digitalWrite(pin, state);
}

void blink()
{
  state = !state;
}

detachInterrupt()

Параметры

interrupt — номер прерывания (int)
pin — номер контакта (только для Due, Zero)
ISR — ISR, которая будет вызываться при возникновении прерывания; у этой функции не должно быть параметров и, к тому же, она не должна ничего возвращать; кроме того, иногда эту функцию также называют «обработчиком прерываний»
mode — определяет, когда должно сработать прерывание; работает, как правило, с четырьмя константами

LOW — прерывание срабатывает, когда контакт переключается в значение LOW
CHANGE — прерывание срабатывает, когда значение контакта меняется
RISING — прерывание срабатывает, когда контакт переключается из LOW в HIGH
FALLING — прерывание срабатывает, когда контакт переключается из HIGH в LOW
HIGH (только для Due, Zero) — прерывание срабатывает, когда контакт переключается в HIGH

Модифицированные библиотеки от Paul Stoffregen

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

Плата ШИМ выводы TimerOne ШИМ выводы TimerThree
Teensy 3.1 3, 4 25, 32
Teensy 3.0 3, 4  
Teensy 2.0 4, 14, 15 9
Teensy++ 2.0 25, 26, 27 14, 15, 16
Arduino Uno 9, 10  
Arduino Leonardo 9, 10, 11 5
Arduino Mega 11, 12, 13 2, 3, 5
Wiring-S 4, 5  
Sanguino 12, 13  

Методы модифицированных библиотек аналогичны описанным выше, но добавлен еще один метод управления запуском таймера:

Возобновляет работу остановленного таймера. Новый период не начинается.

Общие рекомендации по написанию обработчиков прерываний

В заключение хочу привести несколько рекомендаций по написанию обработчиков прерываний.

  • Во-первых, делайте обработчики предельно короткими. Ведь они прерывают выполнение основной программы, а также блокируют обработку других прерываний. По возможности обработчик должен фиксировать только факт возникновения события, изменяя значение переменной. А сама реакция на событие должна выполняться в основной программе при анализе этой переменной.
  • Как уже было сказано, при входе в обработчик устанавливается глобальный запрет на обработку других прерываний. А это в свою очередь влияет на работу функций, использующих прерывания. Будьте с ними осторожнее. Если не уверены в безопасности их вызова, то лучше откажитесь от их использования в обработчике.
  • Возьмите за правило объявлять разделяемые между основной программой и обработчиком переменные как volatile. И не забывайте, что этого квалификатора недостаточно в случае многобайтных переменных — используйте при работе с ними атомарно исполняемые блоки или interrupts/noInterrupts

следующей части

Standard Digital Input and Output — No Interrupts

Set up the Arduino as per the schematic and upload the code below to the microprocessor. Here you read the value of an input, do a conditional comparison, run some lengthy routine and repeat.

This will give unpredictable outputs on the LED due to the lengthy process being at an undetermined point in relation to when the input button is triggered. Sometimes the LED will change state immediately, other times nothing happens, and then sometimes you need to hold the button for a while for the state changed to be recognised.

C++
Copy Code

int pbIn = 2;          int ledOut = 4;        int state = LOW;       
void setup()
{
    pinMode(pbIn, INPUT);
  pinMode(ledOut, OUTPUT);
}

void loop()
{
  state = digitalRead(pbIn);      
  digitalWrite(ledOut, state);    
    for (int i = ; i < 100; i++)
  {
          delay(10);
  }
}

Program Example 2


#define LED1 9
#define LED2 10
#define SW1 2
#define SW2 3

void toggle(byte pinNum) { 
  byte pinState = !digitalRead(pinNum);
  digitalWrite(pinNum, pinState); 
}

void setup()  {
  pinMode(LED1, OUTPUT);  
  pinMode(LED2, OUTPUT);
  digitalWrite(LED1, 0); // LED off
  digitalWrite(LED2, 0); // LED off
  pinMode(SW1, INPUT);
  pinMode(SW2, INPUT);
  attachInterrupt(0, ISR0, FALLING);  
  // interrupt 0 digital pin 2 connected SW0
  attachInterrupt(1, ISR1, RISING); 
  // interrupt 1 digital pin 3 connected SW1
}

void loop() {
  // do nothing
} 

// can't use delay(x) in IRQ routine
void ISR0() { 
  toggle(LED1); 
} 

void ISR1() { 
  toggle(LED2);
} 

The above program introduces several new concepts. There are no «volatile» variables defined or needed. Both INTR0 (DP2) and INTR1 (DP3) both have their own separate ISR routines ISR0() and ISR1() respectively. There is a separate non-interrupt subroutine toggle().

In this case either ISR routines will simply call toggle() and will let toggle() do the work.

Certain functions such as delay() won’t work inside a ISR function so it’s a good idea to use separate subroutines as long as they are not overly complex or time consuming — another interrupt rolling onto an ISR called subroutine that hasn’t finished could be interesting.

In addition functions that use interrupts such as delay() will not work if called from an ISR routine. For example the modified version of toggle() that follows won’t work if called by an ISR but works fine if called from loop() or another non-interrupt subroutine. The selected LED will come on for two seconds then go off.

The ISR routine maintains control of the interrupts until it’s finished and executes a «return» from interrupt command.


void toggle(byte pinNum) { 
  byte pinState = !digitalRead(pinNum);
  digitalWrite(pinNum, pinState); 
  delay(2000);
  pinState = pinState ^ 1;
  digitalWrite(pinNum, pinState); 
} // last brace is understood as return. 

The real solution is not to use the Arduino delay() function but write your own. the delayMicroceconds() doesn’t require the use of interrupts but is limited to about 16,000 — 1000uSec. = 1mSec.; 1,000,000uSec. — 1 Sec. Simply use a for loop:


void myDelay(int x)   {
  for(int i=0; i<=x; i++)   
  {
    delayMicroseconds(1000);
  }
}

Use myDelay(5000) for delay(5000) and the program will work.

Номера прерываний

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

Однако более старые скетчи часто используют прямые номера прерываний. Часто использовались 0 (для цифрового вывода 2) или 1 (для цифрвого вывода 3). В приведенной ниже таблице показаны доступные выводы прерывний на различных платах.

Обратите внимание, что в приведенной ниже таблице номера прерываний это числа, которые должны быть переданы. По историческим причинам эта нумерация не всегда соответствует прерываниям на чипе ATmega (например, int.0 соответствует INT4 на чипе ATmega2560)

Соответствие номеров выводов номерам прерываний на платах Arduino
Плата INT.0 INT.1 INT.2 INT.3 INT.4 INT.5
Uno, Ethernet 2 3        
Mega2560 2 3 21 20 19 18
На базе 32u4 (например, Leonardo, Micro) 3 2 1 7  

Для плата Uno WiFiRev.2, Due, Zero, семейства MKR и 101 номер прерывания = номер вывода.

Просыпаться процессор

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

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

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

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

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