Ардуино параллельные процессы. Прерывания в Arduino

В этой статье будут рассмотрены вопросы параллельного и последовательного подключения нескольких ведомых устройств к шине 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 не поддерживает настоящее распараллеливание задач, или мультипоточность.
Но можно при каждом повторении цикла "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()".
В коде даны более подробные пояснения.
Загрузим код в память Ардуино, запустим. Теперь всё работает в точности так, как надо!

Узнаем, как работать с прерываниями по таймеру. Напишем простую программу с параллельными процессами.

В реальной программе надо одновременно совершать много действий. Во введении я приводил пример . Перечислю, какие действия она совершает:

Операция

Время цикла
Опрашивает 3 кнопки, обрабатывает сигналы с них для устранения дребезга 2 мс
Регенерирует данные семисегментных светодиодных индикаторов 2 мс
Вырабатывает сигналы управления для 2 датчиков температуры DS18B20 и считывает данные с них. Датчики имеют последовательный интерфейс 1-wire. 100 мкс для каждого бита,
1 сек общий цикл чтения
Чтение аналоговых значений тока и напряжения на элементе Пельтье, напряжения питания 100 мкс
Цифровая фильтрация аналоговых значений тока и напряжения 10 мс
Вычисление мощности на элементе Пельтье 10 мс
ПИД (пропорционально интегрально дифференциальный) регулятор стабилизации тока и напряжения 100 мкс
Регулятор мощности 10 мс
Регулятор температуры 1 сек
Защитные функции, контроль целостности данных 1 сек
Управление, общая логика работы системы 10 мс

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

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

В предыдущих уроках мы создали класс для объекта кнопка. Мы сказали, что это класс для обработки сигнала в параллельном процессе. Что для его нормальной работы необходимо вызывать функцию (метод) обработки сигнала в цикле с регулярным периодом (мы выбрали время 2 мс). И тогда в любом месте программы доступны признаки, показывающие текущее состояние кнопки или сигнала.

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

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

Аппаратное прерывание от таймера.

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

С точки зрения программы прерывание это вызов функции по внешнему, не связанному напрямую с программным кодом, событию.

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

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

Библиотека MsTimer2.

Библиотека предназначена для конфигурирования аппаратного прерывания от Таймера 2 микроконтроллера. Она имеет всего три функции:

  • MsTimer2::set(unsigned long ms, void (*f)())

Эта функция устанавливает время периода прерывания в мс. С таким периодом будет вызываться обработчик прерывания f. Он должен быть объявлен как void (не возвращает ничего) и не иметь аргументов. * f – это указатель на функцию. Вместо него надо написать имя функции.

  • MsTimer2::start()

Функция разрешает прерывания от таймера.

  • MsTimer2::stop()

Функция запрещает прерывания от таймера.

Перед именем функций надо писать MsTimer2::, т.к. библиотека написана с использованием директивы пространства имен namespace.

Для установки библиотеки скопируйте каталог MsTimer2 в папку libraries в рабочей папке Arduino IDE. За тем запустите программу Arduino IDE, откройте Скетч -> Подключить библиотеку и посмотрите, что в списке библиотек присутствует библиотека MsTimer2.

Загрузить библиотеку MsTimer2 в zip-архиве можно . Для установки его надо распаковать.

Простая программа с параллельной обработкой сигнала кнопки.

Теперь напишем простую программу с одной кнопкой и светодиодом из урока 6. К плате Ардуино подключена одна кнопка по схеме:

Выглядит это так:

На каждое нажатие кнопки светодиод на плате Ардуино меняет свое состояние. Необходимо чтобы были установлены библиотеки MsTimer2 и Button:

MsTimer2

И оплатите. Всего 40 руб. в месяц за доступ ко всем ресурсам сайта!

// sketch_10_1 урока 10
// Нажатие на кнопку меняет состояние светодиода

#include
#include

#define LED_1_PIN 13 //
#define BUTTON_1_PIN 12 // кнопка подключена к выводу 12

Button button1(BUTTON_1_PIN, 15); // создание объекта - кнопка

void setup() {

MsTimer2::set(2, timerInterupt); // задаем период прерывания по таймеру 2 мс
MsTimer2::start(); //
}

void loop() {

// управление светодиодом
if (button1.flagClick == true) {
// был клик кнопки



}
}

// обработчик прерывания
void timerInterupt() {
button1.scanState(); // вызов метода ожидания стабильного состояния для кнопки
}

В функции setup() задаем время цикла прерывания по таймеру 2 мс и указываем имя обработчика прерывания timerInterrupt . Функция обработки сигнала кнопки button1.scanState() вызывается в обработчике прерывания таймера каждые 2 мс.

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

Квалификатор volatile.

Давайте изменим цикл loop() в предыдущей программе.

void loop() {

while(true) {
if (button1.flagClick == true) break;
}

// был клик кнопки
button1.flagClick= false; // сброс признака
digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия светодиода
}

Логически ничего не поменялось.

  • В первом варианте программа проходила цикл loop до конца и в нем анализировала флаг button1.flagClick.
  • Во втором варианте программа анализирует флаг button1.flagClick в бесконечном цикле while. Когда флаг становится активным, то выходит из цикла while по break и инвертирует состояние светодиода.

Разница только в том, в каком цикле крутится программа в loop или в while.

Но если мы запустим последний вариант программы, то увидим, что светодиод не реагирует на нажатие кнопки. Давайте уберем класс, упростим программу.

#include
#define LED_1_PIN 13 // светодиод подключен к выводу 13
int count=0;

void setup() {
pinMode(LED_1_PIN, OUTPUT); // определяем вывод светодиода как выход
MsTimer2::set(500, timerInterupt); // задаем период прерывания по таймеру 500 мс
MsTimer2::start(); // разрешаем прерывание по таймеру
}

void loop() {

while (true) {
if (count != 0) break;
}

count= 0;
digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN)); // инверсия состояния светодиода
}

// обработчик прерывания
void timerInterupt() {
count++;
}

В этой программе счетчик count увеличивается на 1 в обработчике прерывания каждые 500 мс. В цикле while он анализируется, по break выходим из цикла и инвертируем состояние светодиода. Проще программы не придумаешь, но она тоже не работает.

Дело в том, что компилятор языка C++ по мере своего интеллекта оптимизирует программу. Иногда это не идет на пользу. Компилятор видит, что в цикле while никакие операции с переменной count не производятся. Поэтому он считает, что достаточно проверить состояние count только один раз. Зачем в цикле проверять, то, что никогда не может измениться. Компилятор корректирует код, оптимизируя его по времени исполнения. Проще говоря убирает из цикла код проверки переменной. Понять, что переменная count меняет свое состояние в обработчике прерывания, компилятор не может. В результате мы зависаем в цикле while.

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

Если, например, добавить в цикл while вызов функции delay(), то программа заработает.

while (true) {
if (count != 0) break;
delay(1);
}

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

Иногда это сделать непросто или не так эффективно. Тогда надо использовать квалификатор volatile. Он указывается при объявлении переменной и сообщает компилятору, что не надо пытаться оптимизировать ее использование. Он запрещает компилятору делать предположения по поводу значения переменной, так как переменная может быть изменена в другом программном блоке, например, в параллельном процессе. Также компилятор размещает переменную в ОЗУ, а не в регистрах общего назначения.

Достаточно в программе при объявлении count написать

volatile int count=0;

и все варианты будут работать.

Для программы с управлением кнопкой надо объявить, что свойства экземпляра класса Button могут измениться.

volatile Button button1(BUTTON_1_PIN, 15); // создание объекта - кнопка

По моим наблюдениям применение квалификатора volatile никак не увеличивает длину кода программы.

Сравнение метода обработки сигнала кнопки с библиотекой Bounce.

Существует готовая библиотека для устранения дребезга кнопок Bounce. Проверка состояния кнопки происходит при вызове функции update(). В этой функции:

  • считывается сигнал кнопки;
  • сравнивается с состоянием во время предыдущего вызова update();
  • проверяется, сколько прошло времени с предыдущего вызова с помощью функции millis();
  • принимается решение о том, изменилось ли состояние кнопки.
  • Но это не параллельная обработка сигнала. Функцию update() обычно вызывают в основном, асинхронном цикле программы. Если ее не вызывать дольше определенного времени, то информация о сигнале кнопки будет потеряна. Нерегулярные вызовы приводят к неправильной работе алгоритма.
  • Сама функция имеет достаточно большой код и выполняется намного дольше функций библиотеки Button ().
  • Цифровой фильтрации сигналов по среднему значению там вообще нет.

В сложных программах эту библиотеку лучше не использовать.

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

Рубрика: . Вы можете добавить в закладки.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Когда вы установите эту программу, то удивитесь - насколько она похожа на Arduino IDE. Не удивляйтесь, обе программы сделаны на одном движке.

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

Запустим Arduino IDE и выберем простейший пример вывода данных на Serial Port :

Void setup() { Serial.begin(9600); } void loop() { Serial.println("Hello Kitty!"); // ждем 500 миллисекунд перед следующей отправкой delay(500); }

Запустим пример и убедимся, что код работает.

Получение данных

Теперь мы хотим получить этот же текст в . Запускаем новый проект и напишем код.

Первый шаг - импортировать библиотеку. Идем в Sketch | Import Library | Serial . В скетче появится строка:

Import processing.serial.*; Serial serial; // создаем объект последовательного порта String received; // данные, получаемые с последовательного порта void setup() { String port = Serial.list(); serial = new Serial(this, port, 9600); } void draw() { if (serial.available() > 0) { // если есть данные, received = serial.readStringUntil("\n"); // считываем данные } println(received); //отображаем данные в консоли }

Чтобы обеспечить прием данных с последовательного порта, нам нужен объект класса Serial . Так как с Arduino мы отправляем данные типа String, нам надо получить строку и в Processing.

В методе setup() нужно получить доступный последовательный порт. Как правило, это первый доступный порт из списка. После этого мы можем настроить объект Serial , указав порт и скорость передачи данных (желательно, чтобы скорости совпадали).

Осталось снова подключить плату, запустить скетч от Processing и наблюдать поступаемые данные в консоли приложения.

Processing позволяет работать не только с консолью, но и создавать стандартные окна. Перепишем код.

Import processing.serial.*; Serial serial; // создаем объект последовательного порта String received; // данные, получаемые с последовательного порта void setup() { size(320, 120); String port = Serial.list(); serial = new Serial(this, port, 9600); } void draw() { if (serial.available() > 0) { // если есть данные, // считываем их и записываем в переменную received received = serial.readStringUntil("\n"); } // Настройки для текста textSize(24); clear(); if (received != null) { text(received, 10, 30); } }

Запустим пример ещё раз и увидим окно с надписью, которое перерисовывается в одном месте.

Таким образом мы научились получать данные от Arduino. Это позволит нам рисовать красивые графики или создавать программы контроля за показаниями датчиков.

Отправка данных

Мы можем не только получать данные с платы, но и отправлять данные на плату, заставляя выполнять команды с компьютера.

Допустим, мы будем посылать символ "1" из Processing. Когда плата обнаружит присланный символ, включим светодиод на порту 13 (встроенный).

Скетч будет похож на предыдущий. Для примера создадим небольшое окно. При щелчке в области окна будем отсылать "1" и дублировать в консоли для проверки. Если щелчков не будет, то посылается команда "0".

Import processing.serial.*; Serial serial; // создаем объект последовательного порта String received; // данные, получаемые с последовательного порта void setup() { size(320, 120); String port = Serial.list(); serial = new Serial(this, port, 9600); } void draw() { if (mousePressed == true) { //если мы кликнули мышкой в пределах окна serial.write("1"); //отсылаем 1 println("1"); } else { //если щелчка не было serial.write("0"); //отсылаем 0 } }

Теперь напишем скетч для Arduino.

Char commandValue; // данные, поступаемые с последовательного порта int ledPin = 13; // встроенный светодиод void setup() { pinMode(ledPin, OUTPUT); // режим на вывод данных Serial.begin(9600); } void loop() { if (Serial.available()) { commandValue = Serial.read(); } if (commandValue == "1") { digitalWrite(ledPin, HIGH); // включаем светодиод } else { digitalWrite(ledPin, LOW); // в противном случае выключаем } delay(10); // задержка перед следующим чтением данных }

Запускаем оба скетча. Щёлкаем внутри окна и замечаем, что светодиод загорается. Можно даже не щёлкать, а удерживать кнопку мыши нажатой - светодиод будет гореть постоянно.

Обмен данными

Теперь попытаемся объединить оба подхода и обмениваться сообщениями между платой и приложением в двух направлениях.

Для максимальной эффективности добавим булеву переменную. В результате у нас отпадает необходимость постоянно отсылать 1 или 0 от Processing и последовательный порт разгружается и не передает лишнюю информацию.

Когда плата обнаружит присланную единицу, то меняем булевое значение на противоположное относительно текущего состояния (LOW на HIGH и наоборот). В else используем строку "Hello Kity", которую будем отправлять только в случае, когда не обнаружим "1".

Функция establishContact() отсылает строку, которую мы ожидаем получить в Processing. Если ответ приходит, значит Processing может получить данные.

Char commandValue; // данные, поступаемые с последовательного порта int ledPin = 13; boolean ledState = LOW; //управляем состоянием светодиода void setup() { pinMode(ledPin, OUTPUT); Serial.begin(9600); establishContact(); // отсылаем байт для контакта, пока ресивер отвечает } void loop() { // если можно прочитать данные if (Serial.available() > 0) { // считываем данные commandValue = Serial.read(); if (commandValue == "1") { ledState = !ledState; digitalWrite(ledPin, ledState); } delay(100); } else { // Отсылаем обратно Serial.println("Hello Kitty"); } delay(50); } void establishContact() { while (Serial.available() <= 0) { Serial.println("A"); // отсылает заглавную A delay(300); } }

Переходим к скетчу Processing. Мы будем использовать метод serialEvent() , который будет вызываться каждый раз, когда обнаруживается определенный символ в буфере.

Добавим новую булеву переменную firstContact , которая позволяет определить, есть ли соединение с Arduino.

В методе setup() добавляем строку serial.bufferUntil("\n"); . Это позволяет хранить поступающие данные в буфере, пока мы не обнаружим определённый символ. В этом случае возвращаем (\n), так как мы отправляем Serial.println() от Arduino. "\n" в конце значит, что мы активируем новую строку, то есть это будут последние данные, которые мы увидим.

Так как мы постоянно отсылаем данные, метод serialEvent() выполняет задачи цикла draw() , то можно его оставить пустым.

Теперь рассмотрим основной метод serialEvent() . Каждый раз, когда мы выходим на новую строку (\n), вызывается этот метод. И каждый раз проводится следующая последовательность действий:

  • Считываются поступающие данные;
  • Проверяется, содержат ли они какие-то значения (то есть, не передался ли нам пустой массив данных или "нуль");
  • Удаляем пробелы;
  • Если мы первый раз получили необходимые данные, изменяем значение булевой переменной firstContact и сообщаем Arduino, что мы готовы принимать новые данные;
  • Если это не первый приём необходимого типа данных, отображаем их в консоли и отсылаем микроконтроллеру данные о клике, который совершался;
  • Собщаем Arduino, что мы готовы принимать новый пакет данных.
import processing.serial.*; Serial serial; // создаем объект последовательного порта String received; // данные, получаемые с последовательного порта // Проверка на поступление данных от Arduino boolean firstContact = false; void setup() { size(320, 120); String port = Serial.list(); serial = new Serial(this, port, 9600); serial.bufferUntil("\n"); } void draw() { } void serialEvent(Serial myPort) { //формируем строку из данных, которые поступают // "\n" - разделитель - конец пакета данных received = myPort.readStringUntil("\n"); //убеждаемся, что наши данные не пустые перед тем, как продолжить if (received != null) { //удаляем пробелы received = trim(received); println(received); //ищем нашу строку "A" , чтобы начать рукопожатие //если находим, то очищаем буфер и отсылаем запрос на данные if (firstContact == false) { if (received.equals("A")) { serial.clear(); firstContact = true; myPort.write("A"); println("contact"); } } else { //если контакт установлен, получаем и парсим данные println(received); if (mousePressed == true) { //если мы кликнули мышкой по окну serial.write("1"); //отсылаем 1 println("1"); } // когда вы все данные, делаем запрос на новый пакет serial.write("A"); } } }

При подключении и запуске в консоли должна появится фраза "Hello Kitty". Когда вы будете щёлкать мышкой в окне Processing, светодиод на пине 13 будет включаться и выключаться.

Кроме Processing, вы можете использовать программы PuTTy или написать свою программу на C# использованием готовых классов для работы с портами.

04.Communication: Dimmer

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

Для примера понадобится стандартная схема с резистором и светодиодом на выводе 9.

Скетч для Arduino.

Const int ledPin = 9; // светодиод на выводе 9 void setup() { Serial.begin(9600); // устанавливаем режим на вывод pinMode(ledPin, OUTPUT); } void loop() { byte brightness; // проверяем, есть ли данные от компьютера if (Serial.available()) { // читаем последние полученные байты от 0 до 255 brightness = Serial.read(); // устанавливаем яркость светодиода analogWrite(ledPin, brightness); } }

Код для Processing

Import processing.serial.*; Serial port; void setup() { size(256, 150); println("Available serial ports:"); println(Serial.list()); // Uses the first port in this list (number 0). Change this to select the port // corresponding to your Arduino board. The last parameter (e.g. 9600) is the // speed of the communication. It has to correspond to the value passed to // Serial.begin() in your Arduino sketch. port = new Serial(this, Serial.list(), 9600); // Если вы знаете имя порта, используемой платой Arduino board, то явно укажите //port = new Serial(this, "COM1", 9600); } void draw() { // рисуем градиент от чёрного к белому for (int i = 0; i

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

04.Communication: PhysicalPixel (Зажигаем светодиод мышкой)

Немного изменим задачу. Будем проводить мышкой над квадратом и посылать символ "H" (High), чтобы зажечь светодиод на плате. Когда мышь покинет область квадрата, то пошлём символ "L" (Low), чтобы погасить светодиод.

Код для Arduino.

Const int ledPin = 13; // вывод 13 для светодиода int incomingByte; // переменная для получения данных void setup() { Serial.begin(9600); pinMode(ledPin, OUTPUT); } void loop() { // если есть данные if (Serial.available() > 0) { // считываем байт в буфере incomingByte = Serial.read(); // если это символ H (ASCII 72), то включаем светодиод if (incomingByte == "H") { digitalWrite(ledPin, HIGH); } // если это символ L (ASCII 76), то выключаем светодиод if (incomingByte == "L") { digitalWrite(ledPin, LOW); } } }

Код для Processing.

Import processing.serial.*; float boxX; float boxY; int boxSize = 20; boolean mouseOverBox = false; Serial port; void setup() { size(200, 200); boxX = width / 2.0; boxY = height / 2.0; rectMode(RADIUS); println(Serial.list()); // Open the port that the Arduino board is connected to (in this case #0) // Make sure to open the port at the same speed Arduino is using (9600bps) port = new Serial(this, Serial.list(), 9600); } void draw() { background(0); // Если курсор над квадратом if (mouseX > boxX - boxSize && mouseX boxY - boxSize && mouseY

04.Communication: Graph (Рисуем график)

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

Публикации по теме