Как в ардуино использовать 2 прерывания. Прерывания и многозадачность в Arduino. Прерывания в языке Arduino

С учетом всего сказанного напишем программу, переключающую светодиод. В данном случае она будет это делать по событию переполнения таймера‑счетчика Timer 1 (вектор у нас обозначен: TIM1_OVF). Так как счетчик 16‑разрядный, то событие переполнения будет возникать при каждом 65 536‑м импульсе входной частоты. Если мы зададим коэффициент деления тактовой частоты на входе Timer 1 равным 64, то при 4 МГц частоты генератора мы получим примерно 1 Гц: 4 000 000/64/65 536 = 0,953674 Гц.

Это не совсем то, что нам требуется, и к тому же частота неточно равна одному герцу. Для того чтобы светодиод переключался точно раз в полсекунды (т. е. период его был равен секунде), программу придется немного усложнить, загружая каждый раз в счетные регистры определенное значение, которое рассчитывается просто: если период одного тактового импульса таймера равен 16 мкс (частота 4 000 000/64), то для получения 500 000 микросекунд надо отсчитать таких импульсов 31 250. Так как счетчик суммирующий, а прерывание возникает при достижении числа 65 536, то нужно предварительно загружать в него необходимое число 65 536 – 31250 = 34 286.

Это не единственный способ, но наиболее универсальный, годящийся для всех таймеров. Кстати, именно таким способом реализован отсчет времени в Arduino (см. главу 21 ). Иной способ – использовать прерывание по достижению определенного числа, загруженного в регистр сравнения А или В . Как это делается, мы увидим далее в этой главе. Для того чтобы осуществить само переключение из красного в зеленый, нам придется поступить как раньше, т. е. по каждому событию переполнения перебрасывать два бита в регистре PortD .

Полностью программа тогда будет выглядеть так:

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

Заметки на полях

Кстати, а как остановить запущенный таймер, если это потребуется? Очень просто: если обнулить регистр TCCR1B (тот, в котором задается коэффициент деления тактовой частоты), то таймер остановится. Чтобы запустить его опять с коэффициентом 1/64, нужно снова записать в этот регистр значение 0b00000011.

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

Обратите также внимание на форму записи ldi temp, (1 << TOIE1) . Поскольку бит, обозначаемый как TOIE1, в регистре TIMSK имеет номер 7, то эта запись эквивалентна записи ldi temp,0b10000000 – можно писать и так, и так, и еще кучей разных способов. Например, для запуска таймера с коэффициентом 1/64 требуется, как видно из текста программы, установить младшие два бита регистра TCCR1B. Здесь мы устанавливаем их в temp напрямую, но поскольку эти биты называются CS11 и CS10, то можно записать так:

ldi temp, (1 << CS11) I (1 << CS10)

или даже так:

ldi temp, (3 << CS10)

Подробно этот способ записи приведен в описании AVR‑ассемблера на сайте Atmel .

Подробности

В этой программе есть один тонкий момент, связанный с загрузкой счетных регистров таймера. При чтении и записи 16‑разрядных регистров Timer 1 их содержимое может измениться в промежутке между чтением или записью отдельных 8‑разрядных «половинок» (ведь, например, в данном случае таймер продолжает считать, пока идет обработка прерывания). Потому в 16‑разрядных таймерах AVR предусмотрен специальный механизм чтения и записи таких регистров. При записи первым загружается значение старшего байта, которое автоматически помещается в некий (недоступный для программиста) буферный регистр. Затем, когда поступает команда на запись младшего байта, оба значения объединяются, и запись производится одновременно в обе «половинки» 16‑разрядного регистра. Наоборот, при чтении первым должен быть прочитан младший байт, при этом значение старшего автоматически фиксируется помещением в тот же буферный регистр, и при следующей операции чтения старшего байта его значение извлекается оттуда. Таким образом и при чтении значения оба байта соответствуют одному и тому же моменту времени.

Узнаем, как работать с прерываниями по таймеру. Напишем простую программу с параллельными процессами.

В реальной программе надо одновременно совершать много действий. Во введении я приводил пример . Перечислю, какие действия она совершает:

Операция

Время цикла
Опрашивает 3 кнопки, обрабатывает сигналы с них для устранения дребезга 2 мс
Регенерирует данные семисегментных светодиодных индикаторов 2 мс
Вырабатывает сигналы управления для 2 датчиков температуры DS18B20 и считывает данные с них. Датчики имеют последовательный интерфейс 1-wire. 100 мкс для каждого бита,
1 сек общий цикл чтения
Чтение аналоговых значений тока и напряжения на элементе Пельтье, напряжения питания 100 мкс
Цифровая фильтрация аналоговых значений тока и напряжения 10 мс
Вычисление мощности на элементе Пельтье 10 мс
ПИД (пропорционально интегрально дифференциальный) регулятор стабилизации тока и напряжения 100 мкс
Регулятор мощности 10 мс
Регулятор температуры 1 сек
Защитные функции, контроль целостности данных 1 сек
Управление, общая логика работы системы 10 мс

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

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

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

В одном цикле мы поместили код обработки состояния кнопок и управление светодиодами. А в конце цикла поставили функцию задержки delay(2). Но, время на выполнение программы в цикле меняет общее время цикла. И период цикла явно не равен 2 мс. К тому же, во время выполнения функции delay() программа зависает и не может производить других действий. На сложной программе получится полный хаос.

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

Аппаратное прерывание от таймера.

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

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

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

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

Библиотека MsTimer2.

Библиотека предназначена для конфигурирования аппаратного прерывания от Таймера 2 микроконтроллера. Она имеет всего три функции:

  • MsTimer2::set(unsigned long ms, void (*f)())

Эта функция устанавливает время периода прерывания в мс. С таким периодом будет вызываться обработчик прерывания f. Он должен быть объявлен как void (не возвращает ничего) и не иметь аргументов. * f – это указатель на функцию. Вместо него надо написать имя функции.

  • MsTimer2::start()

Функция разрешает прерывания от таймера.

  • MsTimer2::stop()

Функция запрещает прерывания от таймера.

Перед именем функций надо писать MsTimer2::, т.к. библиотека написана с использованием директивы пространства имен namespace.

Для установки библиотеки скопируйте каталог MsTimer2 в папку libraries в рабочей папке Arduino IDE. За тем запустите программу Arduino IDE, откройте Скетч -> Подключить библиотеку и посмотрите, что в списке библиотек присутствует библиотека MsTimer2.

Загрузить библиотеку MsTimer2 в zip-архиве можно . Для установки его надо распаковать.

Простая программа с параллельной обработкой сигнала кнопки.

Теперь напишем простую программу с одной кнопкой и светодиодом из урока 6. К плате Ардуино подключена одна кнопка по схеме:

Выглядит это так:

На каждое нажатие кнопки светодиод на плате Ардуино меняет свое состояние. Необходимо чтобы были установлены библиотеки MsTimer2 и Button:

MsTimer2

И оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта!

// sketch_10_1 урока 10
// Нажатие на кнопку меняет состояние светодиода

#include
#include

#define LED_1_PIN 13 //
#define BUTTON_1_PIN 12 // кнопка подключена к выводу 12

Button button1(BUTTON_1_PIN, 15); // создание объекта - кнопка

void setup() {

MsTimer2::set(2, timerInterupt); // задаем период прерывания по таймеру 2 мс
MsTimer2::start(); //
}

void loop() {

// управление светодиодом
if (button1.flagClick == true) {
// был клик кнопки



}
}

// обработчик прерывания
void timerInterupt() {
button1.scanState(); // вызов метода ожидания стабильного состояния для кнопки
}

В функции setup() задаем время цикла прерывания по таймеру 2 мс и указываем имя обработчика прерывания timerInterrupt . Функция обработки сигнала кнопки button1.scanState() вызывается в обработчике прерывания таймера каждые 2 мс.

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

Квалификатор volatile.

Давайте изменим цикл loop() в предыдущей программе.

void loop() {

while(true) {
if (button1.flagClick == true) break;
}

// был клик кнопки
button1.flagClick= false; // сброс признака
digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия светодиода
}

Логически ничего не поменялось.

  • В первом варианте программа проходила цикл loop до конца и в нем анализировала флаг button1.flagClick.
  • Во втором варианте программа анализирует флаг button1.flagClick в бесконечном цикле while. Когда флаг становится активным, то выходит из цикла while по break и инвертирует состояние светодиода.

Разница только в том, в каком цикле крутится программа в loop или в while.

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

#include
#define LED_1_PIN 13 // светодиод подключен к выводу 13
int count=0;

void setup() {
pinMode(LED_1_PIN, OUTPUT); // определяем вывод светодиода как выход
MsTimer2::set(500, timerInterupt); // задаем период прерывания по таймеру 500 мс
MsTimer2::start(); // разрешаем прерывание по таймеру
}

void loop() {

while (true) {
if (count != 0) break;
}

count= 0;
digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия состояния светодиода
}

// обработчик прерывания
void timerInterupt() {
count++;
}

В этой программе счетчик count увеличивается на 1 в обработчике прерывания каждые 500 мс. В цикле while он анализируется, по break выходим из цикла и инвертируем состояние светодиода. Проще программы не придумаешь, но она тоже не работает.

Дело в том, что компилятор языка C++ по мере своего интеллекта оптимизирует программу. Иногда это не идет на пользу. Компилятор видит, что в цикле while никакие операции с переменной count не производятся. Поэтому он считает, что достаточно проверить состояние count только один раз. Зачем в цикле проверять, то, что никогда не может измениться. Компилятор корректирует код, оптимизируя его по времени исполнения. Проще говоря убирает из цикла код проверки переменной. Понять, что переменная count меняет свое состояние в обработчике прерывания, компилятор не может. В результате мы зависаем в цикле while.

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

Если, например, добавить в цикл while вызов функции delay(), то программа заработает.

while (true) {
if (count != 0) break;
delay(1);
}

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

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

Достаточно в программе при объявлении count написать

volatile int count=0;

и все варианты будут работать.

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

volatile Button button1(BUTTON_1_PIN, 15); // создание объекта - кнопка

По моим наблюдениям применение квалификатора volatile никак не увеличивает длину кода программы.

Сравнение метода обработки сигнала кнопки с библиотекой Bounce.

Существует готовая библиотека для устранения дребезга кнопок Bounce. Проверка состояния кнопки происходит при вызове функции update(). В этой функции:

  • считывается сигнал кнопки;
  • сравнивается с состоянием во время предыдущего вызова update();
  • проверяется, сколько прошло времени с предыдущего вызова с помощью функции millis();
  • принимается решение о том, изменилось ли состояние кнопки.
  • Но это не параллельная обработка сигнала. Функцию update() обычно вызывают в основном, асинхронном цикле программы. Если ее не вызывать дольше определенного времени, то информация о сигнале кнопки будет потеряна. Нерегулярные вызовы приводят к неправильной работе алгоритма.
  • Сама функция имеет достаточно большой код и выполняется намного дольше функций библиотеки Button ().
  • Цифровой фильтрации сигналов по среднему значению там вообще нет.

В сложных программах эту библиотеку лучше не использовать.

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

Рубрика: . Вы можете добавить в закладки.

Timer1

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

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

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

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

Timer3

Обратите внимание, что библиотека Timer1 может использоваться на Arduino Mega, но она не поддерживает все три выходных вывода OCR1A , OCR1B и OCR1C . Поддерживаются только A и B . OCR1A подключен к выводу 11 на Mega, а OCR1B - к выводу 12. С помощью одного из трех вызовов, которые задают вывод, значение 1 задаст вывод 11 на Mega, а 2 - задаст вывод 12. Библиотека Timer3 была протестирована только на Mega.

Библиотеку для таймера Timer3 можно здесь ()

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

Методы библиотек TimerOne и TimerThree

Настройка

void initialize(long microseconds=1000000); Вы должны вызвать этот метод первым, перед использованием любых других методов библиотеки. При желании можно задать период таймера (в микросекундах), по умолчанию период устанавливается равным 1 секунде. Обратите внимание, что это нарушает работу analogWrite() на цифровых выводах 9 и 10 на Arduino. void setPeriod(long microseconds); Устанавливает период в микросекундах. Минимальный период и максимальная частота, поддерживаемые данной библиотекой, равны 1 микросекунде и 1 МГц, соответственно. Максимальный период равен 8388480 микросекунд, или примерно 8,3 секунды. Обратите внимание, что установка периода изменит частоту срабатывания прикрепленного прерывания и частоту, и коэффициент заполнения на обоих ШИМ выходах.

Управление запуском

void start(); Запускает таймер, начиная новый период. void stop(); Останавливает таймер. void restart(); Перезапускает таймер, обнуляя счетчик и начиная новый период.

Управление выходным ШИМ сигналом

void pwm(char pin, int duty, long microseconds=-1); Генерирует ШИМ сигнал на заданном выводе pin . Выходными выводами таймера Timer1 являются выводы PORTB 1 и 2, поэтому вы должны выбрать один из них, всё остальное игнорируется. На Arduino это цифровые выводы 9 и 10, эти псевдонимы также работают. Выходными выводами таймера Timer3 являются выводы PORTE , соответствующие выводам 2, 3 и 5 на Arduino Mega. Коэффициент заполнения duty задается, как 10-битное значение в диапазоне от 0 до 1023 (0 соответствует постоянному логическому нулю на выходе, а 1023 - постоянной логической единице). Обратите внимание, что при необходимости в этой функции можно установить и период, добавив значение в микросекундах в качестве последнего аргумента. void setPwmDuty(char pin, int duty); Быстрый способ для настройки коэффициента заполнения ШИМ сигнала, если вы уже настроили его, вызвав ранее метод pwm() . Этот метод позволяет избежать лишних действий по включению режима ШИМ для вывода, изменению состояния регистра, управляющего направлением движения данных, проверки необязательного значения периода и прочих действий, которые являются обязательными при вызове pwm() . void disablePwm(char pin); Выключает ШИМ на заданном выводе, после чего вы можете использовать этот вывод для чего-либо другого.

Прерывания

void attachInterrupt(void (*isr)(), long microseconds=-1); Вызывает функцию через заданный в микросекундах интервал. Будьте осторожны при попытке выполнить слишком сложный обработчик прерывания при слишком большой тактовой частоте, так как CPU может никогда не вернуться в основной цикл программы, и ваша программа будет «заперта». Обратите внимание, что при необходимости в этой функции можно установить и период, добавив значение в микросекундах в качестве последнего аргумента. void detachInterrupt(); Отключает прикрепленное прерывание.

Остальные

unsigned long read(); Считывает время с момента последнего переполнения в микросекундах.

Пример 1

В примере ШИМ сигнал с коэффициентом заполнения 50% подается на вывод 9, а прикрепленный обработчик прерывания переключает состояние цифрового вывода 10 каждые полсекунды.

#include "TimerOne.h" void setup() { pinMode(10, OUTPUT); Timer1.initialize(500000); // инициализировать timer1, и установить период 1/2 сек. Timer1.pwm(9, 512); // задать шим сигнал на выводе 9, коэффициент заполнения 50% Timer1.attachInterrupt(callback); // прикрепить callback(), как обработчик прерывания по переполнению таймера } void callback() { digitalWrite(10, digitalRead(10) ^ 1); } void loop() { // ваша программа... }

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

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

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

Пример 2

#include // Данный пример использует прерывание таймера, чтобы // помигать светодиодом, а также продемонстрировать, как // делить переменную между обработчиком прерывания и // основной программой. const int led = LED_BUILTIN; // вывод со светодиодом void setup(void) { pinMode(led, OUTPUT); Timer1.initialize(150000); Timer1.attachInterrupt(blinkLED); // вызывать blinkLED каждые 0.15 сек. Serial.begin(9600); } // Обработчик прерывания будет мигать светодиодом и // сохранять данные о том, сколько раз мигнул. int ledState = LOW; volatile unsigned long blinkCount = 0; // используйте volatile для общих переменных void blinkLED(void) { if (ledState == LOW) { ledState = HIGH; blinkCount = blinkCount + 1; // увеличить значение при включении светодиода } else { ledState = LOW; } digitalWrite(led, ledState); } // Основная программа будет печатать счетчик миганий // в Arduino Serial Monitor void loop(void) { unsigned long blinkCopy; // хранит копию blinkCount // чтобы прочитать переменную, которая записана в обработчике // прерывания, мы должны временно отключить прерывания, чтобы // быть уверенными, что она не изменится, пока мы считываем ее. // Чтобы минимизировать время, когда прерывания отключены, // просто быстро копируем, а затем используем копию, позволяя // прерываниям продолжать работу. noInterrupts(); blinkCopy = blinkCount; interrupts(); Serial.print("blinkCount = "); Serial.println(blinkCopy); delay(100); }

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

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



Просмотров