Arduino: Паралелно и серийно свързване на подчинени устройства към SPI шината. Асоцииране с обработка на паралелни процеси без израз "delay()".

Arduino: Паралелно и серийно свързване на подчинени устройства към SPI шината.  Свързване с обработка Паралелни процеси без оператор
Arduino: Паралелно и серийно свързване на подчинени устройства към SPI шината. Асоцииране с обработка на паралелни процеси без израз "delay()".

След като инсталирате тази програма, ще се изненадате колко подобна е на Arduino IDE. Не се изненадвайте, и двете програми са направени на един и същи двигател.

Приложението има много функции, включително библиотека Сериен, така че можем да свържем трансфера на данни между платката и .

Стартирайте Arduino IDE и изберете най-простият примеризвеждане на данни към Сериен порт:

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

Нека да стартираме примера и да се уверим, че кодът работи.

Получаване на данни

Сега искаме да получим същия текст в . Стартираме нов проект и пишем кода.

Първата стъпка е да импортирате библиотеката. Хайде да отидем до Скица | Импортиране на библиотека | Сериен. Линията ще се появи в скицата:

Импортиране на обработка.serial.*; Сериен сериал; // създаване на обект на сериен порт Получен низ; // данни, получени от серийния порт void setup() ( String port = Serial.list(); serial = new Serial(this, port, 9600); ) void draw() ( if (serial.available() > 0) ( // ако има данни, получено = сериен. readStringUntil("\n"); // четене на данните) println(получено); // показване на данните в конзолата)

За да гарантираме, че данните се получават от серийния порт, се нуждаем от обект от класа Сериен. Тъй като изпращаме String данни от Arduino, трябва да получим низа и в Processing.

В метод настройвам()трябва да получите на разположение сериен порт. Обикновено това е първият наличен порт в списъка. След това можем да настроим обекта Сериен, указващ порта и скоростта на трансфер на данни (желателно е скоростите да съвпадат).

Остава отново да свържете платката, да стартирате скицата от Processing и да наблюдавате постъпващите данни в конзолата на приложението.

Обработката ви позволява да работите не само с конзолата, но и да създавате стандартни прозорци. Нека пренапишем кода.

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

Нека да пуснем примера отново и да видим прозорец с надпис, който е преначертан на едно място.

Така научихме как да получаваме данни от Arduino. Това ще ни позволи да рисуваме красиви графики или да създаваме програми за наблюдение на показанията на сензорите.

Изпращане на данни

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

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

Скицата ще бъде подобна на предишната. Например, нека създадем малък прозорец. Когато щракнете в областта на прозореца, ще изпратим "1" и ще го дублираме в конзолата за проверка. Ако няма щраквания, се изпраща командата "0".

Импортиране на обработка.serial.*; Сериен сериал; // създаване на обект на сериен порт Получен низ; // данни, получени от серийния порт 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; // вграден LED 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 и серийният порт е разтоварен и не предава ненужна информация.

Когато платката открие изпратената единица, ние променяме булевата стойност на противоположната по отношение на текущото състояние ( НИСКОНа ВИСОКОи обратно). IN другоизползваме низа "Hello Kity", който ще изпратим само ако не намерим "1".

функция установяване на контакт ()изпраща низа, който очакваме да получим при обработка. Ако отговорът дойде, обработката може да получи данните.

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

Да преминем към скицата за обработка. Ще използваме метода serialEvent(), който ще се извиква всеки път, когато в буфера бъде намерен определен знак.

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

В метод настройвам()добавете ред serial.bufferUntil("\n");. Това ни позволява да съхраняваме входящи данни в буфер, докато намерим конкретен символ. В този случай връщаме (\n), тъй като изпращаме Serial.println()от Arduino. "\н"в края означава, че ще активираме нов ред, тоест това ще са последните данни, които ще видим.

Тъй като ние непрекъснато изпращаме данни, методът serialEvent()изпълнява циклични задачи рисувам(), можете да го оставите празно.

Сега помислете за основния метод serialEvent(). Всеки път, когато въвеждаме нов ред (\n), този метод се извиква. И всеки път се извършва следната последователност от действия:

  • Входящите данни се четат;
  • Проверява се дали съдържат някакви стойности (т.е. дали ни е предаден празен масив от данни или "null");
  • Премахване на интервали;
  • Ако сме получили необходимите данни за първи път, променяме стойността на булевата променлива firstContactи кажете на Arduino, че сме готови да получим нови данни;
  • Ако това не е първото получаване на необходимия тип данни, ние ги показваме в конзолата и изпращаме данните за направеното щракване към микроконтролера;
  • Казваме на Arduino, че сме готови да получим нов пакет данни.
import processing.serial.*; Сериен сериал; // създаване на обект на сериен порт Получен низ; // данни, получени от серийния порт // Проверка за данни от 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" - разделителят - краят на получения пакет данни = myPort. readStringUntil("\n"); // уверете се, че че нашите данни не са празни преди как да продължим if (received != null) ( //премахване на интервали получено = trim(received); println(received); //потърсете нашия "A" низ, за ​​да започне ръкостискането //if намерени, изчистете буфера и изпратете заявка за данни if (firstContact == false) ( if (received.equals("A")) ( serial.clear(); firstContact = true; myPort.write("A"); println ("контакт"); ) ) else ( //ако контактът е установен, вземете и анализирайте данните println(received); if (mousePressed == true) ( ​​​​//ако сме щракнали върху прозореца serial.write( "1"); //изпратете 1 println(" 1"); ) // когато имате всички данни, направете заявка за нов пакет serial.write("A"); ) ) )

Когато се свържете и стартирате, фразата "Hello Kitty" трябва да се появи в конзолата. Когато щракнете в прозореца за обработка, светодиодът на пин 13 ще се включи и изключи.

В допълнение към Processing можете да използвате PuTTy програми или да напишете своя собствена C# програма, като използвате готови класове за работа с портове.

04.Комуникация: Димер

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

Например, имате нужда от стандартна схема с резистор и светодиод на щифт 9.

Скица за Arduino.

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

Код за обработка

Импортиране на обработка.serial.*; сериен порт; void setup() ( size(256, 150); println("Налични серийни портове:"); println(Serial.list()); // Използва първия порт в този списък (номер 0). Променете това, за да изберете порт // съответстващ на вашата платка Arduino. Последният параметър (напр. 9600) е // скоростта на комуникацията. Тя трябва да съответства на стойността, предадена на // Serial.begin() във вашата скица на Arduino. port = new Serial (това, Serial.list(), 9600); // Ако знаете името на порта, използван от платката Arduino, изрично посочете //port = new Serial(this, "COM1", 9600); ) void draw( ) ( // начертайте градиент от черно към бяло за (int i = 0; i

Стартирайте и преместете мишката върху създадения прозорец във всяка посока. При движение наляво яркостта на светодиода ще намалее, при движение надясно ще се увеличи.

04. Комуникация: PhysicalPixel (Осветете светодиода с мишката)

Нека променим малко проблема. Ще преместим мишката върху квадрата и ще изпратим символа "H" (High), за да светне светодиодът на дъската. Когато мишката напусне зоната на квадрата, ще изпратим знака "L" (Low), за да изключим светодиода.

Код за Arduino.

Const int ledPin = 13; // щифт 13 за LED 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); ) ) )

Код за обработка.

Импортиране на обработка.serial.*; floatboxX; floatboxY; intboxSize=20; booleanmouseOverBox = невярно; сериен порт; void setup() ( size(200, 200); boxX = width / 2.0; boxY = height / 2.0; rectMode(RADIUS); println(Serial.list()); // Отворете порта, към който е свързана платката Arduino (в този случай #0) // Уверете се, че отваряте порта със същата скорост, която Arduino използва (9600bps) port = new Serial(this, Serial.list(), 9600); ) void draw() ( background(0) ); // Ако курсорът е над квадрата if (mouseX > boxX - boxSize && mouseX boxY - boxSize && mouseY

04. Комуникация: Графика (Начертайте графика)

Ако в предишния пример изпратихме данни от компютъра към платката, сега ще изпълним обратната задача - ще получим данни от потенциометъра и ще ги изведем под формата на графика.


Хардуерни прекъсвания

Не можах да намеря забавна снимка за този урок, намерих само лекция по програмиране и самото начало на тази лекция идеално ни обяснява какво прекъсвам. Прекъсването в Arduino може да бъде описано по абсолютно същия начин: микроконтролерът „пуска всичко“, превключва към изпълнение на блок от функции в манипулатора на прекъсвания, изпълнява ги и след това се връща точно на мястото в основния код, където е спрял.

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

Външно хардуерно прекъсване- Това е прекъсване, причинено от промяна в напрежението на щифта на микроконтролера. Основният момент е, че микроконтролерът (изчислителното ядро) не анкетира щифтаИ не губете време за това, друго „парче желязо“ се занимава с закрепване. Веднага щом напрежението на щифта се промени (което означава цифров сигнал, +5 приложено / +5 премахнато) - микроконтролерът получава сигнал, затваря всичко, обработва прекъсването и се връща на работа. Защо е необходимо това? Най-често прекъсванията се използват за откриване на кратки събития - импулси или дори за преброяване на техния брой, без да се зарежда основният код. Хардуерното прекъсване може да улови кратко натискане на бутон или задействане на сензор по време на сложни дълги изчисления или закъснения в кода, т.е. грубо казано - щифтът е анкетиран успоредно на основния код. Също така, прекъсванията могат да събудят микроконтролера от режими за пестене на енергия, когато почти всички периферни устройства са изключени. Нека да видим как да работим с хардуерни прекъсвания в Arduino IDE.

Прекъсвания в Arduino

Нека започнем с факта, че не всички щифтове „могат“ да прекъсват. Да, има такова нещо като pinChangeInterrupts, но ще говорим за това в уроците за напреднали. Сега трябва да разберем, че хардуерните прекъсвания могат да генерират само определени щифтове:

MK / номер на прекъсване 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 (Мега) D2 D3 D21 D20 D19 D18

Както разбрахте от таблицата, прекъсванията имат свой собствен номер, който е различен от пин номера. Има удобна функция digitalPinToInterrupt(пин), който взема ПИН номер и връща номера на прекъсването. Като захраним тази функция с числото 3 на Arduino nano, получаваме 1. Всичко е според таблицата по-горе, функция за мързеливите.

Чрез функцията се свързва прекъсване attachInterrupt(pin, манипулатор, режим):

  • карфица- номер на прекъсване
  • манипулатор- името на функцията за обработка на прекъсвания (трябва да го създадете сами)
  • режим– прекъсване на „режим“ на работа:
    • НИСКО(ниско) - задейства се от сигнал НИСКОна щифт
    • НАГРАЖДАНЕ(растеж) - задейства се, когато сигналът на щифта се промени от НИСКОНа ВИСОКО
    • ПАДАНЕ(drop) - задейства се, когато сигналът на щифта се промени от ВИСОКОНа НИСКО
    • ПРОМЯНА(промяна) - задейства се, когато сигналът се промени (с НИСКОНа ВИСОКОи обратно)

Прекъсването може също да бъде деактивирано чрез функцията detachInterrupt(pin), където отново е pin номер на прекъсване.

Можете също така глобално да деактивирате прекъсванията с функцията noInterrupts()и ги разрешите отново с прекъсва(). Внимавайте с тях! noInterrupts()също така ще спре прекъсванията на таймера и всички времеви функции и генерирането на ШИМ ще се „счупят“ за вас.

Нека разгледаме пример, при който натисканията на бутони се броят в прекъсването, а в главния цикъл се извеждат със закъснение от 1 секунда. Работейки с бутона в нормален режим, е невъзможно да комбинирате такъв груб изход със закъснение:

Променлив int брояч = 0; // променлива на брояча void setup() ( Serial.begin(9600); // отворен порт за комуникация // свързан бутон на D2 и GND pinMode(2, INPUT_PULLUP); \ // D2 е прекъсване 0 // манипулатор - бутон за функция Tick // FALLING - когато се щракне върху бутона, сигналът ще бъде 0 и ние го хващаме attachInterrupt(0, buttonTick, FALLING); ) void buttonTick() ( counter++; // + pressing ) void loop() ( Serial. println (брояч); // забавяне на изхода (1000); // изчакване)

И така, нашият код отчита кликванията дори по време на забавянето! Страхотен. Но какво е летлив? Ние сме декларирали глобална променлива брояч, който ще съхранява броя на кликванията върху бутона. Ако стойността на променливата ще се промени в прекъсването, трябва да информирате микроконтролера за това с помощта на спецификатора летлив, който се записва преди да се посочи типа данни на променливата, в противен случай работата ще бъде некоректна. Просто трябва да запомните това: ако променлива се промени при прекъсване, направете го летлив.

Още няколко важни точки:

  • Променливите, модифицирани в прекъсване, трябва да бъдат декларирани като летлив
  • Прекъсванията нямат закъснения като забавяне ()
  • Не променя стойността си при прекъсване милис()И микро ()
  • При прекъсването изходът към порта не работи правилно ( Serial.print()), също не го използвайте там - зарежда ядрото
  • При прекъсване трябва да се опитате да правите възможно най-малко изчисления и като цяло „дълги“ действия - това ще забави работата на MC с чести прекъсвания! Какво да правя? Прочетете по-долу.

Ако прекъсването улавя някакво събитие, което не трябва да се обработва незабавно, тогава е по-добре да използвате следния алгоритъм за обработка на прекъсванията:

  • В манипулатора на прекъсвания просто повдигнете флага
  • В основния цикъл на програмата проверяваме флага, ако е повдигнат, нулираме го и извършваме необходимите действия
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("Прекъсване!"); ) )

Това е основно всичко, което трябва да знаете за прекъсванията, ние ще анализираме по-специфични случаи в уроците за напреднали.

Видео

Инструкция

Най-общо казано, Arduino не поддържа истинско паралелизиране на задачите или многопоточност.
Но е възможно да се укаже при всяко повторение на цикъла "loop ()" да се проверява дали е дошло времето за изпълнение на някаква допълнителна, фонова задача. В този случай на потребителя ще изглежда, че няколко задачи се изпълняват едновременно.
Например, нека мигаме с дадена честота и в същото време издаваме нарастващи и намаляващи звуци като сирена от пиезо излъчвател.
И светодиода, и вече сме се свързвали с Arduino повече от веднъж. Нека сглобим веригата, както е показано на фигурата. Ако свържете светодиод към цифров изходразличен от "13", не забравяйте за резистор за ограничаване на тока от 220 ома.

Нека да напишем тази скица и да я качим в Arduino.
След дъската се вижда, че скицата не се изпълнява точно както трябва: докато сирената не работи напълно, светодиодът няма да мига и бихме искали светодиодът да е ПО ВРЕМЕ на звука на сирената. Какъв е проблемът тук?
Факт е, че този проблем не може да бъде решен по обичайния начин. Задачите се изпълняват от микроконтролера строго последователно. Операторът "delay()" забавя изпълнението на програмата за определен период от време и докато това време не изтече, следните команди на програмата няма да бъдат изпълнени. Поради това не можем да зададем различна продължителност за всяка задача в цикъла "loop()" на програмата.
Следователно трябва по някакъв начин да симулирате многозадачност.

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

За да разрешим този проблем, ще използваме прекрасната библиотека ArduinoThread, която ви позволява лесно да създавате псевдопаралелни процеси. Работи по подобен начин, но ви позволява да не пишете код за проверка на времето - трябва да изпълните задачата в този цикъл или не. Това намалява количеството код и подобрява четливостта на скицата. Нека проверим библиотеката в действие.
Първо, изтеглете архива на библиотеката от официалния уебсайт https://github.com/ivanseidel/ArduinoThread/archive/master.zip и го разархивирайте в директорията „библиотеки“ на средата за разработка Arduino IDE. След това преименувайте папката "ArduinoThread-master" на "ArduinoThread".

Схемата на свързване ще остане същата. Само програмният код ще се промени. Сега ще бъде същото като в страничната лента.
В програмата създаваме две нишки, всяка от които изпълнява своя собствена операция: едната мига светодиода, втората контролира звука на сирената. Във всяка итерация на цикъла за всяка нишка проверяваме дали е дошло времето за нейното изпълнение или не. Ако пристигне, той се стартира за изпълнение с помощта на метода "run()". Основното нещо е да не използвате оператора "delay()".
По-подробни обяснения са дадени в кода.
Заредете кода в паметта на Arduino, стартирайте го. Сега всичко работи точно както трябва!

Най-общо казано, Arduino не поддържа истинско паралелизиране на задачите или многопоточност. Но е възможно при всяко повторение на цикъла цикъл ()инструктирайте микроконтролера да провери дали е време да изпълни някаква допълнителна, фонова задача. В този случай на потребителя ще изглежда, че няколко задачи се изпълняват едновременно.

Например, нека мига светодиод с дадена честота и едновременно да излъчва нарастващи и намаляващи звуци като сирена от пиезо излъчвател. Вече сме свързвали както светодиода, така и пиезо емитера към Arduino повече от веднъж. Нека сглобим веригата, както е показано на фигурата.

Ако свързвате светодиод към цифров щифт, различен от "13", не забравяйте за резистор за ограничаване на тока от 220 ома.

2 Управление на LED и пиезо зумеризползвайки оператора delay().

Нека да напишем тази скица и да я качим в Arduino.

Const int soundPin = 3; /* деклариране на променлива с номера на извода, към който е свързан пиезоелектричният елемент */ const int ledPin = 13; // декларираме променлива с номера на светодиодния пин void setup()( pinMode(soundPin, ИЗХОД); // декларира пин 3 като изход. pinMode(ledPin, ИЗХОД); // декларира пин 13 като изход. } void loop() (// Контрол на звука: tone(soundPin, 700); // издава звук с честота 700 Hz delay(200); тон (soundPin, 500); // при 500 Hz забавяне (200); тон (soundPin, 300); // при 300 Hz забавяне (200); тон (soundPin, 200); // при 200 Hz забавяне (200); // LED управление: digitalWrite(ledPin, HIGH); // забавяне на пожара (200); digitalWrite(ledPin, LOW); // закъснение за изгасване (200); }

След като го включите, можете да видите, че скицата не се изпълнява точно както ни е необходимо: докато сирената не бъде напълно разработена, светодиодът няма да мига и бихме искали светодиодът да мига по време назвук на сирена. Какъв е проблемът тук?

Факт е, че този проблем не може да бъде решен по обичайния начин. Задачите се изпълняват от микроконтролера строго последователно. Оператор забавяне ()забавя изпълнението на програмата за определен период от време и докато това време не изтече, следните команди на програмата няма да бъдат изпълнени. Поради това не можем да зададем различна продължителност на изпълнение за всяка задача в цикъла. цикъл ()програми. Следователно трябва по някакъв начин да симулирате многозадачност.

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

Вариантът, при който Arduino ще изпълнява задачи псевдопаралелно, е предложен от разработчиците на Arduino. Същността на метода е, че при всяко повторение на цикъла цикъл ()проверяваме дали е време да мига светодиодът (извършване на фонова задача) или не. И ако е, тогава обръщаме състоянието на светодиода. Това е един вид байпас оператор забавяне ().

Const int soundPin = 3; // променлива с номера на пина на пиезоелектричния елемент const int ledPin = 13; // променлива с номер на LED щифт const long ledInterval = 200; // Интервал на мигане на светодиода, msec. int ledState = LOW; // начално състояние на светодиода unsigned long previousMillis = 0; // съхранява времето на предишното запалване на светодиода void setup()( pinMode(soundPin, ИЗХОД); // задайте пин 3 като изход. pinMode(ledPin, ИЗХОД); // задайте пин 13 като изход. } void loop() (// Контрол на звука: tone(soundPin, 700); забавяне (200); тон (soundPin, 500); забавяне (200); тон (soundPin, 300); забавяне (200); тон (soundPin, 200); забавяне (200); // Мигащ светодиод: // време от включването на Arduino, ms: unsigned long currentMillis = millis(); // Ако е време да мигате, if (currentMillis - previousMillis >= ledInterval) ( previousMillis = currentMillis; // тогава запомнете текущо време if (ledState == LOW) ( // и инвертиране на LED състоянието ledState = HIGH; ) else ( ledState = LOW; ) digitalWrite(ledPin, ledState); // превключване на състоянието на светодиода ) }

Значителен недостатък на този метод е, че кодовата секция преди блока за управление на светодиода трябва да се изпълнява по-бързо от интервала от време на мигане на светодиода "ledInterval". В противен случай мигането ще се случва по-рядко от необходимото и няма да получим ефекта на паралелно изпълнение на задачи. По-конкретно, в нашата скица, продължителността на промяната на звука на сирената е 200+200+200+200 = 800 ms, а интервалът на мигане на светодиода е зададен на 200 ms. Но светодиодът ще мига за период от 800 ms, което е 4 пъти повече от това, което сме задали.

Като цяло, ако кодът използва оператора забавяне (), в този случай е трудно да се симулира псевдопаралелизъм, така че е желателно да се избягва.

В този случай би било необходимо блокът за управление на звука на сирената също да проверява дали моментът е дошъл или не и да не използва забавяне (). Но това би увеличило количеството код и би влошило четливостта на програмата.

4 Използване на библиотеката ArduinoThreadза създаване на паралелни нишки

За да разрешим проблема, ще използваме чудесна библиотека Arduino Thread, което ви позволява лесно да създавате псевдопаралелни процеси. Работи по подобен начин, но ви позволява да не пишете код за проверка на времето - трябва да изпълните задачата в този цикъл или не. Това намалява количеството код и подобрява четливостта на скицата. Нека проверим библиотеката в действие.


Първо, изтеглете архива на библиотеката от официалния уебсайт и го разархивирайте в директорията библиотеки/ Arduino IDE среда за разработка. След това преименувайте папката Arduino Thread-master V Arduino Thread.

Схемата на свързване ще остане същата. Само програмният код ще се промени.

#включи // свързване на библиотеката ArduinoThread const int soundPin = 3; // променлива с номера на пина на пиезоелектричния елемент const int ledPin = 13; // променлива с номер на LED пин Thread ledThread = Thread(); // създаване на нишка за управление на LED Thread soundThread = Thread(); // създаване на контролен поток за сирената void setup()( pinMode(soundPin, ИЗХОД); // декларира пин 3 като изход. pinMode(ledPin, ИЗХОД); // декларира пин 13 като изход. ledThread.onRun(ledBlink); // присвояване на задача на нишката ledThread.setInterval(1000); // задаване на интервала за отговор, ms soundThread.onRun(sound); // присвояване на задача на нишката soundThread setInterval(20); // задаване на интервала за отговор, ms } void loop() (// Проверете дали е време да превключите светодиода: if (ledThread.shouldRun()) ledThread.run(); // стартиране на нишката // Проверете дали е време да промените тона на сирената: if (soundThread.shouldRun()) soundThread.run(); // стартиране на нишка } // LED поток: void ledBlink() ( static bool ledStatus = false; // LED състояние Вкл./Изкл. ledStatus = !ledStatus; // инвертиране на състоянието digitalWrite(ledPin, ledStatus); // включване/изключване на светодиода } // Поток от сирена: празен звук() (статичен вътрешен тон = 100; // звуков тон, Hz тон (soundPin, тон); // включете сирената на "ton" Hz if (ton )

В програмата създаваме две нишки - ledThreadИ soundThread, всеки изпълнява своя собствена операция: единият мига светодиода, вторият контролира звука на сирената. Във всяка итерация на цикъла за всяка нишка проверяваме дали е дошло времето за нейното изпълнение или не. Ако пристигне, той се стартира за изпълнение с помощта на метода тичам(). Основното е да не използвате оператора забавяне (). По-подробни обяснения са дадени в кода.


Заредете кода в паметта на Arduino, стартирайте го. Сега всичко работи точно както трябва!

И ето пример за използване Функции на Arduino прикачванеПрекъсване().

Прекъсването е сигнал, който информира процесора за настъпването на някакво събитие, което изисква незабавно внимание. Процесорът трябва да отговори на този сигнал, като прекъсне изпълнението на текущите инструкции и прехвърли контрола към манипулатора на прекъсванията (ISR, Interrupt Service Routine). Манипулаторът е обикновена функция, която пишем сами и поставяме там кода, който трябва да отговори на събитието.

След обслужване на ISR прекъсването, функцията приключва работата си и процесорът щастливо се връща към прекъснатите дейности - продължава да изпълнява кода от мястото, където е спрял. Всичко това се случва автоматично, така че нашата задача е само да напишем манипулатор на прекъсвания, без да нарушаваме нищо и без да принуждаваме процесора да се разсейва от нас твърде често. Ще ви трябва разбиране на веригата, принципите на работа на свързаните устройства и представа колко често може да се извика прекъсване, какви са характеристиките на неговото възникване. Всичко това е основната трудност при работата с прекъсвания.

Хардуерни и софтуерни прекъсвания

Прекъсванията в Arduino могат да бъдат разделени на няколко типа:

  • Хардуерни прекъсвания. Прекъсване на ниво микропроцесорна архитектура. Самото събитие може да възникне в продуктивен момент от външно устройство - например натискане на бутон на клавиатурата, движение компютърна мишкаи така нататък.
  • Софтуерни прекъсвания. Те се стартират вътре в програмата с помощта на специална инструкция. Използва се за извикване на манипулатор на прекъсване.
  • Вътрешни (синхронни) прекъсвания. Вътрешно прекъсване възниква в резултат на промяна или нарушение в изпълнението на програмата (например при достъп до невалиден адрес, невалиден код на операция и др.).

Защо се нуждаем от хардуерни прекъсвания

Хардуерните прекъсвания възникват в отговор на външно събитие и идват от външно хардуерно устройство. В Arduino има 4 вида хардуерни прекъсвания. Всички те се различават по сигнала на щифта за прекъсване:

  • Контактът е изтеглен към земята. Манипулаторът на прекъсване се изпълнява, докато щифтът на прекъсването е LOW.
  • Смяна на сигнала на контакт. В този случай Arduino изпълнява манипулатор на прекъсване, когато възникне промяна на сигнала на щифта за прекъсване.
  • Промяна на сигнала от LOW на HIGH на щифт - при промяна от нисък на висок ще се изпълни манипулатор на прекъсване.
  • Промяна на сигнала от HIGH на LOW на щифт - когато сигналът се промени от висок на нисък, ще се изпълни манипулатор на прекъсване.

Прекъсванията са полезни в програмите на Arduino, тъй като помагат за решаване на проблеми с времето. Например, когато работите с UART, прекъсванията ви позволяват да не следите пристигането на всеки знак. Външното хардуерно устройство издава сигнал за прекъсване, процесорът незабавно извиква манипулатора на прекъсванията, който улавя знака навреме. Това спестява време на процесора, което без прекъсвания би било изразходвано за проверка на състоянието на UART, вместо това всички необходими действия се извършват от манипулатора на прекъсвания, без да се засяга основна програма. Не се изискват специални възможности от хардуерното устройство.

Основните причини за извикване на прекъсване са:

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

Как се изпълняват прекъсванията в Arduino

Когато се получи сигнал за прекъсване, операцията се спира. Започва изпълнението на функцията, която е декларирана за изпълнение при прекъсване. Декларирана функция не може да приема входни стойности и да връща стойности при завършване. Прекъсването не засяга самия код в главния програмен цикъл. За работа с прекъсвания в Arduino се използва стандартна функция прикачванеПрекъсване().

Разликата между внедряването на прекъсвания в различни платки Arduino

В зависимост от хардуерната реализация специфичен моделМикроконтролерът има няколко прекъсвания. Плащане Ардуино Уноима 2 прекъсвания на втория и третия пин, но ако са необходими повече от два изхода, платката поддържа специален режимсмяна на щифтове. Този режим работи чрез промяна на входа за всички щифтове. Разликата в режима на прекъсване за промяна на входа е, че прекъсванията могат да бъдат генерирани на всеки от осемте пина. Обработката в този случай ще бъде по-сложна и по-дълга, тъй като ще трябва да следите последното състояние на всеки от контактите.

На други платки броят на прекъсванията е по-голям. Например, платката има 6 пина, които могат да обработват външни прекъсвания. За всички платки Arduino, когато работите с функцията attachInterrupt (прекъсване, функция, режим), аргументът Inerrupt 0 се свързва с цифров пин 2.

Прекъсвания в езика Arduino

Сега нека преминем към практиката и да поговорим за това как да използвате прекъсвания във вашите проекти.

Синтаксис AttachInterrupt().

Функцията attachInterrupt се използва за работа с прекъсвания. Той служи за свързване на външно прекъсване към манипулатор.

Синтаксис на повикване: прикрепяне на прекъсване (прекъсване, функция, режим)

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

  • прекъсване - номерът на извикваното прекъсване (стандартно 0 - за 2-ри пин, за платката Arduino Uno 1 - за 3-ти пин),
  • функция - името на извиканата функция при прекъсване (важно - функцията не трябва нито да приема, нито да връща стойности),
  • режимът е условието за задействане на прекъсването.

Могат да бъдат зададени следните условия на задействане:

  • LOW - изпълнява се при ниско ниво на сигнала, когато контактът има нулева стойност. Прекъсването може да се повтаря циклично - например при натискане на бутон.
  • ПРОМЯНА - отпред, прекъсването възниква, когато сигналът се промени от висок на нисък или обратно. Изпълнява се веднъж при всяка промяна на сигнала.
  • RISING - Изпълнете прекъсване веднъж, когато сигналът се промени от LOW на HIGH.
  • FALLING - Изпълнете прекъсване веднъж, когато сигналът се промени от HIGH на LOW.4

Важни бележки

Когато работите с прекъсвания, трябва да се вземат предвид следните важни ограничения:

  • Функцията манипулатор не трябва да отнема много време за изпълнение. Работата е там, че Arduino не може да обработва множество прекъсвания едновременно. Докато функцията ви за обработка се изпълнява, всички други прекъсвания ще бъдат игнорирани и може да пропуснете важни събития. Ако трябва да направите нещо голямо, просто предайте обработката на събитието в цикъла main 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() ( // Ние не правим нищо във функцията за цикъл, тъй като целият код за обработка на събития ще бъде във функцията 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; )

Този код ви позволява да премахнете бърборенето и не блокира изпълнението на програмата, какъвто е случаят с функцията за забавяне, която не е разрешена при прекъсвания.

Таймерът прекъсва

Таймерът е брояч, който отчита при определена честота, получена от процесора 16 MHz. Можете да конфигурирате делителя на честотата, за да получите желания режим на броене. Можете също така да конфигурирате брояча да генерира прекъсвания при достигане на дадена стойност.

А прекъсването на таймера ви позволява да изпълните прекъсването веднъж на милисекунда. Arduino има 3 таймера - Timer0, Timer1 и Timer2. Timer0 се използва за генериране на прекъсвания веднъж на милисекунда, което актуализира брояча, който се предава на функцията millis(). Този таймер е осем бита и брои от 0 до 255. Прекъсване се генерира, когато стойността е 255. По подразбиране се използва делител на часовника на 65, за да се получи честота, близка до 1kHz.

Регистрите за сравнение се използват за сравняване на състоянието на таймера и съхранените данни. IN този примеркодът ще генерира прекъсване, когато броячът достигне 0xAF.

TIMSK0 |= _BV(OCIE0A);

Изисква се да се дефинира манипулатор на прекъсване за вектора на прекъсване на таймера. Векторът на прекъсване е указател към местоположението на инструкцията, която ще бъде изпълнена при извикване на прекъсването. Няколко вектора на прекъсване се комбинират в таблица на вектор на прекъсване. Таймерът в този случай ще има името TIMER0_COMPA_vect. В този манипулатор ще се извършват същите действия като в цикъл ().

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() ( )

Обобщаване

Прекъсването в Arduino е доста сложна тема, защото трябва да мислите за цялата архитектура на проекта наведнъж, да си представите как се изпълнява кодът, какви събития са възможни, какво се случва, когато главният код бъде прекъснат. Нямахме за цел да разкрием всички характеристики на работата с тази езикова конструкция, основната цел беше да представим основните случаи на употреба. В следващите статии ще продължим да говорим за прекъсванията по-подробно.