Create external interrupt in arduino

USB амперметр

USB амперметр представляет собой plug and play устройство, способное измерять напряжение и ток на любом USB порту.

В нашем проекте USB амперметр подключается между USB портом компьютера и платой Arduino. Данный амперметр имеет в своем составе резистор сопротивлением 0.05 Ом, с помощью которого и происходит измерение протекающего через устройство тока. USB амперметр снабжен четырех разрядным семисегментным индикатором (аналогичный индикатор мы использовали в проекте часов на Arduino), на котором отображаются измеренные напряжение и ток. Эти данные обновляются каждые 3 секунды.

Спецификация USB амперметра:

  • рабочее напряжение: от 3.5V до 7V;
  • максимальный измеряемый ток: 3A;
  • компактный размер;
  • не требует внешнего источника питания.

Об ISR

ISR (т.е. Interrupt Service Routines, что можно перевести как «обработчик прерываний») — это особый вид функций, имеющих ряд уникальных ограничений, которых нет у других функций. Они не могут иметь никаких параметров и не возвращают никаких данных.

ISR должны быть как можно более короткими и быстрыми. Если в вашем скетче используется несколько ISR, одновременно можно запустить лишь одну, а другие будут выполнятся после — в зависимости от имеющегося приоритета. Счетчик в функции millis() полагается на прерывания, поэтому внутри ISR работать не будет. Поскольку функции delay() для работы тоже требуются прерывания, внутри ISR она тоже работать не будет. Функция micros() первое время будет работать нормально, но спустя 1-2 миллисекунды начнутся перебои. Функции delayMicroseconds() счетчик не требуется, поэтому она будет работать в нормальном режиме.

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

About Interrupt Service Routines

ISRs are special kinds of functions that have some unique limitations most other functions do not have. An ISR cannot have any parameters, and they shouldn’t return anything.

Generally, an ISR should be as short and fast as possible. If your sketch uses multiple ISRs, only one can run at a time, other interrupts will be executed after the current one finishes in an order that depends on the priority they have. relies on interrupts to count, so it will never increment inside an ISR. Since requires interrupts to work, it will not work if called inside an ISR. works initially, but will start behaving erratically after 1-2 ms. does not use any counter, so it will work as normal.

Typically global variables are used to pass data between an ISR and the main program. To make sure variables shared between an ISR and the main program are updated correctly, declare them as .

For more information on interrupts, see Nick Gammon’s notes.

Syntax

(recommended) (not recommended) (Not recommended. Additionally, this syntax only works on Arduino SAMD Boards, Uno WiFi Rev2, Due, and 101.)

Parameters

: the number of the interrupt (): the pin number: the ISR to call when the interrupt occurs; this function must take no parameters and return nothing. This function is sometimes referred to as an interrupt service routine.: defines when the interrupt should be triggered. Four constants are predefined as valid values:

  • LOW to trigger the interrupt whenever the pin is low,

  • CHANGE to trigger the interrupt whenever the pin changes value

  • RISING to trigger when the pin goes from low to high,

  • FALLING for when the pin goes from high to low.

The Due, Zero and MKR1000 boards allows also:

Модифицированные библиотеки от 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  

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

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

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

Теперь давайте перейдем к практике и поговорим о том, как использовать прерывания в своих проектах.

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

Синтаксис вызова: attachInterrupt(interrupt, function, mode)

interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
mode – условие срабатывания прерывания.

Возможна установка следующих вариантов условий срабатывания:

  • LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
  • CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
  • RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
  • FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4

Важные замечания

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

  • Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
  • Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
  • Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит, ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
  • В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).

Introducing ESP8266 Interrupts

Interrupts are useful for making things happen automatically in microcontroller programs and can help solve timing problems.

With interrupts you don’t need to constantly check the current pin value. When a change is detected, an event is triggered – a function is called. This function is called interrupt service routine (ISR).

When an interrupt happens, the processor stops the execution of the main program to execute a task, and then gets back to the main program as shown in the figure below.

This is especially useful to trigger an action whenever motion is detected or whenever a pushbutton is pressed without the need to constantly check its state.

Программирование таймеров в плате 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.

Timer1

Данная библиотека представляет собой набор функций для настройки аппаратного 16-битного таймера Timer1 в ATMega168/328. В микроконтроллере доступно 3 аппаратных таймера, которые могут быть настроены различными способами для получения различных функциональных возможностей. Начало разработки данной библиотеки было вызвано необходимостью быстро и легко установить период или частоту ШИМ сигнала, но позже она разраслась, включив в себя обработку прерываний по переполнению таймера и другие функции. Она может быть легко расширена или портирована для работы с другими таймерами.

Точность таймера зависит от тактовой частоты процессора. Тактовая частота таймера Timer1 определяется установкой предварительного делителя частоты. Этот делитель может быть установлен в значения 1, 8, 64, 256 или 1024.

для тактовой частоты 16 МГц
Делитель Длительность одного отсчета, мкс Максимальный период, мс
1 0,0625 8,192
8 0,5 65,536
64 4 524,288
256 16 2097,152
1024 64 8388,608

В общем:

  • Максимальный период = (Делитель / Частота) × 217
  • Длительность одного отсчета = (Делитель / Частота)

Скачать можно здесь (TimerOne-r11.zip) или с Google Code.

Для установки просто распакуйте и поместите файлы в каталог Arduino/hardware/libraries/Timer1/.

Прерывания по кнопке

Начнем с простого примера: использования прерывания для отслеживания нажатия кнопки. Для начала, мы возьмем скетч, который вы, вероятно, уже видели: пример «Button», включенный в Arduino IDE (вы можете найти его в каталоге «Примеры», проверьте меню Файл → Примеры → 02. Digital → Button).

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

Программирование Arduino

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

В каждой программе для Arduino должны обязательно присутствовать две функции – это функции void setup () и void loop (), иногда их называют «абсолютным минимумом», необходимым для написания программы. Все операции, которые мы запишем внутри void setup (), исполнятся только один раз, а операции, которые мы запишем внутри void loop () – будут исполняться снова и снова. Пример этих функций показан в коде ниже – именно в таком виде они создаются когда вы выбираете пункт меню File -> New.

Arduino

void setup() {
// put your setup code here, to run once:
}

void loop() {
// put your main code here, to run repeatedly:
}

1
2
3
4
5
6
7

voidsetup(){

// put your setup code here, to run once:

}
 

voidloop(){

// put your main code here, to run repeatedly:

}

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

Arduino

pinMode(2,INPUT);
pinMode (3,OUTPUT);

1
2

pinMode(2,INPUT);

pinMode(3,OUTPUT);

Но здесь необходимо внести небольшое изменение в программу – нам желательно чтобы контакт 2, который мы объявили в качестве входного контакта, никогда не был бы в «плавающем» состоянии. Это означает что входной контакт должен быть всегда подсоединен либо к +5 В, либо к земле. А в нашем случае при нажатии кнопки он будет подсоединен к земле, а при отжатой кнопке он будет находиться в плавающем состоянии. Чтобы исключить это нам необходимо задействовать внутренний подтягивающий резистор, который находится внутри микроконтроллера ATmega 328 (то есть снаружи мы этот резистор не видим). Для его задействования необходимо написать соответствующую строчку кода в программе.

С помощью этой строчки кода контакт 2 будет подключаться через подтягивающий резистор к напряжению +5 В всегда когда он не подсоединен к земле. То есть мы должны в одной из написанных нами строчек кода изменить слово INPUT на слово INPUT_PULLUP как показано ниже.

Arduino

pinMode(2,INPUT_PULLUP);

1 pinMode(2,INPUT_PULLUP);

Теперь, когда мы закончили с функцией setup (), перейдем к функции loop (). В этой функции мы должны проверять не подсоединен ли контакт 2 к земле (то есть на его входе низкий уровень – LOW) и если он подсоединен в земле, то мы должны зажечь светодиод при помощи подачи на контакт 3 высокого уровня (HIGH). А если контакт 2 не подсоединен к земле (то есть кнопка не нажата), то мы должны держать светодиод в выключенном состоянии при помощи подачи на контакт 3 низкого уровня (LOW). В программе это будет выглядеть следующим образом:

Arduino

if (digitalRead(2) == LOW)
{
digitalWrite(3,HIGH);
}

else
{
digitalWrite(3,LOW);
}

1
2
3
4
5
6
7
8
9

if(digitalRead(2)==LOW)

{

digitalWrite(3,HIGH);

}

else

{

digitalWrite(3,LOW);

}

В этих строчках кода оператор digitalRead() используется для проверки статуса (состояния) входного контакта. Если контакт подсоединен к земле, то оператор digitalRead() возвратит значение LOW, а если оператор подсоединен к +5 В, то оператор возвратит значение HIGH.

Аналогично, оператор digitalWrite() используется для установки состояния выходного контакта. Если мы установим контакт в состояние HIGH, то на его выходе будет напряжение +5 В, а если мы установим контакт в LOW, то на его выходе будет 0 В.

Таким образом в нашей программе когда мы нажимаем кнопку на контакт 2 будет подана земля и, соответственно, на контакт 3 мы подаем высокий уровень +5 В (HIGH) чтобы зажечь светодиод. Если условие не выполняется – то есть на контакт 2 не подана земля, то мы на контакт 3 подаем низкий уровень 0 В (LOW) чтобы выключить светодиод.

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

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

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

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

Например:

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

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

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

Introducing ESP8266 Timers

For this tutorial, we’ll use timers. We want the LED to stay on for a predetermined number of seconds after motion is detected. Instead of using a delay() function that blocks your code and doesn’t allow you to do anything else for a determined number of seconds, we’ll use a timer.

delay() vs millis()

The delay() function accepts a single int number as an argument. This number represents the time in milliseconds the program has to wait until moving on to the next line of code.

When you call delay(1000) your program stops on that line for 1 second. delay() is a blocking function. Blocking functions prevent a program from doing anything else until that particular task is completed. If you need multiple tasks to occur at the same time, you cannot use delay(). For most projects you should avoid using delays and use timers instead.

Using a function called millis() you can return the number of milliseconds that have passed since the program first started.

Why is that function useful? Because by using some math, you can easily verify how much time has passed without blocking your code.

Blinking an LED using millis() (without delay)

If you’re not familiar with millis() function, we recommend reading this section. If you’re already familiar with timers, you can skip to the PIR motion sensor project.

The following snippet of code shows how you can use the millis() function to create a blink project. It turns an LED on for 1000 milliseconds, and then turns it off.

How the code works

Let’s take a closer look at this blink sketch that works without the delay() function (it uses the millis() function instead).

Basically, this code subtracts the previous recorded time (previousMillis) from the current time (currentMillis). If the remainder is greater than the interval (in this case, 1000 milliseconds), the program updates the previousMillis variable to the current time, and either turns the LED on or off.

Because this snippet is non-blocking, any code that’s located outside of that first if statement should work normally.

You should now be able to understand that you can add other tasks to your loop() function and your code will still be blinking the LED every one second.

You can upload this code to your ESP8266 to test it. The on-board LED should be blinking every second.

Проблемы с контекстом прерываний

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

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

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

About Interrupt Service Routines

ISRs are special kinds of functions that have some unique limitations most other functions do not have. An ISR cannot have any parameters, and they shouldn’t return anything.

Generally, an ISR should be as short and fast as possible. If your sketch uses multiple ISRs, only one can run at a time, other interrupts will be executed after the current one finishes in an order that depends on the priority they have. relies on interrupts to count, so it will never increment inside an ISR. Since requires interrupts to work, it will not work if called inside an ISR. works initially but will start behaving erratically after 1-2 ms. does not use any counter, so it will work as normal.

Typically global variables are used to pass data between an ISR and the main program. To make sure variables shared between an ISR and the main program are updated correctly, declare them as .

For more information on interrupts, see Nick Gammon’s notes.

Syntax

(recommended) (not recommended) (Not recommended. Additionally, this syntax only works on Arduino SAMD Boards, Uno WiFi Rev2, Due, and 101.)

Parameters

: the number of the interrupt. Allowed data types: .: the Arduino pin number.: the ISR to call when the interrupt occurs; this function must take no parameters and return nothing. This function is sometimes referred to as an interrupt service routine.: defines when the interrupt should be triggered. Four constants are predefined as valid values:

  • LOW to trigger the interrupt whenever the pin is low,

  • CHANGE to trigger the interrupt whenever the pin changes value

  • RISING to trigger when the pin goes from low to high,

  • FALLING for when the pin goes from high to low.

The Due, Zero and MKR1000 boards allow also:

Запись ISR

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

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

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

(Обратите внимание, что какой-то старый код использует SIGNAL вместо ISR, однако SIGNAL устарел)

About Interrupt Service Routines

ISRs are special kinds of functions that have some unique limitations most other functions do not have. An ISR cannot have any parameters, and they shouldn’t return anything.

Generally, an ISR should be as short and fast as possible. If your sketch uses multiple ISRs, only one can run at a time, other interrupts will be executed after the current one finishes in an order that depends on the priority they have. millis() relies on interrupts to count, so it will never increment inside an ISR. Since delay() requires interrupts to work, it will not work if called inside an ISR. micros() works initially, but will start behaving erratically after 1-2 ms. delayMicroseconds() does not use any counter, so it will work as normal.

Typically global variables are used to pass data between an ISR and the main program. To make sure variables shared between an ISR and the main program are updated correctly, declare them as .

For more information on interrupts, see Nick Gammon’s notes.

Parameters

: the number of the interrupt (): the pin number (Arduino Due, Zero, MKR1000 only): the ISR to call when the interrupt occurs; this function must take no parameters and return nothing. This function is sometimes referred to as an interrupt service routine.: defines when the interrupt should be triggered. Four constants are predefined as valid values:

  • LOW to trigger the interrupt whenever the pin is low,

  • CHANGE to trigger the interrupt whenever the pin changes value

  • RISING to trigger when the pin goes from low to high,

  • FALLING for when the pin goes from high to low.
    The Due, Zero and MKR1000 boards allows also:

  • HIGH to trigger the interrupt whenever the pin is high.