Вот и подоспел 11-й выпуск из серии «Arduino для начинающих»!рџ�Љ
В этом видео мы будем говорить о прерываниях. А конкретно, о прерываниях по таймеру. О том, как сделать ваши программы более гибкими и избавиться от пресловутой функции delay(), которая, по сути, останавливает работу всего микроконтроллера.
Материалы к видео находятся здесь.
Список остальных выпусков:
#0 Введение в курс. Чего ожидать от этой серии для начинающих.
#1 Немного электроники и схемотехники – без них никуда.
#2 Знакомство с Arduino. Основные компоненты для начала работы.
#3 Arduino изнутри — структура, составляющие и их назначение. Микроконтроллер Atmega328P
#4 Arduino IDE Настройка и установка драйверов. Проверка работоспособности платы Arduino
#5 Первая программа – работаем со светодиодом и кнопкой.
#6 Условные операторы и циклы.
#7 Логические операции И, НЕ, ИЛИ.
#8 Функции и их применение.
#9 Библиотеки — как с ними работать и создать свою собственную.
#10 Передача и прием данных. Библиотека Serial и коды ASCII
Удачных компиляций! 🙂
P.S.: Если вы еще не состоите в сообществе «Arduino & Pi», милости просим сюда.
1504
Узнаем, как работать с прерываниями по таймеру. Напишем простую программу с параллельными процессами.
Предыдущий урок Список уроков Следующий урок
В реальной программе надо одновременно совершать много действий. Во введении я приводил пример программы контроллера холодильника на модуле Пельтье. Перечислю, какие действия она совершает:
Операция |
Время цикла |
Опрашивает 3 кнопки, обрабатывает сигналы с них для устранения дребезга | 2 мс |
Регенерирует данные семисегментных светодиодных индикаторов | 2 мс |
Вырабатывает сигналы управления для 2 датчиков температуры DS18B20 и считывает данные с них. Датчики имеют последовательный интерфейс 1-wire. | |
Чтение аналоговых значений тока и напряжения на элементе Пельтье, напряжения питания | 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 загрузить
Зарегистрируйтесь и оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!
// sketch_10_1 урока 10// Нажатие на кнопку меняет состояние светодиода
#include #include <button>
#define LED_1_PIN 13 // светодиод подключен к выводу 13#define BUTTON_1_PIN 12 // кнопка подключена к выводу 12
Button button1(BUTTON_1_PIN, 15); // создание объекта — кнопка
void setup() { pinMode(LED_1_PIN, OUTPUT); // определяем вывод светодиода как выход MsTimer2::set(2, timerInterupt); // задаем период прерывания по таймеру 2 мс MsTimer2::start(); // разрешаем прерывание по таймеру}
void loop() {
// управление светодиодом if ( button1.flagClick == true ) { // был клик кнопки button1.flagClick= false; // сброс признака digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия состояния светодиода } }
// обработчик прерывания 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 // светодиод подключен к выводу 13int 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();
- принимается решение о том, изменилось ли состояние кнопки.
Далее надо еще считать состояние кнопки функцией read().
- Но это не параллельная обработка сигнала. Функцию update() обычно вызывают в основном, асинхронном цикле программы. Если ее не вызывать дольше определенного времени, то информация о сигнале кнопки будет потеряна. Нерегулярные вызовы приводят к неправильной работе алгоритма.
- Сама функция имеет достаточно большой код и выполняется намного дольше функций библиотеки Button (уроки 7, 8, 9).
- Цифровой фильтрации сигналов по среднему значению там вообще нет.
В сложных программах эту библиотеку лучше не использовать.
В следующем уроке напишем более сложную программу с параллельными процессами. Узнаем, как реализовывать выполнение блоков программы в циклах с различными временными интервалами от одного прерывания по таймеру.
Предыдущий урок Список уроков Следующий урок
Поддержать проект
Timer1
Данная библиотека представляет собой набор функций для настройки аппаратного 16-битного таймера Timer1 в ATMega168/328. В микроконтроллере доступно 3 аппаратных таймера, которые могут быть настроены различными способами для получения различных функциональных возможностей. Начало разработки данной библиотеки было вызвано необходимостью быстро и легко установить период или частоту ШИМ сигнала, но позже она разраслась, включив в себя обработку прерываний по переполнению таймера и другие функции. Она может быть легко расширена или портирована для работы с другими таймерами.
Точность таймера зависит от тактовой частоты процессора. Тактовая частота таймера Timer1 определяется установкой предварительного делителя частоты. Этот делитель может быть установлен в значения 1, 8, 64, 256 или 1024.
Делитель | Длительность одного отсчета, мкс | Максимальный период, мс |
---|---|---|
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/.
Timer3
Обратите внимание, что библиотека Timer1 может использоваться на Arduino Mega, но она не поддерживает все три выходных вывода OCR1A
, OCR1B
и OCR1C
. Поддерживаются только A
и B
. OCR1A
подключен к выводу 11 на Mega, а OCR1B
– к выводу 12. С помощью одного из трех вызовов, которые задают вывод, значение 1 задаст вывод 11 на Mega, а 2 – задаст вывод 12. Библиотека Timer3 была протестирована только на Mega.
Библиотеку для таймера Timer3 можно здесь (TimerThree.zip)
Для установки просто распакуйте и поместите файлы в каталог 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); }
Проблемы с контекстом прерываний
Для обмена данными между кодом обработчика прерывания и остальной частью вашей программы необходимо принять дополнительные меры.
Переменные обычно должны быть типа volatile
. Volatile
говорит компилятору о необходимости избегать оптимизаций, которые предполагают, что переменная не может спонтанно измениться. Поскольку ваша функция может изменять переменные, пока ваша программа использует их, компилятор нуждается в этой подсказке. Но одного volatile
часто не хватает.
При доступе к общим переменным прерывания, как правило, должны быть отключены. Даже с volatile
, если обработчик прерывания изменит многобайтную переменную между последовательностью команд, эта переменная может быть прочитана неправильно. Если данные состоят из нескольких переменных, например, массив и счетчик, прерывания, как правило, должны быть отключены на протяжении всего кода, который получает доступ к данным.
Теги
ArduinoПрерываниеТаймерИспользуемые источники:
- https://pikabu.ru/story/arduino_dlya_nachinayushchikh__preryivaniya_po_taymeru_v_arduino_avr_sozdaem_mnogozadachnoe_ustroystvo_5202527
- http://mypractic.ru/urok-10-preryvanie-po-tajmeru-v-arduino-biblioteka-mstimer2-parallelnye-processy.html
- https://radioprog.ru/post/116