Arduino: Параллельное и последовательное подключение ведомых устройств к шине SPI. Прерывания Arduino с помощью attachInterrupt Ардуино параллельные процессы

Здравствуйте, Андрей. Весьма интересен ваш подход к передаче багажа знаний и опыта, вами накопленного. Очень помогает в начинаниях. Ну и я, начиная осваивать 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 // если измеренная дистанция меньше или равна минимальной — стоим

Инструкция

Вообще говоря, Arduino не поддерживает настоящее распараллеливание задач, или мультипоточность.
Но можно при каждом повторении цикла "loop()" указать проверять, не наступило ли время выполнить некую дополнительную, фоновую задачу. При этом пользователю будет казаться, что несколько задач выполняются одновременно.
Например, давайте будем мигать с заданной частотой и параллельно этому издавать нарастающие и затихающие подобно сирене звуки из пьезоизлучателя.
И светодиод, и мы уже не раз подключали к Arduino. Соберём схему, как показано на рисунке. Если вы подключаете светодиод к цифровому выводу, отличному от "13", не забывайте о токоограничивающем резисторе примерно на 220 Ом.

Напишем вот такой скетч и загрузим его в Ардуино.
После платы видно, что скетч выполняется не совсем так как нам нужно: пока полностью не отработает сирена, светодиод не мигнёт, а мы бы хотели, чтобы светодиод ВО ВРЕМЯ звучания сирены. В чём же здесь проблема?
Дело в том, что обычным образом эту задачу не решить. Задачи выполняются микроконтроллером строго последовательно. Оператор "delay()" задерживает выполнение программы на указанный промежуток времени, и пока это время не истечёт, следующие команды программы не будут выполняться. Из-за этого мы не можем задать разную длительность выполнения для каждой задачи в цикле "loop()" программы.
Поэтому нужно как-то сымитировать многозадачность.

Вариант, при котором Arduino будет выполнять задачи псевдо-параллельно, предложен разработчиками Ардуино в статье https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay.
Суть метода в том, что при каждом повторении цикла "loop()" мы проверяем, настало ли время мигать светодиодом (выполнять фоновую задачу) или нет. И если настало, то инвертируем состояние светодиода. Это своеобразный вариант обхода оператора "delay()".
Существенным недостатком данного метода является то, что участок кода перед блоком управления светодиодом должен выполняться быстрее, чем интервал времени мигания светодиода "ledInterval". В противном случае мигание будет происходить реже, чем нужно, и эффекта параллельного выполнения задач мы не получим. В частности, в нашем скетче длительность изменения звука сирены составляет 200+200+200+200 = 800 мсек, а интервал мигания светодиодом мы задали 200 мсек. Но светодиод будет мигать с периодом 800 мсек, что в 4 раза отличается от того, что мы задали. Вообще, если в коде используется оператор "delay()", в таком случае трудно сымитировать псевдо-параллельность, поэтому желательно его избегать.
В данном случае нужно было бы для блока управления звуком сирены также проверять, пришло время или нет, а не использовать "delay()". Но это бы увеличило количество кода и ухудшило читаемость программы.

Чтобы решить поставленную задачу, воспользуемся замечательной библиотекой ArduinoThread, которая позволяет с лёгкостью создавать псевдо-параллельные процессы. Она работает похожим образом, но позволяет не писать код по проверке времени - нужно выполнять задачу в этом цикле или не нужно. Благодаря этому сокращается объём кода и улучшается читаемость скетча. Давайте проверим библиотеку в действии.
Первым делом скачаем с официального сайта https://github.com/ivanseidel/ArduinoThread/archive/master.zip архив библиотеки и разархивируем его в директорию "libraries" среды разработки Arduino IDE. Затем переименуем папку "ArduinoThread-master" в "ArduinoThread".

Схема подключений останется прежней. Изменится лишь код программы. Теперь он будет такой, как на врезке.
В программе мы создаём два потока, каждый выполняет свою операцию: один мигает светодиодом, второй управляет звуком сирены. В каждой итерации цикла для каждого потока проверяем, пришло ли время его выполнения или нет. Если пришло - он запускается на исполнение с помощью метода "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]);

И приведем пример использования функции Arduino attachInterrupt() .

Прерывание – это сигнал, который сообщает процессору о наступлении какого-либо события, которое требует незамедлительного внимания. Процессор должен отреагировать на этот сигнал, прервав выполнение текущих инструкций и передав управление обработчику прерывания (ISR, Interrupt Service Routine). Обработчик – это обычная функция, которую мы пишем сами и помещаем туда тот код, который должен отреагировать на событие.

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

Аппаратные и программные прерывания

Прерывания в Ардуино можно разделить на несколько видов:

  • Аппаратные прерывания . Прерывание на уровне микропроцессорной архитектуры. Самое событие может произойти в производительный момент от внешнего устройства – например, нажатие кнопки на клавиатуре, движение компьютерной мыши и т.п.
  • Программные прерывания . Запускаются внутри программы с помощью специальной инструкции. Используются для того, чтобы вызвать обработчик прерываний.
  • Внутренние (синхронные) прерывания . Внутреннее прерывание возникает в результате изменения или нарушения в исполнении программы (например, при обращении к недопустимому адресу, недопустимый код операции и другие).

Зачем нужны аппаратные прерывания

Аппаратные прерывания возникают в ответ на внешнее событие и исходят от внешнего аппаратного устройства. В Ардуино представлены 4 типа аппаратных прерываний. Все они различаются сигналом на контакте прерывания:

  • Контакт притянут к земле. Обработчик прерывания исполняется до тех пор, пока на пине прерывания будет сигнал LOW.
  • Изменение сигнала на контакте. В таком случае Ардуино выполняет обработчик прерывания, когда на пине прерывания происходит изменение сигнала.
  • Изменение сигнала от LOW к HIGH на контакте – при изменении с низкого сигнала на высокий будет исполняться обработчик прерывания.
  • Изменение сигнала от HIGH к LOW на контакте – при изменении с высокого сигнала на низкий будет исполняться обработчик прерывания.

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

Основными причинами, по которым необходимо вызвать прерывание, являются:

  • Определение изменения состояния вывода;
  • Прерывание по таймеру;
  • Прерывания данных по SPI, I2C, USART;
  • Аналогово-цифровое преобразование;
  • Готовность использовать EEPROM, флеш-память.

Как реализуются прерывания в Ардуино

При поступлении сигнала прерывания работа приостанавливается. Начинается выполнение функции, которая объявляется на выполнение при прерывании. Объявленная функция не может принимать входные значения и возвращать значения при завершении работы. На сам код в основном цикле программы прерывание не влияет. Для работы с прерываниями в Ардуино используется стандартная функция attachInterrupt() .

Отличие реализации прерываний в разных платах Ардуино

В зависимости от аппаратной реализации конкретной модели микроконтроллера есть несколько прерываний. Плата Arduino Uno имеет 2 прерывания на втором и третьем пине, но если требуется более двух выходов, плата поддерживает специальный режим «pin-change». Этот режим работает по изменению входа для всех пинов. Отличие режима прерывания по изменению входа заключается в том, что прерывания могут генерироваться на любом из восьми контактов. Обработка в таком случае будет сложнее и дольше, так как придется отслеживать последнее состояние на каждом из контактов.

На других платах число прерываний выше. Например, плата имеет 6 пинов, которые могут обрабатывать внешние прерывания. Для всех плат Ардуино при работе с функцией attachInterrupt (interrupt, function, mode) аргумент Inerrupt 0 связан с цифровым пином 2.

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

Теперь давайте перейдем к практике и поговорим о том, как использовать прерывания в своих проектах.

Синтаксис attachInterrupt()

Функция attachInterrupt используется для работы с прерываниями. Она служит для соединения внешнего прерывания с обработчиком.

Синтаксис вызова: attachInterrupt(interrupt, function, mode)

Аргументы функции:

  • interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
  • function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
  • mode – условие срабатывания прерывания.

Возможна установка следующих вариантов условий срабатывания:

  • LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
  • CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
  • RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
  • FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4

Важные замечания

При работе с прерываниями нужно обязательно учитывать следующие важные ограничения:

  • Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
  • Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
  • Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит, ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
  • В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).

Примеры использования attachInterrupt

Давайте приступим к практике и рассмотрим простейший пример использования прерываний. В примере мы определяем функцию-обработчик, которая при изменении сигнала на 2 пине Arduino Uno переключит состояние пина 13, к которому мы традиционно подключим светодиод.

#define PIN_LED 13 volatile boolean actionState = LOW; void setup() { pinMode(PIN_LED, OUTPUT); // Устанавливаем прерывание // Функция myEventListener вызовется тогда, когда // на 2 пине (прерываниие 0 связано с пином 2) // изменится сигнал (не важно, в какую сторону) attachInterrupt(0, myEventListener, CHANGE); } void loop() { // В функции loop мы ничего не делаем, т.к. весь код обработки событий будет в функции myEventListener } void myEventListener() { actionState != actionState; // // Выполняем другие действия, например, включаем или выключаем светодиод digitalWrite(PIN_LED, actionState); }

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

Прерывания по нажатию кнопки с антидребезгом

При прерывании возникает – перед тем, как контакты плотно соприкоснутся при нажатии кнопки, они будут колебаться, порождая несколько срабатываний. Бороться с дребезгом можно двумя способами – аппаратно, то есть, припаивая к кнопке конденсатора, и программно.

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

If(digitalRead(2)==HIGH) { //при нажатии кнопки //Если от предыдущего нажатия прошло больше 100 миллисекунд if (millis() - previousMillis >= 100) { //Запоминается время первого срабатывания previousMillis = millis(); if (led==oldled) { //происходит проверка того, что состояние кнопки не изменилось led=!led; }

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

Прерывания по таймеру

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

И прерывание по таймеру позволяет выполнять прерывание один раз в миллисекунду. В Ардуино имеется 3 таймера – Timer0, Timer1 и Timer2. Timer0 используется для генерации прерываний один раз в миллисекунду, при этом происходит обновление счетчика, который передается в функцию millis (). Этот таймер является восьмибитным и считает от 0 до 255. Прерывание генерируется при достижении значения 255. По умолчанию используется тактовый делитель на 65, чтобы получить частоту, близкую к 1 кГц.

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

TIMSK0 |= _BV(OCIE0A);

Требуется определить обработчик прерывания для вектора прерывания по таймеру. Вектором прерывания называется указатель на адрес расположения команды, которая будет выполняться при вызове прерывания. Несколько векторов прерывания объединяются в таблицу векторов прерываний. Таймер в данном случае будет иметь название TIMER0_COMPA_vect. В этом обработчике будут производиться те же действия, что и в loop ().

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 () останется пустой. void loop() { }

Подведение итогов

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


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

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

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

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

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

Видео

Понравилось? Лайкни нас на Facebook