как проверить кнопку, не выходя из цикла For

b707
Offline
Зарегистрирован: 26.05.2017

В связи с кучей вопросов на форуме - "Почему код в цикле не отзывается на кнопки?" - возникла бредовая идея, как решить эту проблему100500-й раз. Написал маленький тестовый скрипт для проверки . Если кому-то понравится - буду рад.

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

В общем, предлагаю такой вариант - не как замена millis и прерываниям, а как еще один подход. Везде, куда новички суют delay() - вставляем вызов функции, которая за отведенное для делей время прокручивает несколько коротеньких процедур проверки кнопок или других неотложных залач. И, в отличии от настоящего "вредного" делея раскидываем наш "полезный делей" всюду, где в основном коде намечается пауза.

Приведенный ниже тестовый скетч в loop содержит типичный быдлокод новичка - цикл FOR на 2500 оборотов с делеем. но этот делей - "полезный". Во время делея программа успевает мигать диодом и отслеживать кнопку. Нажатие кнопки - диод перестает моргать, по повторному нажатию - снова мигает. Код надергал и разных веток форума, если кто узнает свой - не обижайтесь. Код работы с кнопками целиком взят из сообщений уважаемого qwone (c)  Компилируется, работает.

#include "Task.h"
#define LED_PIN 13
#define BTN_PIN 9

//#1 кнопка кнопка  -> 9 (btn1_pin) 0  нажата / 1 нет
const int btn1_pin = BTN_PIN;
bool btn1, btn1_old; //нажатие кнопки
bool bounce_btn1 = 0; // антидребезговый флаг
// светодиод на пине 13
bool ledON =LOW;

// Tasks - задачки
// предварительно обьявляем процедуры для наших задачек
void blink();            // процедура мигания светодиодом
void check_button();     //  процедцра проверки состояния кнопки

// создаем обьекты класса Task - параметры: ссылка на процедуру и периодичность запуска в мс
Task blink_task(&blink,200);
Task test_btn(&check_button,200);

void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT); 
btn1 = digitalRead(BTN_PIN);
Serial.begin(9600);

blink_task.start(); // запускаем таймеры обоих задач
test_btn.start();
}
/*************************************************************/
void loop() {
 
 for (int i = 0; i < 2500; i++){ // крутим цикл или делаем что-то сложное
  //ds.requestTemperatures(); // например считываем температуру с датчиков
  Serial.print("Cycle count: ");
  // или подключаемся к инету и ждем ответа...
  Serial.print(i);
  Serial.println();
  nblk_Delay(1000);  // и тут - делей 1s
  }
}

/*************************************************************/
void nblk_Delay(unsigned long d) {
unsigned long timeout = millis() + d;
while (timeout > millis()) {
  if (blink_task.timeout() ) { continue;}  //метод .timeout проверяет, не пришло ли время запуска
  if (test_btn.timeout() )   {continue;}   //если да - запускает задачку и возвращает true
                                           //если нет - переходим к следующей
}
}
/*************************************************************/
//процедуры задачек
void blink() {       //мигаем диодом
digitalWrite(LED_PIN, ledON);
ledON = ! ledON;
};  

void check_button() {   // по нажатию кнопки диод перестает мигать, 
                        // при следующем нажатии снова мигает

// код работы с кнопками полностью утащил из ветки про кнопки, автор qwone (c)                        

if (! bounce_btn1 && btn1 != digitalRead(btn1_pin)) { // если прошел фронт изм на выводн
  bounce_btn1 = 1;                                    // выставить флаг
  test_btn.start(20);                                 // ставим след. отсчет кнопки через 20мс
}
else if ( bounce_btn1  ) { // если прошло антидребезговое время 20 ms
 bounce_btn1 = 0;          // то снять флаг
 test_btn.start(200);      // восстанавливаем период опроса кнопки
 btn1_old = btn1;
 btn1 = digitalRead(btn1_pin) ; // прочитать реальное значение на выводе
 if (btn1_old && ! btn1) {      // если обнаружилось что это было нажатие 
   if (blink_task.is_active()) { //и светодиод мигает
     digitalWrite(LED_PIN, LOW); //гасим его
     ledON =LOW;
     blink_task.stop();}         // и останавливаем блинк
   else {
     blink_task.start();}
   }
       
  }
}

 

b707
Offline
Зарегистрирован: 26.05.2017

И еще - зачем это все, если есть millis? Попробую обьяснить.

Традиционно многозадачность (если ее можно так назвать) на ардуине делается при помощи millis. Это всем известно и работает, но на мой вгляд у этого подхода есть существенный недостаток. Чтобы это работало в сложном обьемном коде, он должен быть разбит на кучу небольших и желательно независимых кусков. По-моему, это называется метод конечных автоматов. Мы исполняем маленький кусок программы, запоминаем текущее состояние и возвращаемся в цикл loop(). чтобы проверить кнопки и обработать события. Потом опять возвращаемся к большой задаче. Это хорошо работает, когда таких кусков немного и они независимы. А представьте процесс подключения к удаленному серверу по сети через GPRS -модем? - нужно выставить парметры соединения - подождать ответа. Потом подключится к интернету, запросить IP -  и снова ждать. Потом подключение к серверу... запрос ... ожидание ответа... получение данных ... В общей сложности 8-10 шагов - и каждый с приличными задержками между ними. А остальные процедуры в это время висят. Как решить это при помощи millis ? разбить процедуру подключения к инету на 10 независимых процедур? А если в этой же программе еще три-четыре подобных задачи? - получим 30 процедур и кучу состояний между ними?  - я решил поробовать по другому.

Кстати - если в приведенном мной коде в процедуру nblk_Delay() добавить проверку наличия новых данных, скажем, в сериале - эту функцию можно будет вызывать даже во время ожидания ответа на команды по последовательному порту... наверно :)

Код класса

Task.h

#include <Arduino.h>  
   
class Task {  
        void (* taskP)();
        unsigned long iTimeout;
        unsigned long iDelay;
        bool active;  
      public:  
      	Task (void (* fp)(), unsigned long d = 200);
        void start (unsigned long t);
        void start ();
        void stop ();
        void restart ();
        bool timeout (void); 
        bool is_active (void); 
        unsigned long Time(void);  
    };  

Task.cpp

#include "Task.h"  
    Task::Task (void (* fp)(), unsigned long d) 
    { taskP = fp;
      iDelay = d;
      iTimeout = 0; 
      active = false; 
     }
    
    void Task::start ()  
    { 
      
      iTimeout = millis() + iDelay; 
      active = true; 
      return;  
    };  
    void Task::start (unsigned long t)  
    { 
      iDelay = t; 
      iTimeout = millis() + iDelay; 
      active = true; 
      return;  
    };  
    void Task::restart ()  
    {  
      if (active) {
      iTimeout = millis() + iDelay; }
      return;  
    };  
    void Task::stop (void)  
    {  
      iTimeout = 0; 
      active = false; 
      return;  
    };  
    bool Task::timeout (void)  
    { 
      if ( !active)
      	   return false; 
      if (iTimeout < millis()) {
        (*taskP) (); 
        iTimeout = millis() + iDelay; 
        return true; }
       else 
        return false;   
    }  
    bool Task::is_active (void)  
    { return active;
    }
    unsigned long Task::Time(void)  
     {  
       return iTimeout;  
     }  

сорри за косяки, готов к тухлым помидорам. На ардуино пишу недавно, а с классами так это вообще мой первый опыт

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

b707 пишет:
сорри за косяки, готов к тухлым помидорам. На ардуино пишу недавно, а с классами так это вообще мой первый опыт
Гладко было на бумаге, но забыли про овраги.  Мы не философствуем, а программируем. А программирование это прежде всего практика. Где у вас пример использования данного класса. Вот исходный скетч Ардуины .

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

 Ну и куда что сувать.Так что так и никак иначе

#include "Cl_task.h";//<-- подключаем библиотеку
Cl_task Task;//<-- создаем объект

//-------------Main -----------------
void setup() {
Task.setup();//<-- инициализируем объект

}

void loop() {
Task.loop();//<-- регулярно даем поработать объекту

}

ПС: Вы забыли про два важных метода. Хотя их можно называть иначе.

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

B707 мне даже как-то неудобно говорить, но ты вляпался в богомерзкое переполнение millis. См. строки 46-47 первого скетча. Что там будет при переходе через 0?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

Task.cpp

Строки 19, 26 и 39 - так делать нельзя.

В конкретном проекте можно (если точно известно, что устройство не предназначено для непрерывной работы месяцами) а в универсальном классе нельзя - Вы попадаете на сбой при переполнении счётчика millis().

DoubleFloat
Offline
Зарегистрирован: 08.06.2017

ЕвгенийП пишет:
Строки 19, 26 и 39 - так делать нельзя. В конкретном проекте можно (если точно известно, что устройство не предназначено для непрерывной работы месяцами) а в универсальном классе нельзя - Вы попадаете на сбой при переполнении счётчика millis().

ЕвгенийП, давно и с удовольствием читаю Ваши, а также  dimax и wdrakula сообщения и темы.

Много раз задумывался над аналогичной проблемой. При риске переполнения или тащить на себе как черепаха 128, 256, ...  битный счетчик или каждый раз проверять пограничную ситуацию переполнения. Имхо надо смотреть где "меньше расходуется топлива" .

b707
Offline
Зарегистрирован: 26.05.2017

Ворота, ЕвгенийП - абсолютно с вами согласен, в случае переполнения millis() будет сбой. Но, как говорится, "когда это еще будет...". :)

Если серьезно, не думаю что это сложно исправить. В конкретном скетче достаточно добавить переменную , куда сохранять текущее время при входе в  функцию nblk_Delay(). Если при очередном входе текущее значение окажется меньше предыдущего - - значит системное время перешло через ноль. В этом разе достаточно перезапустить все задачки методом task.restart() - сбой ритма будет лишь в одном цикле, потом опять 2 месяца все будет работать.

Конечно, это костыль. Как слелать это внутри класса - надо подумать.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

не думаю что это сложно исправить.

Исправить это намного проще, чем Вы здесь написали. Достатчоно не прибавлять интервал к стартовому времени, а наоборот, вычитать интервал из текущего millis() и результат сравнивать со стартовым временем (или вычитать стартовое время из текщего millis() и сравнивать с интервалом). Всё - проблема исчезла.

Посмотрите пример на этом сайте. При таком как там подходе проблема просто не возникает и решать её не надо.

b707
Offline
Зарегистрирован: 26.05.2017

qwone пишет:

 Где у вас пример использования данного класса. Вот исходный скетч Ардуины .

Вообще я полагал, что пример использования - это скетч в первом сообщении. Там есть иннициализация и отработка. Но если это неправильно - можно описать и в пустом скетче

#include "Task.h"

//предварительное обьявление callback функций
void blink();            
void check_button();  

//иннициализация двух экхемпляров класса
Task blink_task(&blink,200);
Task test_btn(&check_button,200);

void setup() {
  // put your setup code here, to run once:

  blink_task.start(); // запускаем таймеры обоих задач
  test_btn.start();
}

void loop() {
  // put your main code here, to run repeatedly:

blink_task.timeout();     // аналог Task.loop() 
test_btn.timeout();
}

//реализация callback функций
void blink() {
} ;           
void check_button() {
} ;

 


ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

DoubleFloat пишет:
каждый раз проверять пограничную ситуацию переполнения. Имхо надо смотреть где "меньше расходуется топлива" .

См. мой ответ B707.

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

b707 пишет:

не думаю что это сложно исправить.

Исправить это намного проще, чем Вы здесь написали. Достатчоно не прибавлять интервал к стартовому времени, а наоборот, вычитать интервал из текущего millis() и результат сравнивать со стартовым временем (или вычитать стартовое время из текщего millis() и сравнивать с интервалом). Всё - проблема исчезла.

Посмотрите пример на этом сайте. При таком как там подходе проблема просто не возникает и решать её не надо.

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

Может это срабатывает, потому что счетчик времени у нас беззнаковый и вместо огромного отрицательного числа при вычитании мы получим маленькое положительное? Честно говоря. я сильно плаваю в том, что происходит с конкретными типами в С при выходе за границы диапазона.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, поищите, здесь на форуме это столько раз обсуждалось. Там просто на пальцах и на картинках разбирали.

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

Ну, поищите, здесь на форуме это столько раз обсуждалось. Там просто на пальцах и на картинках разбирали.

Хорошо, поищу.  может просто не обратил внимания.

Но этот косяк, как я понимаю, исправляется одной заменой знака. А в целом про идею что скажете? - в мусор? или "для сельской местности сойдет"?

Планирую заменить своими неблокирующими делеями таймауты в сетевых соединениях.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

А в целом про идею что скажете?

Ну, идея как идея. Вполне имеет право на жизнь. Сам я делаю это намного проще. Я просто написал одиночную функцию типа такой (под рукой нет, напишу "от руки", отнеситесь с понимаем к опечаткам :))

typedef int (* TBackgroundActions)(void);

int _delay(const uint32_t pause, TBackgroundActions backgroundAction = NULL) {
	const uint32_t startTime = millis();
	while (millis() - startTime <= pause) {
		if (backgroundAction) {
			const int baRes = backgroundAction();
			if (baRes) return baRes;
		}
	}
	return 0;
}

Как видите,  к _delay можно обратиться как к обычном delay, а можно указать второй параметр - call-back функцию, которую нужно вызывать в процессе ожидания. call-back функция должна вернуть 0, если ожидание надо продолжать и не 0, если надо прервать. Сама же функция _delay возвращает 0 - если дождалась интервала или то, что вернула call-back функция, если выход был преждевременным.

 

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

Ну, идея как идея. Вполне имеет право на жизнь. Сам я делаю это намного проще. Я просто написал одиночную функцию типа такой (под рукой нет, напишу "от руки", отнеситесь с понимаем к опечаткам :))

typedef int (* TBackgroundActions)(void);

int _delay(const uint32_t pause, TBackgroundActions backgroundAction = NULL) {
	const uint32_t startTime = millis();
	while (millis() - startTime <= pause) {
		if (backgroundAction) {
			const int baRes = backgroundAction();
			if (baRes) return baRes;
		}
	}
	return 0;
}

Как видите,  к _delay можно обратиться как к обычном delay, а можно указать второй параметр - call-back функцию, которую нужно вызывать в процессе ожидания. call-back функция должна вернуть 0, если ожидание надо продолжать и не 0, если надо прервать. Сама же функция _delay возвращает 0 - если дождалась интервала или то, что вернула call-back функция, если выход был преждевременным.

Ну, собственно, я на гениальность открытия и не претендовал :) перед тем как "запилить" свой код - сделал поиск по Гуглю, так как был уверен, что кто-то наверняка уже 200 раз писал что-то подобное. (я вообще предпочитаю не писать сам, а искать в гугле - в 95% случаев нахожу :)

Как ни странно, но на 3-4 первых страницах гугля подходящих ссылок не нашлось.

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, да, я ж говорю, что у меня значительно проще. Что преимущество, а что нет - вопрос неочевидный, т.к. зависит от задачи. Для моих задач меня вполне устраивает фиксированная функция.

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

Ну, да, я ж говорю, что у меня значительно проще. Что преимущество, а что нет - вопрос неочевидный, т.к. зависит от задачи. Для моих задач меня вполне устраивает фиксированная функция.

Согласен, у меня тоже класс можно выкинуть и просто вызывать nblk_Delay() и в ней отрабатывать бекграунд задачки. Вероятно, при подходе к исчерпанию памяти так и надо будет поступить. Хотя вроде класс занимает очень мало места.

Но тут я писал тестовый пример - хотелось сделать удобно. Тем более никогда не писал своих классов, надо было когда-то попробовать... :)

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

зачем это всё?

void setup() {
pinMode(13, OUTPUT); // светодиод
pinMode(3, INPUT_PULLUP); // кнопка
}

void loop() {
digitalWrite(13, !digitalRead(3) && (millis() >> 10) & 1); // мигаем с периодом 1024 при нажатой кнопке.
// вместо digitalRead(3) - любую переменную
// вместо (millis() >> 10) & 1 - любую переменную
// цикл? а, loop - тебе не цикл?
}

 

b707
Offline
Зарегистрирован: 26.05.2017

Клапауций 112 пишет:

зачем это всё?

Если в твоем коде цикл loop() исполняется дольше периода морганий - ритм будет сбиваться.

Добавлю - цикл for в исходном примере вставлен ровно для того, чтобы показать, что подход работает даже с ним. Использовать for вместо loop() я не предлагаю.

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

b707 пишет:

Если в твоем коде цикл loop() исполняется дольше периода морганий - ритм будет сбиваться.

если в твоём коде цикл for исполняется дольше периода морганий - римт будет сбиваться.

b707 пишет:

Добавлю - цикл for в исходном примере вставлен ровно для того, чтобы показать, что подход работает даже с ним. Использовать for вместо loop() я не предлагаю.

добавлю ещё - блинк с делаями исправно работает в циклах for

=====

*кто в курсе, что ТС нам пытается донести?

b707
Offline
Зарегистрирован: 26.05.2017

Клапауций 112 пишет:

если в твоём коде цикл for исполняется дольше периода морганий - римт будет сбиваться.

не будет. В примере из первого сообщения цикл for исполняется больше секунды, а период мигания диода - 200мс.

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

b707 пишет:

Клапауций 112 пишет:

если в твоём коде цикл for исполняется дольше периода морганий - римт будет сбиваться.

не будет. В примере из первого сообщения цикл for исполняется больше секунды, а период мигания диода - 200мс.

ок. вставь в луп и свой фор делай(1000) и убедись, что - нет.

ты же утверждаешь, что всё будет исправно работать.

========

мой пример не будет работать, если в луп воткнуть что-то задерживающее его на 1 секунду(например), а твой - будет.

так, докажи.

b707
Offline
Зарегистрирован: 26.05.2017

Клапауций 112 пишет:

мой пример не будет работать, если в луп воткнуть что-то задерживающее его на 1 секунду(например), а твой - будет.

так, докажи.

Загрузи скетч из первого сообщения и убедись. Ты написал "если в твоём коде цикл for исполняется дольше периода морганий - римт будет сбиваться.".  Мой код без всяких изменией полностью соответвует твоему запросу.-  цикл исполняется дольше секунды и ритм не сбивается.

Конечно, если в цикл for добавить именно длинный delay, например (1000) - мигание собьется. Но разве я где-то утверждал, что будет иначе?  Зато если добавить не один delay(1000), а 5 delay(100), а между ними 5 моих nblk_Delay(100) - диод будет продолжать мигать. Если взять не delay(1000), а какую-то реальную длинную процедуру - в нее тем более не проблема запихать  вызов бекграунд-операций через каждые 50-100 мс основного кода.

Я не настаиваю. Не надо тебе этого - не пользуйся. Кажется чушью - готов выслушать аргументы.

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

b707 пишет:

Конечно, если в цикл for добавить именно длинный delay, например (1000) - мигание собьется. Но разве я где-то утверждал, что будет иначе?  

добавь не делай, а ещё один цикл длительностью более периода моргания.

добавь в свой код в луп что-то или делай(100500) и убедись, что твой фор будет срабатывать с периодом более периода мигания.

ты же мне предлагаешь цикл луп стормозить до периода более цикла мигания - так, сделай тоже со своим for и убедись, что твои претензии к моему коду необоснованы - вместо 100 строк твоего кода, одна строка моего.

phoenixoid
Offline
Зарегистрирован: 14.07.2015

ЕвгенийП пишет:

b707 пишет:

не думаю что это сложно исправить.

Исправить это намного проще, чем Вы здесь написали. Достатчоно не прибавлять интервал к стартовому времени, а наоборот, вычитать интервал из текущего millis() и результат сравнивать со стартовым временем (или вычитать стартовое время из текщего millis() и сравнивать с интервалом). Всё - проблема исчезла.

Посмотрите пример на этом сайте. При таком как там подходе проблема просто не возникает и решать её не надо.

Простите, а данном случае не случится так, что таймаут может удвоится если переменные сравняются при таком значении millis, которому до переполнения на одну миллисекунду меньше чем установленый интервал?  

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

phoenixoid пишет:

Простите, а данном случае не случится так, что таймаут может удвоится если переменные сравняются при таком значении millis, которому до переполнения на одну миллисекунду меньше чем установленый интервал?  

этот сериал будет длиться вечно...

p19cc0r31d3ig1gb1c5unv8v0i.gif

b707
Offline
Зарегистрирован: 26.05.2017

Клапауций 112 пишет:

 убедись, что твои претензии к моему коду необоснованы - вместо 100 строк твоего кода, одна строка моего.

да нет у меня претензий к твоему коду. Эти коды не только делают одно и то же, они по сути одинаковы.  Твой код значительно короче. А мой универсальнее.

 

phoenixoid
Offline
Зарегистрирован: 14.07.2015

Клапауций 112 пишет:

phoenixoid пишет:

Простите, а данном случае не случится так, что таймаут может удвоится если переменные сравняются при таком значении millis, которому до переполнения на одну миллисекунду меньше чем установленый интервал?  

этот сериал будет длиться вечно...

p19cc0r31d3ig1gb1c5unv8v0i.gif

Да понял я, понял что лыжи едут))) Просто почему то стукноло в голову что если из меньшего unsigned вычесть большее то будет 0...

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

b707 пишет:

да нет у меня претензий к твоему коду. Эти коды не только делают одно и то же, они по сути одинаковы.  Твой код значительно короче. А мой универсальнее.

ты не понял - это у меня есть претензии к твоему коду: твой код тормозит луп, и если в луп есть ещё 50 таких же for, как ты понаписал, то представь, что произойдёт.

b707
Offline
Зарегистрирован: 26.05.2017

Клапауций 112 пишет:

ты не понял - это у меня есть претензии к твоему коду: твой код тормозит луп, и если в луп есть ещё 50 таких же for, как ты понаписал, то представь, что произойдёт.

Просто так напихивать задержки в loop() я не предлагаю. Во втором сообщении темы я написал, чем меня не устраивает millis и в каких случаях мой код будет полезен, как мне кажется. Я планирую вставлять свои делеи в те места программы, где система и так простаивает, например ожидая ответа по сетевому или последовательному каналу.

И чего ты привязался к циклу For ? Это не мой цикл :) Я не буду вставлять его в loop :) Ты прочитай. что я написал в самом начале: "...приведенный ниже тестовый скетч в loop содержит типичный быдлокод новичка - цикл FOR на 2500 оборотов с делеем...". Это код новичка, понимаешь? скопированный из соседней ветки для тестирования.

Пример использования моего класса в чистом виде - см. в сообщении #8

Клапауций 112
Клапауций 112 аватар
Offline
Зарегистрирован: 01.03.2017

чем дальше в лес...

Бредовые синдромы
Суждения и умозаключения больных внешне производят впечатление вполне логичных, однако они исходят из неверных посылок и ведут к неверным выводам.

b707 пишет:

Традиционно многозадачность (если ее можно так назвать) на ардуине делается при помощи millis. Это всем известно и работает, но на мой вгляд у этого подхода есть существенный недостаток. Чтобы это работало в сложном обьемном коде, он должен быть разбит на кучу небольших и желательно независимых кусков. По-моему, это называется метод конечных автоматов. Мы исполняем маленький кусок программы, запоминаем текущее состояние и возвращаемся в цикл loop(). чтобы проверить кнопки и обработать события. Потом опять возвращаемся к большой задаче. Это хорошо работает, когда таких кусков немного и они независимы. 

b707
Offline
Зарегистрирован: 26.05.2017

Предлагаю образец использования неблокирующих таймаутов (описанных в первых двух постах темы) при связи с медленными устройствами. В качестве примера взята установка соединения с GPRS модемом AThinker A6 через SoftwareSerial.

Особенность модема А6 в том, что он всегда стартует на скорости порта 115200. Это даже породило слухи, что модем других скоростей не поддерживает. Но это неверно. Этот модем способен самостоятельно подстраиватся под скорость обмена. Чтобы общаться с модемом на скорости например 9600, необходимо несколько секунд подряд посылать ему команды на этой скорости, пока он не начнет устойчиво отвечать "ОК".

Скетч ниже реализует такое подключение - подпрограмма waitConnect в цикле посылает модему А6 команду "АТ", 300мс ожидает ответа, потом делает перерыв 500мс и начинает заново. Условием выхода является три подряд "ОК" от модема или таймаут 10сек.

Чтобы во время этих 10 сек программа не "замирала", задержки 300 и 500мс сделаны неблокирующими, в это время выполняется процедура nblk_Delay, которая проверяет состояние кнопок, мигает светодиодами и тд. Эта же процедура продолжает следить за активностью на порту SoftwareSerial - если в течении таймаута модем откликнется, выполнение бекграунд-задач будет тут же прервано и управление вернется в функцию waitConnect.

Код функции waitConnect основан на скетчах, выложенных в ветке про модем А6 на этом сайте. Автор скетчей - Logic

Код был загружен в Уно и проверен. Для улучшения наглядности при публикации из скетча выкинуты части кода, относящиеся к кнопкам. Они есть в скетче в первом сообщении ветки. Описание класса Task - во втором сообщении темы.

#include <SoftwareSerial.h>
#include "Task.h"
#define LED_PIN 13
#define BTN_PIN 9

SoftwareSerial A6board(10, 11); // RX, TX

// иннициализация кнопок и светиков
// .. выкинуто для улучшения наглядности

// предварительное обьявление процедур
void nblk_Delay(unsigned long d, bool watch_serial = false );

// Tasks - бекграунд-задачки
void blink();            //  процедурая мигания светодиодом
void check_button();     //  процедцра проверки состояния кнопки


// создаем обьекты класса Task 
// параметры: ссылка на процедуру и периодичность запуска в мс
Task blink_task(&blink,300);
Task test_btn(&check_button,200);

/*************************************************************/
void setup() {

Serial.begin(9600);
A6board.begin(9600);

// запускаем таймеры бекграунд задач
blink_task.start();
test_btn.start();

// задержка для включения модема
nblk_Delay(1000);   

// подключение к модему
// процедура waitConnect(timeout) - код процелуры см в конце скетча
// подключение может занять длительное время, поэтому во время ожидания
// ответов от модема вызывается nblk_Delay для отработки бекграунд задач
Serial.println("Connect to modem");
// мы можем не стесняться ставить большой таймаут (10s)
// поскольку он неблокирующий, программа не "заснет"
if (waitConnect(10000))       
   { Serial.println("Connect OK");
     blink_task.start(1000);} // замедляем мигание светодиода если OK
else
   Serial.println("Connect fail");
}

/*************************************************************/
void loop() {
  // двухсторонний обмен между монитором и модемом
  if (A6board.available()) 
     Serial.write(A6board.read());
  if (Serial.available()) 
     A6board.write(Serial.read());
  
  nblk_Delay(1000,true); // неблокирующий таймаут 1сек с отслеживанием Serial
  
}

/*************************************************************/
// процедурв выполнения бекграунд задач во время задержек в основной программе
// параметры - timeout - длительность задержки, мс
//           - watch_serial - флаг, нужно ли отслеживать активность портов
void nblk_Delay(unsigned long timeout, bool watch_serial = false ) {
unsigned long msD = millis();
while (millis() - msD < timeout) {

  // если watch_serial == true, начинаем с проверки активности портов
  if (watch_serial) {           
  
    if (A6board.available())    // если с модема пришли данные 
      return;                   // срочно возвращаемся в основную программу
    if (Serial.available())
      return;
  }
  // сюда помещаем проверку таймаутов, установленных для задач
  if (blink_task.timeout() )     // метод .timeout проверяет, не пришло ли 
   { continue;}                  // время запуска задачи
  if (test_btn.timeout() )       // если да - запускает ее и возвращает true
    {continue;}                  // если нет - переходим к следующей
                                          
}
}
/*************************************************************/
// Подключение к модему A6 AThinker
bool waitConnect(long timeout) {
  byte countok = 0;
  String str;
  uint32_t tstart = millis();
  uint32_t ms = millis();
  while(countok<3)                // нам нужно от модема три ответа ОК подряд
  {  
      ms = millis();
      if( ( ms - tstart) > timeout )
        return false;
    
      str = "";
                     
      A6board.println("AT");       // посылаем команду
      nblk_Delay(300, true);       // Ждём до 300 мс, выполняя бекграунд задачи
                                   // и отслеживая активность модема
                                   // если модем отзовется раньше - 
                                   // программа немедленно вернется из таймаута
      while(A6board.available())
      {
        char ch=A6board.read();
        str+=ch;
      } 
      if(str.indexOf(F("OK"))>-1)  // если ответ ОК - увеличиваем счетчик
        countok++;
      else
        countok = 0;
      
      nblk_Delay(500);             // делаем перерыв между командами 500 мс  
    
  }  
  return true;
}
/*************************************************************/
//бекграунд процедуры 

void blink() {                     // мигаем диодом
digitalWrite(LED_PIN, ledON);
ledON = ! ledON;
};  

void check_button() {              // отслеживаем кнопки

// код работы с кнопками...
}

 

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП - на тему переполнения millis()

Я не нашел обсуждения, о котором вы говорили, поэтому сделал проще - написал скетч для Уно и посмотрел, как происходит переполнение типа unsigned long и проверил разные комбинации расчета таймаутов - через сложение и через вычитание. Я, конечно, убедился, что метод вычитания всегда дает правильный результат, а сложение ошибается. если конец интервала находится до переполнения millis. а текущее время - после. Причем это так просто, что мне стало слегка стыдно, что я не додумался до этого без теста.

К сожалению, исправить код в первых двух сообщениях форума я уже не могу.

Logik
Offline
Зарегистрирован: 05.08.2014

Спасибо, b707, за ссылку на мое авторство скетча, хотя чесно говоря узнаю его с трудом :)

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

b707
Offline
Зарегистрирован: 26.05.2017

Logic

спасибо за отклик. Конечно же, подход "неблокирующих таймаутов" применим далеко не в любом случае. Но я ни в коем случае не отрицаю других вариантов решения таких задач. Думаю, что лучший вариант - это комплекс из нескольких подходов. Очевидно, что таймауты больше подходят для запуска мелких действий, требующих быстрого отклика. А для контроля основного функционала программы больше подойдет переход от состояния в состояние с матрицей событий.

Я сейчас ваяю небольшой проект - автоматические весы с контролем данных по СМС чере модем А6. Соответственно, в программе две главных ветки: первая - состояние весов (иннициализация, установка ноля, периодическое взвештвание, уточнение изменений). Вторая ветка - состояние модема: подключение к сети, прием и отправка СМС, выход в инет. Работу программы с этими двумя главными частями думаю сделать  крупными процедурами в виде "состояний".  А мелочевку вокруг - мигание статусных диодов, отклик на кнопки - оформить через подход "неблокирующих таймаутов". Для крупных процедур не так важна привязка ко времени - нет ничего страшного, если взвешивание произойдет не через 10 минут, а через 11. Зато отклик на кнопку должен быть быстрым - и тут мои "таймауты"  дают преимущество.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

b707. Вот вы сейчас начали писать программу.Но что такое "хорошо написанная программа". Это не та что просто работает, и в ней две (три,... )ветки. Скорее всего это универсальный каркас(фреймворк), который обеспечивает возможность фунционирования этих веток и главное диагностировать ошибки в программе, модифицировать по сиюминутным требованиям заказчика и наконец если появится еще один заказчик с совершенно постороним проектом, то небольшое движение рук и ваша "старая-новая" программа уходит еще к одному "довольному заказчику". Так что ваши "таймауты" надо смотреть с точки зрения структурированости вашей программы. А если у вас структуры программы не наблюдается, то уже никакие приемы не могут ее "испортить".

 

Logik
Offline
Зарегистрирован: 05.08.2014

qwone пишет:

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

Неее.. форум любительский. Он сам себе заказчик )) Потому "хотелки" с "умелками" балансируются четко. Здесь таких проблем нет. А у заказчика баланс достигается применением "платилок" ;)

vidro_boltiv
vidro_boltiv аватар
Offline
Зарегистрирован: 18.10.2016

Готов к тухлым помидорам. Но есть ли желающие помочь? (Откоректировать, или написать робоччий код - за определенную суму денег.).

 

b707
Offline
Зарегистрирован: 26.05.2017

qwone пишет:

b707. Вот вы сейчас начали писать программу.Но что такое "хорошо написанная программа". Это не та что просто работает, и в ней две (три,... )ветки. Скорее всего это универсальный каркас(фреймворк), который обеспечивает возможность фунционирования этих веток и главное диагностировать ошибки в программе, модифицировать...   <skip>    Так что ваши "таймауты" надо смотреть с точки зрения структурированости вашей программы. А если у вас структуры программы не наблюдается, то уже никакие приемы не могут ее "испортить".

qwone, мне кажется я понимаю, что вы имеете в виду. Мне самому стилистически и эстетически мои "таймауты" не очень нравятся. Сейчас в них чувствуется что-то от операторов goto :)

С другой стороны - они функциональны и в них есть смысл. Во многих языках высокого уровня, ориентированных на обработку событий ( events driven) - есть методы вроде Application.DoEvents() - позволяющие выполнять теневую обработку событий во время простоя основной ветви программы. Возможно, мои попытки прикрутить что-то подобное к ардуине пока выглядят как "заплатка" или типичный "костыль", ломающий структуру программы. Однако я надеюсь, что их можно сделать более органичными  хорошому стилю программирования .

Кстати, тема про стиль программирования, каркас и строение программы очень интересна. Очевидно, что традиционная "классическая" линейная структура программы плохо подходит для ардуино. Тут нужна схема, основанная на событиях, примерно как в программировании графических интерфейсов. Но в то же время все совсем не так - в ардуино нет операциоснной системы и ограниченные ресурсы. Было бы классно это обсудить в форуме. Я бы с удовольствием прочитал какие-нибудь "Этюды для начинающих" про различные стили и правильные "каркасы" программ для МК.

b707
Offline
Зарегистрирован: 26.05.2017

Logik пишет:

Неее.. форум любительский. Он сам себе заказчик )) Потому "хотелки" с "умелками" балансируются четко.

это точно, практически всегда программирую для себя - либо решить какую-то задачу, либо просто для удовольствия. Но именно поэтому хочется, чтобы программа не просто работала, а была красивой :) Хоть этого никто и никогда не увидит кроме меня. Мне кажется, красивую и четко выстроенную программу и писать проще, и работает она лучше. Жаль только, что это редко получается :)

Logik
Offline
Зарегистрирован: 05.08.2014

Обсудить это не очень получится. Слишком много народу на форум  пришлых с высокоуровневых интерпретаторов. Они засерают своими стереотипами любую попытку конструктивного разговора. 

Да, событийная модель в крупных проектах чуть ли не единственно возможный подход. События возникают и обрабатываются машинками состояний в быстром лопе. Но генерация событий очень неровномерная и чтоб лоп не тормозился а стек не переполнялся приходится события таки сохранять в очереди FIFO. Код выходит путаный прилично (восновном изза протеворечий между логической разветвленной иерархией обектов и "плоской" моделью событий), но по эффективности сильно превосходит все остальное. И четко разделять какая подзадача на сколько к времени критична и где ей работать: что в прерыванияи, что в лопе, что по по времени через милис, что из лопа но только при пустой очереди  событий, что разделить на части и вызывать из лопа по частям.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Самая большая проблема определить чем выражать событие. Обычно я считал, что собитие это bool переменая , ли по крайней мере byte. Но скорее я склоняюсь к тому что у события нет вообще переменной. Переменая на событие это ненужный расход памяти.  Событие состоит из двух частей "генератора события" и "обработчика события"  . Понятно что обработчик события это указатель на функцию .  Если у вас обработчики события обрабатывают долго, то их пихают в очередь стек и так далее. Что же такое "генератор события" Вот у нас кнопка . Вот ее текущая обработка и вызов нужного обработчика и есть  "генератор события" Так что "генератор события" может иметь несколько "обработчиков" ,но на каждый вариант события.

 Фреймворк (каркас) программы это правила сборки программы , что бы программа легко собиралась и отлаживалась . И отдельные компоненты делаются так что бы они легко собирались в конструкцию. Посмотрите Лего конструктор . У каждой делали есть шипы и впадины. И таким путем они леко собираются в конструкцию. Но только свои. Чужие туда не подойдут.  ОС на Ардуине можно нарисовать. Но вся проблема в том , что народу это не надо. Народ заточен на реализацию "недалеких целей" и напрягаться не будет, для изучения более серьезного. Тут просто глубокое изучение Си рассматривается за экзотику.

Logik
Offline
Зарегистрирован: 05.08.2014

Событие не может быть привязано к обработчику. Мало того обработчиков может быть много для одного события и они могут менятся по ходу работы. Потому указатели на функцию я для этого не использую. Использую либо байт для ID события либо структуру из ID события + указатель на нечто который может интерпретироватся иногда и как 2 байта данных. Чаще первый подход бывает, по крайней мере если речь о кнопках, экране, тачскрине, час реального времени и датчиках то так. Второй требуется если много однообразных обектов - массив датчиков например, тогда данные лучше держать вместе с ID события.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Как это событие не может быть привязано к обработчику. Если событие произошло , то его надо обработать. А если событие произошло и его обрабатывать не надо, то зачем выявлять такое событие. Исключение для событий когда иногда надо обрабатывать, а когда нет. Ну тогда когда не надо , то можно поставить заглушку. 

Дальше . указатель на функцию, для событийного проектирования , то что доктор прописал. Но бывают языки в которых нет указателя на функцию, тогда приходится городить посредника - на switch. Что очень неудобно. Надо добавить в программу новые события , добавляем в switch и это.   А так новые функции-новые обработчики.

  Дальше указатели на функцию бывают , как это удивительно кому-то, разные.  void (*func)(void)- это когда сделать один раз и забыть.size_t  (*func)(void)- повторять регулярно пока к примеру не возвратиться 0. Обработать датчик номер N. void (*func)(int)  если у вас устройство на другой стороне медленного канала тогда будет структура из двух указателей и очередь элементов структуры . Первый указатель это передача заданого пакета, а второй указатель на анализ принятого.  Передатчик регуляно передает пакеты из всей очереди по перврму, а приемник проверяет принятые пакеты по вторым указателям. Если успешно строчку выкидывает. Разумеется этот механизм еще сложнее, но в общих чертах так будет проще.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

vidro_boltiv пишет:

Готов к тухлым помидорам. Но есть ли желающие помочь? (Откоректировать, или написать робоччий код - за определенную суму денег.).

Вы не по адресу обратились. Корректируют и пишут за определённую сумму в разделе Ищу исполнителя

Logik
Offline
Зарегистрирован: 05.08.2014

qwone пишет:

Как это событие не может быть привязано к обработчику. Если событие произошло , то его надо обработать. А если событие произошло и его обрабатывать не надо, то зачем выявлять такое событие. 

Да так, создаются и  обрабатываются в архитектурно разных уровнях вобщем то ничего не знающих друг о друге, ну кроме имени события. На этапе компиляции -имя при исполнении понятно номер. А выявляются такие события по причине того, что генератору событий вобще ничего не известно о том будет ли оно обрабатыватся или нет. Пример - да пожалуста. Висит окно требующее подтверждения кнопки ОК, и ничего другого, но драйвер клавиатуры всеравно фиксирует все нажатия и сыпет их на все обработчики, а уже они решают что им важно , а что нет.

qwone пишет:

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

Исключения будут всегда, когда обработчик не один. Хоть 0, хоть 2.

 

qwone пишет:

Дальше . указатель на функцию, для событийного проектирования , то что доктор прописал. Но бывают языки в которых нет указателя на функцию, тогда приходится городить посредника - на switch. Что очень неудобно. Надо добавить в программу новые события , добавляем в switch и это.   А так новые функции-новые обработчики.

  Дальше указатели на функцию бывают , как это удивительно кому-то, разные.  void (*func)(void)- это когда сделать один раз и забыть.size_t  (*func)(void)- повторять регулярно пока к примеру не возвратиться 0. Обработать датчик номер N. void (*func)(int)  если у вас устройство на другой стороне медленного канала тогда будет структура из двух указателей и очередь элементов структуры . Первый указатель это передача заданого пакета, а второй указатель на анализ принятого.  Передатчик регуляно передает пакеты из всей очереди по перврму, а приемник проверяет принятые пакеты по вторым указателям. Если успешно строчку выкидывает. Разумеется этот механизм еще сложнее, но в общих чертах так будет проще.

Теперь понятно становится. То что вы описываете - это калбеки. Штука хорошая, использую где уместно, например http://arduino.ru/forum/apparatnye-voprosy/gsm-modem-a6-v-rezhime-tcp Даже таблица имеется EventsMap)) Но это  не событийная модель. Не путайте их. Калбеки не заменяют событиях. Это скорей частный случай позволяющий "срезать путь" где ситуация позволит))  В том коде позволила, и вот почему - там всегда выполняется соответствие одно событие-всегда один обработчик константа. Кроме обработчиков EventOk и EventERROR, для них тоже один обработчик, но переменная. Приходится делать подмену, сохранять старые и возвращать их обратно. Явно лишняя возня.

 А в общем случае правило одно событие-один обработчик не выполнимо. Вобщем выше уже это писал. Например событие часов, ежесекундное, в нем как правило заинтересованы почти все модули в программе - кому на экране время переписат, кому датчики обновить, кому на сервер отправить.. А калбек один у модуля часов. Если сделаем ему несколько калбеков (а сколько? ;) - плохо  архитектура модуля зависеть от числа и типа других модулей будет. Прийдется городить разветвитель - плохо, лишняя сущность и зависит от всего, что есть в программе. И городить его под каждое событие, т.к. типы указателей на функции разные, не я знаю что их можна привести к void* ;) но то отдельная песня. 

Отлаживать калбеки - тоже гемор еще тот. Ну вывели указатель. И чего он может сказать, если он от сборки к сборке меняется. Куда проще вывести и понять фиксированый ID.

А switch - ну есть. А это плохо? В нем все просто и ясно. Вот событие, вот реакция. Если претензии к его скорости - так делаем массив указателей, где индекс - то самое ID. 

Важней другое - событийная модель позволяет реализовать и управлять (например разветвлять) потоком событий централизовано, т.к. события однотипные. А калбеки разные, как Вы и заметили были. И централизация там никак

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

калбеки и то что я описал разные вещи. Но я понял,что или я не правильно описываю, а вы неправльно понимаете. Так что объснить вам не смогу.Но пока меня этот подход устраивает. 

Logik
Offline
Зарегистрирован: 05.08.2014

Были бы разные - легко бы обяснялось ;)

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Разные. Общее в них только одно, что под скобки ставится указатель на функцию. Вы же не путаете классы и отдельные функции. А от том что я говорю, даже каллбек классом нельзя назвать. , хотя бы потому что вызывается методом находящимся в loop, а не вызывается какой-то функцией.

Logik
Offline
Зарегистрирован: 05.08.2014

Я понимаю, что каждій художник и видит по своему, но давайте всеже придерживатся общепринятой трактовки терминов.

"Callback (англ. call — вызов, англ. back — обратный) или фу́нкция обра́тного вы́зова впрограммировании — передача исполняемого кода в качестве одного из параметров другого кода. https://ru.wikipedia.org/wiki/Callback_(программирование)

// потому что вызывается методом находящимся в loop, а не вызывается какой-то функцией.

Какая разница откуда оно вызывается. Или по вашему из лопа - не калбек, а таже функция  с тем же указателем вызваная из функции - уже калбек?

Методы класса отличаются от обычных функций наличием неявного параметра и зоной видимости, это уже скоро в садиках учить будут ))) Не уходите от темы.