wtorek, 12 grudnia 2017

Router433 - jak programować. Schematy blokowe, grafy i takie tam różne automaty

Programowanie to sztuka wyborów. A że programiści to umysł ścisły przeważnie wybierają melodię co ją już raz słyszeli. Co ma jednak zrobić (nie)młody adept programowania, który nie słyszał praktycznie żadnej melodii? Może napisać własną....
No to dziś trochę zabawy z układaniem nowych melodii .. pardon .... programów.
Dekodowanie sekwencji nadawanych przez  moduły bezprzewodowe w paśmie 433 MHz i sterowanie  bezprzewodowymi odbiornikami już działa.
Teraz trzeba to wszystko  dowiązać do zmiennych i stworzyć sieci połączeń realizujących potrzebne nam funkcje automatyki. Można również zapakować kody do wirtualnych pinów i pozwolić na zarządzanie nimi przez BLYNKową aplikację. Ale możemy (jeśli potrafimy) ożenić BLYNKa z NODE RED i  mapę połączeń bezprzewodowych urządzeń  generować w komputerze.

Ale zanim zacznę coś majstrować przy głównym programie warto stworzyć kilka podstawowych klocków niezbędnych do budowy routera.
Na początek procedura zapisu nowego kodu do pamięci mikrokontrolera. Urządzenie ma działać niezależnie od  BLYNKa więc odpada przechowywanie danych konfiguracyjnych  na serwerze.

Warunki brzegowe:
  • procedura ma być przelotowa (działająca w poolingu) by nie wstrzymywała normalnej pracy urządzenia
  • efektem procedury mają być zapisane w  pamięci mikrokontolera parametry transmisji poszczególnych urządzeń -  kod + długość impulsu

Działanie procedury

  • Inicjalizacja procedury - załączenie przyciskiem P_kod
  • Jeśli nadawany kod powtórzy się dwa razy po kolei ma być automatycznie zapisany w wybranej komórce pamięci (przypisany do zmiennej). Zapisany zostaje kod i długość impulsu charakterystyczna dla danego typu urządzenia
  • Gdy kod nadawany jest jednokrotnie zapis do pamięci następuje manualnie - po wyłączeniu przycisku P_kod

Ogólny schemat procedury wygląda mniej więcej tak


Teraz to trzeba przełożyć na konkretny kod. A że można to zrobić na kilka sposobów to najlepiej narysować sobie jak program ma działać.

Schemat blokowy - programowanie za pomocą algorytmów (diagramów)

Schemat blokowy (diagram) to zapisany w graficznej formie program. Po cholerę więc rysować prostokąciki i romby zamiast od razu zabrać się za pisanie kodu?  Bo mamy przed sobą czytelny, łatwy do analizy i ewentualnych zmian schemat działania programu. Trochę wiadomości o zasadach tworzenia schematów blokowych jest tu>>>>. tu>>>>>> a tu trochę jak programować z zastosowaniem algorytmów tu>>>>

Program mógłby wyglądać tak



Mógłby .... ale nie spełnia warunku podstawowego - nie jest przelotowy. Procedura zatrzyma działanie całego programu gdy odbierze tylko jeden kod lub każdy następny odebrany kod będzie różny od poprzedniego albo nie zostanie zwolniony przycisk P_kod. Zatrzyma się na jakimś fragmencie kodu zaznaczonym czerwoną strzałką.

Programy projektowane za pomocą diagramów są szczególnie podatne na możliwość tworzenia pętli zawieszających. W zasadzie każda pętla tworząca zamknięty obieg to potencjalne źródło problemu szczególnie gdy spełnienie warunków w pętli uzależnione jest od sygnałów zewnętrznych procesora. A te mogą się nigdy nie pojawić - i wisimy z naszym programem jak bombka na choince.

Jest jeszcze jedna nieprzyjemna cecha schematów blokowych - łatwo się je rysuje ale cholernie ciężko jest je przenieść w kod programu. Dlaczego - jeśli diagram jest do facto graficznym odbiciem kodu?  Ano z powodu słynnego goto, bez którego nie da się tego pięknego algorytmu skopiować w kodzie C. A jak wszyscy wiedzą goto  jest mocno passe wśród prawdziwych programistów.

Program trzeba więc nieco zmodyfikować zmieniając sposób obsługi sygnałów zewnętrznych  tak by umożliwić wyjście programu z procedury mimo, iż nie jest ona jeszcze faktycznie zakończona (zakończenie procedury to zapisanie KODu do pamięci programu).
Ale jeśli wyjdziemy z procedury to po powrocie do niej musimy wiedzieć, iż procedura jest w toku. W tym celu trzeba dodać jakąś zmienną lub zmienić funkcje przycisku P_kod dodając jeszcze jeden przycisk do obsługi zapisu do pamięci.
Przycisk P_kod - stan "1" uruchamia procedurę i sygnalizuje że jest on w toku
Przycisk Z_kod - służy do zapisu kodu do pamięci gdy odebrany jest tylko jeden kod

Druga wersja programu tym razem w wersji nie blokującej programu głównego



Też pięknie. Zapisanie kodu automatycznie (gdy dwukrotnie odczytany został ten sam kod) wymaga co najmniej dwukrotnego wywołana procedury. Ale jeśli kod pojawi się tylko raz i nie zainicjujemy zapisu do pamięci procedura wciąż będzie w toku  Dla pełnej elegancji warto dodać jakiś timer, który po np 10 sek zresetuje procedurę.
Już na pierwszy rzut oka widać czym różnią się oba schematy - w drugim nie ma ani jednej strzałki skierowanej ku górze. Jeśli takowa istnieje oznacza to istnienie zamkniętej pętli, która może zawiesić działanie programu.
Przeniesienie algorytmu do programu wygląda mniej więcej tak

int p_kod = 0; //zmienna stanu przycisku start program
int z_kod = 0; //zmienna stanu przycisku zapisz do pamięci
double o_kod = 0; //zmienna odbieranych kodów
double KOD = 0; // zmienna kodu do zapisu w pamięci
void zapisz_kod() {} //procedura zapisu kodu do pamięci nieulotnej

void do_zapisz_kod() { // resety i zapisz kod
  zapisz_kod();
  reset_all();
}
void reset_all() { // resety po zakończeniu procedury
  o_kod = 0; KOD = 0;
  p_kod = 0; z_kod = 0;
}

void KOD_do_eeprom()
{
  if (p_kod != 0) {
    if (z_kod == 0) {
      if (o_kod != 0) {
        if (KOD == o_kod) {
          do_zapisz_kod();
        } else KOD = o_kod;
      }
    } else do_zapisz_kod();
  }
}

Program wyszedł nad podziw elegancki ale żonglowanie warunkami by wyjść z procedury if  we właściwym miejscu nieźle gotuje mózg. Takie zabawy są świetne jako wprawki do programowania ale przy dłuższych programach niechybnie poplączę się w kolejnych poziomach wywołań. brrrrr

Automaty skończone - programowanie z użyciem grafów

W drugim wariancie schematu blokowego trzeba było użyć dodatkowego sygnału (lub zmiennej), w którym zapamiętana została informacja, iż procedura jest w toku. Taką role w tym przykładzie spełnia przycisk P_kod. Przycisk ma tylko dwie pozycje więc możemy nim opisać jedynie dwa stany wewnętrzne programu. Jeśli zamiast przycisku do oznaczenia stanu użyjemy zmiennej typu byte  możemy zdefiniować 256 wewnętrznych stanów programu. I wtedy mamy właśnie do czynienia z automatem skończonym. Ot taka teoria automatóww dużym przybliżeniu.

Spróbujmy grafem zastąpić program opisany wcześniej schematem blokowym. Wyszło coś takiego:



Ależ proste! Trudno tu dopatrzeć się kilku ważnych szczegółów (np. zapisu do pamięci) ale są. Domyślnie! Zapis jest na przejściu ze stanu 3 do 1.
Jak działa program wg tego grafu? Niby identycznie jak ten z algorytmu. Ale nie do końca.

  1. Naciskamy przycisk P_kod. Stan programu zmienia się z (1) na (2)
  2. Program w tym stanie czeka na odbiór pierwszego kodu ( o_kod != 0)
  3. Gdybyśmy w tym stanie (2) przerwali program (wyłączenie przycisku p_kod) program wróci do stanu początkowego (1)
  4. Po odebraniu kod się zostanie zapisany do zmiennej KOD (KOD = o_kod), zmienna o_kod zostaje wyzerowana (o_kod = 0) a stan programu zmieni się z (2) na (3)
  5. Pogram w tym stanie czeka na odbiór drugiego kodu ( o_kod != 0)
  6. Gdybyśmy w tym stanie (3) przerwali program (wyłączenie przycisku p_kod) program wróci do stanu (1) ale wcześniej ZAPISZE KOD do pamięci
  7. Po odebraniu kolejnego kodu zostanie on porównany ze zmienną KOD (KOD == o_kod ?)
  8. Jeśli kody są różne program pozostaje w stanie (3), nowy kod zostaje zapisany do KOD (KOD = o_kod) a zmienna o_kod zostaje wyzerowana (o_kod = 0). 
  9. Jeśli odebrany nowy kod jest identyczny z KOD  program wróci do stanu (1) ale wcześniej ZAPISZE KOD do pamięci, wyzeruje KOD, wyzeruje 0_kod i ustawi przycisk p_kod w stany WYŁĄCZONY

Program mógłby wyglądać tak

int p_kod = 0; //zmienna stanu przycisku
double o_kod = 0; //zmienna odbieranych kodów
double KOD = 0; // zmienna kodu do zapisu
int stan = 0; // zmienna stanu programu
void zapisz_kod() {}; //procedura zapisu kodu do pamięci nieulotnej

void KOD_do_eeprom()
{
  switch (stan) {
    case 1: {
        o_kod = 0; KOD = 0;
        if (p_kod != 0) stan = 2;
      } break;
    case 2: {
        if (p_kod == 0) stan = 1;
        if (o_kod != 0) {
          KOD = o_kod; o_kod = 0;
          stan = 3;
        }
      } break;
    case 3: {
        if (p_kod == 0) {
          zapisz_kod();
          stan = 1;
        }
        if (o_kod != 0) {
          if (KOD == o_kod) {
            zapisz_kod();
            stan = 1;
          }
        }
      }      break;
    default: break;
  }

Ten kod daleko odbiega od zwięzłości i elegancji programu z algorytmu.  Ma jednak kilka niezaprzeczalnych zalet.

  • bardzo łatwo odwzorowuje się graf w finalny kod
  • jest szalenie prosty do analizy - praktycznie taki kod startuje od razu i nie wymaga żmudnych analiz debuggerem
  • zmiany w algorytmie bardzo łatwo jest nanieść do gotowego kodu. Np dodanie warunku iż zapis kodu następuje po odebraniu trzech jednakowych sekwencji wymaga dodania jednego stanu w grafie i jednego case w kodzie. By uzyskać tą samą funkcję w kodzie powstałym na bazie schematu blokowego trzeba praktycznie przerobić cały program!!
Ten składający się jedynie z trzech stanów wewnętrznych program obejmuje cały algorytm programu zapisu kodu do pamięci. A to tylko dzięki wykorzystaniu innych zmiennych programowych do wyznaczenia kolejnego stanu i wartości sygnału wyjściowego. Taki rodzaj grafu nazywany jest automatem Mealy'ego.
W tym automacie inne sygnały i zmienne programu "robią" za dodatkowe stany automatu ale w sposób cokolwiek ukryty. Jeśli są to tylko zmienne programowe to ok. Ale gdy jest nim sygnał zewnętrzny (np stan rejestru wejściowego portu) to działanie takiego automatu mocno zależy od "stabilności"  tego sygnału. Jeśli wejście cyfrowe jest podatne na zakłócenia to chwilowe zmiany stanów na tym wejściu przełożą się bezpośrednio na działanie programu w sposób przypadkowy i całkowicie nieprzewidywalny Wad tych pozbawiony jest automat Moora ale kosztem zwiększenia ilości stanów wewnętrznych programu.

Rozbudujmy nasz graf o trzy dodatkowe stany 



Te trzy dodatkowe stany nie są związane ze zmianami sygnałów wejściowych  (przycisk, odebrany kod) ale z wynikami wewnętrznych procedur w programie (porównanie zmiennych, zapis do pamięci). W każdym stanie mamy ściśle zdefiniowaną wartość sygnału wyjściowego zależną jedynie od nr stanu. Program jest identyczny z poprzednim grafem ale bez wątpienia jest bardziej czytelny i zrozumiały bez dodatkowego opisu. Jest również łatwiejszy do implementacji szczególnie takiej, która dopuszcza zmiany stanów poza główną pętlą obsługi automatu skończonego (switch - case). Stany posiadające wewnętrzną pętle "czekam" to takie, w których zmiana dokonuje się pod wpływem zewnętrznych sygnałów. Zwykle się ich nie rysuje bo są oczywiste - tu pojawiły się wraz ze smrodkiem dydaktycznym   Stan  3, 5 i 6 to stany zależne jedynie od wewnętrznych operacji  w programie.

Program obsługi zapisu kodu do pamięci uległ niewielkiej modyfikacji

int nrpozkod = 0; //nr pozycji do zapisu kodu
int stan = 1; // zmienna stanu programu
int p_kod = 0; //zmienna stanu przycisku start programu i zapisu do pamięci
long KOD = 0; // zmienna kodu do zapisu w pamięci
int IMPULS = 0; //zmienna długości impulsu kodu do zapisu w pamięci
void zapisz_kod() {} //procedura wywołania zapisu kodu do pamięci nieulotnej
void do_zapisz_kod() { // resety i zapisz kod
  zapisz_kod();
  Blynk.virtualWrite(V112, KOD);
  reset_all();
}
void reset_all() { // resety po zakończeniu procedury
  o_kod = 0; KOD = 0; p_kod = 0; o_impuls = 0;
}
void KOD_do_eeprom() { //graf czyli automat skończony procedury zapisu kodu 433MHz do pamięci
  switch (stan) {
    case 1: {
        if (p_kod != 0) stan = 2; else stan = 1;
        o_kod = 0; KOD = 0;
      } break;
    case 2: {
        if (p_kod == 0) stan = 1;
        if (o_kod != 0) stan = 3;
      } break;
    case 3: {
        KOD = o_kod;
        IMPULS = o_impuls;
        o_kod = 0;
        stan = 4;
      } break;
    case 4: {
        if (p_kod == 0) stan = 5;
        if (o_kod != 0) stan = 6;
      } break;
    case 5: {
        do_zapisz_kod();
        Blynk.virtualWrite(V111, 0);
        stan = 1;
      } break;
    case 6: {
        if (o_kod == KOD) stan = 5; else stan = 3;
      } break;
    default: break;
  }
}

Jest bardziej czytelny i prostszy i nawet nie specjalnie dłuższy niż  w wersji poprzedniej.

W konkursie piękności u mnie zwyciężył automat Mealy'ego. W kolejnych odcinkach przyjrzymy się mu bardziej szczegółowo.

Wcześniejsze odcinki serialu
http://100-x-arduino.blogspot.com/2017/11/router-433-mhz-aczymy-rozne-systemy.html
http://100-x-arduino.blogspot.com/2017/11/router-433-mhz-nauka-czytania-i-pisania.html

Pożyteczne strony
Rysowanie algorytmów i grafów jest możliwe w wielu programach. Jest również wiele stron oferujących rysowanie online. Polecam stronę ze znakomitym narzędziem do rysowania schematów blokowych.
Prosty edytor grafów jest tu >>>> . Prosty ale wystarczający do większości zastosowań. Jego zaletą jest automatyczne rysowanie grafu wg tabeli powiązań.
Analogicznie dostępny jest edytor schematów blokowych. Definiujemy tabelę a algorytm sam się rysuje. Czego to ludzie nie wymyślą!

105

Brak komentarzy:

Publikowanie komentarza