Skocz do zawartości

Wielozadaniowość cz. 3.5


Pomocna odpowiedź

Napisano (edytowany)

Cel artykułu

Rozwinięcie programu z części trzeciej – podział programu na wiele plików. Przykład budowy maszyny stanu na przykładzie skanera komend

W trakcie pisania części 4, po ukończeniu skanera komend i napisaniu interpretera komend pierwszego poziomu uświadomiłem sobie, że program wymaga podziału na wiele plików tak, aby każda funkcjonalność była zawarta w innych plikach. Każde zadanie, które napisałem lub napiszę wie, jak się zainicjować, kiedy się uruchomić, jak zinterpretować dostarczoną do niego komendę. Komunikację międzyprocesową mam zamiar zrealizować poprzez zmienne globalne – dopóki program będzie jednoprocesorowy i nie będzie używał przerwań, zmienne globalne są albo zapisywane, albo odczytywane – nigdy jednocześnie jedno i drugie. Zatem dla każdej funkcjonalności (np. migania diody) potrzebuję, aby moduł dostarczał trzy funkcje do modułu .ino:

void xxx_komenda(char cmd[]); // interpreter komendy 2 pozomu - dla funkcji parser1()

void xxx_init(); // inicjalizacja modułu – dla funkcji init()

int task_xxx(); // wywołanie zadania – dla funkcji loop()

oraz będzie korzystał (lub nie) z pliku global.h

Zmiana typu funkcji task_xxx() z void na int wynika z przewidywanej potrzeby przekazania rezultatu wykonania funkcji do schedulera – funkcji loop().

Następną sprawą jest problem pamięci RAM. Napisy umieszczone w tekście programu zapisywane są w RAMie i potrafią bezproduktywnie zająć znaczną jego część (1 bajt na znak). Stąd przeniosłem napisy używane w trakcie normalnej eksploatacji do pliku msg.h, który będą używać moduły wymagające generowania napisów. Zadaniem tego pliku jest umieszczenie napisów w pamięci stałej.

Cel artykułu

Rozwinięcie programu z części trzeciej – podział programu na wiele plików. Przykład budowy maszyny stanu na podstawie skanera komend


 

W trakcie pisania części 4, po ukończeniu skanera komend i napisaniu interpretera komend pierwszego poziomu uświadomiłem sobie, że program wymaga podziału na wiele plików tak, aby każda funkcjonalność była zawarta w innych plikach. Każde zadanie, które napisałem lub napiszę wie, jak się zainicjować, kiedy się uruchomić, jak zinterpretować dostarczoną do niego komendę. Komunikację międzyprocesową mam zamiar zrealizować poprzez zmienne globalne – dopóki program będzie jednoprocesorowy i nie będzie używał przerwań, zmienne globalne są albo zapisywane, albo odczytywane – nigdy jednocześnie jedno i drugie. Zatem dla każdej funkcjonalności (np. migania diody) potrzebuję, aby moduł dostarczał trzy funkcje do modułu .ino:

void xxx_komenda(char cmd[]); // interpreter komendy 2 pozomu - dla funkcji parser1()

void xxx_init(); // inicjalizacja modułu – dla funkcji init()

int task_xxx(); // wywołanie zadania – dla funkcji loop()

oraz będzie korzystał (lub nie) z pliku global.h

Zmiana typu funkcji task_xxx() z void na int wynika z przewidywanej potrzeby przekazania rezultatu wykonania funkcji do schedulera – funkcji loop().

Następną sprawą jest problem pamięci RAM. Napisy umieszczone w tekście programu zapisywane są w RAMie i potrafią bezproduktywnie zająć znaczną jego część (1 bajt na znak). Stąd przeniosłem napisy używane w trakcie normalnej eksploatacji do pliku msg.h, który będą używać moduły wymagające generowania napisów. Zadaniem tego pliku jest umieszczenie napisów w pamięci stałej.

// stałe
#define LED_PERIOD 1000 // okres przełączania diody
#define DL_BUFORA 16 // długość bufora komendy

void task_dioda() {
static unsigned long PrevLed = 0L; 
if (millis() - PrevLed >= int(LED_PERIOD) ) {
if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
PrevLed = millis();
}
}

int task_skaner() { // skaner danych z portu szeregowego
char bufor[DL_BUFORA]; // bufor na komendę
static int wskaznik = 0; // 
char Znak;
if(Serial.available() < 1) {
return; // nie ma nic do zrobienia
}
else
{ 
Znak = Serial.read();
if(Znak >= ' ')
Serial.print(Znak);
else
Serial.print('~'); 
Serial.print(">");
Serial.println(byte(Znak));
}
return 0;
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
task_dioda();
task_skaner();
}

Teraz spróbuję zmodyfikować skaner tak, aby reagował na początek komendy ( znak ‘>’ ) i koniec komendy ( znaki ‘\n’ – nowa linia i ‘\r’ – powrót karetki). Proszę pamiętać, że Serial posiada wewnętrzne bufory 64-znakowe – do odczytu i zapisu. Opis działania zadania:

Bufor: tablica znaków (char[n]) i wskaźnik na pierwsze wolne miejsce (na początku 0).

START: zadanie znajduje się w stanie 0. Czyta znaki ignorując je do momentu odebrania znaku początek komendy, tj ‘>’. Jeśli go odbierze, to umieszcza go w buforze i ustawia stan 1.

ZNAK_KOMENDY: zadanie znajduje się w stanie 1. Czyta znak i sprawdza, czy jest to koniec komendy (‘\n’ lub ‘\r’). Jeśli tak, zapisuje do bufora ‘\0’ i ustawia stan 2 – koniec komendy.

Jeśli nie, sprawdza, czy znak mieści się w zakresie ‘ ‘ (spacja) - ‘~’ (tylda). Jeśli nie, to przechodzi do stanu 3( odrzucenie bufora: niedozwolony znak, np. polski znak).

Jeśli tak, sprawdza, czy jest miejsce w buforze (proszę pamiętać o miejscu na końcowe ‘\0’). jeśli nie, ustawia stan 3 (odrzucenie bufora: przepełnienie). Jeśli tak, umieszcza znak w buforze.

KOMENDA: Zadanie znajduje się w stanie 2. Wysyła komendę do parsera i ustawia stan 3 (ustawienie stanu początkowego).

KONIEC: zadanie znajduje się w stanie 3. Ustawia wskaźnik bufora na 0 i ustawia stan zadania na 0. Oto kod programu:

 // stałe
#define LED_PERIOD 1000 // okres przełączania diody
#define DL_BUFORA 16    // długość bufora komendy

void task_dioda() {
static unsigned long PrevLed = 0L;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void parser(char txt[]) {  // symulator parsera
        Serial.print("Parser: ");
        Serial.println(txt);
}

void komunikat(char c, int s, char txt[]){
      if(c >= ' ')
      Serial.print(c);
    else
      Serial.print('~'); 
    Serial.print(" - ");
    Serial.print(byte(c));
    Serial.print(" stan=");
    Serial.print(s);
    Serial.print(" bufor=");
    Serial.print('"');
    Serial.print(txt);
    Serial.println('"');
}

int task_skaner() { // skaner danych z portu szeregowego
char bufor[DL_BUFORA];    // bufor na komendę
static int wskaznik = 0;  // pierwsze wolne miejsce w buforze
static int stan = 0;      // stan maszyny stanów
char Znak;
if(Serial.available() < 1) {
    return; // nie ma nic do zrobienia
  }
else
  { 
    switch (stan) {
      case 0:   // oczekiwanie na początek komendy
        Znak = Serial.read(); // odczyt znaku
        wskaznik = 0;             // pierwszy znak komend
        bufor[wskaznik] = '\0';
        if(Znak < ' ' ) Znak = '#'; // białe znaki
        if(Znak == '>'){   
          bufor[wskaznik++] = Znak;
          bufor[wskaznik] = '\0';
          stan = 1;
        }
        komunikat(Znak,stan,bufor);
        break;
      case 1:
        Znak = Serial.read();  // odczyt znaku
        if(Znak == '\r' || Znak == '\n') {
            if(wskaznik > 1)  stan = 2;  else stan = 3;// koniec komendy
          }
        else if(wskaznik > DL_BUFORA - 3) {  // miejsce na '\0'
          Serial.println("Przepełnienie");
          stan = 3;
        }
        else if(Znak >= ' ' && Znak <= '~') { // znaki drukowalne  
          bufor[wskaznik++] = Znak;
          bufor[wskaznik] = '\0';
          komunikat(Znak,stan,bufor);
          }
        else {
          Serial.print(Znak);
          Serial.println("- Nieznany znak");
          stan = 3;
        }
        break;
      case 2: 
        Serial.print("Komenda: ");
        Serial.println(bufor);
        parser(bufor);
        stan = 0;
        break;
      default: // we wszystkich pozostałych przypadkach
        stan = 0;
        wskaznik = 0;             // pierwszy znak bufora
        bufor[wskaznik] = '\0';
        break;
    }
  }
return 0;
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  task_dioda();
  task_skaner();
}

Funkcję task_dioda() również można traktować jako skrajnie prostą maszynę stanu sterowaną zdarzeniami – upływ zadanego odcinka czasu jest zdarzeniem. Włączenie/wyłączenie diody – dwa stany, przełączane zdarzeniem: jeśli minie czas, zmień stan diody na przeciwny.

Funkcja loop() jest schedulerem. Nie wie, co robią zadania, jedynie je po kolei wywołuje. Zadania robią (lub nie robią) swoje i oddają sterowanie. I tu widać, że zablokowanie jednego zadania z jakiegokolwiek powodu kładzie od razu cały system – jak zadnie nie odda sterowania, to inne po prostu nie zostaną uruchomione.

Część czwartą umieszczę po uporaniu się ze zmianą struktury programu.

Proszę o uwagi i komentarze do tego tutoriala


 

 

Edytowano przez Kogut007
poprawa opisu

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...