Programowanie systemów embedded z wykorzystaniem narzędzi graficznych

| Technika

Zadania stawiane większości systemów mikroprocesorowych polegają na monitorowaniu określonych parametrów i wykonywaniu pewnych działań w reakcji na zmiany ich wartości. Tego typu czynności bardzo łatwo opisać za pomocą graficznego modelu maszyny stanów. Graficzny model może zaś zostać automatycznie przekształcony na kod programu, na co pozwalają obecne już na rynku narzędzia. Warto zatem bliżej przyjrzeć się ich możliwościom.

Programowanie systemów embedded z wykorzystaniem narzędzi graficznych

Automat skończony (finite-state machine) to, zgodnie z definicją, abstrakcyjny model zachowania systemu dynamicznego (zmieniającego wartość swoich parametrów wraz z upływem czasu) oparty na tablicy dyskretnych przejść między jego kolejnymi stanami. Mówiąc inaczej, jest to system, który może składać się ze skończonej liczby znanych stanów oraz przejść między nimi. Zarówno każdy ze stanów, jak i każde z przejść charakteryzować się może pewną liczbą czynności (operacji) do wykonania. Ręczne programowanie tego typu układu, szczególnie w przypadkach obejmujących dużą liczbę stanów (kilkadziesiąt lub więcej), może być zadaniem bardzo wymagającym. Dla rozbudowanych układów trudno także utrzymać czytelny oraz zrozumiały kod źródłowy, co sprzyja popełnianiu błędów oraz znacząco utrudnia ich wykrywanie. Z tego powodu opracowano narzędzia graficzne ułatwiające wizualizację zasady pracy systemu (poprzez przedstawienie diagramu stanów), śledzenie ewentualnych zmian oraz generowanie kodu źródłowego. Kod wygenerowany przez tego typu oprogramowanie przyjmuje zazwyczaj postać ogólnego szablonu bez odniesień do specyficznej platformy czy funkcji sprzętowych. Wymaga zatem integracji z wykorzystywaną platformą sprzętową.

Automat skończony jest pod wieloma względami idealnym modelem do tworzenia oprogramowania na potrzeby większości systemów mikroprocesorowych. Najważniejszą wspólną cechą tego typu systemów jest konieczność interakcji z otoczeniem za pomocą czujników oraz elementów wykonawczych (aktuatorów). Do typowych przykładów wykorzystywanych czujników zaliczyć można sensory ruchu, temperatury, wilgotności czy jasności, zaś wśród powszechnie używanych aktuatorów znajdują się wyświetlacze, diody LED, zawory oraz silniki. Systemy te mają skończoną liczbę możliwych stanów i z całkowitą pewnością zawsze znajdują się w jednym z nich, co powoduje, że bardzo łatwo przedstawić ich działanie za pomocą diagramu stanów.

Prawdopodobnie jednym z najprostszych przykładów systemu opartego na modelu automatu skończonego może być opis działania przełącznika oświetlenia, co przedstawione zostało na rysunku 1. System ma jedynie dwa stany, które można określić jako On (przełącznik włączony) oraz Off (przełącznik wyłączony). W tym samym czasie aktywny może być tylko jeden z tych stanów. Zmiana z jednego stanu na drugi odbywa się wskutek przejścia (transition). W omawianym przykładzie przejście odbywa się na skutek wystąpienia zdarzenia naciśnięcia przycisku określanego jako buton_pressed.

 
Rys. 1. Prosty diagram stanów opisujący pracę przełącznika oświetlenia

Samo nakreślenie diagramu stanów dla programowanego urządzenia może być bardzo pomocne, szczególnie w początkowej fazie projektu – na etapie uzgadniania oraz analizy zasady działania urządzenia. Pozwala to na bardzo czytelne przedstawienie idei działania układu, dzięki czemu schemat ten może znaleźć zastosowanie także w późniejszych etapach – tworzenia oprogramowania oraz testowania. Tworzenie tego modelu za pomocą narzędzi niebędących częścią zintegrowanego środowiska projektowego niesie jednak ze sobą wiele zagrożeń, z których podstawowym jest brak końcowej zgodności pomiędzy modelem graficznym oraz kodem programu. Jeśli kod (czyli również zasada działania układu) zostanie zmodyfikowany w trakcie dalszych prac nad projektem, istnieje duża szansa na to, że zmiana ta nie zostanie uwzględniona w diagramie stanów. W rezultacie osoba testująca nie będzie mogła opracować poprawnych procedur testowych, opierając się na otrzymanym diagramie. Wszystkie te kłopoty skutkować zaś będą koniecznością dodatkowych uzgodnień, co wiąże się z większym nakładem pracy i opóźnieniem w realizacji projektu.

Korzystając z tego spostrzeżenia, opracowano narzędzia pozwalające na integrację procesu rysowania diagramu z pisaniem kodu programu. Tego typu oprogramowanie dokonuje automatycznego generowania kodu źródłowego w wybranym języku programowania (zazwyczaj dostępne opcje to C, C++, Python lub Java) na podstawie uprzednio przygotowanego modelu automatu skończonego. Takie podejście do tworzenia oprogramowania, w którym pierwszym i podstawowym źródłem wiedzy o działaniu urządzenia jest model automatu skończonego opisujący ten układ, określane jest jako MDD, czyli Model-Driven Development. Wykorzystanie zintegrowanych narzędzi znacząco wspiera realizację tego podejścia, ponieważ eliminuje ryzyko braku synchronizacji pomiędzy modelem a rzeczywistym kodem źródłowym.

Na rynku znaleźć można obecnie wiele różnych środowisk wspierających oraz częściowo automatyzujących tworzenie oprogramowania w modelu MDD. W artykule przedstawiony zostanie przykładowy projekt wykonany z pomocą narzędzia Yakindu Statechart Tools, którego użycie do celów niekomercyjnych jest darmowe.

Narzędzie to pozwala na wykreślenie diagramu stanów, następnie zaś na jego podstawie generuje szablon kodu źródłowego. Każda edycja diagramu automatycznie przekłada się na zmianę oprogramowania. W środowisko wbudowano narzędzia symulacyjne pozwalające na testowanie zachowania stworzonego projektu. Generowany kod nie jest zależny od żadnej platformy sprzętowej, wymaga zatem dostosowania do potrzeb docelowego systemu, czyli dopisania funkcji zależnych od specyficznych parametrów sprzętowych oraz cech implementacji.

Przykład: automatyczne sterowanie oświetleniem

Dla zobrazowania wykorzystania wspomnianych narzędzi przedstawiona zostanie konstrukcja przykładowego projektu automatycznego sterowania oświetleniem klatki schodowej. Zasada działania systemu jest dość prosta – składa się ze źródła światła, przełącznika naściennego oraz czujnika ruchu, pozwalającego na automatyczne wykrycie obecności osoby w otoczeniu oraz zapalenie światła na określony czas.

W systemie wyróżnić można trzy tryby pracy:

  • Stan wyłączenia;
  • Stan włączenia aktywowany przełącznikiem i kontrolowany przez wbudowany timer. Oświetlenie samoczynnie wyłącza się po zdefiniowanym okresie.
  • Tryb automatyczny – stan oświetlenia kontrolowany jest przez czujnik ruchu.
  • Wybór trybu pracy odbywa się za pomocą przycisku, zaś obecny status urządzenia sygnalizowany jest przez umieszczone w systemie dwie diody LED.

Na podstawie przedstawionego opisu stworzyć można diagram stanów dla systemu, co zostało przedstawione na rysunku 2.

 
Rys. 2. Diagram przedstawiający działanie przykładowego systemu sterowania oświetlenie

Zmiana pomiędzy stanami Off , Timer oraz Motion_Automatic wyzwalana jest przez zdarzenia – naciśnięcie przycisku albo upływ określonego czasu (mierzony przez układ timera). Po jednokrotnym naciśnięciu przycisku układ przechodzi do stanu Timer, światło zapala się i samoczynnie gaśnie po 30 sekundach. Jeśli w tym czasie (gdy urządzenie znajduje się w stanie Timer) przycisk naciśnięty zostanie ponownie, system przechodzi do trybu Motion_Automatic. Za każdym razem, gdy czujnik ruchu wykryje czyjąś obecność, światło zapalane jest na 30 sekund. Licznik czasu resetowany jest po każdej detekcji ruchu przez czujnik. Jedna z diod LED sygnalizuje pracę w trybie Timer, druga w trybie Motion_Automatic, zaś w trybie Off obie pozostają zgaszone. Za pomocą przedstawionego diagramu stanów udało się wyczerpująco opisać pracę wszystkich komponentów systemu.

Jeśli opisany za pomocą diagramu układ przeznaczony jest do uruchomienia w systemie wbudowanym, możliwe jest generowanie kodu źródłowego bezpośrednio na podstawie diagramu. Wygenerowany kod zawiera całą logikę pracy systemu, pozbawiony jest jedynie funkcji typowo sprzętowych, operujących na zasobach docelowej platformy. Dla powyższego przykładu konieczne jest dopisanie fragmentów zawierających obsługę przycisku, timera, czujnika ruchu, a także zmianę stanu oświetlenia oraz diod LED.

Istnieje wiele sposobów programowej implementacji modelu automatu skończonego. Do najpopularniejszych z nich zalicza się metody oparte na tablicach stanów, wyrażeniach typu switch- case oraz, szczególnie w przypadku języków zorientowanych obiektowo, na wzorcu projektowym Stan (State pattern). Wykorzystywane w omawianym przykładzie narzędzie domyślnie generuje kod źródłowy z wykorzystaniem wyrażenia switch-case, co pozwala zapewnić dobrą wydajność przy dużej czytelności kodu.

Automatycznie wygenerowany kod źródłowy

Jak wspomniano, otrzymany kod oparty jest w głównej mierze na wyrażeniu switch-case. Główną część programu zawarto w funkcji runCycle, która przedstawiona jest na listingu 1.

/* List. 1. Główna część kodu */

void Lightswitch::runCycle()
{
   clearOutEvents();
   for (stateConfVectorPosition = 0;
      stateConfVectorPosition < maxOrthogonalStates;
      stateConfVectorPosition++)
      {
      switch (stateConfVector[stateConfVectorPosition])
      {
      case lightswitch_Off :
      {
         lightswitch_Off _react(true);
         break;
      }
      case lightswitch_Timer :
      {
         lightswitch_Timer_react(true);
         break;
      }
      case lightswitch_Motion_Automatic_motion_Motion :
      {
         lightswitch_Motion_Automatic_motion_Motion_react(true);
         break;
      }
      case lightswitch_Motion_Automatic_motion_No_Motion :
      {
         lightswitch_Motion_Automatic_motion_No_Motion_react(true);
         break;
      }
      default:
         break;
      }
   }
      clearInEvents();
}

Funkcja runCycle wywoływana jest za każdym razem, gdy pojawia się którekolwiek ze zdarzeń powodujących przejścia pomiędzy stanami. Dokonuje ona iteracji po wszystkich ortogonalnych stanach systemu, wykonując niezbędne czynności związane z przejściem pomiędzy poszczególnymi trybami pracy. Przykładowo, tryb Off ma tylko jedną możliwą sekwencję wejścia (podczas której wyłączane jest oświetlenie) oraz wyjścia. Zachowanie to opisane jest w funkcji lightswitch_Off _react z listingu 2.

/* List. 2. Obsługa stanu Off */

sc_boolean Lightswitch::lightswitch_Off _react(const sc_boolean try_
transition) {
   /* The reactions of state Off . */
   sc_boolean did_transition = try_transition;
   if (try_transition)
   {
   if (iface.button_raised)
   {
      exseq_lightswitch_Off ();
      enseq_lightswitch_Timer_default();
      react();
   } else
   {
      did_transition = false;
   }
 }
   if ((did_transition) == (false))
   {
      did_transition = react();
   }
   return did_transition;
}

Na listingu 3 zaprezentowano fragment kodu odpowiadający za przejście do stanu Timer. W pierwszej kolejności następuje sprawdzenie, czy zdarzenie naciśnięcia przycisku faktycznie miało miejsce. Następnie wywoływana jest sekwencja wyjścia ze stanu Off oraz wejścia do stanu Timer.

/* List. 3. Obsługa wyjścia ze stanu Off */

if (iface.button_raised)
   {
      exseq_lightswitch_Off ();
      enseq_lightswitch_Timer_default();
   react();
   }

Implementacja z wykorzystaniem Arduino Uno

 
Rys. 3. Schemat połączeń opisywanego układu oparty na platformie Arduino Uno

Na rysunku 3 przedstawiono schemat połączeń do realizacji projektu w oparciu o platformę Arduino Uno. Do płytki podłączono przycisk, dwie diody LED (piny 9 oraz 10) oraz czujnik ruchu (pin 7) – dla uproszczenia główne źródło światła symulowane jest przez diodę umieszczoną na płytce procesora.

Całość oprogramowania składa się z dwóch części – kodu wygenerowanego automatycznie na podstawie diagramu stanów oraz dopisanych funkcji sprzętowych.

Automatycznie generowany kod zawiera już elementy interfejsu pozwalające na obsługę oraz interakcję z modelem automatu skończonego. Wygenerowany kod napisany został w języku C++, zatem cały model zdefiniowany jest jako jedna klasa. Podczas pracy z systemem należy zatem odnosić się do poszczególnych metod zdefiniowanego uprzednio obiektu tej klasy, co pokazano przykładowo na listingu 4.

/* List. 4. Obsługa systemu opiera się na metodach klasy reprezentującej model automatu skończonego */

Lightswitch lightswitch;
   int main(){
      lightswitch.init();
      lightswitch.enter();
      lightswitch.raise_button();
 }

Podstawowym celem jest jednak uruchomienie projektu z wykorzystaniem platformy sprzętowej. W tym celu konieczne będzie napisanie prostego oprogramowania wykonującego w pętli następujące czynności:

  • Sprawdzenie sygnałów wejściowych (czujnik, przycisk, timer) pod kątem zmian wartości.
  • Przekazanie otrzymanych informacji na wejście modelu automatu skończonego.
  • Przetwarzanie informacji przez model automatu skończonego.
  • Sprawdzenie wyjść modelu automatu skończonego i reakcja na nie.

Pierwszą czynnością może być implementacja obsługi timera. Dla platformy Arduino można użyć do tego celu funkcji milis, zwracającej liczbę milisekund, która upłynęła od momentu uruchomienia systemu. Wartość ta jest cyklicznie przekazywana do modelu automatu skończonego, co przedstawia listing 5.

/* List. 5. Aktualizacja wartości timera w modelu automatu skończonego */

long now = millis();
   if(now - time_ms > 0) {
      timerInterface->proceed(now - time_ms);
      time_ms = millis();
   }

Wykrycie określonych zdarzeń za pomocą funkcji sprzętowych może spowodować wywołanie odpowiadających im zdarzeń zdefiniowanych w modelu automatu skończonego. Na listingu 6 przedstawiono przykład takiego działania dla kodu implementującego obsługę przycisku oraz czujnika.

/* List. 6. Obsługa przycisku i czujnika oraz uruchomienie odpowiednich metod modelu automatu skończonego */

if(buttonPressed) {
   lightswitch.raise_button();
   buttonPressed = false;
   }
   // read out motion sensor
   if(digitalRead(7)) {
      lightswitch.raise_motion();
   }

Po zakończeniu przetwarzania wszystkich sygnałów wejściowych model automatu skończonego samodzielnie ustawia odpowiednie wartości pól reprezentujących stany wyjściowe – w tym przypadku są to pola light (oświetlenie), led_timer (dioda LED sygnalizująca tryb Timer) oraz led_motion (dioda LED sygnalizująca tryb Motion_Automatic). Odpowiedni kod przedstawiono na listingu 7.

/* List. 7. Ustawienie stanów wyjściowych na podstawie wartości pól automatu skończonego */

// set light
digitalWrite(13, lightswitch.get_light());
// set mode LEDs
digitalWrite(9, lightswitch.get_led_timer());
digitalWrite(10, lightswitch.get_led_motion());

Dla zwiększenia energooszczędności urządzenia, w stanie Off , procesor może być umieszczany w trybie uśpienia. Naciśnięcie przycisku przez użytkownika spowoduje zgłoszenie przerwania oraz wybudzenie układu. Należy zauważyć, że podczas uśpienia nie ma możliwości aktualizacji wartości timera (bazując na przedstawionej implementacji opartej na funkcji milis), jednak w omawianym przykładzie nie ma takiej konieczności – w stanie Off nie jest on wykorzystywany. Odpowiedni fragment kodu przedstawiono na listingu 8.

/* List. 8. Przejście procesora w stan uśpienia w trybie O */

if(lightswitch.isStateActive(Lightswitch::lightswitch_Off )) {
   enterSleep();
}

Podsumowanie

W powyższym przykładzie opisano praktyczną realizację prostego projektu w oparciu o model automatu skończonego oraz narzędzia graficzne umożliwiające automatyczne generowanie kodu źródłowego na podstawie diagramu stanów. Do głównych zalet tego podejścia zaliczyć można dużą czytelność otrzymywaną na każdym etapie projektu, a także konieczność oddzielenia kodu odpowiadającego za logikę pracy systemu od części sprzętowej oraz wynikającą z tego łatwość ewentualnej zmiany platformy sprzętowej. Korzystanie ze zintegrowanych narzędzi pozwala ponadto utrzymać wzajemną aktualność kodu źródłowego oraz diagramu stanów przez wszystkie fazy cyklu projektowego.

 

Damian Tomaszewski