Skocz do zawartości

Okresowe wołanie funkcji w Arduini


Behlur_Olderys

Rekomendowane odpowiedzi

Hej,

Chciałem się podzielić kodem, który dziś napisałem na potrzeby mojego montażu (do trybu testowo-awaryjnego).

Jest to prosta, ale wszechstronna klasa odpalająca dowolną akcję w ściśle określonych interwałach.

Jeśli np. chcemy, żeby nasz kod wykonywał jeden krok silnika krokowego na 12345 mikrosekundy z dużą dokładnością (kilkanaście us), to jest w sam raz rozwiązanie.

Kod wygląda tak:

 

void setup() {
   Serial.begin(115200);
}

template <typename Callable>  // Callable to dowolny obiekt posiadający operator()
struct ConstantTimedCaller{
  ///////////////////////////////////////////
  ConstantTimedCaller(const unsigned long interval, Callable callable):
  // przekazujemy do konstruktora interwał i instancję obiektu (tworzy kopię!) żeby ją sobie wywoływać. 
    m_timeStart(micros()),
    m_timeAwaited(0ul),
    m_interval(interval),
    m_callable(callable)
  {
    SetAwaitedTime(0ul);
  }

  ///////////////////////////////////////////
  void SetAwaitedTime(const unsigned long leftover){
    m_timeAwaited = (unsigned long)(m_timeStart + m_interval - leftover);
  }
  
  ///////////////////////////////////////////
  // ta funkcja ogarnia odliczanie czasu i należy jej użyć w kodzie tam, gdzie chcemy coś realnie zrobić :)
  ///////////////////////////////////////////
  void CallWhenTime(){
    const unsigned long timeNow = micros();

    if (timeNow > m_timeAwaited){
      m_timeStart = timeNow;
      SetAwaitedTime((unsigned long)(timeNow - m_timeAwaited)); // ta różnica jest kluczowa pomiędzy kolejnymi wywołaniami
      m_callable(); // wywołaj operator() obiektu, który tutaj przekazałeś
    }
  }
  
  unsigned long m_timeStart;
  unsigned long m_timeAwaited;
  const unsigned long m_interval;
  Callable m_callable;
};

///////////////////////////////////////////
// Przykładowa funkcja do testów - wysyła na serial aktualny czas:
///////////////////////////////////////////
void printTime(){
  char lineBuffer[64] = {0};
  sprintf(lineBuffer, "Time is: %20lu", micros());
  Serial.println(lineBuffer);
}

typedef void (*voidFunctionPtr)(); 
// ten typedef jest po to, żeby powiedzieć template'owi jaki typ ma funkcja printTime (bo konstruktor się sam nie domyśli)
ConstantTimedCaller<voidFunctionPtr> serialPrintCaller(1234567ul, printTime);
  
///////////////////////////////////////////
// główna funkcja w Arduino
///////////////////////////////////////////
void loop(){
  serialPrintCaller.CallWhenTime(); // tak się tego używa :)
  
  // a tutaj poniżej już można sobie wywoływać cokolwiek chcemy. serialPrintCaller i tak wywoła się dokładnie o czasie!
  jakasFunkcja();
  costam();
  jakisInnyCallerZInnymInterwalem.CallWhenTime(); // też będzie działał zupełnie niezależnie!
}

 

 

Bardziej skomplikowany przykład użycia:

 

 

struct OnBoardLedLighter{
  OnBoardLedLighter():
    m_isLedOn(false)
  {}

  void operator()(){ // ta funkcja jest odpalana przez callera. Można tu robić cokolwiek!
    toggle();
  }
  
  void toggle(){ // mrugamy ledem...
    if (m_isLedOn){
      digitalWrite(LED_BUILTIN, LOW);
      m_isLedOn = false;
    }else{
      digitalWrite(LED_BUILTIN, HIGH);
      m_isLedOn = true;
    }
  }
  bool m_isLedOn;
};

OnBoardLedLighter onBoardLedLighter;
ConstantTimedCaller<OnBoardLedLighter> ledLighterCaller(900000ul, onBoardLedLighter);
  
void loop(){
    ledLighterCaller.CallWhenTime(); // będzie zapalać i gasić leda co 900 ms
}

 

Żeby sprawdzić, czy dobrze działa włączamy kod z pierwszego przykładu, uruchamiamy Serial Monitor i czytamy:

					różnica	
				
	Time is:	     2469172	1234572	
	Time is:	     3703744	1234576	
	Time is:	     4938320	1234544	
	Time is:	     6172864	1234572	
	Time is:	     7407436	1234572	
	Time is:	     8642008	1234576	
	Time is:	     9876584	1234572	
	Time is:	    11111156	1234552	
	Time is:	    12345708	1234592	
	Time is:	    13580300	1234548	
	Time is:	    14814848	1234556	
	Time is:	    16049404	1234580	
	Time is:	    17283984	1234552	
	Time is:	    18518536	1234580	
	Time is:	    19753116	1234556	
	Time is:	    20987672	1234584	
	Time is:	    22222256	1234560	
------------------------------------------------
Średnia:				1234567.29411765

 

Oczywiście, średnia to już policzone w excelu dla tych danych :)

Jak widać, już kilkanaście próbek daje dosyć fajną średnią zbliżoną do oczekiwanej 1234567 mikrosekund.

Dla 180 próbek średnia wyszła już: 1234567.09090909 :)

To znaczy, że nawet jeśli momentami trochę przyspiesza lub spowalnia (odchylenie standardowe ok.13 us) to zasadniczo w dłuższym okresie czasu jesteśmy naprawdę bardzo dobrze synchronizowani.

 

Nie jest to nic nowego ani odkrywczego, ale wydaje mi się, że to bardzo użyteczny kod, np. do sterowania krokowcem zgodnie z ruchem nieba...

Można mieć kilka takich callerów uruchomionych jednocześnie i będą doskonale współdziałać. To bardzo wygodne.

 

Pozdrawiam!

 

PS: będę badał zachowanie w okolicach 4294967295 mikrosekundy, bo wtedy przekręca się licznik unsigned long (ponad godzinę od uruchomienia). Raczej nie powinno się nic złego dziać, ale może "przeskoczyć" kilka kroków.

Edytowane przez Behlur_Olderys
  • Lubię 2
Odnośnik do komentarza
Udostępnij na innych stronach

Niestety arduino i timery nie bardzo siebie lubią. Jakiś czas temu próbowałem zrobić sobie proste wysyłanie próbek z ADC z częstotliwością 1kHz i co się okazało, miałem bardzo zmienny bitrate, dodatkowo trzeba było szacowac ile taktów zegara zajmie dana funkcja i wliczyć w to opóźnienia. Do naprawdę szybkich i dokładnych akcji polecam STMy z DMA (direct memory access) - wtedy można zrobić prawdziwy system czasu rzeczywistego..

Odnośnik do komentarza
Udostępnij na innych stronach

1 hour ago, sidiouss said:

STMy z DMA

Schodzimy coraz niżej :)

 

Co do samego kodu, dość pokrętnie napisane, np:


m_timeStart = timeNow;
// leftover = timeNow - m_timeAwaited
// in SetAwaitedTime
m_timeAwaited = (unsigned long)(m_timeStart + m_interval - leftover);

// so
m_timeAwaited = m_interval + m_timeAwaited

Propozycja:

// constructor
nextCall = now() + interval;
delta = 0;

//active loop
unsigned long n = now();
if (nextCall - (delta >> 8) < n) {
  nextCall += interval;
  delta = ( (abs(nextCall - n) << 7) + (delta*4) )/5;  // old delta is weighted 4, new delta is weighted 1, this needs to be checked on hardware to determine right weights
  
  
  callWhatever();
}

W zaproponowanym rozwiązaniu:

- kolejne czasy liczę zawsze od bazowego (nextCall += interval), więc niedokładności wywołań nie wpływają na kolejne wywołania,

- kod jest odporny na przepełnienia, po prostu arytmetyka zadziała,

- gdyby stosować proste if (nextCall < n to czasy odchylały by się zawsze w jedną stronę, było by spóźnienie, stąd delta która odchyla to w stronę przeciwną

- delta za każdym razem się koryguje (średnia ważona, waga 4 na starą wartość, waga 1 na nową)

- delta ma 8 bitów dokładności po przecinku, nowa jest przesuwana o 7 bitów bo interesuje nas połowa delty żeby rozłożyć czasu równomiernie po obu stronach

- jeśli na Arduino mnożenie i dzielenie jest drogie (nie wiem tego) to *4 można zamienić na << 2

 

Nie kompilowałem/debuggowałem/testowałem tego kodu, więc ręki nie dam sobie uciąć, że wszystko działa poprawnie - ale pokazuje o co chodzi.

 

EDIT: ta delta może być przedwczesną optymalizacją - bez tego pewnie średnio zadziała tak samo dobrze. 

 

Edytowane przez Sebo_b
  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

Godzinę temu, sidiouss napisał:

Niestety arduino i timery nie bardzo siebie lubią. Jakiś czas temu próbowałem zrobić sobie proste wysyłanie próbek z ADC z częstotliwością 1kHz i co się okazało, miałem bardzo zmienny bitrate, dodatkowo trzeba było szacowac ile taktów zegara zajmie dana funkcja i wliczyć w to opóźnienia. Do naprawdę szybkich i dokładnych akcji polecam STMy z DMA (direct memory access) - wtedy można zrobić prawdziwy system czasu rzeczywistego..

problemy z wysyłką przez UART?

to wina błędnego wyliczenia preskalera. Najprościej zastosować kwarc "przyjazny" dla seriala i wszystkie problemy znikną

 

pozdrawiam

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

12 minut temu, ZbyT napisał:

problemy z wysyłką przez UART?

to wina błędnego wyliczenia preskalera. Najprościej zastosować kwarc "przyjazny" dla seriala i wszystkie problemy znikną

 

pozdrawiam

 

1 minutę temu, Sebo_b napisał:

1kHz? Rozumiem, że to 1 kbps. 20 lat temu na 8051 wyciągałem stabilne 115200bps (choć nie na timerach). Więc 1kHz dzisiaj to nie powinien być żaden problem. 

Panowie, nie prędkość transmisji, bo to jest oczywiste, że najprostszy mikrokontroler będzie w stanie przesłać nawet 1Mbs bez zakłóceń, mi chodziło o próbkowanie ADC 1kHz i przesłanie tego w czasie rzeczywistym, czyli zarejestrować 1000 próbek w ciągu sekundy i potem je przesłać :)

Arduino na 8Mhz nie wyrabia się, bo samo sobie wprowadza opóźnienia związane z próbkowaniem ADC i przesyłaniem przez UART. Gdy zaprogramuje się Arduino jako zwykłego AVRa wykorzystując sam język C, to problem nie występuje. Coś za coś - w Arduino mamy prosty język programowania kosztem niektórych funkcjonalności. W STMie przy próbkowaniu 12 kanałów ADC (multipleksowaniem) i wysyłaniem UARTem przez DMA udało mi się stabilne uzyskać 3000 próbek na sekundę z wysyłaniem 1Mbps :) Ale już nie offtopuję ;)

  • Lubię 3
Odnośnik do komentarza
Udostępnij na innych stronach

Godzinę temu, Sebo_b napisał:

Schodzimy coraz niżej :)

 

Co do samego kodu, dość pokrętnie napisane, np:



m_timeStart = timeNow;
// leftover = timeNow - m_timeAwaited
// in SetAwaitedTime
m_timeAwaited = (unsigned long)(m_timeStart + m_interval - leftover);

// so
m_timeAwaited = m_interval + m_timeAwaited

Propozycja:


// constructor
nextCall = now() + interval;
delta = 0;

//active loop
unsigned long n = now();
if (nextCall - (delta >> 8) < n) {
  nextCall += interval;
  delta = ( (abs(nextCall - n) << 7) + (delta*4) )/5;  // old delta is weighted 4, new delta is weighted 1, this needs to be checked on hardware to determine right weights
  
  
  callWhatever();
}

W zaproponowanym rozwiązaniu:

- kolejne czasy liczę zawsze od bazowego (nextCall += interval), więc niedokładności wywołań nie wpływają na kolejne wywołania,

- kod jest odporny na przepełnienia, po prostu arytmetyka zadziała,

- gdyby stosować proste if (nextCall < n to czasy odchylały by się zawsze w jedną stronę, było by spóźnienie, stąd delta która odchyla to w stronę przeciwną

- delta za każdym razem się koryguje (średnia ważona, waga 4 na starą wartość, waga 1 na nową)

- delta ma 8 bitów dokładności po przecinku, nowa jest przesuwana o 7 bitów bo interesuje nas połowa delty żeby rozłożyć czasu równomiernie po obu stronach

- jeśli na Arduino mnożenie i dzielenie jest drogie (nie wiem tego) to *4 można zamienić na << 2

 

Nie kompilowałem/debuggowałem/testowałem tego kodu, więc ręki nie dam sobie uciąć, że wszystko działa poprawnie - ale pokazuje o co chodzi.

 

EDIT: ta delta może być przedwczesną optymalizacją - bez tego pewnie średnio zadziała tak samo dobrze. 

 

 

Ciekawe, chyba masz rację z tym timeAwaited, ciekawe co się stanie jak tak napiszę :) Może w weekend znajdę trochę czasu...

Ale operatory przesunięcia w Twoim kodzie jakoś mnie zniechęcają strasznie :)

Odnośnik do komentarza
Udostępnij na innych stronach

1 hour ago, Behlur_Olderys said:

 

Ciekawe, chyba masz rację z tym timeAwaited, ciekawe co się stanie jak tak napiszę :) Może w weekend znajdę trochę czasu...

Ale operatory przesunięcia w Twoim kodzie jakoś mnie zniechęcają strasznie :)

Może za mało embedded kodu widziałeś :) Można to owrapować w define'y - ale moim zdaniem wtedy nie jest czytelne. Jak usuniesz delta, bo myślę, że z nią to przesadziłem (zrób testy na sprzęcie, sam jestem ciekaw) - to kod się uprasza do kilku linii i imo jest mega czytelny.

 

Oczywiście założenie jest, że chcesz mieć jak najdokładniej zachowany czas całej sekwencji, a nie różnicę pomiędzy wywołaniami.

 

// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() ) {
  nextCall += interval;
  callWhatever();
}

 

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

59 minut temu, Sebo_b napisał:

Może za mało embedded kodu widziałeś :) Można to owrapować w define'y - ale moim zdaniem wtedy nie jest czytelne. Jak usuniesz delta, bo myślę, że z nią to przesadziłem (zrób testy na sprzęcie, sam jestem ciekaw) - to kod się uprasza do kilku linii i imo jest mega czytelny.

 

Oczywiście założenie jest, że chcesz mieć jak najdokładniej zachowany czas całej sekwencji, a nie różnicę pomiędzy wywołaniami.

 


// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() ) {
  nextCall += interval;
  callWhatever();
}

 

Może powinienem się zacząć uczyć więcej embedded, jest z tym dużo pracy dla programistów ostatnio :)

 

Odnośnik do komentarza
Udostępnij na innych stronach

Na poczatku swojej przygody z Arduino napisalem cos takiego. Przygotowane z mysla o interwalometrze dla lustrzanki. Zadania typu "wykonaj 10 zdjec z interwalem 2s, poczekaj 12s przed startem".

 

Uzycie:
- co 2000 ms wykonaj funkcje blinkLED

timer.addEverytime(2000, blinkLED);


- wykonaj dwa razy funkcje itWorks co 2000 ms, poczekaj 300ms przed pierwszym wykonaniem
timer.addTask(300, 2, 2000, itWorks);

class RZTimer
{
  public:
    size_t tasks, tasksET;
    typedef void( * makeThisT )();

    typedef struct {
      unsigned long time;
      unsigned int interval, iterations, iterationsDone;
      makeThisT makeThis;
    } parameter;
    parameter *parameters;

    typedef struct {
      unsigned long timeET;
      unsigned int intervalET;
      makeThisT makeThisET;
    } parameterET;
    parameterET *parametersET;

    void run();
    size_t addTask(unsigned int, unsigned int, unsigned int, void (*makeT)());
    void removeTask(size_t);
    size_t addEverytime(unsigned int, void (*makeT)());
    void removeEverytime(size_t);
    size_t findTaskID(void (*makeT)());
    size_t findTaskIDEverytime(void (*makeT)());
};

size_t RZTimer::addTask(unsigned int _wait, unsigned int _iterations, unsigned int _interval, void (*makeT)())
{
	tasks++;
	parameters = (parameter*) realloc(parameters, tasks * sizeof(parameter));
	parameters[tasks - 1].time = millis() + _wait;
	parameters[tasks - 1].interval = _interval;
	parameters[tasks - 1].iterations = _iterations;
	parameters[tasks - 1].iterationsDone = 0;
	parameters[tasks - 1].makeThis = makeT;
	
	if (parameters[tasks - 1].iterations == 0)
	{
		parameters[tasks - 1].iterations = 1;
	}
	return tasks - 1;
}

size_t RZTimer::addEverytime(unsigned int _interval, void (*makeT)())
{
	tasksET++;
	parametersET = (parameterET*) realloc(parametersET, tasksET * sizeof(parameterET));
	parametersET[tasksET - 1].timeET = millis();
	parametersET[tasksET - 1].intervalET = _interval;
	parametersET[tasksET - 1].makeThisET = makeT;
	
	//makeT();
	return tasksET - 1;
}

void RZTimer::run()
{
	for (size_t i = 0; i < tasks; i++) {
		if (parameters[i].iterations)
		{
			if (static_cast<long>(millis() - parameters[i].time) >= 0)
			{
				parameters[i].time += parameters[i].interval;
				parameters[i].makeThis();
				parameters[i].iterationsDone = parameters[i].iterationsDone + 1;
				if (parameters[i].iterationsDone >= parameters[i].iterations)
				{
					removeTask(i);
				}
			}
		}
	}
	
	for (size_t i = 0; i < tasksET; i++) {
		if (static_cast<long>(millis() - parametersET[i].timeET) >= 0)
		{
			parametersET[i].timeET += parametersET[i].intervalET;
			parametersET[i].makeThisET();
		}
	}
}

void RZTimer::removeTask(size_t _task)
{
	if (_task < tasks) {
		for (size_t i = _task; i < tasks - 1; i++) {
			parameters[i] = parameters[i + 1];
		}
		tasks--;
		parameters = (parameter*) realloc(parameters, tasks * sizeof(parameter));
	}
}

void RZTimer::removeEverytime(size_t _task)
{
	if (_task < tasksET) {
		for (size_t i = _task; i < tasksET - 1; i++) {
			parametersET[i] = parametersET[i + 1];
		}
		tasksET--;
		parametersET = (parameterET*) realloc(parametersET, tasksET * sizeof(parameterET));
	}
}

size_t RZTimer::findTaskID(void (*makeT)())
{
	for (size_t i = 0; i < tasks; i++) {
		if (parameters[i].makeThis == makeT) {
			return i;
		}
	}
	return (size_t) ~0;   
}

size_t RZTimer::findTaskIDEverytime(void (*makeT)())
{
	for (size_t i = 0; i < tasksET; i++) {
		if (parametersET[i].makeThisET == makeT) {
			return i;
		}
	}
	return (size_t) ~0;   
}

RZTimer timer;
void blinkLED();

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  timer.addEverytime(2000, blinkLED);
  auto itWorks = []()->void {Serial.print("It Works! "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(300, 2, 2000, itWorks);
}

void loop() {
  timer.run();
}

void blinkLED()
{
  Serial.print("ON ");
  Serial.print(millis());
  Serial.println(" ms");
  digitalWrite(LED_BUILTIN, HIGH);
  auto blinkLEDoff = []()->void {digitalWrite(LED_BUILTIN, LOW); Serial.print("OFF "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(1000, 1, 0, blinkLEDoff);
}

 

Edytowane przez r.ziomber
Odnośnik do komentarza
Udostępnij na innych stronach

9 hours ago, Behlur_Olderys said:

jest z tym dużo pracy dla programistów ostatnio

Wciąż jest dużo, ale już coraz mniej. Oprócz pracy typowo dla elektronika: komórki od czasu iOSa i Androida to już nie embedded. Zostały telewizory, ale tam też coraz więcej Androida, więc jeśli nie pracujesz u SoC vendora (np MTK) to też nie embedded. Automotive się jeszcze broni rękami i nogami przed Googlem i Applem.

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

W dniu 23.08.2018 o 13:16, Sebo_b napisał:

Może za mało embedded kodu widziałeś :) Można to owrapować w define'y - ale moim zdaniem wtedy nie jest czytelne. Jak usuniesz delta, bo myślę, że z nią to przesadziłem (zrób testy na sprzęcie, sam jestem ciekaw) - to kod się uprasza do kilku linii i imo jest mega czytelny.

 

Oczywiście założenie jest, że chcesz mieć jak najdokładniej zachowany czas całej sekwencji, a nie różnicę pomiędzy wywołaniami.

 


// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() ) {
  nextCall += interval;
  callWhatever();
}

 

Miałeś rację, działa dokładnie tak samo! :) W sumie to prosta kalkulacja, trudno żeby matematyka się myliła, a jednak byłem trochę zaskoczony :)

Kod jeszcze bardziej się upraszcza, a średni interwał pomiędzy wywołaniami i tak się zgadza co do ułamka mikrosekundy - super! (właśnie sprawdziłem na żywo)

 

Edytowane przez Behlur_Olderys
  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

W dniu 23.08.2018 o 23:32, r.ziomber napisał:

Na poczatku swojej przygody z Arduino napisalem cos takiego. Przygotowane z mysla o interwalometrze dla lustrzanki. Zadania typu "wykonaj 10 zdjec z interwalem 2s, poczekaj 12s przed startem".

 

Uzycie:
- co 2000 ms wykonaj funkcje blinkLED

timer.addEverytime(2000, blinkLED);


- wykonaj dwa razy funkcje itWorks co 2000 ms, poczekaj 300ms przed pierwszym wykonaniem
timer.addTask(300, 2, 2000, itWorks);


class Timer
{
  public:
    unsigned long *time, *timeET;
    unsigned int *interval, *intervalET, *iterations, *iterationsDone;
    int tasks, tasksET;
    typedef void( * makeThisT )();
    makeThisT *makeThis;
    makeThisT *makeThisET;
    void run();
    int addTask(unsigned int, unsigned int, unsigned int, void (*makeT)());
    void removeTask(int);
    int addEverytime(unsigned int, void (*makeT)());
    void removeEverytime(int);
};
void blinkLED();

Timer timer;

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  timer.addEverytime(2000, blinkLED);
  auto itWorks = []()->void {Serial.print("It Works! "); Serial.println(millis());};
  timer.addTask(300, 2, 2000, itWorks);
}

void loop() {
  timer.run();
}

int Timer::addTask(unsigned int w, unsigned int it, unsigned int in, void (*makeT)())
{
  tasks++;
  time = (unsigned long*) realloc(time, tasks * sizeof(unsigned long));
  time[tasks - 1] = millis() + w;
  interval = (unsigned int*) realloc(interval, tasks * sizeof(unsigned int));
  interval[tasks - 1] = in;
  iterations = (unsigned int*) realloc(iterations, tasks * sizeof(unsigned int));
  iterations[tasks - 1] = it;
  iterationsDone = (unsigned int*) realloc(iterationsDone, tasks * sizeof(unsigned int));
  iterationsDone[tasks - 1] = 0;
  makeThis = (makeThisT*) realloc(makeThis, tasks * sizeof(makeThisT));
  makeThis[tasks - 1] = makeT;
  if (iterations[tasks - 1] == 0)
  {
    iterations[tasks - 1] = 1;
  }
  return tasks - 1;
}

int Timer::addEverytime(unsigned int in, void (*makeT)())
{
  tasksET++;
  timeET = (unsigned long*) realloc(timeET, tasksET * sizeof(unsigned long));
  timeET[tasksET - 1] = millis();
  intervalET = (unsigned int*) realloc(intervalET, tasksET * sizeof(unsigned int));
  intervalET[tasksET - 1] = in;
  makeThisET = (makeThisT*) realloc(makeThisET, tasksET * sizeof(makeThisT));
  makeThisET[tasksET - 1] = makeT;
  //makeT();
  return tasksET - 1;
}

void Timer::run()
{
  for (int i = 0; i < tasks; i++) {
    if (iterations[i])
    {
      if ((millis() >= time[i]))
      {
        time[i] += interval[i];
        makeThis[i]();
        iterationsDone[i] = iterationsDone[i] + 1;
        if (iterationsDone[i] >= iterations[i])
        {
          removeTask(i);
        }
      }
    }
  }

  for (int i = 0; i < tasksET; i++) {
    if ((millis() >= timeET[i]))
    {
      timeET[i] += intervalET[i];
      makeThisET[i]();
    }
  }
}

void Timer::removeTask(int _task)
{
  if (_task < tasks) {
    for (int i = _task; i < tasks - 1; i++) {
      time[i] = time[i + 1];
      interval[i] = interval[i + 1];
      iterations[i] = iterations[i + 1];
      iterationsDone[i] = iterationsDone[i + 1];
      makeThis[i] = makeThis[i + 1];
    }
    tasks--;
    time = (unsigned long*) realloc(time, tasks * sizeof(unsigned long));
    interval = (unsigned int*) realloc(interval, tasks * sizeof(unsigned int));
    iterations = (unsigned int*) realloc(iterations, tasks * sizeof(unsigned int));
    iterationsDone = (unsigned int*) realloc(iterationsDone, tasks * sizeof(unsigned int));
    makeThis = (makeThisT*) realloc(makeThis, tasks * sizeof(makeThisT));
  }
}

void Timer::removeEverytime(int _task)
{
  if (_task < tasksET) {
    for (int i = _task; i < tasksET - 1; i++) {
      timeET[i] = timeET[i + 1];
      intervalET[i] = intervalET[i + 1];
      makeThisET[i] = makeThisET[i + 1];
    }
    tasksET--;
    timeET = (unsigned long*) realloc(timeET, tasksET * sizeof(unsigned long));
    intervalET = (unsigned int*) realloc(intervalET, tasksET * sizeof(unsigned int));
    makeThisET = (makeThisT*) realloc(makeThisET, tasksET * sizeof(makeThisT));
  }
}

void blinkLED()
{
  Serial.print("ON ");
  Serial.println(millis());
  digitalWrite(LED_BUILTIN, HIGH);
  auto blinkLEDoff = []()->void {digitalWrite(LED_BUILTIN, LOW); Serial.print("OFF "); Serial.println(millis());};
  timer.addTask(1000, 1, 0, blinkLEDoff);
}

 

 

Nie mogę powstrzymać się od komentarza - zboczenie zawodowe:

To dziwne, że można używać auto i lambdy z C++11, a nie można używać std::vectora, który mocno ułatwiłby sprawę :)

Przez to, że masz "managera" tasków, który musi pilnować wskaźników, indeksów i pamięci Twój kod jest bardzo skomplikowany, a przecież z praktycznego punktu widzenia trzymanie wszystkich tasków razem w jednym obiekcie pamięci nie daje jakichś bezpośrednich korzyści, może poza tym, że jeśli task "wygaśnie" to go usuwasz i nie przetwarzasz niepotrzebnie (choć to marginalny zysk kilku cykli procesora na ewaluację 'ifa'). W moim rozwiązaniu każdy "task" to osobny obiekt, przez co nie muszę się martwić pamięcią (jest alokowana na stosie) ani indeksami (nie ma żadnych tablic).

Zasadniczo rozróżnianie tasków "zwykłych" i ET wydaje się również niepotrzebne - no, chyba że oszczędność pamięci.

Oczywiście te uwagi są pisane z perspektywy kogoś, kto na co dzień dysponuje 48-rdzeniowymi procesorami z setkami GB RAM-u, dlatego pewnie właśnie taka jest różnica między kodem "zwykłym" a embedded. Dlatego proszę,  nie traktuj tego jako krytykę, a raczej jako analizę porównawczą :)

 

 

PS

Przepraszam za dwa posty, ale to dwie różne sprawy :) Może moderator jakiś połączy te dwa posty jeśli tak lepiej...

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

Ale Arduino bez dodatkowych bibliotek nie obsluguje std::vector (w ogole STD) i stad takie mieszanie C++ z C.

https://github.com/maniacbug/StandardCplusplus

6 godzin temu, Behlur_Olderys napisał:

Zasadniczo rozróżnianie tasków "zwykłych" i ET wydaje się również niepotrzebne - no, chyba że oszczędność pamięci.

Zadanie "doczesne" przechowuje jedynie informacje o interwale. Wykonywane kilka razy dodatkowo musi miec informacje o ilosci cykli i "poczekalni" przed pierwszym wykonaniem.

 

6 godzin temu, Behlur_Olderys napisał:

Dlatego proszę,  nie traktuj tego jako krytykę, a raczej jako analizę porównawczą

Ale ja oczekuje krytyki! Chce sie uczyc od zawodowych programistow i doskonalic swoje znikome umiejetnosci.

Jesli tylko masz czas napisac, z checia wyslucham, co zrobilem zle!

 

Mam jeszcze glupi pomysl, o ktory byc moze rozbuduje swoj timer. Terminarz oparty na... delay(). No dobra, nie na delay(), a na uspijMikrokontroler(), ale w zewnetrznym zachowaniu to prawie to samo :) Po prostu znajde najblizsze zadanie w tabelicach time i timeET i do tego czasu uspie mikrokontroler. Bedzie to dodatkowa opcja, bo oczywiscie nienadajaca sie do kazdego projektu.

PS. Ktos kiedys powiedzial, ze na delay() nie da sie zrobic wielozadaniowosci. Dlatego tez chce ja zrobic :P

Edytowane przez r.ziomber
Odnośnik do komentarza
Udostępnij na innych stronach

Czy moge prosic o weryfikacje? Zamiast

W dniu 23.08.2018 o 13:16, Sebo_b napisał:

 


// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() ) {
  nextCall += interval;
  callWhatever();
}

proponuje

#include <limits.h>

// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() && now() - nextCall < ULONG_MAX / 2 ) {
  nextCall += interval;
  callWhatever();
}

Powod: przedzial czasu obejmujacy przekroczenie zakresu zmiennej. now() bedzie wtedy olbrzymie a nextCall male. Warunek nextCall < now() zostanie wiec spelniony.

Standardowe rozwiazanie https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay nie sprawia klopotow z overflow, ale kumuluje opoznienie kolejnych krokow.

Edytowane przez r.ziomber
  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

4 hours ago, r.ziomber said:

przedzial czasu obejmujacy przekroczenie zakresu zmiennej

Masz rację, że jest tam bug, a nawet dwa:

1. Overflow nextCall - tak jak napisałeś, now() będzie duże, nextCall małe

2. Overflow now() bo nie mamy pewności czy pod koniec zakresu zdąży się wykonać, czyli nextCall będzie duże, a now() małe

 

To są dodatkowe cornercase'y, więc || a nie && w Twoim rozwiązaniu powinien to załatwić, tylko że pierwszy warunek zawiera się w tym co zaproponowałeś - więc wystarczyło by :

if ( now() - nextCall < ULONG_MAX / 2 ) {

// lub - bez dzielenia (choć to tylko estetyka w tym przypadku)
                                 
if ( now() - nextCall < (ULONG_MAX >> 1) ) {

// lub - moim zdaniem bardziej czytelnie
                                 
if ( now() - nextCall < LONG_MAX )
  
// lub - bez użycia limits
  
if ( now() - nextCall < (~0ul >> 1) )
  
// a ja bym napisał to tak, żeby było dla każdego czytelne (choć nie widać tak od razu, że obsługuje corner-case'y)
  
if ( static_cast<long>(now() - nextCall) >= 0 ) {

 

Edytowane przez Sebo_b
  • Lubię 2
Odnośnik do komentarza
Udostępnij na innych stronach

3 godziny temu, r.ziomber napisał:

Czy moge prosic o weryfikacje? Zamiast

proponuje


#include <limits.h>

// constructor
nextCall = now() + interval;

//active loop
if ( nextCall < now() && now() - nextCall < ULONG_MAX / 2 ) {
  nextCall += interval;
  callWhatever();
}

Powod: przedzial czasu obejmujacy przekroczenie zakresu zmiennej. now() bedzie wtedy olbrzymie a nextCall male. Warunek nextCall < now() zostanie wiec spelniony.

Standardowe rozwiazanie https://www.arduino.cc/en/Tutorial/BlinkWithoutDelay nie sprawia klopotow z overflow, ale kumuluje opoznienie kolejnych krokow.

 

 

Sytuacja jest taka:

now = 1022

interval = 10

timeToWait = 1021

 

1. Start

2. Warunek spełniony 

3. timeToWait := 7 (1031mod 1024)

4. doSomething(now==1022)

5. Powiedzmy, że mija 1ms (now:=1023)

6. Znowu wchodzimy do warunku

7. Warunek spełniony (7 < 1023)

8. timeToWait = 17

9. doSomething (now==1023)

10. Mija znowu 1ms (now:=0)

11. timeToWait > now()

Następne doSomething odpali się za 17ms. Będziemy mieli 3 doSomething w czasie 20ms. Czyli średnio wychodzi ok :) Więc overflow timeToWait to nic groźnego na dłuższą skalę. 

 

Overflow now() jest dużo gorszy, bo timeToWait będzie np. 1023 a now() przekroczy już 0 i stracimy wszystkie wywołania!

 

Napisałbym testy do tych funkcji i sprawdził wszystkie case'y z automatu :)

 

static_cast wygląda najbardziej elegancko, ale znowu - muszę sam się przekonać :)

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

Z tym co zaproponowane overflowy nie powinny mieć wpływu na średnie czasy - arytmetyka zadziała.

 

1 minute ago, Behlur_Olderys said:

static_cast wygląda najbardziej elegancko, ale znowu - muszę sam się przekonać :)

Tutaj nie ma się co przekonywać, to trochę jak byś przyjechał z Anglii (pisanie w C) i twierdził, że musisz się przekonać do prawostronnego ruchu (C++) :) Piszesz w C++ to używać "pożądnych" castów. Co więcej, zaczniesz (lepiej nie) używać wielodziedziczenia i C casty się wysypią.

Odnośnik do komentarza
Udostępnij na innych stronach

12 hours ago, r.ziomber said:

Ale ja oczekuje krytyki! Chce sie uczyc od zawodowych programistow i doskonalic swoje znikome umiejetnosci.

Jesli tylko masz czas napisac, z checia wyslucham, co zrobilem zle!

Na szybko zrefaktorowałem Twój kod, a w sumie to napisałem go od nowa zachowując logikę. Nie testowałem tego kodu (tylko syntax), więc jest pewnie sporo bugów. Ale wyłapanie bugów to dobre ćwiczenie :)

 

#include <limits.h>
#include <cstdlib>

class Timer {

public:

	typedef void(*Callback)(void*);

	Timer();
	~Timer();
	/**
	 * count < 0 for unlimited execution
	 * returns task number or 0 if fail;
	 **/
	size_t addTask(long delay, long interval, int count, Callback callback, void* cbParam);
	void removeTask(size_t taskId);

	void run();

	void cleanUp();

private:

	struct Task {
		long interval;
		unsigned long nextCall;
		int count;
		size_t taskId;

		Callback cb;
		void* cbParam;
	};

	Task* tasks;
	size_t tasksSize;
	size_t taskIdCounter;
	bool clean;
	unsigned long nextCall;

};

Timer::Timer():
	tasks(NULL), tasksSize(0), clean(true), nextCall(ULONG_MAX), taskIdCounter(0)
{
}

Timer::~Timer() {
	if (tasks)
		free(tasks);
}

size_t Timer::addTask(long delay, long interval, int count, Timer::Callback callback, void* cbParam) {

	if (count == 0 || callback == NULL || interval == 0)
		return 0;

	Task *t = NULL;
	unsigned long now = ::now();

	if (!clean) {
		for (size_t i = 0; i < tasksSize; ++i) {
			if (tasks[i].count == 0) {
				if (t == NULL) {
					t = tasks+i;
					clean = true;					
				}
				else {
					clean = false;
				}
			}
		}
	}
	else {

		Task* newTasks = static_cast<Task*>(realloc(tasks,(tasksSize+1)*sizeof(Task)));
		if (!newTasks)
			return 0;
		tasks = newTasks;
		t = newTasks + tasksSize++;
	}

	t->interval = interval;
	t->nextCall = now + interval + delay;
	t->count = count;
	t->cb = callback;
	t->cbParam = cbParam;
	t->taskId = ++taskIdCounter;

	if ( static_cast<long>(nextCall - t->nextCall) >= 0)
		nextCall = t->nextCall;

	return t->taskId;
}

void Timer::removeTask(size_t taskId) {

// we don't need to update nextCall, in the worst case we end up with one empty loop in run()
//	unsigned long nextCallSoFar = ULONG_MAX;

	for (size_t i = 0; i < tasksSize; ++i) {
		if (tasks[i].taskId == taskId) {
			tasks[i].count = 0;
			clean = false;

//			if ( nextCall != tasks[i].nextCall)
				return;
		}
//		else if (static_cast<long>(nextCallSoFar - tasks[i].nextCall) >= 0) {
//			nextCallSoFar = tasks[i].nextCall;
//		}
	}

//	nextCall = nextCallSoFar;
}

void Timer::run() {

	unsigned long now = ::now();

	if ( static_cast<long>(now - nextCall) >= 0 ) {

		for (size_t i = 0; i < tasksSize; ++i) {
			
			nextCall = ULONG_MAX;

			if (tasks[i].count != 0 && static_cast<long>(now - tasks[i].nextCall) >= 0) {

				tasks[i].cb( tasks[i].cbParam);

				if (tasks[i].count > 0 && --tasks[i].count == 0)
					clean = false;
				else 
					tasks[i].nextCall = now + tasks[i].interval;
			}

			if ( static_cast<long>(nextCall - tasks[i].nextCall) >= 0)
				nextCall = tasks[i].nextCall;
		}
	}

}

void Timer::cleanUp() {

	if (clean)
		return;

	Timer *head = tasks, *tail = tasks + tasksSize - 1;

	while (head < tail) {

		if (head->count == 0 && tail->count != 0) {
			*head  = *tail;
			tail->count = 0;
		}

		if (head->count != 0)
			++head;
		if (tail->count == 0)
			--tail;
	}

	size_t newTasksSize = head-tasks;
	Task* newTasks = static_cast<Task*>(realloc(tasks,newTasksSize*sizeof(Task)));
	if (newTasks == NULL)
		return;	// we basically keep it unclan

	tasks = newTasks;
	tasksSize = newTasksSize;
	clean = true;
}

 

Edytowane przez Sebo_b
  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

1 godzinę temu, Sebo_b napisał:

Z tym co zaproponowane overflowy nie powinny mieć wpływu na średnie czasy - arytmetyka zadziała.

 

Tutaj nie ma się co przekonywać, to trochę jak byś przyjechał z Anglii (pisanie w C) i twierdził, że musisz się przekonać do prawostronnego ruchu (C++) :) Piszesz w C++ to używać "pożądnych" castów. Co więcej, zaczniesz (lepiej nie) używać wielodziedziczenia i C casty się wysypią.

Chodzi mi o sprawdzenie, czy corner case'y będą dobrze pokryte, bo wydaje się że tak, ale ja już dawno przestałem ufać swojemu rozumieniu czytanego kodu, i muszę mieć wszystko przetestowane :) static_cast to wiadomo, że elegancka konstrukcja.

 

24 minuty temu, Sebo_b napisał:

Na szybko zrefaktorowałem Twój kod

Jakoś nie podoba mi się stosowanie raz long, raz unsigned long.


Ogólnie - gdyby zaimplementować z boku gdzieś najprostszą protezę std::vector to mój "idealny" kod wyglądałby tak: (pseudo kod raczej)

 

struct Task{
   virtual bool IsValid(){ return true; }
   virtual void PerformIfReady();
}

struct TaskWithCount : Task;

vector<Task> tasks;
tasks.emplace_back(Task(1));
tasks.emplace_back(TaskWithCount(1, 10));

for (task : tasks){
    if (!task.IsValid()){
       tasks.erase(task);
    }else{
      task.PerformIfReady();
    }
}

Oczywiście PerformIfReady trzeba by zaimplementować odpowiednio, tak jak sugerujecie, ale chodzi mi tylko o ogólną architekturę.

 

 

 

PS

Zrobił się wątek programistyczny na forum astro :) 

 

 

  • Lubię 1
Odnośnik do komentarza
Udostępnij na innych stronach

1 minute ago, Behlur_Olderys said:

Jakoś nie podoba mi się stosowanie raz long, raz unsigned long.

Nie rozumiem, w którym miejscu?

 

2 minutes ago, Behlur_Olderys said:

gdzieś najprostszą protezę std::vector

To jest oczywiście kolejny krok, wydzielenie kolekcji na zewnątrz. Ale z drugiej strony jest to taka prosta struktura, że można pominąć.

 

 

Odnośnik do komentarza
Udostępnij na innych stronach

Dołącz do dyskusji

Możesz dodać zawartość już teraz a zarejestrować się później. Jeśli posiadasz już konto, zaloguj się aby dodać zawartość za jego pomocą.

Gość
Dodaj odpowiedź do tematu...

×   Wklejono zawartość z formatowaniem.   Usuń formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Odnośnik został automatycznie osadzony.   Przywróć wyświetlanie jako odnośnik

×   Przywrócono poprzednią zawartość.   Wyczyść edytor

×   Nie możesz bezpośrednio wkleić grafiki. Dodaj lub załącz grafiki z adresu URL.

×
×
  • Dodaj nową pozycję...

Powiadomienie o plikach cookie

Umieściliśmy na Twoim urządzeniu pliki cookie, aby pomóc Ci usprawnić przeglądanie strony. Możesz dostosować ustawienia plików cookie, w przeciwnym wypadku zakładamy, że wyrażasz na to zgodę.