Как и положено начинающему, я
- Обеспечить псевдомногозадачность, чтобы разные события происходили в своё время, со своими интервалами и не блокировали друг-друга.
- Было удобно этим пользоваться.
- Можно было оформить как библиотеку и легко включать в другие проекты без копипастов.
Подсмотрев, что большинство ардуинских библиотек сделаны с применением ООП, я тоже решил не выделываться и написал класс SmartDelay, который можно получить с гитхаба как zip для добавления в Arduino IDE или сделать git clone в ~/Arduino/libraries/
В результате получилось вот такое.
#include <SmartDelay.h>
SmartDelay foo(1000000UL); // в микросекундах
void loop () {
if (foo.Now()) {
// Код здесь выполняется каждый интервал в микросекундах, указанный в конструкторе выше.
}
//Прочий код
}
Метод Now() возвращает true, если интервал прошёл. В этом случае отсчёт начинается снова на тот же интервал. То есть, Now() каждый раз «перезаряжается» автоматически.
Классическое мигание светодиодом можно сразу усложнить до мигания двумя. Например, лампочки подключены к ножкам 12 и 11, должны мигать с интервалом в 1с и 777мс соответственно.
#include <SmartDelay.h>
SmartDelay led12(1000000UL);
SmartDelay led11(777000UL);
setup () {
pinMode(12,OUTPUT);
pinMode(11,OUTPUT);
}
byte led12state=0;
byte led11state=0;
void loop () {
if (led12.Now()) {
digitalWrite(12,led12state);
led12state=!led12state;
}
if (led11.Now()) {
digitalWrite(11,led11state);
led11state=!led11state;
}
}
В цикле можно выполнять ещё что-то, мигание светодиодов не будет блокировать выполнение этого кода.
Понятно, что это не полная замена delay(), который останавливает поток на заданное время, надо писать программу всегда как МКА (механизм конечных автоматов). То есть, хранить состояние и в зависимости от него переходить к нужному месту кода.
Старый вариант:
...
action1();
delay(1000);
action2();
delay(500);
action3();
...
Новый вариант:
byte state=0;
SmartDelay d();
...
switch (state) {
case 0:
action1();
d.Set(1000000UL);
state=1;
break;
case 1:
if (d.Now()) {
action2();
d.Set(500000UL);
state=2;
}
break;
case 2:
if (d.Now()) {
action3();
d.Stop();
state=0;
}
break;
}
...
Метод Set(интервал) устанавливает новый интервал и возвращает старый. Просто посмотреть на интервал можно методом Get();
Stop() останавливает обработку и Now() всегда возвращает false.
Start() возобновляет работу и Now() начинает работать как обычно.
Если надо притормозить подсчёт времени, но не останавливать совсем, то есть метод Wait(). Например, если мигает светодиод 12, а при нажатии кнопки не мигает, достаточно добавить вот такой код в loop() в примере с двумя диодами выше:
...
if (digitalRead(9)) led12.Wait();
...
Так, при высоком уровне сигнала на 9 ноге диод на 12 мигать не будет и продолжит, когда там появится 0.
Когда по такому «таймеру» отрисовывается экран, например, и параллельно обрабатываются кнопки, то бывает нужно перерисовать экран или часть сразу после нажатия на кнопку, а не ждать окончания интервала. Для этого служит метод Reset(), после которого следующий вызов Now() вернёт true. Например:
SmartDelay display(1000000UL);
void loop() {
if (btClick()) display.Reset(); // ткнул в кнопку, надо отрисовать экранчик.
if (display.Now()) screenRedraw(); // отрисовка экранчика.
}
Из багов я вижу только, что не учитывается переполнение счётчика микросекунд, а в остальном да, надо почистить код. Мне не нравится, как сделан Reset(), пока думаю.
Объектный подход мне понравился, позволяет спрятать весь код в библиотеку, в которую можно потом уже никогда не заглядывать. Теперь эта маленькая библиотечка живёт во всем моих проектах :)
> Проект на GitHub
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (40)
lopatoid
09.01.2017 20:57+4Всё придумано до нас!
http://robotsbigdata.com/docs-arduino-timer.html
Есть в ардуиновском library manager, так что даже с гитхаба качать ничего не надо.
aso
10.01.2017 09:39О-хо-хонюшки, а не проще ли запилить «многозадачность» в духе RTOS?
Ну, т.е. — main loop, и отдельные «задачи», вызываемые по таймеру — каждую секунду,
каждые 100мс, 10мс, etc.
Понятно, что это не настоящая многозадачность — «задачи» должны сами успевать завершаться до следующего цикла, но, тем не менее…nwwind
10.01.2017 11:59Задача каждая должна укладываться в квант времени. Здесь реализована ленивая кооперативная многозадачность, которой часто и так хватает, а частенько именно её и надо.
aso
10.01.2017 12:10Только в этой «многозадачности» не видать «задач».
А так, написать некий кусок кода без явно выделенного шедулера в том или оном виде, и назвать его «многозадачностью» — можно.
Но зачем?
vvzvlad
10.01.2017 13:29Почему бы тогда не взять rtos?
aso
10.01.2017 13:50Да лучше, наверное.
Но люди, зачем-то, изобретают велосипеды — я этот факт не обсуждал.
nwwind
10.01.2017 17:10Можно и ртос, если влезет. Я с нею ещё не играл.
Боюсь, что «взять ртос» примерно равно «забыть про arduino ide».kahi4
10.01.2017 20:01https://www.hackster.io/feilipu/using-freertos-multi-tasking-in-arduino-ebc3cc
Для ардуино есть несколько портов free rtos. Да и чему там не влазить? Если не брать всякие аллокаторы и прочее.
nwwind
10.01.2017 20:09Есть, я попробовал уже. Сделаю так и так, посмотрю, что получится, можно сравнить будет.
У меня на подходе конкретное изделие :) как раз.
Keroro
10.01.2017 09:50Таймер это хорошо, но помимо времени бывает нужно неблокируеще ждать ответы от других устройств, уровень на ножке и т.д. Мне для этих целей нравится библиотека Protothreads. Портируется в Ардуину за 2 клика, и получаем почти что ОСРВ кооперативного типа.
Пример из практики: нужно сделать так, чтоб кнопка срабатывала при нажатии большем 3 сек, при этом основная программа не блокировалась в проверке уровня на кнопке.
//в основном цикле: main() { //bla bla bla PT_SCHEDULE(ScanKey(&ScanKey_pt)); //неблокирующее сканирование клавиатуры //bla bla bla } //сама функция проверки, преобразуется из обычной функции в неблокирующую 3-мя строчками кода. //void ScanKey(void) PT_THREAD(ScanKey(struct pt *pt)) { PT_BEGIN(pt); if (keys.SW3) { timer_set(&timer_service, KEY_DELAY); PT_WAIT_WHILE(pt, ((!SW3) && (timer_expired(&timer_service)==0))); // "ждем" пока кнопку удерживают и не истёк таймаут if(timer_expired(&timer_service) && (!SW3)) { //время вышло а кнопка нажата-всё ОК Regim=1; } else Regim=0; } PT_END(pt); }//eof keys
assad77
10.01.2017 10:40в целом направление правильное. это путь в сторону state машины. а чистым таймером в случае если хочется асинхронного выполнения все равно не обойдешься.
nwwind
10.01.2017 12:02Спасибо.
state-machine под это будет в следующей статье. Я там с клавиатурой разбирался и в итоге получился абстректный класс с МКА внутри. Сегодня/завтра опубликую.
talkingpipe
10.01.2017 12:05Я предпочёл эдакую кооперативную многозадачность (здравствуй, Windows 3.1 :) ).
https://github.com/emelianov/Run
Использование прерываний это не отменяет: в прерывании выставил флаг, а в основном цикле выполнил ресурсоемкую часть.
nwwind
10.01.2017 12:06Использование таймера заставляет всё делать с ним, то есть, нельзя абстрагироваться от него.
Ээээ…
Ну, если я спрячу таймер1 внутри своего класса, а потом захочу в самом скетче им попользоваться (я же не знаю потроха класса SmartDelay), то код превратится в тыкву.
В задачах как раз стояло сделать нечно, что можно спрятать, подключить и забыть про код внутри. Ардуино-стайл некий.
a-tk
10.01.2017 12:06Поздравляю, Вы изобрели очередную простенькую кооперативную многозадачность.
Основная проблема здесь в том, что семантика его использования достаточно страшная и непонятная.
Если сделать нечто наподобие такого (код большой, прячу под спойлер и отбрасываю много чего)
Фрагмент .h-файла#define TASK_CLASS(TypeName) TypeName #define TASK_BEGIN(TypeName, Locals) class TASK_CLASS(TypeName) : public StatefullTaskBase { private: struct Locals; public: virtual bool Step() override { switch (this->state) { case -1: return true; case 0: #define TASK_BODY_END ;} return true; } #define TASK_CLASS_END }; #define TASK_END TASK_BODY_END TASK_CLASS_END #define TASK_YIELD() this->state = __LINE__; return false; case __LINE__: #define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__: #define TASK_YIELD_WHILE(cond) this->state = __LINE__; case __LINE__: if ((cond)) return false; #define SECOND *1000LL #define SECONDS SECOND #define MINUTE *(60LL*1000LL) #define MINUTES MINUTE #define HOUR *(3600LL*1000LL) #define HOURS HOUR #define TASK_SLEEP(timeout) this->sleep.Start(timeout); TASK_WAIT_FOR(&this->sleep); #define TASK_PERIODICALLY(period, action) for (;;) {this->sleep.Start(period); action; TASK_WAIT_FOR(&this->sleep);} #define TASK_POLL(action) for(;;) {action; TASK_YIELD();} #define TASK_WAIT_CONDITION(callback) TASK_WAIT_FOR(callback) #define TASK_WAIT_SIGNAL(hSignal) TASK_WAIT_FOR(hSignal) #define TASK_SET_SIGNAL(hSignal) hSignal->Set() #define TASK_WAIT_VALUE(hValueHolder, variable) TASK_WAIT_FOR(hValueHolder); variable = hValueHolder->Get(); #define TASK_SET_VALUE(hValueHolder, value) hValueHolder->Set(value);
nwwind
10.01.2017 12:07Спасибо за макросы, я как раз ломаю голову над ними, чтобы заменить if(obj.Now) { действие }
С другой стороны, так тоже ничего, понятно.a-tk
10.01.2017 17:03+1Кстати, в той реализации, которую Вы здесь видите, у таски есть состояния «активна», «заблокирована» и «завершена» и планировщик по-разному себя с ними ведёт.
Там выше есть строчка:
#define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__:
Так вот, WaitFor указывает планировщику, что таска заблокирована на указанном объекте, после чего сохраняется состояние. Выполнение с точки case __LINE__: начнётся после того, как объект станет сигнальным. Это чем-то напоминает дескрипторы, на которых выполняются блокирующие вызовы в операционке.
rstepanov
10.01.2017 13:30+2Купите уже «синюю таблетку» на STM32 и пишите под FreeRTOS, ардуино — это детский сад, ясельная группа.
nwwind
10.01.2017 16:11+1Я ещё в яслях :)
У меня есть stm32, это сильно не то. Там порог входа сильно выше. Там всё очень сложно и даже помигать светодиодом чтобы, надо мозг ломать долго. Ардуина для поделок, я их и делаю.
Bismuth208
10.01.2017 16:07Год назад делал подобное на С, только по истечению интервала вызывались функции. В итоге вышел менеджер задач на state machine. Через погода нашел это: https://geektimes.ru/post/255770/
nwwind
10.01.2017 16:15Я не хотел делать на колбеках такое изначально. Там выше интересный код с использованием __LINE__ в качестве состояния, кстати. И вообще, макросы заслуживают там вдумчивого чтения.
Я, если и начну делать что-то дальше, сделаю на ООП плюсово и наследованием от класса SmartTask, который будет дёргать методы порождённого класса, а SmartOS :) будет по списку бегать таких тасков.
wyfinger
10.01.2017 16:15Конечно я проголосовал за «Полезно», но не пойму чем подход с циклической проверкой времени хуже?
Вернее оно у Вас и реализовано, но поскольку это микроконтроллер, когда я писал для него, казалось целесообразнее делать наиболее понятно, чтобы все сразу на виду. Вот как-то так, или здесь.
Но, наверное, это дело вкуса.nwwind
10.01.2017 16:18Задача стояла максимально спрятать код, сделать библиотеку а ля ардуина. У меня там кода всего ничего, но он не мешает прочтению кода уже пользовательского, по делу, прячет переменные состояния и таймера в приватные у класса. То есть, не отвлекает от основной логики.
neco
11.01.2017 06:46Древние древние мудрецы, изучавшие когда-то теорию программирования знали о существовании…
кончеечных автоматов…
еще можно посмотреть вот сюда и вот сюда
kahi4
http://playground.arduino.cc/Code/Timer1 Все остальное — костыли
nwwind
Это из той же оперы, на самом деле. Всё-равно заставляет переписывать логику программы.
В прерывании же надо что-то сделать и быстро выйти. С моим подходом нет такого жёсткого отграничения. Понятно, что это не реальное время и не многозадачность, так, припарка. Для ардуининых задач пока хватает :)
Timer1 — сила, но…
kahi4
Не совсем.
Во-первых, по таймеру ограничение по времени (не знаю точно про ардуино, я про общий случай) — успеть отработать до того, как он вызовется снова. В целом, этого обычно вполне достаточно, чтобы успеть переключить задачу.
Во-вторых, вообще есть два пути: либо конечный автомат (state machine), либо многопоточность. Первый реализуется без каких-либо таймеров, часто используется во всяких устройствах типа микроволновки. Когда же устройству пора становится умнее, например, обрабатывать нажатия на кнопки и рисовать что-то на экран, первое время костыль вроде вашего smartDelay подойдет, но чем раньше от него отказаться, тем лучше. Благо, многозадачность делается в несколько строк кода. И еще больше благо — проверенных временем и миллиардами различных устройств реализаций предостаточно, тот же rtos (да, он больше про реальное время, но выпотрошить и достать только диспетчер задач из него можно).
Ну и в третьих — обрабатывать нажатые кнопки тоже стоит по прерываниям. Но там все хитрее и в случае с ардуиной (уж не знаю, умеет ли она в прерывания на пинах, ATmega328, на чем она построена, вроде как умеет, но не на всех ножках) может быть достаточно проблематично и проще действительно влепить планировщик задач.
Ну и планировщик задач на две (или больше заранее известные) задачи делается в 5 строчек кода.
Конечно, если нужен шедулер, который будет выполнять функцию ровно раз в секунду — уже будет гораздо сложнее, ваш код тут даже больше подойдет, но я не представляю себе таких задач на практике.
У прерываний есть одна важная особенность — они позволяют уводить контроллер в режим ожидания, когда ничего не нужно считать. Опять же, маловероятно что в проекте на ардуино кто-то будет париться энергопотреблением, но перспективы роста — они такие.
roboter
Я могу ошибаться, но помоему у UNO всего 2 прерывания (hardware).
nwwind
так и есть, но есть нюанс, там есть три группы прерываний и можно назначить им обработчик. Погугли changeInterrupt или как-то так. Но обработчик ставится на группу и внутри него надо разбираться, какая нога дёрнула, это делается не очень переносимым кодом. Не ардуинский стиль получается.
a-tk
Это прерывания по приходу внешних сигналов. Всего же векторов прерываний куда больше. Только надо иметь в виду, что прерывания таймеров могут использоваться какими-то библиотеками, которые «застолбили» их за собой.
nwwind
rtos да, но она тоже память ест. Речь не идёт о чём-то серьёзном и да, для микроволновки или теплицы сгодится. Для промышленного применения, возможно, не стоит и ide ардуиновской пользоваться, верно?
на атмега328 всего два внешних прерывания, прочие можно обрабатывать, но это будет точно не переносимый код и сильно зависимый от камня. Прочие прерывания группируются по нескольку портов и их надо разделять ещё как-то в обработчике.
Я думаю перейти с микросекунд в более удобные миллисекунды, практика показала, что лишние нули лично мне не нужны, кстати.
Перспективы же роста — перейти на stm32 :)
HunterSpy
Ардуиной вообще не стоит пользоваться если проект предполагает серию. Но для одиночных изделий вполне сносно.
p.s. Исключение когда проект делается любителем ардуино, для таких же ардуинщиков там может быть и серия.
nwwind
+100500
ППКС :)
Lerk
Ардуино вообще по сути набор костылей над atmega328.
И если уж изучать работу таймера, то лучше таки по гайду типа такого: Newbie's Guide to AVR Timers © Dean Camera ( http://www.github.com/abcminiuser/avr-tutorials/blob/master/Timers/Output/Timers.pdf?raw=true )
nwwind
Ардуино — это кубики для начинающих и для простых поделок. Для этого прокладка ардуины, его абстракция над железом заслуживают всяческого уважения и поощрения. :)