Управление несколькими процессами с помощью millis() - создание диспетчера задач
- Войдите на сайт для отправки комментариев
Сб, 22/01/2022 - 20:32
В последнее время новички часто задают один и тот же вопрос: как заставить какие-то процессы выполняться независимо друг от друга. Стандартная отсылка к скетчу BlinkWithoutDelay очевидно не всегда помогает новичку понять суть вопроса. Здесь мы попробуем разжевать вопрос более подробно и попытаемся создать некое подобие диспетчера задач.
Просьба пока не цитировать следующие несколько постов, возможно, их придется редактировать
Рассматривать вопрос будем на примере все тех же светодиодов. Заставим работать независимо друг от друга 4 светодиода. Итак, нам нужно, чтобы первый светодиод мигал с интервалом 500 милисекунд. Немного сосредотачиваемся и строго по фэншую пишем такой код:
Если выполняется условие if (millis() - led1_timer >= led1_interval) (стр.4), значит после последней сработки прошло 500 милисекунд, и снова настал момент выполнения кода управления первым светодиодом. Меняем состояние светодиода на противоположное (стр.9 и стр.11) и сохраняем текущее значение таймера для следующей сработки (стр.8). Через следующие 500 милисекунд.
Второй светодиод должен плавно разгораться и плавно гаснуть в течение примерно двух секунд. Код для него будет таким:
Как видим, коды во многом совпадают. Сделаем себе зарубку на память. И продолжим.
Немного усложним задачу - третий светодиод должен мигать с неравномерным интервалом: включаться на 100 милисекунд и выключаться на 400 милисекунд. Код для него будет выглядеть так:
И наконец четвертый светодиод. Он будет мигать с интервалом 300 милисекунд, но включится и выключится всего пять раз, после чего включаться больше не будет. Его код:
Здесь мы сначала проверяем количество срабатываний задачи (flash_count < 10) и, если оно составило 10 (5 * 2 - светодиод должен пять раз включиться и пять раз выключиться), то светодиод больше включаться не должен.
Итого - четыре процесса, выполняющиеся независимо друг от друга и не мешающие выполнению остального кода программы, если таковой имеется. Как видим, для успешного выполнения у каждой задачи должен быть свой таймер и своя проверка на его срабатывание.
Итоговый скетч для всех четырех светодиодов.
Светодиоды подключаются к цифровым выводам 4, 5, 6 и 7 ардуино.
Мы справились, все четыре светодиода мигают совершенно независимо друг от друга и, более того, не мешают выполнению других задач. Однако код получился очень громоздкий и, главное, во многом повторяющийся. Попробуем его причесать.
Сразу оговорюсь - рассматриваемый здесь способ лишь один из многих. Некоторые другие способы привели ниже другие участники обсуждения.
Итак, мы имеем четыре задачи. Каждая задача имеет: 1 - интервал срабатывания, 2 - собственный таймер. Кроме того, задача может выполняться или не выполняться (см. четвертый светодиод). Т.е. задача должна иметь статус - активна или неактивна, выполняется или не выполняется. Итого имеем три обязательных свойства. А значит мы можем описать абстрактную задачу - ее свойства и методы (конечно, правильнее всего было бы оформить все это в виде класса, но мы - новички в программировании, поэтому будем делать как проще). Попробуем описать задачу в виде структуры. Назовем ее taskData.
Для каждой задачи мы должны иметь возможность изменять интервал ее выполения (см. третий светодиод) и устанавливать ее статус, т.е. запускать или останавливать выполнение задачи (см. четвертый светодиод). Итак, установка интервала выполнения:
Здесь все просто - в функцию передается ссылка на задачу, в которой нужно изменить интервал, и новое значение интервала.
Теперь остановка выполнения задачи:
Здесь тоже все просто - в функцию передается ссылка на задачу, выполнение которой нужно остановить, и статусу этой задачи присваивается значение false.
И для законченности картины - запуск выполнения задачи:
В эту функцию также передается ссылка на задачу, выполнение которой нужно запустить. Статусу этой задачи присваивается значение true, и таймеру задачи присваивается текущее значение millis(), т.е. интервал выполнения задачи начинает отсчитываться с этого момента.
И самое главное - каждая задача должна выполнять какие-то действия (в нашем случае - включать и выключать свои светодиоды). А значит нужно иметь возможность проверить, должна ли задача сработать в данный момент времени или еще нет. Т.е. нужна еще одна функция:
В функцию опять таки передается ссылка на задачу, которую нужно проверить. Функция вернет true только в случае, если задача активна, и момент ее срабатывания наступил, в противном случае будет возвращен false.
В итоге наша абстрактная задача приобрела следующий вид:
Теперь можно приступить к практическому использованию наших наработок.
Повторим, что нам нужно сделать:
1. Первый светодиод мигает с интервалом 500 милисекунд.
2. Второй светодиод плавно разгорается и плавно гаснет в течение примерно двух секунд.
3. Третий светодиод мигает с неравномерным интервалом - включен 100 милисекунд и выключен 400 милисекунд.
4. Четвертый светодиод мигает с интервалом 300 милисекунд пять раз и выключается.
Немного усложним задачу - третий светодиод начнет мигать только после того, как четвертый светодиод окончательно отключится.
Для начала, собственно, создадим наши задачи. Для этого нужно объявить и инициализировать четыре переменные типа taskData.
Каждая задача должна выполнять какое-то действие. А значит должна быть функция, которая будет вызываться при срабатывании задачи. В нашем случае это включение и выключение светодиодов.
Итак, первый светодиод:
При каждом срабатывании задачи светодиод будет менять свое состояние с включенного на выключенное и наоборот. Т.е. светодиод будет мигать с интервалом 500 милисекунд.
Второй светодиод:
Третий светодиод:
Здесь кроме включения и выключения светодиода каждый раз изменяется интервал срабатывания задачи.
И четвертый светодиод:
Здесь добавлен блок управления задачами led3 (запускается) и led4 (останавливается) после того, как четвертый светодиод мигнет 5 раз.
И теперь осталась самая малость. Нужна функция, которая будет проверять состояние задач и запускать выполнение соответствующих функций в нужный момент. Назовем ее tick().
Эта функция должна вызываться как можно чаще, чтобы не иметь проблем с пропусками выполнения задач, а потому поместим ее вызов в loop().
Ну и итоговый скетч:
Светодиоды подключаются к цифровым выводам 4, 5, 6 и 7 ардуино.
Выполняю Вашу просьбу "не цитировать".
"чтобы первый светодиод мигал с частотой 500 милисекунд"
Ну что на это можно сказать? Пожалуй, лишь: "нам удалось ввести систему в такой режим, когда частота колебаний не зависит от периода"
Итак, подведем итоги.
Мы сумели создать некоторое подобие диспетчера задач, позволяющее легко добавлять в скетч новые задачи, которые будут выполняться независимо от остального кода. При этом задачи могут выполнять самые разные действия - управлять светодиодами индикации, опрашивать датчики, выводить информацию на экран и т.д., и т.п. Кроме того у нас появилась возможность управления этими задачами, т.е. мы можем запускать, останавливать и изменять интервалы выполнения буквально на ходу.
Для добавления новой задачи достаточно сделать три шага:
1. Создать задачу, например:
2. Написать функцию, которая будет выполняться при срабатывании задачи, например:
3. В функцию tick() добавить новый блок if, который будет проверять эту задачу и вызывать при необходимости ее функцию:
И после этих манипуляций функция newTask() будет автоматически выполняться через каждые 1000 милисекунд.
ЗЫ: и бонусом мы добились некоторого уменьшения использования памяти. Мелочь, а приятно ))
Выполняю Вашу просьбу "не цитировать".
"чтобы первый светодиод мигал с частотой 500 милисекунд"
Ну что на это можно сказать? Пожалуй, лишь: "нам удалось ввести систему в такой режим, когда частота колебаний не зависит от периода"
Уел ))
Заменил на "мигал с интервалом 500 милисекунд". Остальные тоже ))
Ждём когда к обсуждению подключится DetSimen
Уел ))
По хорошему там бы еще в "стандартном" для Ардуино стиле "Си с классами" вместо четырех строк
достаточно было бы одной:
По хорошему там бы еще в "стандартном" для Ардуино стиле "Си с классами" вместо четырех строк
достаточно было бы одной:
В этом случае в структуру нужно добавлять еще одно поле типа void (*)(void) и привязывать туда функцию.
Собственно, такую библиотеку я для себя уже давно написал, но тут для новичков... ))
Не поможет это новичкам. Ведь они уверены что процесс это скетч который можно скачать надурняк из интернета. А на деле процесс это некий автомат со своими состояниями и переходами на внешним воздействиям на другие состояния.
Не поможет это новичкам. Ведь они уверены что процесс это скетч который можно скачать надурняк из интернета.
До сих пор считал, что только новоприбывшие путаются в терминах "новичок" и "халявщик". Но, оказывается, и кое-кто из старожилов разделяет те же заблуждения.
В этом случае в структуру нужно добавлять еще одно поле типа void (*)(void) и привязывать туда функцию.
Не обязательно.
Делаешь своего наследника и переписываешь для него метод run() (а при необходимости еще и дописываешь свои методы). И более или менее единообразно управляешь четырьмя светодиодами, двумя релюшками, сервой и мотором. А также опрашиваешь несколько кнопок и датчиков.
В этом случае в структуру нужно добавлять еще одно поле типа void (*)(void) и привязывать туда функцию.
Не обязательно.
Делаешь своего наследника и переписываешь для него метод run() (а при необходимости еще и дописываешь свои методы). И более или менее единообразно управляешь четырьмя светодиодами, двумя релюшками, сервой и мотором. А также опрашиваешь несколько кнопок и датчиков.
Зачем? В случае с классом функция tick() вообще делается методом класса. И уже сама в дебрях класса делает всю работу. Нужно только указать, какой задаче какая callback-функция соответствует. И собрать все задачи в один массив. И заодно иметь идентификаторы задач (суть индекс задачи в массиве) для управления ими из вне. Но это уже явно не для новичков
Независимо это иллюзия!
я бы назвал все эти миллисы и тикеры « условно последовательно/параллельно»
главное - внутри каждого дожно быть НА ПОРЯДКИ короче, чем все вместе взятое. Тогда создается иллюзия параллельности.
но лишь ИЛЛЮЗИЯ!
Независимо это иллюзия!
я бы назвал все эти миллисы и тикеры « условно последовательно»
главное - внутри каждого дожно быть НА ПОРЯДКИ короче, чем все вместе взятое. Тогда создается иллюзия параллельности.
но лишь ИЛЛЮЗИЯ!
Это естественно. Никакой реальной многопоточности в МК не может быть в принципе
Подготовки по языку не хватает. И логику таймера смешивать с логикой задачи - дурь. Сделай четко таймер и используй его в любом контектсте.
Сделай четко таймер и используй его в любом контектсте.
Еще раз - это некое подобие пособия для новичков. Для себя я эту задачу решил давно
Это вредное пособие.
Это вредное пособие.
ну не совсем. Для v258 точно полезно, что он все это расписал. А если ему еще и на ошибки укажут - вообще будет хорошо.
В остальном rkit прав. Новички подобные гайды не читают.. Разве что от гайвера :)
Ждём когда к обсуждению подключится DetSimen
А я тута каким боком? У мня давно таймеры от задач отделены.
В остальном rkit прав. Новички подобные гайды не читают.. Разве что от гайвера :)
Это не он написал, а qwone. rkit вообще никогда по делу не пишет )))
А если ему еще и на ошибки укажут - вообще будет хорошо.
Вот это было бы по делу
Я на этом сайте уже не первый день, и те, кто со мной общался, знают, что конструктивную критику я всегда принимаю. Но касательно темы топика хочу сразу пояснить - тема не о написании оптимального кода. Иначе здесь не было бы никаких флагов led_state, ни повторяющихся функций. Здесь это все только для максимальной наглядности. Ибо рассчитано не на зубров ))
Идею каждую задачу оформлять классом я тоже понимаю, но здесь идея другая - диспетчер задач, т.е. управление всеми задачами из одного места. И да, оформить это классом тоже было бы правильнее, но этот топик НЕ о написании классов.
Поэтому критику типа "setLed1 и setLed2 суть одно и тоже" или "напиши шаблон класса и его используй" конструктивной здесь я считать не буду - в реальной жизни они НЕ обязаны быть одинаковыми, и свой класс диспетчера задач я написал давно и давно использую ))
Зачем этот топик вообще? Чтобы на следующий вопрос "как заставить какие-то процессы выполняться независимо друг от друга" было куда отправить вопрошающего ))
:-)
Тут глубже...
Есть два и более скетчей с делееями и без кнопок. Как их соединить, не ломая структуру внешнего вида каждого скетча и добавить кнопки - всё через миллис (без всяких классов, наследований, доп. библиотек и т.п.)?
:-)
Ещё проще: есть скетчи - для снежинки и звёздочки на умных пикселях, по три эффекта в каждом с цикличным перебором. Как именно их объединить, а не полностью переписать и добавить кнопку выбора "фефекта-моргалки"?
v258 - не надо для новичков писать плохо, потому что они, якобы, хорошо не поймут. Это путь в тупик. А писать заведомо неоптимально, с размножением кода копи-пастой одинаковых кусков - это плохо.
b707, неконструктивно ))
ну что поделать. Я вообще не люблю учителей и учеников :)
ну что поделать. Я вообще не люблю учителей и учеников :)
так кто ты? два обнулённых топора?
ну что поделать. Я вообще не люблю учителей и учеников :)
Просто ты не умеешь их готовить (с) )))
У меня еще хуже - из меня в принципе плохой учитель. Но в данном случае я таки претензии на повторяющийся код не принимаю. По причине, указанной выше ))
v258 - не надо для новичков писать плохо, потому что они, якобы, хорошо не поймут. Это путь в тупик.
Это вы разрабам итальянским скажите. У них что ни пример в официальном тьюториале, то через строку delay(1000);
Не смотрел подробно, просто пробежался.
Не как оценку. а просто как мнение про "многопоточность":
Каждый программист в свое время проходит стадию разработки универсальной многопоточности для Ардуино. Это как возрастные поллюции у ребенка - немного стыдно, другим не стоит рассказывать, но естественно и без этого не бывает взросления.
Став старше, и хорошо контролируя природные позывы, программист вынужденно приходит к правильным выводам:
1. Нельзя написать универсальную многопоточность для 8-ми битных AVR, исключительно потому, что некуда деть требуемый стек. Он не оставит памяти для полезного кода.
2. Все псевдо-универсальные способы требуют приложения головы "ардуинщика" (при наличии). В таком случае лучше бы "ардуинщику" просто научиться автоматному программированию, чем изучать ЛЮБОЙ суррогат.
3. В контроллерах на ARM обычно достаточно памяти и есть либо библиотека thread или целая FreeRTOS (и подобные). И не нужно собирать велосипед из водопроводных труб.
=======================================================
ВЫВОД: Не нужно создавать суррогаты для облегчения жизни новичкам. Пусть просто учатся строить автоматы. Даже Дедовы "Таймеры и Сообщения" новичок пусть использует не потому, что носом ткнули, а потому, что он понял ЧТО и ЗАЧЕМ ему нужно.
Повтор: новичков, даже если отбиваются, нужно привязывать к батарее и учить писать автоматы. Просто на С++. Вообще без библиотек, пока не поймут какие им библиотеки нужны. Тогда можно отвязывать! ;)) (это иносказание, а не призыв к противоправным действиям).
ОК, не буду мешать.
v258 - а вы не ведитесь на неконструктив, делайте как считаете нужным.
Добавка - полностью согласен с Дракулой. примерно это и хотел выразить.
Не смотрел подробно, просто пробежался.
Не как оценку. а просто как мнение про "многопоточность":
Каждый программист в свое время проходит стадию разработки универсальной многопоточности для Ардуино. Это как возрастные поллюции у ребенка - немного стыдно, другим не стоит рассказывать, но естественно и без этого не бывает взросления.
Став старше, и хорошо контролируя природные позывы, программист вынужденно приходит к правильным выводам:
1. Нельзя написать универсальную многопоточность для 8-ми битных AVR, исключительно потому, что некуда деть требуемый стек. Он не оставит памяти для полезного кода.
2. Все псевдо-универсальные способы требуют приложения головы "ардуинщика" (при наличии). В таком случае лучше бы "ардуинщику" просто научиться автоматному программированию, чем изучать ЛЮБОЙ суррогат.
3. В контроллерах на ARM обычно достаточно памяти и есть либо библиотека thread или целая FreeRTOS (и подобные). И не нужно собирать велосипед из водопроводных труб.
=======================================================
ВЫВОД: Не нужно создавать суррогаты для облегчения жизни новичкам. Пусть просто учатся строить автоматы. Даже Дедовы "Таймеры и Сообщения" новичок пусть использует не потому, что носом ткнули, а потому, что он понял ЧТО и ЗАЧЕМ ему нужно.
Повтор: новичков, даже если отбиваются, нужно привязывать к батарее и учить писать автоматы. Просто на С++. Вообще без библиотек, пока не поймут какие им библиотеки нужны. Тогда можно отвязывать! ;)) (это иносказание, а не призыв к противоправным действиям).
Ты шо там, трезвый шоле пишешь?
А то мне и добавить нече тока согласится!
Изменил коды примеров, повторяющихся кодов больше нет
Изменил коды примеров, повторяющихся кодов больше нет
А если найду?
ПС. Где код?
Другой пример со светодиодами. Это конечно не диспетчер, но что бы понимать что есть и другие варианты.
Изменил коды примеров, повторяющихся кодов больше нет
А если найду?
ПС. Где код?
Посты #1, #3 и разъяснение в посте #23 ))
Другой пример со светодиодами. Это конечно не диспетчер, но что бы понимать что есть и другие варианты.
Это не совсем то. Здесь чистый блинк с разными периодами. У меня же светодиоды - только для примера.
Но тоже интересно ))
Другой пример со светодиодами.
Дублирование кода убрано, но не совсем. В исходнике - да. Но дефайник развернется и в исполняемом коде будет дублирование. Почему макрос blinc не функция? К тому же задача решена не в полном объеме. Там для 3 и 4-го светодиодов есть допусловия.
Это не точная копия, просто для примера.) Функция - если только inline, потому как переменные не могут быть общими. Но даже force_ inline/always_inline, как я понимаю, не гарантирует что компилятор выполнит указание.(
Посмотрел, переделал так, а то ИДЕ ругалась упорно и долго.
Да, похоже что это даже сложнее для начинающих.(
В лупе должно быть, естественно:
Но совсем не так как у вас, lilik.
Green, я не понял твою задумку - разъясни. У меня тоже ругался компилятор, пока не изменил твой код вот так:
Да какая там задумка! Препроцессор в loop() заменяет blink c аргументами на описанный выше.
Ошибся я в loop-е. Только тема то v258, а мы тут макросы перетираем.)
Да какая там задумка!
А... А меня смутили слова lilik, что не компилируется. Попробовал - действительно. Оказалось очепятка в loop() )))
В лупе должно быть, естественно:
А ниче, что повторяющиеся действия не оформлены в цикл? ;)
А ниче, что повторяющиеся действия не оформлены в цикл? ;)
What is this?
В loop-е ведь всё. Вот и повторяется.
This is laga!
Будет 20 светодиодов, буш 20 раз повторять вызов. Циклы делать в бурсе не учили?
А ниче, что повторяющиеся действия не оформлены в цикл? ;)
What is this?
В loop-е ведь всё. Вот и повторяется.
А мне, как бывшему ПэХэПэшнику интересно, как это конструкцию LED{x}_PIN в Си сделать можно? В PHP то запросто, там и функции так можно вызывать и даже объекты создавать ))
Чисто тебе, как бывшему ПэХэПэшнику. Делай как то так
Ну и блинк поправить, теперь он получает номер записи в led, где все переменные относящиеся к этому светодиоду лежат. Если надо подробней (ну т.е. решить задачу ТС в полном объеме), то позже. Пришло время трапезы.