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

Вообще говоря, Arduino не поддерживает настоящее распараллеливание задач, или мультипоточность. Но можно при каждом повторении цикла loop() указать микроконтроллеру проверять, не наступило ли время выполнить некую дополнительную, фоновую задачу. При этом пользователю будет казаться, что несколько задач выполняются одновременно.

Например, давайте будем мигать светодиодом с заданной частотой и параллельно этому издавать нарастающие и затихающие подобно сирене звуки из пьезоизлучателя. И светодиод, и пьезоизлучатель мы уже не раз подключали к Arduino. Соберём схему, как показано на рисунке.

Если вы подключаете светодиод к цифровому выводу, отличному от "13", не забывайте о токоограничивающем резисторе примерно на 220 Ом.

2 Управление светодиодом и пьезоизлучателем с помощью оператора delay()

Напишем вот такой скетч и загрузим его в Ардуино.

Const int soundPin = 3; /* объявляем переменную с номером пина, на который подключён пьезоэлемент */ const int ledPin = 13; // объявляем переменную с номером пина светодиода void setup() { pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. } void loop() { // Управление звуком: tone(soundPin, 700); // издаём звук на частоте 700 Гц delay(200); tone(soundPin, 500); // на частоте 500 Гц delay(200); tone(soundPin, 300); // на частоте 300 Гц delay(200); tone(soundPin, 200); // на частоте 200 Гц delay(200); // Управление светодиодом: digitalWrite(ledPin, HIGH); // зажигаем delay(200); digitalWrite(ledPin, LOW); // гасим delay(200); }

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

Дело в том, что обычным образом эту задачу не решить. Задачи выполняются микроконтроллером строго последовательно. Оператор delay() задерживает выполнение программы на указанный промежуток времени, и пока это время не истечёт, следующие команды программы не будут выполняться. Из-за этого мы не можем задать разную длительность выполнения для каждой задачи в цикле loop() программы. Поэтому нужно как-то сымитировать многозадачность.

3 Параллельные процессы без оператора "delay()"

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

Const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода const long ledInterval = 200; // интервал мигания светодиодом, мсек. int ledState = LOW; // начальное состояние светодиода unsigned long previousMillis = 0; // храним время предыдущего срабатывания светодиода void setup() { pinMode(soundPin, OUTPUT); // задаём пин 3 как выход. pinMode(ledPin, OUTPUT); // задаём пин 13 как выход. } void loop() { // Управление звуком: tone(soundPin, 700); delay(200); tone(soundPin, 500); delay(200); tone(soundPin, 300); delay(200); tone(soundPin, 200); delay(200); // Мигание светодиодом: // время с момента включения Arduino, мсек: unsigned long currentMillis = millis(); // Если время мигать пришло, if (currentMillis - previousMillis >= ledInterval) { previousMillis = currentMillis; // то запоминаем текущее время if (ledState == LOW) { // и инвертируем состояние светодиода ledState = HIGH; } else { ledState = LOW; } digitalWrite(ledPin, ledState); // переключаем состояние светодиода } }

Существенным недостатком данного метода является то, что участок кода перед блоком управления светодиодом должен выполняться быстрее, чем интервал времени мигания светодиода "ledInterval". В противном случае мигание будет происходить реже, чем нужно, и эффекта параллельного выполнения задач мы не получим. В частности, в нашем скетче длительность изменения звука сирены составляет 200+200+200+200 = 800 мсек, а интервал мигания светодиодом мы задали 200 мсек. Но светодиод будет мигать с периодом 800 мсек, что в 4 раза больше того, что мы задали.

Вообще, если в коде используется оператор delay() , в таком случае трудно сымитировать псевдо-параллельность, поэтому желательно его избегать.

В данном случае нужно было бы для блока управления звуком сирены также проверять, пришло время или нет, а не использовать delay() . Но это бы увеличило количество кода и ухудшило читаемость программы.

4 Использование библиотеки ArduinoThread для создания параллельных потоков

Чтобы решить поставленную задачу, воспользуемся замечательной библиотекой ArduinoThread , которая позволяет с лёгкостью создавать псевдо-параллельные процессы. Она работает похожим образом, но позволяет не писать код по проверке времени - нужно выполнять задачу в этом цикле или не нужно. Благодаря этому сокращается объём кода и улучшается читаемость скетча. Давайте проверим библиотеку в действии.


Первым делом скачаем с официального сайта архив библиотеки и разархивируем его в директорию libraries/ среды разработки Arduino IDE. Затем переименуем папку ArduinoThread-master в ArduinoThread .

Схема подключений останется прежней. Изменится лишь код программы.

#include // подключение библиотеки ArduinoThread const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода Thread ledThread = Thread(); // создаём поток управления светодиодом Thread soundThread = Thread(); // создаём поток управления сиреной void setup() { pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. ledThread.onRun(ledBlink); // назначаем потоку задачу ledThread.setInterval(1000); // задаём интервал срабатывания, мсек soundThread.onRun(sound); // назначаем потоку задачу soundThread.setInterval(20); // задаём интервал срабатывания, мсек } void loop() { // Проверим, пришло ли время переключиться светодиоду: if (ledThread.shouldRun()) ledThread.run(); // запускаем поток // Проверим, пришло ли время сменить тональность сирены: if (soundThread.shouldRun()) soundThread.run(); // запускаем поток } // Поток светодиода: void ledBlink() { static bool ledStatus = false; // состояние светодиода Вкл/Выкл ledStatus = !ledStatus; // инвертируем состояние digitalWrite(ledPin, ledStatus); // включаем/выключаем светодиод } // Поток сирены: void sound() { static int ton = 100; // тональность звука, Гц tone(soundPin, ton); // включаем сирену на "ton" Гц if (ton }

В программе мы создаём два потока - ledThread и soundThread , каждый выполняет свою операцию: один мигает светодиодом, второй управляет звуком сирены. В каждой итерации цикла для каждого потока проверяем, пришло ли время его выполнения или нет. Если пришло - он запускается на исполнение с помощью метода run() . Главное - не использовать оператор delay() . В коде даны более подробные пояснения.


Загрузим код в память Ардуино, запустим. Теперь всё работает в точности так, как надо!

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

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

Параллельное подключение устройств к шине SPI

При параллельном подключении несколько ведомых устройств используют общие провода SCLK , MOSI и MISO , при этом каждый ведомый имеет свою линию SS . Ведущий определяет устройство, с которым осуществляется обмен , путем формирования низкого сигнала на его SS .
Видно, что для подключения n устройств требуется n линий SS , то есть для функционирования SPI-среды с n ведомыми нужно выделить под это n+3 ноги микроконтроллера.

Последовательное подключение устройств к шине SPI

При последовательном подключении устройств они используют общие провода SCLK и SS , а выход одного подсоединяется во вход другого. MOSI ведущего подключается к первому устройству, а MISO - к последнему. То есть для ведущего на шине SPI это как бы одно устройство.
Такое подключение позволяет построить, например, из двух 8-битных сдвиговых регистров один 16-битный, чем мы сейчас и займемся.
Остается отметить прелесть такого подключения: подключи хоть 3, хоть 8 устройств, это займет всего 4 ноги на контроллере.

Последовательное соединение двух сдвиговых регистров
Еще раз взглняем на сдвиговый регистр 74HC595:

Мы помним, что DS - есть пин последовательного ввода, а Q0-Q7 пины последовательного вывода. Q7S , который мы не использовали, когда у нас был всего один регистр в схеме, - это последовательный вывод регистра. Он находит свое применение, когда мы передаем больше 1 байта в регистры. Через этот пин последовательно протолкнутся все байты, предназначенные для последующих регистров, а последний передаваемый байт останется в первом регистре.


Подсоединяя пин Q7S одного первого регистра к пину DS второго (и так далее, если это необходимо), получаем двойной (тройной и т.д.) регистр.

Подключение двойного 7-сегментного дисплея

Двойной 7-семисегментный дисплей это, как правило, устройство с 18-ю ногами, по 9 на каждый символ. Взглянем на схему (мой дисплей имеет маркировку LIN-5622SR и есть большая вероятность того, что его схема подключения окажется уникальна):

Это дисплей с общим анодом, что говорит о необходимости подачи на com1 и com2 высокого уровня ТТЛ, а для зажигания диода - низкий уровень на соответствующей ноге. Если у вас дисплей с общим катодом, делать нужно наоборот!

Подключим дисплей, как показано на схеме:

Левый дисплей подключаем к первому регистру: 1A к ноге Q0, 1B к ноге Q1, 1C к ноге Q2 и т.д. Общий контакт com1 подключаем на землю. Точно так же поступаем с правым дисплеем: 2A к ноге Q0, 2B к ноге Q1, и т.д., общий контакт com2 - на землю.

Схема будет выглядеть не так, как на картинке, если расположение выводов у дисплея отличается от моего, здесь просто нужно быть внимательным при подключении. Если дисплей с общим катодом, то com1 и com2 подключаются на питание!

Простая змейка на двойном 7-сегментном дисплее

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

Наш цикл будет состоять из восьми кадров, на каждом из которых будет зажигаться определенные три светодиода. На первом кадре будут гореть 1E, 1F, 1A (см. схему), на втором - 1F, 1A, 2A, на третьем - 1A, 2A, 2B и так далее, на восьмом - 1D, 1E, 1F.

Снова, для удобства, составим табличку байтов, помня, что по умолчанию биты передаются, начиная со старшего, т.е. 2h.

Кадр

1 abcd efgh

2 abcd efgh

hex

0111 0011

1111 1111

EC FF

0111 1011

0111 1111

ED EF

0111 1111

0011 1111

EF CF

1111 1111

0001 1111

FF 8F

1111 1111

1000 1111

FF 1F

1110 1111

1100 1111

7F 3F

1110 0111

1110 1111

7E 7F

1110 0011

1111 1111

7C FF


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

#include <SPI .h> //подключаем библиотеку SPI
enum { reg = 9 }; //выбираем линию SS регистра на 9-м пине Arduino

void setup () {
SPI .begin (); //инициализируем SPI
//переводим выбранный для передачи пин в режим вывода
pinMode (reg, OUTPUT );
}


void loop () {
//Заполняем массив байтами, которые будем передавать
static uint8_t digit =
{0xFF,0xCE,0xFF,0xDE,0xFC,0xFE,0xF8,0xFF,
0xF1,0xFF,0xF3,0xF7,0xF7,0xE7,0xFF,0xC7 };
//передаем по два байта из массива и защелкиваем регистры
for (int i=0;i<16;i+=2){
digitalWrite (reg, LOW );
SPI .transfer (digit[i]);
SPI .transfer (digit);
digitalWrite (reg, HIGH );
delay (80); //пауза между кадрами
}
}


Видео работы программы:

Параллельные процессы в Arduino

Почему разработчики Arduino уделяют особое внимание примеру Blink without delay ?

Обычно программа Arduino линейна - сначала делает одно, потом другое. В примере выше мы использовали функциюdelay(80) , чтобы каждый кадр рисовался через 80 миллисекунд после предыдущего. Однако ведь эти 80 миллисекунд процессор ничего не делает и никому не дает ничего делать! Для запуска двух и более параллельных процессов нам нужно поменять концепцию построения программы, отказавшись от delay() .

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

В Arduino есть штука, которая отсчитывает время с начала работы программы, называется она millis() . С ее-то помощью и организуется "распараллеливание" задач.

Итоговый проект: часы и хитрая змейка


Соберем такую схему:

Левый и средний регистры у нас работают с точки зрения ведущего как одно устройство, а правый регистр - как другое. Видно, что эти два устройства используют один и тот же провод SCLK (13-й пин Arduino, провод показан оранжевым) и MOSI (11-й пин, желтый цвет), SS используются разные (пины 8 и 9, зеленый цвет). Подключение 7-сегментных дисплеев к регистрам показано для моих конкретных моделей и, вероятно, не будет совпадать с вашим.


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

Последовательность байтов для этой змейки будет такая:

Static uint8_t snake =


Теперь суть: функция millis() сидит и считает миллисекунды от начала начал. В начале каждого цикла loop мы запоминаем значение millis() в переменную timer. Заводим переменные snakeTimerPrev и digitTimerPrev , которые будут хранить в себе момент предыдущего события: для snakeTimerPrev - это включение предыдущего кадра анимации змейки, для digitTimerPrev - включение предыдущей цифры. Как только разница текущего времени (timer ) и предыдущего (snakeTimerPrev или digitTimerPrev ) становится равна заданному периоду (в нашем случае - 80 и 1000 мс, соответственно), мы производим передачу следующего кадра/байта.

Таким образом,

  • каждые 80 мс контроллер будет опускать сигнал на линии SS двойного дисплея, передавать два байта и отпускать линию.
  • каждую секунду контроллер будет опускать сигнал на линии SS одиночного дисплея, передавать один байт и отпускать линию.
Реализуем это на Arduino. Я уже все подробно описывал до этого, думаю, нет смысла комментировать.

#include <SPI .h>

enum { snakePin = 9, digitPin = 8 };
unsigned long timer=0, snakeTimerPrev=0, digitTimerPrev=0;
int i=0, j=0;



void setup () {
SPI.begin();
pinMode(digitPin, OUTPUT );
pinMode(snakePin, OUTPUT );
}


void loop () {
static uint8_t digit =
{0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};
static uint8_t snake =
{0xFF,0x9E,0xFF,0xDC,0xFF,0xF8,0xFF,0xF1,
0xFF,0xE3,0xFF,0xA7,0xBF,0xAF,0xBD,0xBF,
0xBC,0xFF,0xDC,0xFF,0xCE,0xFF,0xC7,0xFF,
0xE3,0xFF,0xB3,0xFF,0xBB,0xBF,0xBF,0x9F};


timer= millis ();


if (timer-snakeTimerPrev>80){
digitalWrite (snakePin, LOW );
SPI.transfer (snake[j]);
SPI.transfer (snake);
digitalWrite (snakePin, HIGH );
j<30 ? j+=2: j=0;
snakeTimerPrev=timer;
}
if (timer-digitTimerPrev>1000){
digitalWrite (digitPin, LOW );
SPI.transfer (digit[i]);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Забавную картинку к этому уроку я найти не смог, нашёл только какую-то лекцию по программированию, и вот самое начало этой лекции отлично объясняет нам, что такое прерывание . Прерывание в Ардуино можно описать абсолютно точно так же: микроконтроллер “всё бросает”, переключается на выполнение блока функций в обработчике прерывания, выполняет их, а затем возвращается ровно к тому месту основного кода, в котором остановился.

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

External hardware interrupt – это прерывание, вызванное изменением напряжения на пине микроконтроллера. Основная суть состоит в том, что микроконтроллер (вычислительное ядро) не занимается опросом пина и не тратит на это время , пином занимается другая “железка”. Как только напряжение на пине изменяется (имеется в виду цифровой сигнал, +5 подали/+5 убрали) – микроконтроллер получает сигнал, бросает все дела, обрабатывает прерывание, и возвращается к работе. Зачем это нужно? Чаще всего прерывания используются для детектирования коротких событий – импульсов, или даже для подсчёта их количества, не нагружая основной код. Аппаратное прерывание может поймать короткое нажатие кнопки или срабатывание датчика во время сложных долгих вычислений или задержек в коде, т.е. грубо говоря – пин опрашивается параллельно основному коду . Также прерывания могут будить микроконтроллер из режимов энергосбережения, когда вообще практически вся периферия отключена. Посмотрим, как работать с аппаратными прерываниями в среде Arduino IDE.

Прерывания в Arduino

Начнём с того, что не все пины “могут” в прерывания. Да, есть такая штука, как pinChangeInterrupts , но о ней мы поговорим в продвинутых уроках. Сейчас нужно понять, что аппаратные прерывания могут генерировать только определённые пины:

МК / номер прерывания 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 на Ардуино нано, мы получим 1. Всё по таблице выше, функция для ленивых.

Подключается прерывание при помощи функции attachInterrupt(pin, handler, mode) :

  • pin – номер прерывания
  • handler – имя функции-обработчика прерывания (её нужно создать самому)
  • mode – “режим” работы прерывания:
    • LOW (низкий) – срабатывает при сигнале LOW на пине
    • RISING (рост) – срабатывает при изменении сигнала на пине с LOW на HIGH
    • FALLING (падение) – срабатывает при изменении сигнала на пине с HIGH на LOW
    • CHANGE (изменение) – срабатывает при изменении сигнала (с LOW на HIGH и наоборот)

Также прерывание можно отключить при помощи функции detachInterrupt(pin) , где pin – опять же номер прерывания .

А ещё можно глобально запретить прерывания функцией noInterrupts() и снова разрешить их при помощи interrupts() . Аккуратнее с ними! noInterrupts() остановит также прерывания таймеров, и у вас “сломаются” все функции времени и генерация ШИМ.

Давайте рассмотрим пример, в котором в прерывании считаются нажатия кнопки, а в основном цикле они выводятся с задержкой в 1 секунду. Работая с кнопкой в обычном режиме, совместить такой грубый вывод с задержкой – невозможно:

Volatile int counter = 0; // переменная-счётчик void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); \ // D2 это прерывание 0 // обработчик - функция buttonTick // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим attachInterrupt(0, buttonTick, FALLING); } void buttonTick() { counter++; // + нажатие } void loop() { Serial.println(counter); // выводим delay(1000); // ждём }

Итак, наш код считает нажатия даже во время задержки! Здорово. Но что такое volatile ? Мы объявили глобальную переменную counter , которая будет хранить количество нажатий на кнопку. Если значение переменной будет изменяться в прерывании, нужно сообщить об этом микроконтроллеру при помощи спецификатора volatile , который пишется перед указанием типа данных переменной, иначе работа будет некорректной. Это просто нужно запомнить: если переменная меняется в прерывании – делайте её volatile .

Ещё несколько важных моментов:

  • Переменные, изменяемые в прерывании, должны быть объявлены как volatile
  • В прерывании не работают задержки типа delay()
  • В прерывании не меняет своё значение millis() и micros()
  • В прерывании некорректно работает вывод в порт (Serial.print() ), также не стоит там его использовать – это нагружает ядро
  • В прерывании нужно стараться делать как можно меньше вычислений и вообще “долгих” действий – это будет тормозить работу МК при частых прерываниях! Что же делать? Читайте ниже.

Если прерывание отлавливает какое-то событие, которое необязательно обрабатывать сразу, то лучше использовать следующий алгоритм работы с прерыванием:

  • В обработчике прерывания просто поднимаем флаг
  • В основном цикле программы проверяем флаг, если поднят – сбрасываем его и выполняем нужные действия
volatile boolean intFlag = false; // флаг void setup() { Serial.begin(9600); // открыли порт для связи // подключили кнопку на D2 и GND pinMode(2, INPUT_PULLUP); // D2 это прерывание 0 // обработчик - функция buttonTick // FALLING - при нажатии на кнопку будет сигнал 0, его и ловим attachInterrupt(0, buttonTick, FALLING); } void buttonTick() { intFlag = true; // подняли флаг прерывания } void loop() { if (intFlag) { intFlag = false; // сбрасываем // совершаем какие-то действия Serial.println("Interrupt!"); } }

Это в принципе всё, что нужно знать о прерываниях, более конкретные случаи мы разберём в продвинутых уроках.

Видео

Здравствуйте, Андрей. Весьма интересен ваш подход к передаче багажа знаний и опыта, вами накопленного. Очень помогает в начинаниях. Ну и я, начиная осваивать arduino имею желание прогрессировать. Тем более, что с посторонней помощью у меня это получается быстрее. Итак: сперва моя задача была сделать робота, едущего по линии. Сделал — все отлично. Но дальше, снабжая его дополнительными опциями, не понимал — почему он перестал корректно реагировать на линию. Набрел на эту статью и понял причину.

Теперь у меня к вам вопрос: в ниже указанном и готовом скетче, учитывая проблемы с delay, нужно ли мне везде, где присутствует эта функция перейти на millis? Если так, то я понимаю, что скетч придется переделывать почти весь? И не совсем понятно, как использовать millis в замере расстояния? Спасибо.

//Робот с функцией следования по белой полосе

// **********************Установка выводов моторов ************************

int MotorLeftSpeed = 5; // Левый (А) мотор СКОРОСТЬ - ENA

int MotorLeftForward = 4; // Левый (А) мотор ВПЕРЕД - IN1

int MotorLeftBack = 3; // Левый (А) мотор НАЗАД - IN2

int MotorRightForward = 8; // Правый (В) мотор ВПЕРЕД - IN3

int MotorRightBack = 7; // Правый (В) мотор НАЗАД - IN4

int MotorRightSpeed = 9; // Правый (В) мотор СКОРОСТЬ - ENB

// **********************Установка выводов УЗ датчиков***********************

int trigPinL = 14; // задание номера вывода левого trig УЗ датчика

int echoPinL = 15; // задание номера вывода левого echo УЗ датчика

int trigPinC = 10; // задание номера вывода центрального trig УЗ датчика

int echoPinC = 11; // задание номера вывода центрального echo УЗ датчика

int trigPinR = 12; // задание номера вывода правого trig УЗ датчика

int echoPinR = 13; // задание номера вывода правого echo УЗ датчика

// ********************* Установка выводов датчиков линии *******************

const int LineSensorLeft = 19; // вход левого датчика линии

const int LineSensorRight = 18; // вход правого датчика линии

int SL; // статус левого сенсора

int SR; // статус правого сенсора

// *********************Установка вывода световой и звуковой сигнализации**************

int Light = 2; // задание номера вывода световой сигнализации

int Zumm = 6; // задание номера вывода зуммера

int ledState = LOW; // этой переменной устанавливаем состояние светодиода

long previousMillis = 0; // храним время последнего переключения светодиода

long interval = 300; // интервал между включение/выключением светодиода (0,3 секунды)

// *********************Переменная измерение дистанции датчиками*************

unsigned int impulseTimeL=0;

unsigned int impulseTimeC=0;

unsigned int impulseTimeR=0;

long distL=0; // дистанция, измеренная левым УЗ датчиком

long distC=0; // дистанция, измеренная центральным УЗ датчиком

long distR=0; // дистанция, измеренная правым Уз датчиком

// *********************************** SETUP ********************************

Serial.begin (9600); // запускаем серийный порт (скорость 9600)

//*************** Задаем контакты моторов****************

pinMode (MotorRightBack, OUTPUT); // Правый (В) мотор НАЗАД

pinMode (MotorRightForward, OUTPUT); // Правый (В) мотор ВПЕРЕД

pinMode (MotorLeftBack, OUTPUT); // Левый (А) мотор НАЗАД

pinMode (MotorLeftForward, OUTPUT); // Левый (А) мотор ВПЕРЕД

delay (duration);

//*************** Задаем контакты датчиков полосы**************

pinMode (LineSensorLeft, INPUT); // определением pin левого датчика линии

pinMode (LineSensorRight, INPUT); // определением pin правого датчика линии

// ***************Задание режимов выводов УЗ датчиков**********************

pinMode (trigPinL, OUTPUT); // задание режима работы вывода левого trig УЗ датчика

pinMode (echoPinL, INPUT); // задание режима работы вывода левого echo УЗ датчика

pinMode (trigPinC, OUTPUT); // задание режима работы вывода центрального trig УЗ датчика

pinMode (echoPinC, INPUT); // задание режима работы вывода центрального echo УЗ датчика

pinMode (trigPinR, OUTPUT); // задание режима работы вывода правого trig УЗ датчика

pinMode (echoPinR, INPUT); // задание режима работы вывода правого echo УЗ датчика

// ***************Задаем контакты световой и звуковой сигнализации********************************

pinMode (Zumm,OUTPUT); // задание режима работы вывода зуммера

pinMode (Light,OUTPUT); // задание режима работы вывода световой сигнализации

// ****************** Основные команды движения ******************

void forward (int a, int sa) // ВПЕРЕД

analogWrite (MotorRightSpeed, sa);

analogWrite (MotorLeftSpeed, sa);

void right (int b, int sb) // ПОВОРОТ ВПРАВО (одна сторона)

digitalWrite (MotorRightBack, LOW);

digitalWrite (MotorLeftBack, LOW);

digitalWrite (MotorLeftForward, HIGH);

analogWrite (MotorLeftSpeed, sb);

void left (int k, int sk) // ПОВОРОТ ВЛЕВО (одна сторона)

digitalWrite (MotorRightBack, LOW);

digitalWrite (MotorRightForward, HIGH);

analogWrite (MotorRightSpeed, sk);

digitalWrite (MotorLeftBack, LOW);

void stopp (int f) // СТОП

digitalWrite (MotorRightBack, LOW);

digitalWrite (MotorRightForward, LOW);

digitalWrite (MotorLeftBack, LOW);

digitalWrite (MotorLeftForward, LOW);

// **************************Измерение дистанции*********************

void izmdistL () // измерение дистанции левым УЗ датчиком

digitalWrite (trigPinL, HIGH);

digitalWrite (trigPinL, LOW); // импульс 10мС на вывод trig УЗ датчика для измерения расстояния

impulseTimeL = pulseIn (echoPinL, HIGH); // считывание расстояния с УЗ датчика

distL=impulseTimeL/58; // Пересчитываем в сантиметры

void izmdistC () // измерение дистанции центральным УЗ датчиком

digitalWrite (trigPinC, HIGH);

digitalWrite (trigPinC, LOW); // импульс 10мС на вывод trig УЗ датчика для измерения расстояния

impulseTimeC = pulseIn (echoPinC, HIGH); // считывание расстояния с УЗ датчика

distC=impulseTimeC/58; // Пересчитываем в сантиметры

void izmdistR () // измерение дистанции центральным УЗ датчиком

digitalWrite (trigPinR, HIGH);

digitalWrite (trigPinR, LOW); // импульс 10мС на вывод trig УЗ датчика для измерения расстояния

impulseTimeR = pulseIn (echoPinR, HIGH); // считывание расстояния с УЗ датчика

distR=impulseTimeR/58; // Пересчитываем в сантиметры

// *********************************** LOOP *********************************

// ********************** Режим следования по ЛИНИИ *************************

// *********************световая и звуковая сигнализация**************

tone (Zumm,900); // включаем звук на 900 Гц

tone (Zumm,900); // включаем звук на 800 Гц

unsigned long currentMillis = millis ();

if (currentMillis — previousMillis > interval) //проверяем не прошел ли нужный интервал, если прошел то

previousMillis = currentMillis; // сохраняем время последнего переключения

if (ledState == LOW) // если светодиод не горит, то зажигаем, и наоборот

ledState = HIGH;

digitalWrite (Light, ledState); // устанавливаем состояния выхода, чтобы включить или выключить светодиод

// ************************ Измерение дистанции************************

Serial.println (distL);

Serial.println (distC);

Serial.println (distR);

if (distL>50 && distC>50 && distR>50) // если измеренная дистанция больше 50 сантиметров — едем

SL = digitalRead (LineSensorLeft); // считываем сигнал с левого датчика полосы

SR = digitalRead (LineSensorRight); // считываем сигнал с правого датчика полосы

// ************************* Следование по черной линии ***********************

// РОБОТ на полосе - едем прямо

if (SL == LOW & SR == LOW) // БЕЛЫЙ - БЕЛЫЙ - едем ПРЯМО

forward (10, 100);// ПРЯМО (время, скорость)

// РОБОТ начинает смещаться с полосы - подруливаем

else if (SL == LOW & SR == HIGH) // ЧЕРНЫЙ — БЕЛЫЙ - поворот ВЛЕВО

left (10, 100);// поворот ВЛЕВО (время, скорость)

else if (SL == HIGH & SR == LOW) // БЕЛЫЙ — ЧЕРНЫЙ - поворот ВПРАВО

right (10, 100);// поворот ВПРАВО (время, скорость)

// ФИНИШ — РОБОТ видит обоими датчиками полосу

else if (SL == HIGH & SR == HIGH) // ЧЕРНЫЙ — ЧЕРНЫЙ — СТОП

stopp (50);// СТОП

else // если измеренная дистанция меньше или равна минимальной — стоим