-
Приветствую, уважаемые,
Подскажите пожалуйста, в Arduino IDE предусмотрен механизм обработки исключений/ошибок?
Вот есть нарядная библиотечка String — красиво, удобно, но что будет, если ей памяти не хватит?
-
IDE вообще к исключениям не имеет никакого отношения. В в библиотеке Wiring не предусмотрено ничего подобного, а у микроконтроллера нет специальных аппаратных средств для реализации механизма исключений. Программная реализация исключений через прерывания, в принципе, возможна, но это будет жалкая имитация и транжирство и без того ничтожных ресурсов МК. Просто пишите код аккуратно.
Что конкретно в Wiring/String будет — в код нужно смотреть. Возможно просто предварительная проверка аргументов не позволит сделать ошибку. А так теоретически можно и получить отказ в выделении памяти и просто ничего не сделать, можно записать данные «не туда», можно на стек наехать и улететь в неизвестном направлении. Все глюки к вашим услугам.
-
А какие ошибки Вы имеете ввиду?Если считаете что не хватит памяти вместо String попробуйте работу с массивом. Посмотрите сюда http://tinyurl.com/merjphg А вообще процы в Ардуино очень мощные а задачи не очень сложные поэтому проблемы я не вижу.Вот попробуйте передачу/приём через USART на ATinny13 сделать-вот это шедевр.Или учите Ассемблер тогда.
Забавную картинку к этому уроку я найти не смог, нашёл только какую-то лекцию по программированию, и вот самое начало этой лекции отлично объясняет нам, что такое прерывание. Прерывание в Ардуино можно описать абсолютно точно так же: микроконтроллер “всё бросает”, переключается на выполнение блока функций в обработчике прерывания, выполняет их, а затем возвращается ровно к тому месту основного кода, в котором остановился.
Прерывания бывают разные, точнее их причины: прерывание может вызвать АЦП, таймер (урок по прерываниям таймера) или буквально пин микроконтроллера. Такие прерывания называются внешними аппаратными, и именно о них мы сегодня поговорим.
External hardware interrupt – это прерывание, вызванное изменением напряжения на пине микроконтроллера. Основная суть состоит в том, что системное ядро микроконтроллера не занимается опросом пина и не тратит на это время. Но как только напряжение на пине меняется (цифровой сигнал) – микроконтроллер получает сигнал, бросает все дела, обрабатывает прерывание, и возвращается к работе.
Зачем это нужно? Чаще всего прерывания используются для детектирования коротких событий – импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря – пин опрашивается параллельно основному коду. Также прерывания могут будить МК из режимов энергосбережения, когда вообще практически вся периферия отключена. Посмотрим, как работать с аппаратными прерываниями в среде Arduino IDE.
Прерывания в Arduino
Arduino Nano (AVR)
У микроконтроллера есть возможность получать прерывания с любого пина, такие прерывания называются PCINT и работать с ними можно только при помощи сторонних библиотек (вот отличная), либо вручную (читай у меня вот тут). В этом уроке речь пойдёт об обычных прерываниях, которые называются INT, потому что стандартный фреймворк Ардуино умеет работать только с ними. Таких прерываний и соответствующих им пинов очень мало:
МК / номер прерывания | INT 0 | INT 1 | INT 2 | INT 3 | INT 4 | INT 5 |
ATmega 328/168 (Nano, UNO, Mini) | D2 | D3 | – | – | – | – |
ATmega 32U4 (Leonardo, Micro) | D3 | D2 | D0 | D1 | D7 | – |
ATmega 2560 (Mega) | D2 | D3 | D21 | D20 | D19 | D18 |
Как вы поняли из таблицы, прерывания имеют свой номер, который отличается от номера пина. Есть кстати удобная функция digitalPinToInterrupt(pin)
, которая принимает номер пина и возвращает номер прерывания. Скормив этой функции цифру 3
на Arduino Nano, мы получим 1
. Всё по таблице выше, функция для ленивых.
Wemos Mini (esp8266)
На esp8266 прерывание можно настроить стандартными средствами на любом пине.
Обработчик прерывания
Сначала нужно объявить функцию-обработчик прерывания, эта функция будет выполнена при срабатывании прерывания:
- Для AVR Arduino это функция вида
void имя(){}
- Для ESP8266/32 функция создаётся с атрибутом
IRAM_ATTR
илиICACHE_RAM_ATTR
. Подробнее читай в уроке про esp8266.
К коду внутри этой функции есть некоторые требования:
- Переменные, которые изменяют своё значение в прерывании, должны быть объявлены со спецификатором
volatile
. Пример:volatile byte val;
- Не работают задержки типа
delay()
- Не меняет своё значение
millis()
иmicros()
- Некорректно работает вывод в порт
Serial.print()
- Нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при частых прерываниях:
- Вычисления с
float
- Работа с динамической памятью (функции new(), malloc(), realloc() и прочие)
- Работа со String-строками
- Вычисления с
Подключение прерывания
Подключается прерывание при помощи функции attachInterrupt(pin, handler, mode)
:
pin
– пин прерывания- Для AVR Arduino это номер прерывания (см. таблицу выше)
- Для ESP8266 это номер GPIO или D-пин на плате (как в уроке про цифровые пины)
handler
– имя функции-обработчика прерывания, которую мы создалиmode
– режим работы прерывания:RISING
(рост) – срабатывает при изменении сигнала с LOW на HIGHFALLING
(падение) – срабатывает при изменении сигнала с HIGH на LOWCHANGE
(изменение) – срабатывает при изменении сигнала (с LOW на HIGH и наоборот)LOW
(низкий) – срабатывает постоянно при сигнале LOW (не поддерживается на ESP8266)
Прерывание можно отключить при помощи функции detachInterrupt(pin)
.
Можно глобально запретить прерывания функцией noInterrupts()
и снова разрешить их при помощи interrupts()
. Аккуратнее с ними! noInterrupts()
остановит также прерывания таймеров, и у вас “сломаются” все функции времени и генерация ШИМ.
Пример
Давайте рассмотрим пример, в котором в прерывании считаются нажатия кнопки, а в основном цикле они выводятся с задержкой в 1 секунду. Работая с кнопкой в обычном режиме, совместить такой грубый вывод с задержкой невозможно:
volatile int counter = 0; // переменная-счётчик void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим attachInterrupt(0, btnIsr, FALLING); } void btnIsr() { counter++; // + нажатие } void loop() { Serial.println(counter); // выводим delay(1000); // ждём }
Ловим событие
Если прерывание отлавливает какое-то короткое событие, которое необязательно обрабатывать сразу, то лучше использовать следующий алгоритм работы с прерыванием:
- В обработчике прерывания просто поднимаем флаг (
volatile bool
переменная) - В основном цикле программы проверяем флаг, если поднят – сбрасываем его и выполняем нужные действия
volatile bool intFlag = false; // флаг void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); attachInterrupt(0, buttonTick, FALLING); } void buttonTick() { intFlag = true; // подняли флаг прерывания } void loop() { if (intFlag) { intFlag = false; // сбрасываем // совершаем какие-то действия Serial.println("Interrupt!"); } }
Следующий возможный сценарий: нам надо поймать сигнал с “датчика” и сразу на него отреагировать однократно до появления следующего сигнала. Если датчик – кнопка, нас поджидает дребезг контактов. С дребезгом лучше бороться аппаратно, но можно решить проблему программно: запомнить время нажатия и игнорировать последующие срабатывания. Рассмотрим пример, в котором прерывание будет настроено на изменение (CHANGE
).
void setup() { // прерывание на D2 (UNO/NANO) attachInterrupt(0, isr, CHANGE); } volatile uint32_t debounce; void isr() { // оставим 100 мс таймаут на гашение дребезга // CHANGE не предоставляет состояние пина, // придётся узнать его при помощи digitalRead if (millis() - debounce >= 100 && digitalRead(2)) { debounce = millis(); // ваш код по прерыванию по высокому сигналу } } void loop() { }
Вы скажете: но ведь millis()
Не меняет значение в прерывании! Да, не меняет, но он меняется между прерываниями! Это в принципе всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.
Видео
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
I am working with socket communication in Arduino, and I need the try/catch block for proper handling, what do you guys suggest? My search on the internet wasn’t successful.
edit:
The code I am working with uses the WiFly module to interact with a mobile application, I am building a robot module with some controls over mobile application using Android.
Everything works just fine, but sometimes the socket gets disconnected, so I need to add handling for such cases, something similar to try/catch block, but I didn’t find similar block for Arduino.
My code:
void loop() {
Client client = server.available();
if (client) {
while (client.connected()) {
if (client.available()) {
// Serial.print("client connected n");
char c = client.read();
if(c == 'L')
turnLeft();
if(c == 'R')
turnRight();
if(c == 'F')
goForward();
if(c == 'B')
goBackward();
if(c == 'S')
Stop();
Serial.print(c);
}
}
// give the web browser time to receive the data
delay(100);
client.stop();
}
}
asked Apr 19, 2012 at 12:55
4
The Arduino reference is not listing try catch
(for details of why see, for example, this related answer). And I assume, that implementing try catch on a µ-controller could be kind of difficult/impossible.
Try catch in most languages is a quite expensive operation: The program stack get copied once for the try block and for each catch block. In case the try goes wrong the try-block stack will be discarded and one of the catch block stacks will be executed.
I am not an expert of cpu architecture, but I can imagine, that this needs a lot of memory swapping and similar operations — it should be hard to achieve with a simple µ-controller.
It might worth to look how C-Programmers do patterns similar to try/catch
answered Apr 19, 2012 at 13:45
vikingosegundovikingosegundo
52k14 gold badges137 silver badges178 bronze badges
0
Arduino doesn’t support exception handling. However, you don’t need to use exception handling to make your code robust. By simply checking the return values of functions that can fail you can achieve the same end.
Since client.connected()
is checked every time around the loop, and since client.available()
will return 0 if not connected the only possible failure that is not already being handled is the return from client.read()
.
You can fix this, for example, by changing the line:
char c = client.read();
to:
int i = client.read();
if (i == -1) {
break;
}
char c = (char) i;
answered Apr 19, 2012 at 15:23
Matthew MurdochMatthew Murdoch
30.7k30 gold badges95 silver badges127 bronze badges
2
/* this builds successfully */
try {
/* code */
} catch(String error) {
}
Tyler2P
2,30424 gold badges22 silver badges31 bronze badges
answered Apr 2 at 21:46
1
Отсутствие на Arduino операционной системы совершенно не означает, что невозможно решить проблему многозадачности для этого контроллера. Для этого просто нужна своеобразная методика, частью которой является использование прерываний.
Это могут быть прерывания по таймеру или внешние прерывания, уведомляющие о событиях, воздействующих на систему извне.
Прерывания и их источники
В процессе работы управляющий процессор выполняет определенные операции, а прерывание вызывает их остановку и в соответствие с кодом заставляет выполнить операции с более высоким приоритетом.
Проще говоря, прерывания – это набор приоритетов для тех или иных процессов, исполняемых контроллером.
Этот процесс имеет название обработчик прерываний и дает возможность присоединить к себе определенную функцию. Она будет вызываться при поступлении сигнала прерывания. Процессор, вернувшись из обработчика, приступит к выполнению тех операций, в процессе исполнения которых поступил сигнал прерывания.
Источником сигнала прерывания может стать таймер Arduino или процесс изменения состояния одного из контактов (пинов). Еще одним источником может стать какой-либо вход внешних прерываний: нужный сигнал появится при изменении его состояния.
Прерывание можно заставить работать с помощью кода, который поможет отвечать на прерывание. При этом нет необходимости писать код в loop (), который используют для систематической проверки приоритета прерывания.
В момент получения сигнала процессор автоматически остановится, вызвав обработчик. При этом нет нужды беспокоиться о времени отклика на нажатие кнопки или продолжительной реакции на выполняемую программу.
Особенности прерывания по таймеру
Как же управлять временем и запускать процессы в нужном вам порядке? При работе с микросхемой Arduino для этого можно использовать millis(), ее эффективность зависит от постоянного обращения к ней. Тогда, в случае вызова этой функции, можно будет понять – наступило время определенной операции или нет.
Добиться эффективности можно при вызове millis() несколько раз в миллисекунду, но это довольно расточительно. Для вызова функции раз в миллисекунду (что является оптимальным вариантом) необходимо использовать таймер. Его можно установить на интервал в миллисекунду и добиться желаемого результата.
Микроконтроллер Arduino Uno укомплектован тремя таймерами: из них timer0 предназначен для генерации прерываний с интервалом в одну миллисекунду. При этом будет постоянно обновляться счетчик, передающий информацию функции millis(). Вести точный подсчет таймеру позволяет определенная частота, получаемая из 16 МГц процессора.
Arduino, при необходимости, позволяет произвести конфигурацию делителя частоты с подбором оптимального режима счета.
Функция timer0 оперирует тактовым делителем на 64 и изменять это значение не стоит. Оно позволяет получить частоту прерывания, близкую к 1 кГц, оптимальную для целей большинства схемотехников. Если попытаться изменить данный параметр, то можно нарушить работу функции millis().
Регистры сравнения и их роль в генерации прерывания
Регистры сравнения выполняют функцию анализа хранимых данных с текущим состоянием счетчика прерывания. Например, регистр сравнения (OCR0A) эффективно применяется для прерывания в середине счета.
Программный код, пример которого приведен ниже, позволит генерировать функцию TIMER0_COMPA при прохождении счетчика 0xAF:
// Timer0 уже используется millis() — мы создаем прерывание где-то // в середине и вызываем ниже функцию «Compare A» OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); |
Для оптимизации работы кода и микроконтроллера следует полностью отказаться от loop(). Для этого необходимо определение по вектору обработчика прерывания с помощью функции TIMER0_COMPA_vect. Благодаря ей обработчик и будет выполнять все те операции, что раньше делались в loop().
Данный код позволит вернуться к использованию функции delay(), благодаря чему все классические мерцающие светодиоды или работающие сервоприводы будут функционировать без проблем, но при этом мы получим удобный и практичный таймер.
// Прерывание вызывается один раз в миллисекунду 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); } |
Какие внешние воздействия вызывают прерывание
Вызвать внешние прерывания могут определенные действия из внешней среды. Это может быть простым нажатием на кнопку или срабатыванием используемого датчика. При этом нет необходимости вести постоянный опрос вывода GPIO о происходящих изменениях.
Микроконтроллер Arduino может иметь несколько пинов, способных обрабатывать внешние прерывания. Так, на плате Arduino Uno их два, а на Arduino Mega 2560 – 6. Продемонстрировать их функционал можно с помощью кнопки сброса сервопривода. Для этого при написании кода в класс Sweeper необходимо добавит функцию reset(). Она способна установить нулевое положение и перетаскивать в нее сервопривод.
void reset() { pos = 0; servo.write(pos); increment = abs(increment); } |
Соединить обработчик с внешним прерыванием поможет другая функция – attachInterrupt(). На приведенных в качестве примеров микроконтроллерах Interrupt0 реализована на втором контакте. Она сообщает микроконтроллеру о том, что на данном входе ожидается спад сигнала.
pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING); |
Достаточно нажать кнопку и сигнал действительно падает до минимального уровня, вызывая обработчик Reset.
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 |
#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(), но есть и такие, которые позволяют произвести желаемую настройку таймеров и сгенерировать прерывания. Оптимальным вариантом для этого являются библиотеки TimerThree и TimerOne, разработанные Paul Stoffregan.
Благодаря им, схемотехники получают широкий выбор возможностей, с помощью которых можно сконфигурировать прерывания с помощью таймера. Первая из этих библиотек не работает с Adruino Uno, но прекрасно зарекомендовала себя с платами Teensy, микроконтроллерами Adruino Mega2560 и Adruino Leonardo.
Недостатком Adruino Uno является наличие всего двух ходов, предназначенных для работы с внешними прерываниями. Если требуется большее количество подобных пинов, то отчаиваться не стоит, ведь этот микроконтроллер поддерживает pin-change – прерывания по изменению входа и работает это на всех восьми входах.
Их отличие от простых внешних прерываний – в сложности обработки, так как схемотехнику требуется отслеживать последнее из известных состояний пинов. Только в этом случае можно будет понять, какой из них вызвал прерывание.
Наиболее информативное и практичной библиотекой для прерываний по изменению входа является PinChangeInt.
Прерывания: основные правила работы
В ходе реализации проекта может потребоваться несколько прерываний, но если каждое из них будет иметь максимальный приоритет, то фактически его не будет ни у одной из функций. По этой же причине не рекомендуется использовать более десятка прерываний.
Обработчики должны применяться только к тем процессам, которые имеют максимальную чувствительность ко временным интервалам. Не стоит забывать, что пока программа находится в обработчике прерывания – все другие прерывания отключены. Большое количество прерываний ведет к ухудшению их ответа.
В момент, когда действует одно прерывание, а остальные отключаются, возникает два важных нюанса, которые должен учитывать схемотехник. Во-первых, время прерывание должно быть максимально коротким.
Это позволит не пропустить все остальные запланированные прерывания. Во-вторых, при обработке прерывания программный код не должен требовать активности от других прерываний. Если этого не предотвратить, то программа просто зависнет.
Не стоит использовать длительную обработку в loop(), лучше разработать код для обработчика прерывания с установкой переменной volatile. Она подскажет программе, что дальнейшая обработка не нужна.
Если вызов функции Update() все же необходим, то предварительно необходимо будет проверить переменную состояния. Это позволит выяснить, необходима ли последующая обработка.
Перед тем, как заняться конфигурацией таймера, следует произвести проверку кода. Таймеры Anduino стоит отнести к ограниченным ресурсам, ведь их всего три, а применяются они для выполнения самых разных функций. Если запутаться с использованием таймеров, то ряд операций может просто перестать работать.
Какими функциями оперирует тот или иной таймер?
Для микроконтроллера Arduino Uno у каждого из трех таймеров свои операции.
Так Timer0 отвечает за ШИМ на пятом и шестом пине, функции millis(), micros(), delay().
Другой таймер – Timer1, используется с ШИМ на девятом и десятом пине, с библиотеками WaveHC и Servo.
Timer2 работает с ШИМ на 11 и 13 пинах, а также с Tone.
Схемотехник должен позаботиться о безопасном использовании обрабатываемых совместно данных. Ведь прерывание останавливает на миллисекунду все операции процессора, а обмен данных между loop() и обработчиками прерываний должен быть постоянным. Может возникнуть ситуация, когда компилятор ради достижения своей максимальной производительности начнет оптимизацию кода.
Результатом этого процесса будет сохранение в регистре копии основных переменных кода, что позволит обеспечить максимальную скорость доступа к ним.
Недостатком этого процесса может стать подмена реальных значений сохраненными копиями, что может привести к потере функциональности.
Чтобы этого не произошло нужно использовать переменную voltatile, которая поможет предотвратить ненужные оптимизации. При использовании больших массивов, которым требуются циклы для обновлений, нужно отключить прерывания на момент этих обновлений.
Смотрите также
Adruino имеет несколько типов прерываний.
Прерывание – это процесс, с помощью которого arduino останавливает свою обычную задачу или прекращает цикл и переходит к функции прерывания, чтобы выполнить заданную задачу функции прерывания.
Внешнее прерывание создается извне.В arduino uno есть только два внешних вывода прерывания. Это цифровой вывод 2 и цифровой вывод 3.
Прерывания – очень важный механизм Arduino, позволяющий внешним устройствам взаимодействовать с контроллером при возникновении разных событий. Установив обработчик аппаратных прерываний в скетче, мы сможем реагировать на включение или выключение кнопки, нажатие клавиатуры, мышки, тики таймера RTC, получение новых данных по UART, I2C или SPI. В этой статье мы узнаем, как работают прерывания на платах Ардуино Uno, Mega или Nano и приведем пример использования функции Arduino attachInterrupt().
Прерывание – это сигнал, который сообщает процессору о наступлении какого-либо события, которое требует незамедлительного внимания.
Процессор должен отреагировать на этот сигнал, прервав выполнение текущих инструкций и передав управление обработчику прерывания (ISR, Interrupt Service Routine).
Обработчик – это обычная функция, которую мы пишем сами и помещаем туда тот код, который должен отреагировать на событие.
После обслуживания прерывания ISR функция завершает свою работу и процессор с удовольствием возвращается к прерванным занятиям – продолжает выполнять код с того места, в котором остановился.
Все это происходит автоматически, поэтому наша задача заключается только в том, чтобы написать обработчик прерывания, ничего при этом не сломав и не заставляя процессор слишком часто отвлекаться на нас.
Понадобится понимание схемы, принципов работы подключаемых устройств и представление о том, как часто может вызываться прерывание, каковы особенности его возникновения.
Все это и составляет основную сложность работы с прерываниями.
Аппаратные и программные прерывания
Прерывания в Ардуино можно разделить на несколько видов:
- Аппаратные прерывания. Прерывание на уровне микропроцессорной архитектуры. Самое событие может произойти в производительный момент от внешнего устройства – например, нажатие кнопки на клавиатуре, движение компьютерной мыши и т.п.
- Программные прерывания. Запускаются внутри программы с помощью специальной инструкции. Используются для того, чтобы вызвать обработчик прерываний.
- Внутренние (синхронные) прерывания. Внутреннее прерывание возникает в результате изменения или нарушения в исполнении программы (например, при обращении к недопустимому адресу, недопустимый код операции и другие).
Зачем нужны аппаратные прерывания
Аппаратные прерывания возникают в ответ на внешнее событие и исходят от внешнего аппаратного устройства. В Ардуино представлены 4 типа аппаратных прерываний. Все они различаются сигналом на контакте прерывания:
- Контакт притянут к земле. Обработчик прерывания исполняется до тех пор, пока на пине прерывания будет сигнал LOW.
- Изменение сигнала на контакте. В таком случае Ардуино выполняет обработчик прерывания, когда на пине прерывания происходит изменение сигнала.
- Изменение сигнала от LOW к HIGH на контакте – при изменении с низкого сигнала на высокий будет исполняться обработчик прерывания.
- Изменение сигнала от HIGH к LOW на контакте – при изменении с высокого сигнала на низкий будет исполняться обработчик прерывания.
Прерывания полезны в программах Ардуино, так как помогают решать проблемы синхронизации.
Например, при работе с UART прерывания позволяют не отслеживать поступление каждого символа. Внешнее аппаратное устройство подает сигнал прерывания, процессор сразу же вызывает обработчик прерывания, который вовремя захватывает символ. Это позволяет экономить процессорное время, которое без прерываний тратилось бы на проверку статуса UART, вместо этого все необходимые действия выполняются обработчиком прерывания, не затрагивая главную программу. Особых возможностей от аппаратного устройства не требуется.
Основными причинами, по которым необходимо вызвать прерывание, являются:
- Определение изменения состояния вывода;
- Прерывание по таймеру;
- Прерывания данных по SPI, I2C, USART;
- Аналогово-цифровое преобразование;
- Готовность использовать EEPROM, флеш-память.
Как реализуются прерывания в Ардуино
При поступлении сигнала прерывания работа в цикле loop() приостанавливается. Начинается выполнение функции, которая объявляется на выполнение при прерывании. Объявленная функция не может принимать входные значения и возвращать значения при завершении работы. На сам код в основном цикле программы прерывание не влияет. Для работы с прерываниями в Ардуино используется стандартная функция attachInterrupt().
Отличие реализации прерываний в разных платах Ардуино
В зависимости от аппаратной реализации конкретной модели микроконтроллера есть несколько прерываний.
Плата Arduino Uno имеет 2 прерывания на втором и третьем пине, но если требуется более двух выходов, плата поддерживает специальный режим «pin-change».
Этот режим работает по изменению входа для всех пинов. Отличие режима прерывания по изменению входа заключается в том, что прерывания могут генерироваться на любом из восьми контактов.
Обработка в таком случае будет сложнее и дольше, так как придется отслеживать последнее состояние на каждом из контактов.
На других платах число прерываний выше. Например, плата Ардуино Мега 2560 имеет 6 пинов, которые могут обрабатывать внешние прерывания.
Для всех плат Ардуино при работе с функцией attachInterrupt (interrupt, function, mode) аргумент Inerrupt 0 связан с цифровым пином 2.
Плата | int.0 | int.1 | int.2 | int.3 | int.4 | int.5 |
Uno, Ethernet | 2 | 3 | ||||
Mega2560 | 2 | 3 | 21 | 20 | 19 | 18 |
Leonardo | 3 | 2 | 0 | 1 | 7 | |
Due | (см. ниже) |
Прерывания в языке Arduino
Теперь давайте перейдем к практике и поговорим о том, как использовать прерывания в своих проектах.
Синтаксис attachInterrupt()
attachInterrupt(interrupt, function, mode)
attachInterrupt(pin, function, mode) (только для Arduino Due)
Плата Arduino DUE поддерживает прерывания на всех линиях ввода-вывода.
Для нее можно не использовать функцию digitalPinToInterrupt и указывать номер вывода прямо в параметрах attachInterrupt.
Функция attachInterrupt используется для работы с прерываниями. Она служит для соединения внешнего прерывания с обработчиком.
Синтаксис вызова: attachInterrupt(interrupt, function, mode)
Аргументы функции:
- interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
- function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
- mode – условие срабатывания прерывания.
Возможна установка следующих вариантов условий срабатывания:
- LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
- CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
- RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
- FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4
В Arduino Due доступно еще одно значение:
- HIGH — прерывание будет срабатывать всякий раз, когда на выводе присутствует высокий уровень сигнала (только для Arduino Due).
Каждый новый вызов функции attachInterrupt привязывает новый обработчик прерывания, то есть если ранее была привязана другая функция-обработчик, то она уже не будет вызываться.
Аналогичная ситуация с параметром mode, определяющим тип событий, на которые должен реагировать микроконтроллер.
Другими словами нельзя, например, задать отдельные обработчики для FALLING и RISING для одного входа внешнего прерывания.
Важные замечания
При работе с прерываниями нужно обязательно учитывать следующие важные ограничения:
- Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
- Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
- Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит, ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
- В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).
Прерывания по кнопке
Начнем с простого примера: использования прерывания для отслеживания нажатия кнопки. Для начала, мы возьмем скетч, который вы, вероятно, уже видели: пример «Button», включенный в Arduino IDE (вы можете найти его в каталоге «Примеры», проверьте меню Файл → Примеры → 02. Digital → Button).
const int buttonPin = 2; // номер вывода с кнопкой
const int ledPin = 13; // номер вывода со светодиодом int
buttonState = 0; // переменная для чтения состояния кнопки
void setup()
{
// настроить вывод светодиода на выход:
pinMode(ledPin, OUTPUT);
// настроить вывод кнопки на вход:
pinMode(buttonPin, INPUT);
}
void loop()
{
// считать состояние кнопки:
buttonState = digitalRead(buttonPin);
// проверить нажата ли кнопка.
// если нажата, то buttonState равно HIGH:
if (buttonState == HIGH) { // включить светодиод: digitalWrite(ledPin, HIGH); }
else { // погасить светодиод: digitalWrite(ledPin, LOW); }
}
В том, что вы видите здесь, нет ничего шокирующего и удивительного: всё, что программа делает снова и снова, это прохождение через цикл loop()
и чтение значения buttonPin
.
Предположим на секунду, что вы хотели бы сделать в loop()
что-то еще, что-то большее, чем просто чтение состояния вывода.
Вот здесь и пригодится прерывание. Вместо того, чтобы постоянно наблюдать за состоянием вывода, мы можем поручить эту работу прерыванию и освободить loop() для выполнения в это время того, что нам необходимо! Новый код будет выглядеть следующим образом:
const int buttonPin = 2; // номер вывода с кнопкой
const int ledPin = 13; // номер вывода со светодиодом
volatile int buttonState = 0; // переменная для чтения состояния кнопки
void setup()
{
// настроить вывод светодиода на выход:
pinMode(ledPin, OUTPUT); // настроить вывод кнопки на вход: pinMode(buttonPin, INPUT); // прикрепить прерывание к вектору
ISR attachInterrupt(0, pin_ISR, CHANGE);
}
void loop()
{
// Здесь ничего нет!
}
void pin_ISR()
{
buttonState = digitalRead(buttonPin);
digitalWrite(ledPin, buttonState);
}
Циклы и режимы прерываний
Здесь вы заметите несколько изменений.
Первым и самым очевидным из них является то, что loop()
теперь не содержит никаких инструкций!
Мы можем обойтись без них, так как вся работа, которая ранее выполнялась в операторе if/else
, теперь выполняется в новой функции pin_ISR()
.
Этот тип функций называется обработчиком прерывания: его работа состоит в том, чтобы быстро запуститься, обработать прерывание и позволить процессору вернуться обратно к основной программе (то есть к содержимому loop()
).
При написании обработчика прерывания следует учитывать несколько важных моментов, отражение которых вы можете увидеть в приведенном выше коде:
- обработчики должны быть короткими и лаконичными. Вы ведь не хотите прерывать основной цикл надолго!
- у обработчиков нет входных параметров и возвращаемых значений. Все изменения должны быть выполнены на глобальных переменных.
Вам, наверное, интересно: откуда мы знаем, когда запустится прерывание? Что его вызывает?
Третья функция, вызываемая в функции setup()
, устанавливает прерывание для всей системы.
Данная функция, attachInterrupt()
, принимает три аргумента:
- вектор прерывания, который определяет, какой вывод может генерировать прерывание. Это не сам номер вывода, а ссылка на место в памяти, за которым процессор Arduino должен наблюдать, чтобы увидеть, не произошло ли прерывание. Данное пространство в этом векторе соответствует конкретному внешнему выводу, и не все выводы могут генерировать прерывание! На Arduino Uno генерировать прерывания могут выводы 2 и 3 с векторами прерываний 0 и 1, соответственно. Для получения списка выводов, которые могут генерировать прерывания, смотрите документацию на функцию
attachInterrupt
для Arduino; - имя функции обработчика прерывания: определяет код, который будет запущен при совпадении условия срабатывания прерывания;
- режим прерывания, который определяет, какое действие на выводе вызывает прерывание. Arduino Uno поддерживает четыре режима прерывания:
RISING
– активирует прерывание по переднему фронту на выводе прерывания;FALLING
– активирует прерывание по спаду;CHANGE
– реагирует на любое изменение значения вывода прерывания;LOW
– вызывает всякий раз, когда на выводе низкий уровень.
И резюмируя, наша настройка attachInterrupt()
соответствует отслеживанию вектора прерывания 0 (вывод 2), чтобы отреагировать на прерывание с помощью pin_ISR()
, и вызвать pin_ISR()
всякий раз, когда произойдет изменение состояния на выводе 2.
Volatile
Еще один момент, на который стоит указать: наш обработчик прерывания использует переменную buttonState
для хранения состояния вывода.
Проверьте определение buttonState
: вместо типа int
, мы определили его, как тип volatile int
.
В чем же здесь дело? volatile
является ключевым словом языка C, которое применяется к переменным.
Оно означает, что значение переменной находится не под полным контролем программы.
То есть значение buttonState
может измениться и измениться на что-то, что сама программа не может предсказать – в этом случае, пользовательский ввод.
Еще одна полезная вещь в ключевом слове volatile
заключается в защите от любой случайной оптимизации.
Компиляторы, как выясняется, выполняют еще несколько дополнительных задач при преобразовании исходного кода программы в машинный исполняемый код.
Одной из этих задач является удаление неиспользуемых в исходном коде переменных из машинного кода.
Так как переменная buttonState
не используется или не вызывается напрямую в функциях loop()
или setup()
, существует риск того, что компилятор может удалить её, как неиспользуемую переменную.
Очевидно, что это неправильно – нам необходима эта переменная! Ключевое слово volatile
обладает побочным эффектом, сообщая компилятору, что эту переменную необходимо оставить в покое.
Удаление неиспользуемых переменных из кода – это функциональная особенность, а не баг компиляторов. Люди иногда оставляют в коде неиспользуемые переменные, которые занимают память.
Это не такая большая проблема, если вы пишете программу на C для компьютера с гигабайтами оперативной памяти. Однако, на Arduino оперативная память ограничена, и вы не хотите тратить её впустую!
Даже C компиляторы для компьютеров будут поступать точно так же, несмотря на массу доступной системной памяти. Зачем? По той же причине, по которой люди убирают за собой после пикника – это хорошая практика, не оставлять после себя мусор.
Подводя итоги
Прерывания – это простой способ заставить вашу систему быстрее реагировать на чувствительные к времени задачи.
Они также обладают дополнительным преимуществом – освобождением главного цикла loop()
, что позволяет сосредоточить в нем выполнение основной задачи системы (я считаю, что использование прерываний, как правило, позволяет сделать мой код немного более организованным: проще увидеть, для чего разработан основной кусок кода, и какие периодические события обрабатываются прерываниями).
Пример, показанный здесь, – это самый базовый случай использования прерываний; вы можете использовать для чтения данных с I2C устройства, беспроводных передачи и приема данных, или даже для запуска или остановки двигателя.
(текст взят с https://radioprog.ru/post/114)
Разберем ещё пример работы с внешними прерываниями с использованием описанных функций:
#define ledPin 13 #define interruptPin 2 // Кнопка между цифровым пином 2 и GND volatile byte state = LOW; void setup() { pinMode(ledPin, OUTPUT); pinMode(interruptPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(interruptPin), blink, FALLING); } void loop() { digitalWrite(ledPin, state); } void blink() { state = !state; }
В функции setup мы настраиваем тринадцатый пин на вывод, чтобы управлять встроенным светодиодом.
Второй пин подтягиваем к питанию, это обеспечит на нем сигнал высокого уровня.
Далее функцией attachInterrupt задаем функцию-обработчик blink, которая должна вызываться при изменении сигнала на втором пине от высокого уровня к низкому (FALLING).
Внутри функции blink мы изменяем значение переменной state, что впоследствии приводит к включению и выключению светодиода в функции loop.
Таким образом можно управлять светодиодом, не опрашивая кнопку в основной программе.
Загрузите этот скетч в Ардуино и проверьте его работу, добавив кнопку между вторым цифровым выводом и землей (или просто замыкая их проводом).
В целом скетч будет работать как и задумывалось, но вы заметите, что иногда светодиод не реагирует на нажатие кнопки.
Такое поведение вызвано дребезгом контактов кнопки: многократное изменение сигнала на цифровом входе 2 приводит к повторным вызовам обработчика.
Как следствие значение переменной state может остаться не измененным.
К этой проблеме мы вернемся чуть позже, а пока продолжим разбор примера. В нем остался еще один момент, требующий пояснения – ключевое слово volatile.
volatile
volatile – это квалификатор типа переменной, сообщающий компилятору о том, что значение переменной может измениться в любой момент.
Компилятор учитывает этот факт при построении и оптимизации исполняемого кода.
Чтобы не объяснять назначение volatile абстрактно, давайте рассмотрим работу компилятора на следующем фрагменте кода:
byte A = 0; byte B; void loop() { A++; B = A + 1; }
Переменные A и B – это ячейки в памяти микроконтроллера. Для того чтобы микроконтроллер мог что-то сделать с ними (изменить, сравнить и т.п.) их значения должны быть загружены из памяти во внутренние регистры. Поэтому при компиляции данного фрагмента будет сгенерирован код вида:
- Загрузить из памяти значение A в регистр Р1
- Загрузить в регистр Р2 константу 1
- Сложить значение Р2 с Р1 (результат в Р2)
- Сохранить значение регистра Р2 в памяти по адресу A
- Сложить содержимое регистра Р1 с константой 2
- Сохранить значение регистра Р1 в памяти по адресу B
Считывание значения переменной А в регистр происходит в самом начале кода.
Если на одном из приведенных шагов поступит запрос прерывания, при обработке которого значение переменной A (ячейки памяти) будет изменено, то после возвращения в функцию loop микроконтроллер будет работать с ее неактуальным значением, оставшимся в регистре.
Использование квалификатора volatile как раз позволяет избежать подобных ситуаций.
При обращении к переменной, объявленной как volatile, микроконтроллер всегда будет считывать ее актуальное значение из памяти, а не использовать считанное ранее (то есть такой код будет генерировать компилятор).
Конечно, не всегда отсутствие volatile приводит к ошибке, всё зависит от логики программы.
Так приведенный выше пример использования прерываний для управления светодиодом будет работать и без volatile.
Но в других случаях использование этого квалификатора поможет избежать трудновыявляемых ошибок.
Поэтому лучше просто взять за правило: всегда использовать volatile при объявлении переменных, которые обработчик прерывания использует совместно с другими функциями.
Разберем еще один интересный пример:
#define interruptPin 2 volatile byte f = 0; void setup() { pinMode(interruptPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(interruptPin), buttonPressed, FALLING); } void loop() { while (f == 0) { // Что-то делаем в ожидании нажатия кнопки } // Кнопка нажата } void buttonPressed() { f = 1; }
Цикл внутри функции loop должен выполняться до тех пор, пока значение переменной f равно нулю.
А измениться оно должно в обработчике прерывания при нажатии на кнопку.
Если бы мы объявили переменную f без квалификатора volatile, то компилятор, “видя”, что значение переменной внутри цикла не изменяется и условие выхода из цикла остается ложным, заменил бы цикл на бесконечный.
Так работает оптимизация кода при компиляции. Войдя в такой цикл микроконтроллер просто зависнет.
Объявление переменной с квалификатором volatile гарантирует, что эта переменная не получит какой-либо оптимизированный тип доступа.
При работе с прерываниями и совместном использовании переменных обработчиком и основной программой нужно помнить очень важный момент: AVR микроконтроллеры являются 8-битными и для загрузки 16- или 32-разрядного значения из памяти требуется несколько отдельных операций.
Соответственно, возможна ситуация, когда микроконтроллер считает из памяти младший байт переменной, после чего поступит запрос прерывания и значение данной переменной будет изменено обработчиком.
После возврата в основную программу микроконтроллер считает из памяти старший байт, получив при этом не просто старое значение, а совершенно другое, что может привести к ошибке работы программы.
Для исключения подобных ситуаций можно либо запретить обработку прерываний на время обращения к разделяемым переменным при помощи функций interruptsи noInterrupts, либо поместить обращение к такой переменной в атомарно исполняемый блок кода.
Функции interrupts и noInterrupts
Данные функции служат для разрешения и запрета обработки прерываний соответственно.
Они могут быть полезны при обращении к переменной, значение которой изменяется обработчиком прерывания (описанная выше ситуация).
Или если код чувствителен к времени выполнения и потому должен выполняться без прерываний.
В таком случае код должен быть обрамлен указанными функциями:
noInterrupts(); // Запрещаем обработку прерываний // Критичный к времени выполнения код interrupts(); // Разрешаем обработку прерываний
Только учтите, что на прерываниях реализована работа многих модулей: таймеры-счетчики, UART, I2C, SPI, АЦП и другие.
Запретив обработку прерываний, вы не сможете, например, использовать класс Serial или функции millis, micros.
Поэтому избегайте длительной блокировки прерываний.
Кроме функций interrupts и noInterrupts для этих же целей можно использовать функции sei и cli – разрешить и запретить прерывания соответственно.
Разницы между ними нет, просто последние являются частью набора библиотек AVR Libc, другие же введены разработчиками IDE Arduino в качестве альтернативы, более легкой для запоминания и восприятия в программе. Согласитесь, noInterrupts более говорящее название, чем cli.