Книга: Программируем Arduino. Профессиональная работа со скетчами.
3. Прерывания и таймеры
3. Прерывания и таймеры
Прерывания позволяют микроконтроллерам откликаться на события без необходимости постоянно проверять выполнение каких-либо условий, чтобы определить момент, когда произошли важные изменения. В дополнение к возможности подключать источники прерываний к некоторым контактам можно также использовать прерывания, генерируемые таймером.
Аппаратные прерывания
Для демонстрации использования прерываний вернемся вновь к цифровым входам. Часто для определения момента некоторого входного события (например, нажатия кнопки) используется такой код:
void loop()
{
if (digitalRead(inputPin) == LOW)
{
// Выполнить какие-то действия
}
}
Этот код постоянно проверяет уровень напряжения на контакте inputPin, и, когда digitalRead возвращает LOW, выполняются какие-то действия, обозначенные комментарием // Выполнить какие-то действия. Это вполне рабочее решение, но что если внутри функции loop требуется выполнить массу других операций? На все эти операции требуется время, поэтому есть вероятность пропустить короткое нажатие на кнопку, пока процессор будет занят чем-то другим. На самом деле пропустить факт нажатия на кнопку почти невозможно, потому что по меркам микроконтроллера она остается нажатой очень долго.
Но как быть с короткими импульсами от датчика, которые могут длиться миллионные доли секунды? Для приема таких событий следует использовать прерывания, определяя функции, которые будут вызываться по этим событиям, независимо от того, чем занят микроконтроллер. Такие прерывания называют аппаратными прерываниями (hardware interrupts).
В Arduino Uno только два контакта связаны с аппаратными прерываниями, из-за чего они используются очень экономно. В Leonardo таких контактов пять, на больших платах, таких как Mega2560, их намного больше, а в Due все контакты поддерживают возможность прерывания.
Далее рассказывается, как работают аппаратные прерывания. Чтобы опробовать представленный пример, вам понадобятся дополнительная макетная плата, кнопка, сопротивление на 1 кОм и несколько соединительных проводов.
На рис. 3.1 изображена собранная схема. Через сопротивление на контакт D2 подается напряжение HIGH, пока кнопка не будет нажата, в этот момент произойдет заземление контакта D2 и уровень напряжения на нем упадет до LOW.
Загрузите в плату Arduino следующий скетч:
// sketch 03_01_interrupts
int ledPin = 13;
void setup()
{
pinMode(ledPin, OUTPUT);
attachInterrupt(0, stuffHapenned, FALLING);
}
void loop()
{
}
void stuffHapenned()
{
digitalWrite(ledPin, HIGH);
}
Рис. 3.1. Электрическая схема для испытания прерываний
Помимо настройки контакта LED на работу в режиме цифрового выхода функция setup с помощью еще одной строки связывает функцию с прерыванием. Теперь в ответ на каждое прерывание автоматически будет вызываться эта функция. Рассмотрим эту строку внимательнее, потому что аргументы вызываемой здесь функции выглядят несколько необычно:
attachInterrupt(0, stuffHapenned, FALLING);
Первый аргумент — 0 — это номер прерывания. Было бы понятнее, если бы номер прерывания совпадал с номером контакта, но это не так. В Arduino Uno прерывание 0 связано с контактом D2, а прерывание 1 — с контактом D3. Ситуация становится еще более запутанной из-за того, что в других моделях Arduino эти прерывания связаны с другими контактами, а кроме того, в Arduino Due нужно указывать номер контакта. На плате Arduino Due с прерываниями связаны все контакты.
Я еще вернусь к этой проблеме, а пока перейдем ко второму аргументу. Этот аргумент — stuffHappened — представляет имя функции, которая должна вызываться для обработки прерывания. Данная функция определена далее в скетче. К таким функциям, их называют подпрограммами обработки прерываний (Interrupt Service Routine, ISR), предъявляются особые требования. Они не могут иметь параметров и ничего не должны возвращать. В этом есть определенный смысл: даже при том что они вызываются в разных местах в скетче, нет ни одной строки кода, осуществляющей прямой вызов ISR, поэтому нет никакой возможности передать им параметры или получить возвращаемое значение.
Последний параметр функции, attachInterrupt — это константа, в данном случае FALLING. Она означает, что подпрограмма обработки прерывания будет вызываться только при изменении напряжения на контакте D2 с уровня HIGH до уровня LOW (то есть при падении — falling), что происходит в момент нажатия кнопки.
Обратите внимание на отсутствие какого-либо кода в функции loop. В общем случае эта функция может содержать код, выполняющийся, пока не произошло прерывание. Сама подпрограмма обработки прерываний просто включает светодиод L.
Когда вы будете экспериментировать, после сброса Arduino светодиод L должен погаснуть. А после нажатия на кнопку — сразу зажечься и оставаться зажженным до следующего сброса.
Поэкспериментировав, попробуйте изменить последний аргумент в вызове attachInterrupt на RISING и выгрузите измененный скетч. После перезапуска Arduino светодиод должен оставаться погашенным, потому что напряжение на контакте хотя и имеет уровень HIGH, но с момента перезапуска оставалось на этом уровне. До этого момента напряжение на контакте не падало до уровня LOW, чтобы потом подняться (rising) до уровня HIGH.
После нажатия и удержания кнопки в нажатом состоянии светодиод должен оставаться погашенным, пока вы ее не отпустите. Отпускание кнопки вызовет прерывание, связанное с контактом D2, потому что, пока кнопка удерживалась нажатой, уровень напряжения на контакте был равен LOW, а после отпускания поднялся до HIGH.
Если во время опробования выяснится, что происходящее у вас не соответствует описанию, приведенному ранее, это, скорее всего, обусловлено эффектом дребезга контактов в кнопке. Этот эффект вызывается тем, что кнопка не обеспечивает четкий переход между состояниями «включено»/«выключено», вместо этого в момент нажатия происходит многократный переход между этими состояниями, пока не зафиксируется состояние «включено». Попробуйте нажимать кнопку энергичнее, это должно помочь получить четкий переход между состояниями без эффекта дребезга.
Другой способ опробовать этот вариант скетча — нажать кнопку и, удерживая ее, нажать и отпустить кнопку сброса Reset на плате Arduino. Затем, когда скетч запустится, отпустить кнопку на макетной плате, и светодиод L загорится.
Контакты с поддержкой прерываний
Вернемся теперь к проблеме именования прерываний. В табл. 3.1 перечислены наиболее распространенные модели плат Arduino и приведено соответствие номеров прерываний и контактов в них.
Таблица 3.1. Контакты с поддержкой прерываний в разных моделях Arduino
Модель | Номер прерывания | Примечания | |||||
---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | ||
Uno | D2 | D3 | — | — | — | — | |
Leonardo | D3 | D2 | D0 | D1 | D7 | — | Действительно, по сравнению с Uno первые два прерывания назначены разным контактам |
Mega2560 | D2 | D3 | D21 | D20 | D19 | D18 | |
Due | — | — | — | — | — | — | Вместо номеров прерываний функции attachInterrupt следует передавать номера контактов |
Смена контактов первых двух прерываний в Uno и Leonardo создает ловушку, в которую легко попасть. В модели Due вместо номеров прерываний функции attachInterrupt следует передавать номера контактов, что выглядит более логично.
Режимы прерываний
Режимы прерываний RISING (по положительному перепаду) и FALLING (по отрицательному перепаду), использовавшиеся в предыдущем примере, чаще всего используются на практике. Однако существует еще несколько режимов. Эти режимы перечислены и описаны в табл. 3.2.
Таблица 3.2. Режимы прерываний
Режим | Действие | Описание |
---|---|---|
LOW | Прерывание генерируется при уровне напряжения LOW | В этом режиме подпрограмма обработки прерываний будет вызываться постоянно, пока на контакте сохраняется низкий уровень напряжения |
RISING | Прерывание генерируется при положительном перепаде напряжения, с уровня LOW до уровня HIGH | — |
FALLING | Прерывание генерируется при отрицательном перепаде напряжения, с уровня HIGH до уровня LOW | — |
HIGH | Прерывание генерируется при уровне напряжения HIGH | Этот режим поддерживается только в модели Arduino Due и, подобно режиму LOW, редко используется на практике |
Включение внутреннего импеданса
В схеме в предыдущем примере использовалось внешнее «подтягивающее» сопротивление. Однако на практике сигналы, вызывающие прерывания, часто заводятся с цифровых выходов датчиков, и в этом случае нет необходимости использовать «подтягивающее» сопротивление.
Но если роль датчика играет кнопка, подключенная точно так же, как макетная плата на рис. 3.1, есть возможность избавиться от сопротивления, включив внутреннее «подтягивающее» сопротивление с номиналом около 40 кОм. Для этого нужно явно настроить режим INPUT_PULLUP для контакта, связанного с прерыванием, как показано в строке, выделенной жирным шрифтом:
void setup()
{
pinMode(ledPin, OUTPUT);
pinMode(2, INPUT_PULLUP);
attachInterrupt(0, stuffHapenned, FALLING);
}
Подпрограммы обработки прерываний
Иногда может показаться, что возможность обрабатывать прерывания, пока выполняется функция loop, дает простой способ обработки событий, таких как нажатия клавиш. Но в действительности накладываются очень жесткие ограничения на то, что можно или нельзя делать в подпрограммах обработки прерываний.
Подпрограммы обработки прерываний должны быть короткими и быстрыми настолько, насколько это возможно. Если во время работы подпрограммы обработки прерываний возникнет другое прерывание, эта подпрограмма не будет прервана, а полученный сигнал будет просто проигнорирован. Это, например, означает, что, если прерывания используются для измерения частоты, вы можете получить неверное значение.
Кроме того, пока выполняется подпрограмма обработки прерываний, код в функции loop простаивает.
На время обработки прерывания автоматически отключаются. Такое решение предохраняет от путаницы между подпрограммами, прерывающими друг друга, но имеет нежелательные побочные эффекты. Функция delay использует таймеры и прерывания, поэтому она не будет работать в подпрограммах обработки прерываний. То же относится к функции millis. Попытка использовать millis для получения числа миллисекунд, прошедших с момента последнего сброса платы, чтобы таким способом выполнить задержку, не приведет к успеху, так как она будет возвращать одно и то же значение, пока подпрограмма обработки прерываний не завершится. Однако вы можете использовать функцию delayMicroseconds, которая не использует прерываний.
Прерывания используются также во взаимодействиях через последовательные порты, поэтому не пытайтесь использовать Serial.print или функции чтения из последовательного порта. Впрочем, вы можете попробовать, и иногда они даже будут работать, но не ждите от такой связи высокой надежности.
Оперативные переменные
Так как подпрограмма обработки прерываний не может иметь параметров и не может ничего возвращать, нужен какой-то способ передачи информации между ней и остальной программой. Обычно для этого используются глобальные переменные, как показано в следующем примере:
// sketch 03_02_interrupt_flash
int ledPin = 13;
volatile boolean flashFast = false;
void setup()
{
pinMode(ledPin, OUTPUT);
attachInterrupt(0, stuffHapenned, FALLING);
}
void loop()
{
int period = 1000;
if (flashFast) period = 100;
digitalWrite(ledPin, HIGH);
delay(period);
digitalWrite(ledPin, LOW);
delay(period);
}
void stuffHapenned()
{
flashFast = ! flashFast;
}
В этом скетче функция loop использует глобальную переменную flashFast, чтобы определить период задержки. Подпрограмма обработки изменяет значение этой переменной между true и false.
Обратите внимание на то, что в объявление переменной flashFast включено слово volatile. Вы можете успешно разрабатывать скетч и без спецификатора volatile, но он совершенно необходим, потому что в отсутствие этого спецификатора компилятор C может генерировать машинный код, кэширующий значение переменной в регистре для увеличения производительности. Если, как в данном случае, кэширующий код будет прерван, он может не заметить изменения значения переменной.
В заключение о подпрограммах обработки прерываний
Когда будете писать подпрограммы обработки прерываний, помните следующие правила.
• Подпрограммы должны действовать быстро.
• Для передачи данных между подпрограммой обработки прерываний и остальной программой должны использоваться переменные, объявленные со спецификатором volatile.
• Не используйте delay, но можете использовать delayMicroseconds.
• Не ожидайте высокой надежности взаимодействий через последовательные порты.
• Не ожидайте, что значение, возвращаемое функцией millis, изменится.
Разрешение и запрет прерываний
По умолчанию прерывания в скетчах разрешены и, как упоминалось ранее, автоматически запрещаются на время работы подпрограммы обработки прерываний. Однако есть возможность явно запрещать и разрешать прерывания в программном коде, вызывая функции noInterrupts и interrupts. Эти функции не имеют параметров, и первая из них запрещает прерывания, а вторая — разрешает.
Явное управление может понадобиться, чтобы исключить возможность прерывания фрагмента кода, например, выводящего последовательность данных или генерирующего последовательность импульсов и точно выдерживающего временные интервалы с помощью функции delayMicroseconds.
Прерывания от таймера
Вызов подпрограмм обработки прерываний можно организовать не только по внешним событиям, но и по внутренним событиям изменения времени. Такая возможность особенно полезна, когда требуется выполнять некоторые операции через определенные интервалы времени.
Библиотека TimerOne упрощает настройку прерываний от таймера. Ее можно найти и загрузить по адресу http://playground.arduino.cc/Code/Timer1.
Следующий пример показывает, как с помощью TimerOne сгенерировать последовательность импульсов прямоугольной формы с частотой 1 кГц. Если в вашем распоряжении имеется осциллограф или мультиметр с возможностью измерения частоты, подключите его к контакту 12, чтобы увидеть сигнал (рис. 3.2).
Рис. 3.2. Последовательность прямоугольных импульсов, сгенерированная с помощью таймера
// sketch_03_03_1kHz
#include <TimerOne.h>
int outputPin = 12;
volatile int output = LOW;
void setup()
{
pinMode(12, OUTPUT);
Timer1.initialize(500);
Timer1.attachInterrupt(toggleOutput);
}
void loop()
{
}
void toggleOutput()
{
digitalWrite(outputPin, output);
output = ! output;
}
То же самое можно было бы реализовать с помощью delay, но применение прерываний от таймера позволяет организовать выполнение любых других операций внутри loop. Кроме того, использование функции delay не позволит добиться высокой точности, потому что время, необходимое на изменение уровня напряжения на контакте, не будет учитываться в величине задержки.
ПРИМЕЧАНИЕ
Все ограничения для подпрограмм обработки внешних прерываний, о которых рассказывалось ранее, распространяются также на подпрограммы обработки прерываний от таймера.
Представленным способом можно установить любой интервал между прерываниями в диапазоне от 1 до 8 388 480 мкс, то есть примерно до 8,4 с. Величина интервала передается функции initialize в микросекундах.
Библиотека TimerOne дает возможность также использовать таймер для генерирования сигналов с широтно-импульсной модуляцией (Pulse Width Modulation, PWM) на контактах 9 и 10 платы. Это может показаться излишеством, потому что то же самое делает функция analogWrite, но применение прерываний позволяет обеспечить более точное управление сигналом PWM. В частности, используя такой подход, можно организовать измерение протяженности положительного импульса в диапазоне 0…1023 вместо 0…255 в функции analogWrite. Кроме того, при использовании analogWrite частота следования импульсов в сигнале PWM составляет 500 Гц, а с помощью TimerOne можно эту частоту увеличить или уменьшить.
Чтобы сгенерировать сигнал PWM с применением библиотеки TimerOne, используйте функцию Timer1.pwm, как показано в следующем примере:
// sketch_03_04_pwm
#include <TimerOne.h>
void setup()
{
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
Timer1.initialize(1000);
Timer1.pwm(9, 512);
Timer1.pwm(10, 255);
}
void loop()
{
}
Здесь выбран период следования импульсов, равный 1000 мкс, то есть частота сигнала PWM составляет 1 кГц. На рис. 3.3 показана форма сигналов на контактах 10 (вверху) и 9 (внизу).
Рис. 3.3. Широтно-импульсный сигнал с частотой 1 кГц, сгенерированный с помощью TimerOne
Ради интереса давайте посмотрим, до какой степени можно увеличить частоту сигнала PWM. Если уменьшить длительность периода до 10, частота сигнала PWM должна увеличиться до 100 кГц. Форма сигналов, полученных с этими параметрами, показана на рис. 3.4.
Несмотря на наличие существенных переходных искажений, что вполне ожидаемо, протяженность положительных импульсов все же остается довольно близкой к 25 и 50% соответственно.
Рис. 3.4. Широтно-импульсный сигнал с частотой 100 кГц, сгенерированный с помощью TimerOne
В заключение
Прерывания, которые иногда кажутся идеальным решением для непростых проектов, могут осложнить отладку кода и не всегда оказываются лучшим способом решения трудных задач. Тщательно обдумайте возможные решения, прежде чем переходить к их использованию. В главе 14 мы познакомимся с другим приемом преодоления сложностей, связанных с тем, что Arduino не может выполнять более одной задачи одновременно.
Мы еще вернемся к прерываниям в главе 5, где рассмотрим возможность их применения для уменьшения потребления электроэнергии платой Arduino за счет периодического перевода ее в режим энергосбережения, и в главе 13, где прерывания будут применяться для увеличения точности обработки цифровых сигналов.
В следующей главе мы познакомимся с приемами увеличения производительности Arduino до максимума.
- Об авторе
- Благодарности
- Введение
- 1. Программирование Arduino
- 2. Под капотом
- 3. Прерывания и таймеры
- 4. Ускорение Arduino
- 5. Снижение потребления электроэнергии
- 6. Память
- 7. Интерфейс I2C
- 8. Взаимодействие с устройствами 1-Wire
- 9. Взаимодействие с устройствами SPI
- 10. Программирование последовательного интерфейса
- 11. Программирование интерфейса USB
- 12. Программирование сетевых взаимодействий
- 13. Цифровая обработка сигналов
- 14. Многозадачность с единственным процессом
- 15. Создание библиотек
- Приложение. Компоненты
- Сноски из книги
- Содержание книги
- Популярные страницы
- Настоящий обработчик прерывания
- Часы, таймеры и периодические уведомления
- Прерывания
- 12.8. Сервисы и прерывания BIOS
- Подпрограмма обработки прерывания
- 6 Раздражающие прерывания
- 10.6.6. Создание возможности для прерывания функций: siginterrupt()
- 14.3.3. Интервальные таймеры: setitimer() и getitimer()
- Подсистема прерывания контроллера msCAN12.
- Глава 6 Прерывания и обработка прерываний
- Глава 10 Таймеры и управление временем
- Регистрация обработчика прерывания