
Сегодня я буду основываться на методе, описанном в публикации Многозадачность на Arduino и исследую несколько типов прерываний Arduino, а также покажу, каким же образом их использовать, чтобы помочь Arduino управлять еще большим числом задач. Я расскажу, как использовать прерывания таймера, чтобы все работало как часы и рассмотрю внешние прерывания, дающие нам уведомления о внешних событиях.
Что такое прерывания?
Прерывание — это сигнал, который сообщает процессору, что нужно немедленно остановить то, что он в настоящий момент делает, и произвести некоторые операции, имеющие высокий приоритет. Эта обработка с высоким приоритетом называется обработчиком прерываний (interrupt handler).
Если мы реализуем некоторую функцию и присоедим ее к прерыванию, то эта функция будет вызвана всякий раз, когда появится сигнал прерывания. По возвращении из обработчика прерывания, процессор продолжит обрабатывать то, что он делал до прерывания.
Прерывания могут быть созданы несколькими источниками:
- Одним из таймеров Arduino, формируя прерывания по таймеру;
- Изменением состояния одного из входов внешних прерываний;
- Изменение состояния одного из группы пинов.
При использовании прерываний, нет необходимости писать код в loop (), постоянно проверяющий высокоприоритетное условие прерывания. Не нужно беспокоиться о медленной реакции или пропущенных нажатиях кнопок из-за слишком долго выполняющихся подпрограмм.
Процессор будет автоматически останавливать то, что он делает в момент возникновения прерывания и вызвать ваш обработчик прерывания. Вам просто нужно написать код, отвечающий на прерывание всякий раз, когда оно происходит.
Прерывания по таймеру Arduino
В прошлой публикации про многозадачность я рассматривал, как использовать функцию millis () для управления временем. Но для того, чтобы это работало, нам нужно вызывать millis () каждый раз в цикле, чтобы увидеть, не настало ли время сделать что-то. Это, своего рода, расточительство — вызывать millis () чаще, чем один раз в миллисекунду, только чтобы узнать, что время не изменилось. Было бы неплохо, если бы мы проверяли это только один раз в миллисекунду.
Таймер и прерывания по таймеру позволяют нам сделать именно это. Мы можем установить таймер, чтобы он прерывал нас один раз в миллисекунду. Таймер будет, на самом деле, сообщать нам, что пора проверить часы!
Arduino имеет три таймера: Timer0, Timer1 и Timer2. Timer0 уже настроен для генерации миллисекундных прерываний, обновляя счетчик миллисекунд, передаваемый в millis (). Это именно то, что нам нужно!
Таймеры — это простые счетчики, которые считают с некоторой частотой, получаемой из системных 16Мгц. Мы можем сконфигурировать делитель частоты для получения требуемой частоты и различных режимов счета. Мы также можем настроить их для генерации прерываний при достижении таймером некоторых заданных значений.
Timer0 являющийся 8-битным, считает от 0 до 255 и генерирует прерывание, при переполнении (превышении значения в 255). Он использует тактовый делитель на 64 по умолчанию, чтобы дать нам частоту прерываний 976.5625 Гц (достаточно близко к 1 кГц для наших целей). Мы не будем изменять частоту в Timer0, потому что это точно нарушит работу millis ()!
Регистры сравнения
В регистрах сравнения хранятся данные, которые постоянно сравниваются с состоянием таймера/счетчика. Установим регистр сравнения (OCR0A) для генерации другого прерывания где-то в середине этого счета. Приведенный ниже код будет генерировать прерывание TIMER0_COMPA всякий раз, когда значение счетчика проходит 0xAF.
1 2 3 4 | // Timer0 уже используется millis() - мы создаем прерывание где-то // в середине и вызываем ниже функцию "Compare A" OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); |
Затем определим обработчик прерывания для вектора прерывания по таймеру, называемому TIMER0_COMPA_vect. В этом обработчике прерывания будет делаться все то, что мы ранее делали в loop ().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Прерывание вызывается один раз в миллисекунду SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); //if(digitalRead(2) == HIGH) { sweeper2.Update(currentMillis); led1.Update(currentMillis); } led2.Update(currentMillis); led3.Update(currentMillis); } |
Что оставляет нас с абсолютно пустым loop ()
1 2 3 | void loop() { } |
Здесь мы теперь можем делать все что угодно. Можно даже вернуться к использованию delay ()! Это не затронет наши мигающие светодиоды и вращающиеся сервы. Они будут по-прежнему независимо вызываться один раз в миллисекунду.
Напишем новый код, независимо управляющий миганием светодиодов и вращением сервоприводов, но уже используя таймер.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | #include <Servo.h> class Flasher { // Переменные-члены класса // Устанавливаются при запуске int ledPin; // Номер пина со светодиодом long OnTime; // длительность ВКЛ в мс long OffTime; // длительность ВЫКЛ в мс // Текущее состояние int ledState; // устанавливает состояние светодиода unsigned long previousMillis; // время последнего обновления светодиода // Конструктор - создает Flasher // и инициализирует переменные-члены // и состояние public: Flasher(int pin, long on, long off) { ledPin = pin; pinMode(ledPin, OUTPUT); OnTime = on; OffTime = off; ledState = LOW; previousMillis = 0; } void Update(unsigned long currentMillis) { if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) { ledState = LOW; // ВЫКЛ previousMillis = currentMillis; // Запомнить время digitalWrite(ledPin, ledState); // Обновить состояние светодиода } else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) { ledState = HIGH; // ВКЛ previousMillis = currentMillis; // Запомнить время digitalWrite(ledPin, ledState); // Обновить состояние светодиода } } }; class Sweeper { Servo servo; // объект servo int pos; // текущее положение сервы int increment; // увеличивать перемещение на каждом интервале int updateInterval; // время между обновлениями unsigned long lastUpdate; // последнее обновление положения public: Sweeper(int interval) { updateInterval = interval; increment = 1; } void Attach(int pin) { servo.attach(pin); } void Detach() { servo.detach(); } void Update(unsigned long currentMillis) { if((currentMillis - lastUpdate) > updateInterval) // пришло время обновляться { lastUpdate = millis(); pos += increment; servo.write(pos); if ((pos >= 180) || (pos <= 0)) // конец вращения { // обратное направление increment = -increment; } } } }; Flasher led1(11, 123, 400); Flasher led2(12, 350, 350); Flasher led3(13, 200, 222); Sweeper sweeper1(25); Sweeper sweeper2(35); void setup() { sweeper1.Attach(9); sweeper2.Attach(10); // Timer0 уже используется millis() - прерываемся где-то // посередине и вызываем ниже функцию "Compare A" OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); } // Прерывание вызывается один раз в миллисекунду, // ищет любые новые данные, и сохраняет их SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); if(digitalRead(2) == HIGH) { sweeper2.Update(currentMillis); led1.Update(currentMillis); } led2.Update(currentMillis); led3.Update(currentMillis); } void loop() { } |
Внешние прерывания
В отличие от прерываний по таймеру, внешние прерывания вызываются внешними событиями. Например, когда нажата кнопка, или же получен импульс от датчика вращения. Однако, также как и с прерываниями по таймеру, нам не нужно постоянно опрашивать выводы GPIO на изменения.
На используемой мной Arduino Mega 2560 имеется 6 пинов, обрабатывающих внешние прерывания (на Arduino UNO таких входов только 2). В этом примере я подключу кнопку к одному из них и использую ее для сброса сервоприводов.
Во-первых, давайте добавим функцию reset () в наш класс Sweeper. Функция reset () устанавливает положение в ноль и немедленно перемещает туда серву.
1 2 3 4 5 6 | void reset() { pos = 0; servo.write(pos); increment = abs(increment); } |
Затем, добавим вызов функции attachInterrupt (), соединяющую внешнее прерывание с обработчиком. На Arduino Mega 2560, также как и на Arduino UNO, Inerrupt 0 ассоциировано с цифровым пином 2. Мы говорим, что ждем «спада» фронта сигнала на этом выводе. При нажатии кнопки сигнал «спадает» от высокого до низкого уровня и вызывается обработчик прерывания Reset.
1 2 | pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING); |
И здесь есть обработчик прерывания. Он просто вызывает функции, сбрасывающие сервы.
1 2 3 4 5 | void Reset() { sweeper1.reset(); sweeper2.reset(); } |
Теперь, при нажатии на кнопку, серва останавливает вращение и немедленно возвращается в нулевую позицию.
Схема у нас та же, что и в прошлый раз.
Полный скетч с таймерами и внешними прерываниями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | #include <Servo.h> class Flasher { // Переменные-члены класса // Устанавливаются при запуске int ledPin; // Номер пина со светодиодом long OnTime; // длительность ВКЛ в мс long OffTime; // длительность ВЫКЛ в мс // Текущее состояние int ledState; // устанавливает состояние светодиода unsigned long previousMillis; // время последнего обновления светодиода // Конструктор - создает Flasher // и инициализирует переменные-члены // и состояние public: Flasher(int pin, long on, long off) { ledPin = pin; pinMode(ledPin, OUTPUT); OnTime = on; OffTime = off; ledState = LOW; previousMillis = 0; } void Update(unsigned long currentMillis) { if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) { ledState = LOW; // ВЫКЛ previousMillis = currentMillis; // Запомнить время digitalWrite(ledPin, ledState); // Обновить состояние светодиода } else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) { ledState = HIGH; // ВКЛ previousMillis = currentMillis; // Запомнить время digitalWrite(ledPin, ledState); // Обновить состояние светодиода } } }; class Sweeper { Servo servo; // объект servo int pos; // текущее положение сервы int increment; // увеличивать перемещение на каждом интервале int updateInterval; // время между обновлениями unsigned long lastUpdate; // последнее обновление положения public: Sweeper(int interval) { updateInterval = interval; increment = 1; } void Attach(int pin) { servo.attach(pin); } void Detach() { servo.detach(); } void reset() { pos = 0; servo.write(pos); increment = abs(increment); } void Update(unsigned long currentMillis) { if((currentMillis - lastUpdate) > updateInterval) //пришло время обновляться { lastUpdate = millis(); pos += increment; servo.write(pos); if ((pos >= 180) || (pos <= 0)) // конец вращения { // обратное направление increment = -increment; } } } }; Flasher led1(11, 123, 400); Flasher led2(12, 350, 350); Flasher led3(13, 200, 222); Sweeper sweeper1(25); Sweeper sweeper2(35); void setup() { sweeper1.Attach(9); sweeper2.Attach(10); // Timer0 уже используется millis() - прерываемся где-то // посередине и вызываем ниже функцию "Compare A" OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING); } void Reset() { sweeper1.reset(); sweeper2.reset(); } // Прерывание вызывается один раз в миллисекунду, // ищет любые новые данные, и сохраняет их SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); // if(digitalRead(2) == HIGH) { sweeper2.Update(currentMillis); led1.Update(currentMillis); } led2.Update(currentMillis); led3.Update(currentMillis); } void loop() { } |
И результат
Библиотеки для работы с прерываниями
В сети можно найти несколько библиотек для работы с таймерами. Многие просто постоянно опрашивают millis () так же, как мы делали это в прошлый раз. Но есть некоторые, которые позволяют настроить таймеры для генерации прерываний.
Отличные библиотеки TimerOne и TimerThree от Paul Stoffregan дают множество низкоуровневых возможностей для конфигурирования прерываний по таймеру. Библиотека TimerThree не работает на Arduino UNO. Ее можно использовать с Leonardo, Mega 2560 и некоторыми платами Teensy.
Arduino UNO имеет только два входа внешних прерываний. Но что делать если требуется более двух входов? К счастью Arduino UNO поддерживает прерывания «pin-change» (по изменению входа) для всех пинов.
Прерывания по изменению входа — это тоже самое что и внешние прерывания. Разница в том, что первые генерируются по изменению состояния на любом из 8 соответствующих пинов. Они немного более сложны в обработке, так как вы должны отслеживать последнее известное состояние всех 8 пинов, чтобы выяснить, какой из 8 пинов вызвал это прерывание.
Библиотека PinChangeInt реализует удобный интерфейс для прерываний по изменению входа.
Правила работы с прерываниями
Для того, чтобы все работало гладко, лучше использовать не более 10 различных прерываний.
Если все имеет высокий приоритет, значит высокого приоритета нет ни у чего
Обработчики прерываний должны использоваться для обработки только приоритетных, чувствительных ко времени событий. Помните, что прерывания отключены, пока вы находитесь в обработчике прерывания. Если вы попытаетесь сделать слишком много на уровне прерывания, вы получите худший ответ на другие прерывания.
Одно прерывание в каждый момент времени
Когда программа находится в функции обработки прерывания, то другие прерывания отключены. Это имеет два важных следствия:
- Работа, выполняемая в функции обработки прерывания должна быть короткой, чтобы не пропустить не одного прерывания.
- Код, выполняемый в функции обработки прерывания не должен вызывать ничего такого, что требовало бы, чтобы прерывания были активны (например, delay () , или что-нибудь, использующее шину I2C). Это приведет к зависанию программы.
Отложите длительную обработку в loop ()
Если вам необходимо произвести обширную обработку в ответ на прерывание, используйте обработчик прерывания для того, чтобы сделать то, что необходимо, а затем установите переменную состояния volatile, показывающую, что дальнейшая обработка не требуется. При вызове функции Update () из loop () проверьте переменную состояния, на предмет того, не требуется ли какая-либо последующая обработка.
Проверка перед переконфигурированием таймера
Таймеры являются ограниченным ресурсом. На Arduino UNO их всего 3, и они используются для многих вещей. Если вы запутались с конфигурацией таймера, некоторые вещи могут перестать работать. Например, на Arduino UNO:
- Timer0 - используется millis (), micros (), delay () и ШИМ на пинах 5 и 6
- Timer1 - используется для Servo, библиотеки WaveHC и ШИМ на пинах 9 и 10
- Timer2 - используется Tone и ШИМ на пинах 11 и 13
Безопасное совместное использование данных
Поскольку прерывание приостановит все, что процессор делает, мы должны побеспокоиться об обмене данными между обработчиками прерываний и кодом в loop ().
Иногда компилятор попытается оптимизировать свой код для увеличения производительности. Иногда, в результате этой оптимизации, копии часто используемых переменных будут сохраняться в регистре для быстрого доступа к ним. Проблема в том, что если одна из этих переменных совместно используется обработчиком прерывания и кодом в loop (), то, в конечном итоге, в ней может обнаружиться несвежая копия вместо реального значения. Маркировка переменной как voltatile сообщает компилятору, что не нужно производить эти потенциально опасные оптимизации.
Даже пометка переменной как volatile будет не достаточным в случае, если переменная больше, чем целое (например, строка, массив, структура и т.п.). Большим переменным иребуется несколько циклов инструкций для обновления, и если прерывание происходит в середине этого обновления, данные могут быть повреждены. Если у вас есть большие переменные или структуры, которые совместно используются с обработчиком прерываний, вы должны отключить прерывания при обновлении их из loop (). Замечу, что прерывания отключены в обработчике прерывания уже по умолчанию.
Очень познавательная статья.
Есть вопрос: Как будет работать программа когда переполнится и обнулится millis ()?
Хороший вы задали вопрос, Валерий! Примерно через 50 дней программа работать перестанет: счетчик millis () обнулится и условия if ((currentMillis — lastUpdate) > updateInterval), (currentMillis — previousMillis >= OnTime) и (currentMillis — previousMillis >= OffTime) возможно никогда больше и не выполнятся. Чтобы этого избежать при условии, что не сильно критичен единичный сбой раз в 50 дней в длительности обновления состояний, то можно сравнивать не разность на превышение заданных интервалов, а модуль разностей. Например, для первого условия это будет выглядеть следующим образом:
Если же даже сбой один раз в 50 дней в работе программы неприемлем, то необходимо отдельно обработать событие переполнения счетчика, учитывая, что переменная типа unsigned long может принимать значения от 0 до (2^32-1).
Спасибо за ответ.
Добрый день всем.
Я не волшебник я только учусь.
(железным програмированием никогда не занимался — а обычное в школе институте проходили)
вы написали ...
как одно из решений

if (abs (currentMillis — lastUpdate) > updateInterval)
{
...
}
то есть, если я правильно понял — один раз в 50 дней доходим до максимального значения МИЛИСЕК-мах -потом сбрасывание в ноль будет «пропуск» одного интервала? ( если я правильно понял).
а что если написать
long interval = 1000;
.
.
if (millis () % interval == 0 )
{
...
}
все будет работать также ??
Николай, проблема тут вот в чем. Ход мыслей у вас правильный, но операция взятия отстатка от деления работает только с числами типа int (оба операнда должны быть типа int). Функция millis возвращает значение типа unsigned long. В комментарии к другой статейке, также посвященной многозадачности, я предложил одно из возможных вариантов отслеживания сброса счетчика.
Спасибо
Странно. У меня отлично работает операция взятия остатка (%), в случае, когда первый операнд типа unsigned long, а второй константа.
Например:
Оба счетчика выводится на дисплей, поэтому результат можно контролировать.
Андрей, не вводите людей в заблуждение!
1) Все арифметические операции (сложение, вычитание, умножение, деление, деление по модулю) над всеми числами разрешены и поддерживаются. Это МК не поддерживает ряд операций, в частности деление и деление по модулю, а также операции с плавающей точкой, но Вы же пишите на Си/Си++, а здесь вся арифметика выполняется даже если она аппаратно не поддерживается (попросту эмулируется, точнее реализуется программно).
2) выражение
работает всегда, даже когда «millis переполняется» т.к. currentMillis и lastUpdate объявлены как unsigned т.е. беззнаковые, они по определению не могут быть отрицательными. В результате операции (currentMillis — lastUpdate) у вас получится положительное число (это магия переполнения
другое дело, что у вас происходит сравнение знакового и беззнакового, это не хорошо (наврерняка компилятор указал вам на это), ну и в теории результат вычитания может быть больше разрядной сетки updateInterval (это же int), т.е. максимальный интервал ~32секунды.
Андрей, спасибо за невероятно познавательные и полезные статьи и ответы!
Скажите. правильно ли я понимаю, что millis переполняется только при месяце непрерывной работы, без выключения питания? Или речь о месяце вообще, при обычном режиме включения-выключения?
Да, правильно. Переполнение происходит через месяц непрерывной работы. Если питание отключить/включить, то счетчик при включении обнуляется.
Есть такая функция
Как её переделать для Arduino DUE? Там ведь таймеры 32-бит...
Для Arduino DUE работа с таймерами и прерываниями значительно отличается от описанного выше и выходит за рамки этой статьи. Могу порекомендовать вам ознакомится с осуждением работы с таймерами Arduino DUE на форуме arduino.cc. Пример кода взятый оттуда:
Здравствуйте! Подскажите пожалуйста почему неадекватно работает функция analogRead () во внешнем прерывании int0?
analogRead запрещает прерывания и любой другой код не может выполнится во время работы analogRead. Подробнее в статье Аналоговые измерения с Arduino.
особенно полезно последние предупреждения
я почти что решил перейти на прерывания вместо отслеживания через millis ()
> Помните, что прерывания отключены, пока вы находитесь в обработчике прерывания. Если вы попытаетесь сделать слишком много на уровне прерывания, вы получите худший ответ на другие прерывания.
Это ошибочное утверждение.
Позволю процитировать уважаемого DI HALT:
что будет если во время обработки одного прерывания придет другое? Оно потеряется?
Нет, не потеряется — у него взведется флаг и как только мы завершим первый обработчик автоматом произойдет переход к отложенному прерыванию
(easyelectronics.ru/avr-uc... -si-chast-3.html)
В случае с прерываниями микроконтроллер выполняет только одну программу, а прерывания нужны не для многозадачности, а работы с интерфейсными устройствами микроконтроллера.
Для организации меню достаточно одну кнопку завести на прерывание это будет вход в меню. Однако при такой реализации во время хождения по меню все инструкции основного цикла выполняться не будут.
Опечатка:
Маркировка переменной как volTAtile сообщает компилятору, что не нужно производить эти потенциально опасные оптимизации.
При нажатие на кнопку, получается следующиее, когда сервы не вернулись на в позицию «ноль» то светодиоды не работают, вернее не много сбиваются с ритма?
Добрый день! Подскажите новичку, пожалуйста!
Пишу код для определения скорости вращения. Датчик — оптрон с обвязкой, покупной. Этот код:
почему-то не работает, в сериал выдает следующие значения:
Особенно интересно второе значение, 249473 — 249474 = 0 КАК?
Откуда берутся inf в результатах и как заставить код работать?
надо бы привести к нужному типу
или
Вообще Ваш код должен в первый раз выдать значение newTime, ведь oldTime будет равно нулю.
Константин,
это не ошибка, это тонкость, которую нужно знать, корни ее лежат в атомарности операции (точнее в данном случае в ее отсутсвии).
У вас переменные oldTime и newTime объявлены как unsigned long, т.е. занимают 4 байта и они используются как в «фоне» (loop), так и в прерывании (pinChange). Не знаю с какой частотой у вас идут прерывания, но прерывания (вызов pinChange может произойти в любой момент выполнения любой команды в loop), и получается что пока выводилась первая строчка, значения переменной(ых) узменились... Но чтобы такого не происходило, есть один способ:
А что за датчик? Название, ссылка?
Константин,
это результат вычислений:
i = newTime — oldTime; (равно нулю)
s = 1000 / i; (равно бесконечности, т.к. вообще то на ноль делить нельзя...) поэтому выводится «inf» вместо бесконечности
хотя, тут скорее ошибка с вашей стороны, «s» это же наверное секунды?
может все таки s = i / 1000; ???
если все верно то вероятно нужно обрабатывать этот случай (деление на ноль) по особому...