Как выполнять параллельные задачи (Threads) в программе для Arduino
В микропроцессорной технике параллельно выполняющиеся задачи называются тредами (Threads) – потоками. Это очень удобно, ведь часто бывает необходимо выполнять несколько операций одновременно. А можно ли заставить микроконтроллер Arduino выполнять сразу несколько задач, как настоящий процессор? Сейчас посмотрим.
Нам понадобится:
- Arduino UNO или иная совместимая плата;
- 1 светодиод (вот из такого набора, например);
- 1 пьезопищалка (типа этой);
- соединительные провода (рекомендую вот такой набор);
- макетная плата (breadboard);
- персональный компьютер со средой разработки Arduino IDE.
Инструкция по созданию параллельных потоков в программе для Arduino
1Схема подключения для демонстрации потоков в работе с Arduino
Вообще говоря, Arduino не поддерживает настоящее распараллеливание задач, или мультипоточность. Но можно при каждом повторении цикла loop() указать микроконтроллеру проверять, не наступило ли время выполнить некую дополнительную, фоновую задачу. При этом пользователю будет казаться, что несколько задач выполняются одновременно.
Например, давайте будем мигать светодиодом с заданной частотой и параллельно этому издавать нарастающие и затихающие подобно сирене звуки из пьезоизлучателя. И светодиод, и пьезоизлучатель мы уже не раз подключали к Arduino. Соберём схему, как показано на рисунке.
Если вы подключаете светодиод к цифровому выводу, отличному от "13", не забывайте о токоограничивающем резисторе примерно на 220 Ом.

2 Управление светодиодом и пьезоизлучателемс помощью оператора delay()
Напишем вот такой скетч и загрузим его в Ардуино.
const int soundPin = 3; /* объявляем переменную с номером пина, на который подключён пьезоэлемент */ const int ledPin = 13; // объявляем переменную с номером пина светодиода void setup() { pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. } void loop() { // Управление звуком: tone(soundPin, 700); // издаём звук на частоте 700 Гц delay(200); tone(soundPin, 500); // на частоте 500 Гц delay(200); tone(soundPin, 300); // на частоте 300 Гц delay(200); tone(soundPin, 200); // на частоте 200 Гц delay(200); // Управление светодиодом: digitalWrite(ledPin, HIGH); // зажигаем delay(200); digitalWrite(ledPin, LOW); // гасим delay(200); }
После включения видно, что скетч выполняется не совсем так как нам нужно: пока полностью не отработает сирена, светодиод не мигнёт, а мы бы хотели, чтобы светодиод мигал во время звучания сирены. В чём же здесь проблема?
Дело в том, что обычным образом эту задачу не решить. Задачи выполняются микроконтроллером строго последовательно. Оператор delay() задерживает выполнение программы на указанный промежуток времени, и пока это время не истечёт, следующие команды программы не будут выполняться. Из-за этого мы не можем задать разную длительность выполнения для каждой задачи в цикле loop() программы. Поэтому нужно как-то сымитировать многозадачность.
3Параллельные процессы без оператора "delay()"
Вариант, при котором Arduino будет выполнять задачи псевдо-параллельно, предложен разработчиками Ардуино. Суть метода в том, что при каждом повторении цикла loop() мы проверяем, настало ли время мигать светодиодом (выполнять фоновую задачу) или нет. И если настало, то инвертируем состояние светодиода. Это своеобразный вариант обхода оператора delay().
const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода const long ledInterval = 200; // интервал мигания светодиодом, мсек. int ledState = LOW; // начальное состояние светодиода unsigned long previousMillis = 0; // храним время предыдущего срабатывания светодиода void setup() { pinMode(soundPin, OUTPUT); // задаём пин 3 как выход. pinMode(ledPin, OUTPUT); // задаём пин 13 как выход. } void loop() { // Управление звуком: tone(soundPin, 700); delay(200); tone(soundPin, 500); delay(200); tone(soundPin, 300); delay(200); tone(soundPin, 200); delay(200); // Мигание светодиодом: // время с момента включения Arduino, мсек: unsigned long currentMillis = millis(); // Если время мигать пришло, if (currentMillis - previousMillis >= ledInterval) { previousMillis = currentMillis; // то запоминаем текущее время if (ledState == LOW) { // и инвертируем состояние светодиода ledState = HIGH; } else { ledState = LOW; } digitalWrite(ledPin, ledState); // переключаем состояние светодиода } }
Существенным недостатком данного метода является то, что участок кода перед блоком управления светодиодом должен выполняться быстрее, чем интервал времени мигания светодиода "ledInterval". В противном случае мигание будет происходить реже, чем нужно, и эффекта параллельного выполнения задач мы не получим. В частности, в нашем скетче длительность изменения звука сирены составляет 200+200+200+200 = 800 мсек, а интервал мигания светодиодом мы задали 200 мсек. Но светодиод будет мигать с периодом 800 мсек, что в 4 раза больше того, что мы задали.
Вообще, если в коде используется оператор delay(), в таком случае трудно сымитировать псевдо-параллельность, поэтому желательно его избегать.
В данном случае нужно было бы для блока управления звуком сирены также проверять, пришло время или нет, а не использовать delay(). Но это бы увеличило количество кода и ухудшило читаемость программы.
4Использование библиотеки ArduinoThreadдля создания параллельных потоков
Чтобы решить поставленную задачу, воспользуемся замечательной библиотекой ArduinoThread, которая позволяет с лёгкостью создавать псевдо-параллельные процессы. Она работает похожим образом, но позволяет не писать код по проверке времени – нужно выполнять задачу в этом цикле или не нужно. Благодаря этому сокращается объём кода и улучшается читаемость скетча. Давайте проверим библиотеку в действии.

Первым делом скачаем с официального сайта архив библиотеки и разархивируем его в директорию libraries/ среды разработки Arduino IDE. Затем переименуем папку ArduinoThread-master в ArduinoThread.
Схема подключений останется прежней. Изменится лишь код программы.
#include <Thread.h> // подключение библиотеки ArduinoThread const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода Thread ledThread = Thread(); // создаём поток управления светодиодом Thread soundThread = Thread(); // создаём поток управления сиреной void setup() { pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. ledThread.onRun(ledBlink); // назначаем потоку задачу ledThread.setInterval(1000); // задаём интервал срабатывания, мсек soundThread.onRun(sound); // назначаем потоку задачу soundThread.setInterval(20); // задаём интервал срабатывания, мсек } void loop() { // Проверим, пришло ли время переключиться светодиоду: if (ledThread.shouldRun()) ledThread.run(); // запускаем поток // Проверим, пришло ли время сменить тональность сирены: if (soundThread.shouldRun()) soundThread.run(); // запускаем поток } // Поток светодиода: void ledBlink() { static bool ledStatus = false; // состояние светодиода Вкл/Выкл ledStatus = !ledStatus; // инвертируем состояние digitalWrite(ledPin, ledStatus); // включаем/выключаем светодиод } // Поток сирены: void sound() { static int ton = 100; // тональность звука, Гц tone(soundPin, ton); // включаем сирену на "ton" Гц if (ton <= 500) { // до частоты 500 Гц ton += 100; // увеличиваем тональность сирены } else { // по достижении 500 Гц ton = 100; // сбрасываем тональность до 100 Гц } }
В программе мы создаём два потока – ledThread и soundThread, каждый выполняет свою операцию: один мигает светодиодом, второй управляет звуком сирены. В каждой итерации цикла для каждого потока проверяем, пришло ли время его выполнения или нет. Если пришло – он запускается на исполнение с помощью метода run(). Главное – не использовать оператор delay(). В коде даны более подробные пояснения.

Загрузим код в память Ардуино, запустим. Теперь всё работает в точности так, как надо!
Поблагодарить автора:
Поделиться
Related items
22 comments
-
Кирилл Пятница, 13 Сентябрь 2019 14:49 Ссылка на комментарий
Вау,спс бро.Ты решил одну из самых сложных(как для меня) задач.
Press F to pay respect -
-
Максим Пятница, 01 Ноябрь 2019 10:10 Ссылка на комментарий
Есть возможность в этой библиотеке менять значение времени срабатывания для потока?
Пример:
Хочу, чтобы при определенных условиях поток срабатывал через минуту, а если эти условия не выполняются, поток выполняется раз в 10 минут. Как изменить время срабатывания? Пробовал задать время через переменную, но при ее изменении поток срабатывает с изначальным интервалом. -
aave1 Пятница, 01 Ноябрь 2019 19:24 Ссылка на комментарий
Максим, сделайте два потока: один с интервалом минута, а другой - 10 минут. У каждого потока есть свойство "enabled", через которое вы можете активировать или деактивировать любой поток в любое время.
Например:
if (что-то) {
myThread.enabled = true;
}
else {
myThread.enabled = false;
}
Возможно стоит подумать в этом направлении. -
Леонид Кукленко Воскреснье, 26 Январь 2020 03:12 Ссылка на комментарий
Здравствуйте. Скопировал ваш пример - сам по себе он работает хорошо. Заменил рабочие части обоих новых loop на свои (по отдельности они работают отлично) и программа выводит в монитор порта только информацию из участка setup. Есть ли какие-то ограничения на использование каких либо функций в новых loop (delay используется один раз)? И если кто-то сталкивался, подскажите, пожалуйста, как это решить.
-
aave1 Воскреснье, 26 Январь 2020 16:17 Ссылка на комментарий
Леонид, возможно в вашем скетче есть какой-то участок кода, из которого программа никогда не выходит. Без самого кода трудно сказать точнее. Можете разместить здесь хотя бы псевдокод участка, который не срабатывает?
-
Леонид Кукленко Воскреснье, 26 Январь 2020 17:55 Ссылка на комментарий
Здравствуйте, aave1. Вот один из void к примеру, Обычный вольтметр, в цикле идет усреднение за период времени. Остальные части как в примере
int Uphre = A0;
float Uanalog = 0;
float Udigital[Kolichestvo_izmereniy];
float Uavgdigital = 0;
int i = 0;
void Dannie()
{
for(i=0; i -
Леонид Кукленко Воскреснье, 26 Январь 2020 17:57 Ссылка на комментарий
Kolichestvo_izmereniy; i++){
Uanalog = analogRead(Uphre);
Udigital[i] = Uanalog*5.0/1024.0;
Uavgdigital += Udigital[i];
delay(Vremya_izmereniya_min*60000/Kolichestvo_izmereniy);
}
Serial.println(Uavgdigital/Kolichestvo_izmereniy);
Uavgdigital = 0;
} -
Леонид Кукленко Воскреснье, 26 Январь 2020 23:32 Ссылка на комментарий
Извините за беспокойство, ошибка была в разделе setup, действительно цикл мешал, спасибо за наводку
-
aave1 Понедельник, 27 Январь 2020 18:03 Ссылка на комментарий
Рад, что вы разобрались! Я ещё не успел вникнуть в вашу проблему.
-
Сергей Воскреснье, 05 Июль 2020 04:39 Ссылка на комментарий
понятия потока тут недопустимо, даже таймеры грузят МК, это вам не СТМ32, у которого конвейер.
-
aave1 Понедельник, 06 Июль 2020 19:43 Ссылка на комментарий
Сергей, конечно, это не поток в классическом понимании, но тем не менее. Даже библиотека называетсяThreads.
-
Олег Среда, 14 Октябрь 2020 07:31 Ссылка на комментарий
Друзья, с отцом сварили ворота для двора дома, в электронике соображаю хорошо а вот с программированием все плохо, подскажите мне скетч, который можно использовать в моём случае
1.есть ворота и один двигатель переменного тока на 220 вольт.
2.есть релюхи для реверса двигателя.
3.есть симисторы и оптотранзисторы для управления симисторами
4. есть ардуино UNO
5. есть 4 датчика холла
нужен скетч с такой раскладкой... одна кнопка при нажатии которой, ардуино на какой то пин даёт команду подать напряжение на реле после чего начинает шумировать с другого пина симистор плавно до ста процентов, примерно пройдя путь 80-90% стоит еще один татчик который начинает замедлять ход мотора от 100 до скажем 10 % и в конце еще датчик который говорит о том что нужно выключить мотор. (отключить питание от реле)
и в обратном порядке тоже самое, другое реле но те же датчики только в обратном направлении.
Сразу скажу с таймером у нас не получилось, дело в том что в разное время суток, разное напряжение в сети, и это мучение.... то на 20% не закрывает то на 10% заходи кто хочет (((((( помогите ребята знающие . 0le9@list.ru -
Антон4ик Воскреснье, 24 Январь 2021 19:37 Ссылка на комментарий
всем привет!
Хочу сразу представиться -- я дилетант-любитель, которому стало интересна тема с ардуино. Поставил себе задачу сделать умную теплицу для начала. Как обычно бывает, хочется чтобы она могла всё из одной коробочки. На первом этапе хочу создать одновременную работу реле от датчика температуры и реле от датчика температуры. Столкнулся потребностью в многозадачности на ардуино.
Почитав тут и на Гидхабе про эту библиотеку, я создал скетч (точнее на что меня хватило), но что-то не работает.... может подскажите что не так в скетче? Или обратить внимание на железо? Буду очень благодарен в помощи. P.S. Скетч прилагается:
#include "DHT.h" // подключение библиотеки DHT
#include "Thread.h" // подключение библиотеки ArduinoThread
DHT dht_4(4,DHT22);
long vlaga;
Thread vlagaThread = Thread(); // создаём поток управления датчиком движения
long Temperatura;
Thread TemperaturaThread = Thread(); // создаём поток управления градусником
void setup()
{
Serial.begin(9600);
vlaga = map(analogRead(A0),0,1023,0,100);
dht_4.begin();
Temperatura = dht_4.readTemperature();
pinMode(vlaga, OUTPUT);
pinMode(Temperatura, OUTPUT);
vlagaThread. enabled = true;// назначаем потоку задачу
vlagaThread. setInterval(1000);// задаём интервал срабатывания, мсек
TemperaturaThread. enabled = true;// назначаем потоку задачу
TemperaturaThread.setInterval(50);// задаём интервал срабатывания, мсек
}
void loop()
{
Serial.println((String("показания") + String(map(analogRead(A0),0,1023,0,100)) + String(" градусы") + String(dht_4.readTemperature())));
// Проверим, пришло ли время переключиться по земле:
if (vlagaThread.shouldRun())
vlagaThread.run(); // запускаем поток
// Проверим, пришло ли время температуры воздуха:
if (TemperaturaThread.shouldRun())
TemperaturaThread.run(); // запускаем поток
}
// Поток датчика земли:
void vlagaZemli()
{
if (vlaga = 30) {
digitalWrite(5,HIGH);
}
else {
digitalWrite(5,LOW);
}
} -
aave1 Воскреснье, 24 Январь 2021 21:00 Ссылка на комментарий
Антон4ик, добрый день! Попробуйте в функции vlagaZemli() заменить "=" на "==". А также в функции loop() уберите вывод в монитор порта "Serial.println", т.к. там ошибка, или замените на
Serial.println("показания" + (String)map(analogRead(A0),0,1023,0,100) + " градусы" + (String)dht_4.readTemperature()); -
Anton4ik Воскреснье, 31 Январь 2021 07:22 Ссылка на комментарий
aave1, здравствуйте!
Я воспользовался Вашими советами и сделал как рекомендовали, но у меня так и не переключается реле при изменении значения теипературы.
если будет возможность, подскажите пожалуйста, что не так со скетчем? Буду очень благодарен
P.s. не рубите с плеча сырое дарование
#include "DHT.h"
#include "Thread.h" // подключение библиотеки ArduinoThread
#include "Adafruit_Sensor.h"
DHT dht_4(4,DHT22);
long vlaga;
Thread vlagaThread = Thread(); // создаём поток управления датчиком движения
long Temperatura;
Thread TemperaturaThread = Thread(); // создаём поток управления градусником
void setup()
{
Serial.begin(9600);
vlaga = map(analogRead(A0),0,1023,0,100);
dht_4.begin();
Temperatura = dht_4.readTemperature();
pinMode(vlaga, OUTPUT);
pinMode(Temperatura, OUTPUT);
vlagaThread. enabled = true;// назначаем потоку задачу
vlagaThread. setInterval(10*1000);// задаём интервал срабатывания, мсек
TemperaturaThread. enabled = true;// назначаем потоку задачу
TemperaturaThread.setInterval(5*1000);// задаём интервал срабатывания, мсек
}
void loop()
{
Serial.println((String("показания") + String(map(analogRead(A0),0,1023,0,100)) + String(" градусы") + String(dht_4.readTemperature())));
// Проверим, пришло ли время переключиться по земле:
if (vlagaThread.shouldRun())
vlagaThread.run(); // запускаем поток
// Проверим, пришло ли время температуры воздуха:
if (TemperaturaThread.shouldRun())
TemperaturaThread.run(); // запускаем поток
}
// Поток датчика температуры:
void TemperaturaVozduha() {
if (Temperatura >= 27) {
digitalWrite(7,1);
}
else {
digitalWrite(7,0);
}
}
// Поток датчика земли:
void vlagaZemli() {
if (vlaga >= 50) {
digitalWrite(3,LOW);
}
else {
digitalWrite(3,HIGH);
}
} -
aave1 Понедельник, 01 Февраль 2021 08:39 Ссылка на комментарий
Anton4ik! Тут есть несколько ошибок.
1) задавать pinMode() не надо, т.к. влага и температура это не пины (ножки на плате), а просто переменные, которые хранят измеренные значения.
3) Самое главное - вы не назначаете потокам задачу. Вы создаёте поток и запускаете, но он не знает, что делать. Чтобы указать ему задачу, нужно добавить vlagaThread.onRun(vlagaZemli). Аналогично нужно дать задачу и потоку измерения температуры. -
MikiBot Четверг, 09 Декабрь 2021 11:40 Ссылка на комментарий
Недостатки замечательной библиотеки ArduinoThread:
Если остановить поток (thread.enabled = false;),
а потом, в нужный момент запустить (thread.enabled = true;),
то он НЕ начнется сначала, может тут же и закончится,
что не соответствует вашим ожиданиям.
Время изменить тоже не позволяет. -
-
Игорь Среда, 16 Февраль 2022 08:20 Ссылка на комментарий
Добрый день!
Многопоточность со светодиодами и звуковыми излучателями, это хорошо и просто.
У меня вопрос, как осуществить при такой многопоточности сравнение сигналов низкого уровня на двух и более входных портах от кнопок, чтобы при этом отрабатывали выходные порты один, два или три? -
AlexeyHolik Среда, 06 Июль 2022 12:51 Ссылка на комментарий
Я хотел сделать часы на Ардуино. Я попытался воспользоваться вашей библиотекой Thread. Нихрена не работает. И вообще сколько бы я не устанавливал разных библиотек для разных скетчей, никогда ничего не работает. Похоже, я слишком тупой.
-
aave1 Среда, 06 Июль 2022 19:08 Ссылка на комментарий
Алексей, ну зачем же вы так. Возможно из-за большого количества библиотек и бессистемного подхода ничего не получается. Попробуйте взяться за какую-нибудь одну и довести дело до конца. Например, эта библиотека точно проверена. Но ещё дело может быть в том, что сами библиотеки постоянно обновляются, и статья, написанная несколько месяцев назад, уже может содержать устаревшие данные. К сожалению, это так. Но в статьях описан сам принцип и подход к данным вопросам. Просто наберитесь терпения, и всё у вас получится!