Pull to refresh

Comments 48

Это из той же оперы, на самом деле. Всё-равно заставляет переписывать логику программы.
В прерывании же надо что-то сделать и быстро выйти. С моим подходом нет такого жёсткого отграничения. Понятно, что это не реальное время и не многозадачность, так, припарка. Для ардуининых задач пока хватает :)
Timer1 — сила, но…

Не совсем.


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


Во-вторых, вообще есть два пути: либо конечный автомат (state machine), либо многопоточность. Первый реализуется без каких-либо таймеров, часто используется во всяких устройствах типа микроволновки. Когда же устройству пора становится умнее, например, обрабатывать нажатия на кнопки и рисовать что-то на экран, первое время костыль вроде вашего smartDelay подойдет, но чем раньше от него отказаться, тем лучше. Благо, многозадачность делается в несколько строк кода. И еще больше благо — проверенных временем и миллиардами различных устройств реализаций предостаточно, тот же rtos (да, он больше про реальное время, но выпотрошить и достать только диспетчер задач из него можно).


Ну и в третьих — обрабатывать нажатые кнопки тоже стоит по прерываниям. Но там все хитрее и в случае с ардуиной (уж не знаю, умеет ли она в прерывания на пинах, ATmega328, на чем она построена, вроде как умеет, но не на всех ножках) может быть достаточно проблематично и проще действительно влепить планировщик задач.


Ну и планировщик задач на две (или больше заранее известные) задачи делается в 5 строчек кода.


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


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

Я могу ошибаться, но помоему у UNO всего 2 прерывания (hardware).
так и есть, но есть нюанс, там есть три группы прерываний и можно назначить им обработчик. Погугли changeInterrupt или как-то так. Но обработчик ставится на группу и внутри него надо разбираться, какая нога дёрнула, это делается не очень переносимым кодом. Не ардуинский стиль получается.
Это прерывания по приходу внешних сигналов. Всего же векторов прерываний куда больше. Только надо иметь в виду, что прерывания таймеров могут использоваться какими-то библиотеками, которые «застолбили» их за собой.
rtos да, но она тоже память ест. Речь не идёт о чём-то серьёзном и да, для микроволновки или теплицы сгодится. Для промышленного применения, возможно, не стоит и ide ардуиновской пользоваться, верно?

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

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

Перспективы же роста — перейти на stm32 :)
Ардуиной вообще не стоит пользоваться если проект предполагает серию. Но для одиночных изделий вполне сносно.

p.s. Исключение когда проект делается любителем ардуино, для таких же ардуинщиков там может быть и серия.
Ардуино вообще по сути набор костылей над atmega328.
И если уж изучать работу таймера, то лучше таки по гайду типа такого: Newbie's Guide to AVR Timers © Dean Camera ( http://www.github.com/abcminiuser/avr-tutorials/blob/master/Timers/Output/Timers.pdf?raw=true )
Ардуино — это кубики для начинающих и для простых поделок. Для этого прокладка ардуины, его абстракция над железом заслуживают всяческого уважения и поощрения. :)
Чёрт! Там даже протокол такой же. Пойду убью себя ап стену… :(
О-хо-хонюшки, а не проще ли запилить «многозадачность» в духе RTOS?
Ну, т.е. — main loop, и отдельные «задачи», вызываемые по таймеру — каждую секунду,
каждые 100мс, 10мс, etc.
Понятно, что это не настоящая многозадачность — «задачи» должны сами успевать завершаться до следующего цикла, но, тем не менее…
Задача каждая должна укладываться в квант времени. Здесь реализована ленивая кооперативная многозадачность, которой часто и так хватает, а частенько именно её и надо.
Только в этой «многозадачности» не видать «задач».
А так, написать некий кусок кода без явно выделенного шедулера в том или оном виде, и назвать его «многозадачностью» — можно.
Но зачем?
Почему бы тогда не взять rtos?
Да лучше, наверное.
Но люди, зачем-то, изобретают велосипеды — я этот факт не обсуждал.
Можно и ртос, если влезет. Я с нею ещё не играл.
Боюсь, что «взять ртос» примерно равно «забыть про arduino ide».
Есть, я попробовал уже. Сделаю так и так, посмотрю, что получится, можно сравнить будет.
У меня на подходе конкретное изделие :) как раз.
Таймер это хорошо, но помимо времени бывает нужно неблокируеще ждать ответы от других устройств, уровень на ножке и т.д. Мне для этих целей нравится библиотека 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
Хммм… Пойду там макросы потрошить. Интересно.
в целом направление правильное. это путь в сторону state машины. а чистым таймером в случае если хочется асинхронного выполнения все равно не обойдешься.
Спасибо.
state-machine под это будет в следующей статье. Я там с клавиатурой разбирался и в итоге получился абстректный класс с МКА внутри. Сегодня/завтра опубликую.
Я предпочёл эдакую кооперативную многозадачность (здравствуй, Windows 3.1 :) ).
https://github.com/emelianov/Run
Использование прерываний это не отменяет: в прерывании выставил флаг, а в основном цикле выполнил ресурсоемкую часть.
Использование таймера заставляет всё делать с ним, то есть, нельзя абстрагироваться от него.
Ээээ…
Ну, если я спрячу таймер1 внутри своего класса, а потом захочу в самом скетче им попользоваться (я же не знаю потроха класса SmartDelay), то код превратится в тыкву.
В задачах как раз стояло сделать нечно, что можно спрятать, подключить и забыть про код внутри. Ардуино-стайл некий.
Поздравляю, Вы изобрели очередную простенькую кооперативную многозадачность.
Основная проблема здесь в том, что семантика его использования достаточно страшная и непонятная.
Если сделать нечто наподобие такого (код большой, прячу под спойлер и отбрасываю много чего)
Фрагмент .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);


, то можно делать независимые таски в таком духе:
Пример таски
DEFINE_TELEMETRY(PowerMonitorRecord)
{
	u16 Value;
};

TASK_BEGIN(PowerMonitorTask, {})
TASK_PERIODICALLY(5 SECONDS,
	telemetry << CreateRecord(GetState())
);
TASK_BODY_END


PowerMonitorRecord GetState()
{
	return PowerMonitorRecord{ 0 };
}


TASK_CLASS_END


Да, макросы, но результат явно менее страшный, чем кодирование руками конечного автомата.
Спасибо за макросы, я как раз ломаю голову над ними, чтобы заменить if(obj.Now) { действие }
С другой стороны, так тоже ничего, понятно.
Обратите внимание на конструкцию вида
bool TaskFunc(int &state)
{
switch (state)
{
case 0: // начальное
//
  state = __LINE__; return false; case __LINE__: // в одну строку.
default:
  state = -1;
  return true;
}
}
Кстати, в той реализации, которую Вы здесь видите, у таски есть состояния «активна», «заблокирована» и «завершена» и планировщик по-разному себя с ними ведёт.
Там выше есть строчка:
#define TASK_WAIT_FOR(Object) this->WaitFor(Object); this->state = __LINE__; return false; case __LINE__:
Так вот, WaitFor указывает планировщику, что таска заблокирована на указанном объекте, после чего сохраняется состояние. Выполнение с точки case __LINE__: начнётся после того, как объект станет сигнальным. Это чем-то напоминает дескрипторы, на которых выполняются блокирующие вызовы в операционке.
Купите уже «синюю таблетку» на STM32 и пишите под FreeRTOS, ардуино — это детский сад, ясельная группа.
Я ещё в яслях :)
У меня есть stm32, это сильно не то. Там порог входа сильно выше. Там всё очень сложно и даже помигать светодиодом чтобы, надо мозг ломать долго. Ардуина для поделок, я их и делаю.
Год назад делал подобное на С, только по истечению интервала вызывались функции. В итоге вышел менеджер задач на state machine. Через погода нашел это: https://geektimes.ru/post/255770/
Я не хотел делать на колбеках такое изначально. Там выше интересный код с использованием __LINE__ в качестве состояния, кстати. И вообще, макросы заслуживают там вдумчивого чтения.
Я, если и начну делать что-то дальше, сделаю на ООП плюсово и наследованием от класса SmartTask, который будет дёргать методы порождённого класса, а SmartOS :) будет по списку бегать таких тасков.
Конечно я проголосовал за «Полезно», но не пойму чем подход с циклической проверкой времени хуже?
Вернее оно у Вас и реализовано, но поскольку это микроконтроллер, когда я писал для него, казалось целесообразнее делать наиболее понятно, чтобы все сразу на виду. Вот как-то так, или здесь.
Но, наверное, это дело вкуса.
Задача стояла максимально спрятать код, сделать библиотеку а ля ардуина. У меня там кода всего ничего, но он не мешает прочтению кода уже пользовательского, по делу, прячет переменные состояния и таймера в приватные у класса. То есть, не отвлекает от основной логики.
Спасибо. Обе статейки были прочитаны, конечно же.
Через while проще же без delay и у вас какое-то нестабильное моргание, то есть у меня какие-то разные промежутки выдает через Ваш код.

void loop() { q=0;
  while (q<80000) { // здесь можем увеличивать или уменьшать паузу, меняя значение q меньше какого-то численного значения
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  q++;
  }
  q=0;
  while (q<80000) {    
  digitalWrite(LED_BUILTIN, LOW);
  q++; }    // turn the LED off by making the voltage LOW     
  
}

И какой период моргания будет тут? Кстати, какого типа у нас тут q? int?
1) Я мерил на глаз сколько нужно мне (это чуть-меньше секунды вроде) так-то можно точно посчитать вроде через тактовую частоту, но мне лень.
2) float

p/s
Я думал что это банальное решение без delay через while, но что-то поисковик такое решение через while не дает мне. Я вроде читал про такое в первых мануалах про Ардуино.
Чуть больше секунды пауза на глаз, сорри.
Вот полный код:

float q=0;
  


void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
  
}


void loop() { q=0;
  while (q<80000) {// здесь можем увеличивать или уменьшать паузу, меняя значение q меньше какого-то численного значения
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  q++;
  }
  q=0;
  while (q<80000) {    // wait for a second
  digitalWrite(LED_BUILTIN, LOW);
  q++; }    // turn the LED off by making the voltage LOW     
  
}


Я бы за такой код Вас уволил.
Вы меня заинтриговали, что не так? Сначала спрашиваете где q=80000 не int ли это? Можете посмотреть какое максимальное значение принимает int: arduino.ru/Reference/Int
Потом выражаете свое фи, хотя код мегапрост и работает с четким миганием и без delay, что мне и нужно было.
Потому что проблем в этом коде уйма.
Во-первых, использование инкремента для вещественных типов не рекомендуется, поскольку возможны ситуации, когда «проинкрементированное» значение будет равно исходному.
Во-вторых, Ваш код чувствителен к частоте контроллера, к интенсивности возникновения прерываний, к реализации операции digitalWrite (теоретически constexpr-эквивалент может быть выполнен за 1 такт, но если его пока нет, это не значит что он не может появиться).
И да, глобальная переменная с неадекватным именем вырвала глаза.
Sign up to leave a comment.

Articles