Ардуино уно аппаратное прерывание по таймеру. Прерывание таймера по переполнению

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

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

Операция

Время цикла
Опрашивает 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 ().
  • Цифровой фильтрации сигналов по среднему значению там вообще нет.

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

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

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

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

За основу мы возьмем очень популярную среди разработчиков устройств на МК книгу, автором которой является А.В. Евстифеев. По ссылкам в конце статьи Вы сможете найти проект в и проект в . В этой статье мы разберем работу 8-ми битного Т/С Т2, который входит в состав Т/С МК Atmega8.

Итак, что же такое Таймер/Счетчик? Т/С - это один из модулей МК AVR с помощью которого можно отмерять определенные промежутки времени, организовать ШИМ и многие другие задачи. В зависимости от модели МК, количество Т/С может составлять 4 и более. Пример тому - МК Atmega640х, 1280х/1281х, 2560х/2561х, которые содержат на своем борту 6 Т/С: два 8-ми битных и четыре 16-ти битных. МК Atmega8 содержит в себе три Т/С: Т0 и Т2 с разрядностью 8 бит, Т1 с разрядностью 16 бит.

Давайте подробнее рассмотрим Т/С Т2 микроконтроллера Atmega8.

Этот таймер может работать в нескольких режимах: Normal, Phase correct PWM, CTC (сброс при совпадении), Fast PWM. Подробнее о каждом режиме Вы можете прочитать в книге.

Данный Т/С состоит из регистра управления, счетного регистра, регистра сравнения, регистра состояния асинхронного режима. Структурная схема Т2 приведена на рис.1

Рассмотрим в теории как же работает данный модуль. Чтобы для начала Вам было понятнее, мы не будем рассматривать все лишние примочки таймера и рассмотрим самый обычный его режим - NORMAL. Для себя определим что МК тактируется от внутреннего RC-генератора с частотой 1МГц и таймер настроен на работу в режиме NORMAL.

Тактовые импульсы поступают на вход clk i\o и попадают в предделитель таймера. Предделитель может быть настроен, по Вашим потребностям, на прямой проход тактовых импульсов или делить входящие импульсы, пропуская только их определенную часть. Поделить входящие импульсы можно на /8, /64, /256, /1024. Так как у нас Т\С может работать в асинхронном режиме, то при включении его в этот режим количество предделителей существенно вырастает, но мы их рассматривать пока не будем. С предделителя тактовые импульсы поступают в блок управления и уже с него попадают в счетный регистр. Счетный регистр в свою очередь совершает инкремент на каждый входящий импульс. Счетный регистр Т2 8-ми битный, поэтому он может считать только до 255. Когда наступает переполнение счетного регистра, он сбрасывается в 0 и в этом же такте начинает считать заново. Так же в момент переполнения счетного регистра устанавливается флаг TOV2 (флаг прерывания по переполнению) регистра TIFR.

Теперь, раз уж мы затронули такие слова, как РЕГИСТР, самое время с ними познакомится. Для начала мы затронем только те регистры, с которыми будем непосредственно работать, дабы не забивать мозг лишней информацией.

TCNT2 - счетный регистр, о его работе мы уже говорили.

TCCR2 - регистр управления таймером.

TIMSK - регистр маски прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

TIFR - регистр флагов прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

А теперь о каждом подробно:

Регистр управления TCCR2. Содержимое этого регистра вы можете посмотреть на рис.2.


рис.2

Биты 0-2 отвечают за тактирование таймера. Установка определенных комбинаций в этих битах настраивает предделитель данного таймера. Если все три бита сброшены - таймер выключен.

Биты 3,6 отвечают за режим работы таймера.

Биты 4,5 нужны для настройки поведения вывода ОСn (проще говоря, используются при настройке ШИМ)

И последний бит этого регистра - бит 7. С его помощью мы можем принудительно изменять состояние вывода ОСn.

Регистр маски прерываний - TIMSK. Его мы видим на рисунке №3

Из этого регистра нас интересуют только два последних бита, биты 6 и 7. Этими битами мы разрешаем работу прерываний.

Бит 6, если в него записать единицу, разрешает прерывание по событию "Переполнение Т\С Т2"

Бит 7, если в него записать еди ницу, разрешает прерывание по событию "Совпадение счетного регистра с регистром сравнения"

Регистр флагов прерываний TIFR. Его мы видим на рисунке №4

рис.4

В этом регистре нас так же интересуют два последних бита: биты 6 и 7.

Бит 6 - флаг, устанавливается по событию "Переполнение Т\С Т2"
Бит 7 - флаг, устанавливается по событию "Совпадение счетного регистра с регистром сравнения"

Эти биты сбрасываются автоматически при выходе из обработчика прерывания, но для надежности их можно сбрасывать самостоятельно, сбрасывая эти биты в "0".

Остальные биты регистров TIMSK и TIFR используются Т\С Т0 и Т1. Как вы уже заметили, у битов этих регистров даже названия совпадают, за исключением цифры в конце названия, которая и указывает к какому таймеру данный бит применИм.

Осталось рассмотреть две несложные таблички, а именно: таблица, в которой описано управление тактовым сигналом (рис. 6), и таблица, в которой описано, как в общем настроить таймер (рис.5).

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

Вот мы и закончили с теорией, и пора приступить к практической части. Сразу оговорюсь.

ЧАСЫ, КОТОРЫЕ ПОЛУЧАТСЯ В ХОДЕ ИЗУЧЕНИЯ ДАННОЙ СТАТЬИ, НЕ ОБЛАДАЮТ ВЫСОКОЙ ТОЧНОСТЬЮ. ДАННАЯ СТАТЬЯ ОРИЕНТИРОВАННА НА ОБЩИЕ ПРИНЦИПЫ РАБОТЫ С ТАЙМЕРАМИ.

Открываем Studio 6, создаем проект и выбираем Atmega8.

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

< avr/io.h > #include < avr/interrupt.h >

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

Во второй строчке кода подключается библиотека с общим описанием регистров нашего МК. Так же в ней всем регистрам присвоены читабельные имена.

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

TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7);

На этом настройка нашего таймера закончена. Давайте подробнее рассмотрим последние три строки кода.

В первой строке мы разрешили прерывания по событию "Переполнение таймера\счетчика Т2"

И в третьей строкой мы глобально разрешили прерывания. Это можно было также написать следующим образом:

Asm("sei");

Остается добавить обработчик прерывания и код наших часов реального времени.

ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; }

В коде, который находится в обработчике прерывания, нет ничего сложного и нового для Вас. Внимание обратим только на переменную takt и волшебную цифру "4". Откуда взялась эта цифра? Давайте рассмотрим подробно этот момент.

Мы знаем, что наш МК работает от внутреннего генератора с частотой 1МГц, таймер тактируется с предделителем \1024, считать наш таймер может до 255. Зная эти параметры мы можем посчитать сколько переполнений он совершит за 1 секунду

1 000 000 \ 1024 \ 256 = 3,814697.....

Ну, а так как мы учимся работать с таймерами и не ставили цель получить суперточный ход часов, мы округляем наш результат и получаем "4". Т.е. за 1 секунду таймер переполнится ~4 раза.

Почему мы делили на 256 если таймер считает только до 255? Потому что "0" это тоже число. Думаю, здесь все понятно.

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

Вот весь листинг программы которая у нас получилась.

#define F_CPU 1000000UL #include < avr/io.h > #include < avr/interrupt.h > unsigned char takt = 0; unsigned char sek = 0; unsigned char min=0; unsigned char hour=0; ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; } int main(void) { TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7); while(1) { } }

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

В архиве Вы найдете проект с выводом информации на дисплей от nokia5110, проект в Proteus 7 и все нужные файлы и библиотеки для работы.

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

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); }

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

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

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

Аппаратные прерывания

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

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;

pinMode(ledPin, OUTPUT);

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 для контакта, связанного с прерыванием, как показано в строке, выделенной жирным шрифтом:

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;

pinMode(ledPin, OUTPUT);

attachInterrupt(0, stuffHapenned, FALLING);

int period = 1000;

if (flashFast) period = 100;

digitalWrite(ledPin, HIGH);

digitalWrite(ledPin, LOW);

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

int outputPin = 12;

volatile int output = LOW;

pinMode(12, OUTPUT);

Timer1.initialize(500);

Timer1.attachInterrupt(toggleOutput);

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

pinMode(9, OUTPUT);

pinMode(10, OUTPUT);

Timer1.initialize(1000);

Timer1.pwm(9, 512);

Timer1.pwm(10, 255);

Здесь выбран период следования импульсов, равный 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 до максимума.

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

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

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

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

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

Если вызов функции Update() все же необходим, то предварительно необходимо будет проверить переменную состояния. Это позволит выяснить, необходима ли последующая обработка.

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

Какими функциями оперирует тот или иной таймер?

Для микроконтроллера Arduino Uno у каждого из трех таймеров свои операции.

Так Timer0 отвечает за ШИМ на пятом и шестом пине, функции millis() , micros() , delay() .

Другой таймер – Timer1, используется с ШИМ на девятом и десятом пине, с библиотеками WaveHC и Servo.

Timer2 работает с ШИМ на 11 и 13 пинах, а также с Tone .

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

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

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

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

Похожие статьи