Programowanie zoptymalizowane pod względem wykorzystania czasu procesora

Programowanie zoptymalizowane pod względem wykorzystania czasu procesora

Na "starym" easy-sofcie był artykuł Jacka Domańskiego, który zginął przy przenosinach. Na Waszą prośbę - umieszczam go ponownie. W artykule Jado przedstawia swoje sposoby na programowanie mikrokontrolerów jednoukladowych (8051, PIC i podobne) w języku asemblera - ze szczególnym zwróceniem uwagi na jak najmniejsze obciążenie procesora przez dużą liczbę podprogramów (współdzielenie czasu procesora). Takie podejście sprawia, że układ mikroprocesorowy staje się jakby układem wielozadaniowym, a przy okazji zostaje zminimalizowane niebezpieczeństwo zawieszania się programów i utknięcia całości w martwym punkcie.

Z pewnością wiele z przedstawionych poniżej sposobów i sposobików będzie Wam znanych, część być może bedzie nowych - generalnie chodzi jednak o całościowe uświadomienie sobie takiej koncepcji programowania tak, aby później można ją było świadomie stosować w praktyce (programowanie: od koncepcji do realizacji). Opisane tu sposoby będą przydatne zwłaszcza przy pisaniu większych programów, gdzie się "wiecej dzieje jednocześnie", ale mogą być również stosowane przy prostszych programach. Tyle tytułem wstępu - chciałbym jednocześnie zachęcić wszystkich programujących do opisywania własnych sposobów na programowanie - zwłaszcza o opisy koncepcji, podejścia do problemu i jego rozwiązania, bo dostępne jest mnóstwo przykładów, fragmentów procedur, etc ,etc, ale brak jest bardziej ogólnych zasad programowania, a wydaje mi się że zapotrzebowanie na tego rodzaju literaturę jest bardzo duże.
 
Uświadomienie sobie problemu to już połowa sukcesu.
Przeważnie piszący programy skupiają się na sposobie rozwiązania problemu - jak najprościej (jeśli nie "jak w ogóle") napisać programik, który będzie realizował przewidziane przez nich zadanie. Jest to podejście słuszne, aczkolwiek "mikroskopowe". Trochę na zasadzie: " bierze się procedurę obsługi I2C, do tego dodaje obsługę klawiatury, wyświetlacza LCD, łączy sie razem i mamy programik". Potrzebne jest również myślenie "makroskopowe", które będzie nadrzędne, będzie obejmowało całość koncepcji programu. Jednym słowem - planowanie. Oczywiście ma to szczególnie sens przy większych programach, gdzie łatwo się jest pogubić w gąszczu procedurek, etykiet, skoków itp.
Częstokroć skupiając się na problemie bieżącym zapominamy o całości programu i o tym jaki wpływ na pozostałe procedury i programy bedzie miala opracowywana przez nas procedura. W skrajnym przypadku może to prowadzić do powstania sytuacji, kiedy napisane przez nas programy - każdy oddzielnie, działają prawidłowo, ale kiedy połączymy je wszystkie razem, to okazuje się, ze wzajemnie zakłócaja sobie pracę. W rezultacie program nie działa albo działa "skokowo", gubiąc po drodze dane. Jak temu przeciwdziałać - poniżej.
 
Wizualizacja koncepcji działania procesora. Martwe pętle.
Dla ułatwienia zrozumienia działania procesora przygotowałem przykład opisowy - tzw. "łopatologiczne przedstawienie". Jednym z głównych zadań jakie wykonuje procesor jest tzw. pętla główna. Wyobraźmy ją sobie jako dośc duży, kwadratowy pokój. Pośrodku pokoju stoi człowieczek, który jest odpowiednikiem bieżącego procesu wykonywanego przez procesor.
Na początku naszych rozważań nasza pętla głowna jest pusta:
 
 petla_glowna:
     nop
     goto petla_glowna
 
Wlączamy zasilanie procesora - teraz nasz czlowieczek zaczyna chodzic sobie wokól pokoju zupelnie nic nie wykonując - po prostu chodzi w kólko. Aby dać mu jakieś zajęcie wprowadzamy do akcji podprogramy.   Wyobraźmy je sobie jako pokoje umieszczone wokól naszego głownego pomieszczenia.   Nasz czlowieczek idąc wokół scian, gdy napotyka  pokój, wchodzi do niego i wykonuje znalezione tam przez niego zadanie. Po czym wraca do pokoju głownego, idzie dalej wzdłóż sciany, do nastepnego pokoju i wykonuje następne zadanie.... I tak dalej, w kółko Macieju...
 
petla_glowna:
    podprogram_1
    podprogram_2
    podprogram_3
    podprogram_4
    ...
    podprogram_n
    goto petla_glowna
 
Na razie sytuacja jest prosta, ale jak to w życiu bywa, zaczynaja sie utrudnienia. Okazuje się bowiem, że nie zawsze jest możliwa natychmiastowa realizacja zadania przez procesor (czyli naszego czlowieczka).  Nie dowieźli danych i nasz człowieczek musi czekać na nie.   Siada więc sobie gdzieś na krzeselku albo chodzi w kólko po pokoiku podprogamu. Wyglada to tak:
 
petla_glowna:
    podprogram_1
    podprogram_2
    podprogram_3
    podprogram_4
    ...
    podprogram_n
    goto petla_glowna 
  
podprogram_1:
   sprawdz_czy_sa_dane
      jesli_nie - goto podprogram_1
      jesli_tak - wykonaj podprogram 
   i powroc z podprogramu
 
Jak widzimy nasz czlowieczek uzależniony jest od przeplywu danych, musi siedzieć w pokoiku i czekać na nie. A przecież ma do wykonania prace takze w innych pokojach.  Może akurat w ktorymś z nich są juz wszystkie potrzebne dane, a on siedząc tutaj marnuje tylko czas, podczas gdy moglby wykonać inne zadania. I tu dochodzimy do sedna problemu. Są to wlaśnie tzw. martwe pętle, podczas których procesor testuje jakiś warunek i czekając na niego nie wykonuje nic więcej. Wprowadźmy więc usprawnienie. Nasz człowieczek po wejściu do każdego pokoju będzie sprawdzał czy są dla niego dane - jeśli będą -  wykona zadanie, jeśli nie - opuści on pokój i pójdzie sprawdzać następne pokoje. Jeśli w którymś z nich bedą dane, będzie wykonywał tam swoje zadanie. A oto jak to wyglada w praktyce:
 
petla_glowna:
    podprogram_1
    podprogram_2
    podprogram_3
    podprogram_4
    ...
    podprogram_n
    goto petla_glowna
 
podprogram_1:
   sprawdz_czy_sa_dane
      jesli_nie - wyskocz z  podprogramu
      jesli_tak - wykonaj podprogram 
  i powroc z podprogramu
 
W ten sposób nie mamy blokady  podprogamów w oczekiwaniu na dane. Nawet gdyby z jakiś powodów dana ta nie nadeszła nigdy, to program nie zawiesi się w martwej pętli - pozostałe podprogramy będą się dalej wykonywać bez zakłoceń. Oczywiście, bywają pewne krytyczne czasowo operacje, które muszą zangażowac całą moc obliczeniową procesora, gdyż obsługa w tym czasie innych zadań spowodowałaby utratę części danych.  W tej sytuacji rozsądnym rozwiązaniem tego problemu jest wykorzystanie przerwań - zwłaszcza, gdy dane przychodzą asynchroniczne.  
 
Przekazywanie informacji przez  flagi
Najprostszym sposobem na sygnalizacje czy nadeszły ważne dane są flagi. Flaga jest bitem, ktorego ustawienie lub wyzerowanie informuje procesor o stanie danych. W postaci programu wygląda to nastepująco:
 
podprogram_1:
   sprawdz_czy_flaga_ustawiona
      jesli_nie - wyskocz z  podprogramu
      jesli_tak - wykonaj podprogram 
   i  powroc z podprogramu
 
Flaga może być ustawiana przez inny podprogram (ten, który zbiera dla nas dane), jak i przez nasz wykonawczy podprogram (gdy np. chcemy zasygnalizować innym podprogramom, że wykorzystaliśmy już dane i mogą one ładować następną porcję).
 
podprogram_1:
   sprawdz_czy_flaga_ustawiona
      jesli_nie - wyskocz z  podprogramu
      jesli_tak - wykonaj podprogram 
      zeruj_flage 
 i  powroc z podprogramu
 
podprogram_2:
   sprawdz_czy_flaga_wyzerowana
      jesli_nie - wyskocz z  podprogramu
      jesli_tak -  pobierz_dane_i_umiesc_je_w_zmiennych
     ustaw_flage  
 i  powroc z podprogramu
 
 
W ten sposób, jak widzimy, następuje dwustronna wymiana informacji pomiedzy podprogramami - jednocześnie są one na tyle niezależne, że nawet w wypadku błędu (przerwa w dopływie nowych danych do programu 2) nie następuje zawieszenie się pracy programu głównego (tak czy tak podprogram zawsze wyskakuje). To pozwala np. na utworzenie dodatkowego podprogramu monitorujacego, który w wypadku wystąpienia błedu przywraca normalną pracę systemu (restart programowy, czyszczenie zmiennych, ponowna inicjalizacja otoczenia itp.). Flagi mogą być sprawdzane, ustawiane lub zerowane przez kilka programów, jeśli podprogramy korzystają z tych samych danych.
Czasami zdarza się, że chcielibyśmy mieć kontrolę nad wykonywaniem sie pewnych podprogramów w odniesieniu do innych podprogramów.  Na przykład taka sytuacja:  jeśli wykonuje się poprogram nr 1, to w tym czasie nie powinien wykonywać się podprogram nr 2, bo oba korzystają ze wspólnych zmiennych i mogą wystąpić błedy. Taka sytuacja zdarza się, gdy jeden z podprogramów wykonuje się w pętli głównej, a drugi w przerwaniu.  Podczas obsługi przerwania, program z pętli głownej zostaje przerwany "w trakcie" i jest wykonywany program z przerwania. Jeśli istnieje niebezpieczeństwo zamazania danych przez program z przerwania, należy również uzyć flag dla zapobieżenia niechcianej modyfikacji.  
 
 podprogram_2:    ; wykonywany w pętli głównej
    ustaw_flage_wykonywania_programu
    wykonaj_podprogram
    wyzeruj_flage_wykonywania_programu 
    i powroc z podprogramu
 
podprogram_3:   ; wykonywany w przerwaniu
   sprawdz_czy_flaga_wykonywania_podprogramu_2_ustawiona
   jesli_tak - wyskocz z  podprogramu
   jesli_nie -  wykonaj podprogram
   i powroc z podprogramu
 
W ten sposób jeden podprogram jest zsynchronizowany z drugim - sa wzajemnie powiazane i nie nastapi nigdy "zderzenie danych".
Za pomocą flag można też przekazywać programom informacje o upływie czasu - są to tzw. znaczniki czasowe. Zasada jest prosta - jeden z programów (najczęściej jest to program obsługi przerwania, wywoływany cyklicznie co określony odcinek czasu) po obliczeniu zadanego odcinka czasu, ustawia flagę informującą pozostałe programy, że "czas upłynął". Podprogram z pętli głównej, testuje cały czas tą flagę i gdy wykryje jej ustawienie - wykonuje określoną sekwencję działań. Oto przykład:
 
podprogram_1:   ; wykonywany w przerwaniu
   sprawdz_czy_uplynal_zadany_czas 
   jesli_nie -  wyskocz z podprogramu
   jesli_tak - ustaw flage TimeOn
   i powroc z podprogramu
 
podprogram_2:    ; wykonywany w pętli głównej
   sprawdz_flage_TimeOn
   jesli nieustawiona - wyskocz z podprogramu
   jesli ustawiona - wykonaj_podprogram i wyzeruj_flage 
   i powroc z podprogramu
 
W ten sposob nasz podprogram z pętli głównej wykonuje się np. co 1 s, zamiast wielokrotnie szybciej - nie obciąża to nadmiernie procesora i pozwala na wykonywanie innych podprogramów w tym czasie. Tego typu zwolnione wykonywanie się programów ma sens w wypadku np.odczytu pomiarów, gdy nie zależy nam na dużej ilości próbek na sekundę - zwłaszcza, gdy do tego dochodzą opóźnienia związane z czasem konwersji wyników przez urządzenia zewnętrzne.
 
Przekazywanie informacji przez zmiennei podział podprogramów na stany
Flagi ze wzgledu na swoją jednobitowość nadają się do sygnalizacji  sytuacji dwustanowych - "jest" albo "nie ma". Czasami potrzebujemy zasygnalizowac więcej stanów - wówczas z pomocą przyjdą nam zmienne. W jednym bajcie możemy przekazać do 256 stanów, co jest wystarczające w większości wypadków.
Należy odróżnić tzw. zmienne sterujące procesami od zmienych przekazujących bezpośrednie dane - np. o temperaturze otoczenia. Odpowiednia budowa podprogramów pozwala na zbudowanie pewnych zespołów decyzyjnych - sterujących wykonaniem podprogramu w zależności od wartości zmiennej sterujacej. Innymi słowy, podprogram składa się z kilku - kilkunastu ścieżek wykonania, a wybór konkretnej ścieżki zależy od wartości naszej zmiennej sterujacej. Taki podział podprogramów na kilka - kilkanascie sekcji ułatwia programowanie ze względu na pewną modularność programu, przyporzadkowanie ścieżek programu różnym sytuacjom w systemie i wreszcie - co najwazniejsze - pozwala na dokonywanie wstrzymań działania podprogramu wewnątrz niego, bez stosowania pętli martwych. Nasz podprogram zostaje jakby "zamrozony" w jednym ze stanów (określanym przez zmienną sterującą) na dowolnie długi czas. W tym czasie może on czekać na pojawienie się danych, skończenie wykonywania się innego programu czy inną sytuacje. Jednocześnie nie jest wstrzymywana praca procesora - cały czas przeskakuje on od jednego podprogramu do drugiego wykonując powierzone mu zadania. Ale oto przykład programu:
 
podprogram_1:
   sprawdz_wartosc_zmiennej_sterujacej
    jesli_wartosc =0 skocz do etykiety 'sciezka_0'
    jesli_wartosc =1 skocz do etykiety 'sciezka_1'  
    jesli_wartosc =2 skocz do etykiety 'sciezka_2'
    jesli_wartosc =n skocz do etykiety 'sciezka_n'
 
sciezka_0:
    sprawdz_czy sa_dane
    jesli nie - wyskocz z podprogramu (ew. skok do etykiety wspolne)
    jesli tak - pobierz dane
    ustaw zmienna_sterujaca_na_1
    wyskocz_z_podprogramu  
 
sciezka_1:
    wykonaj_dzialanie_zwiazane_z_obrobka_danych
    ustaw zmienna_sterujaca_na_2
    wyskocz_z_podprogramu  (ew. skok do etykiety wspolne) 
 
sciezka_2:
   sprawdz_czy_mozna_dokonywać_opracji_zapisu
    jesli nie - wyskocz z podprogramu (ew. skok do etykiety wspolne)
    jesli tak - zapisz dane
    ustaw zmienna_sterujaca_na_n
    wyskocz_z_podprogramu   
...
sciezka_n:
    wykonaj_dzialanie_tej_sciezki
     ...
    ustaw zmienna_sterujaca_na_0  ; tu wracamy do stanu poczatkowego podprogramu
    wyskocz_z_podprogramu   
 
wspolne:
     i  powroc z podprogramu
 
Napisany w ten sposób podprogram, wywoływany cyklicznie w pętli, bedzie kolejno przechodził wszystkie swoje stany, w każdym bedąc tak długo, jak to potrzebne, a jednocześnie nie będzie martwego oczekiwania na dane.
Jak widzimy, podprogram w każdym ze swoich stanów dokonuje modyfikacji zmiennej sterującej - może to być inkrementacja, dekrementacja, wpisanie bezpośredniej wartości, skoki po kilka stanów na raz. To pozwala na dużą elastyczność naszego podprogramu. W pewnym sensie jest to samomodyfikacja programu, choć wykonana w sposób dozwolony (w przeciwieństwie do modyfikacji kodu programu w pamięci). 
W przerwach na oczekiwanie na dane procesor może wykonywać obsługę innych programów nie tracąc przy tym kontroli nad bieżąco wykonywanym podprogramem. Jednocześnie w każdej chwili możemy stwierdzić na jakim etapie wykonywania jest nasz podprogram. Wystarczy w tym celu sprawdzić zmienną sterującą.  
Ale pójdźmy jeszcze dalej - ten popdprogram będzie się wykonywał cyklicznie, po kolei przechodząc swoje stany od 0 do n i tak w kółko. Czasami jednak chcemy, aby podprogram wykonywał się tylko raz (podobnie, jak to ma miejsce w przypadku zwykłych podprogramów). Dodajmy zatem jeszcze jeden stan - stan spoczynkowy, w ktorym nasz podprogram, mimo że wywoływany cyklicznie, nie wykonuje nic. Będzie to wygladało następująco:
 
podprogram_1:
   sprawdz_wartosc_zmiennej_sterujacej
    jesli_wartosc =0 skocz do etykiety 'sciezka_0'
    jesli_wartosc =1 skocz do etykiety 'sciezka_1'  
    jesli_wartosc =2 skocz do etykiety 'sciezka_2'
    jesli_wartosc =3 skocz do etykiety 'sciezka_3'
    jesli_wartosc =n skocz do etykiety 'sciezka_n'
 
sciezka_0:
   wyskocz z podprogramu (ew. skok do etykiety wspolne)
 
sciezka_1:
    sprawdz_czy sa_dane
    jesli nie - wyskocz z podprogramu (ew. skok do etykiety wspolne)
    jesli tak - pobierz dane
    ustaw zmienna_sterujaca_na_1
    wyskocz_z_podprogramu  
 
sciezka_2:
    wykonaj_dzialanie_zwiazane_z_obrobka_danych
    ustaw zmienna_sterujaca_na_2
    wyskocz_z_podprogramu  (ew. skok do etykiety wspolne)
 
sciezka_3:
   sprawdz_czy_mozna_dokonywać_opracji_zapisu
    jesli nie - wyskocz z podprogramu (ew. skok do etykiety wspolne)
    jesli tak - zapisz dane
    ustaw zmienna_sterujaca_na_n
    wyskocz_z_podprogramu   
...
sciezka_n:
    wykonaj_dzialanie_tej_sciezki
     ...
    ustaw zmienna_sterujaca_na_0  ; tu wracamy do stanu poczatkowego podprogramu
    wyskocz_z_podprogramu   
 
wspolne:
    i  powroc z podprogramu
 
W tej sytuacji - po jednorazowym przejściu przez wszystkie stany, nasz podprogram zatrzymuje się w stanie 0, gdzie praktycznie nic nie robi (minimalnie obciążając przy tym procesor). Jedynym sposobem wyprowadzenia go z tego stanu jest wpisanie do zmiennej sterującej wartości 1 (albo też i innej, różnej od 0, bo możemy skakać do dowolnego fragmentu naszego podprogramu). W ten sposób dowolny, inny program może wyzwalać nasz podprogram jedynie zmieniając wartość zmienej sterującej. Po jednorazowym wyzwoleniu naszego podprogramu zaczyna on już jakby "żyć samodzielnie", aż do jego pełnego wykonania. Można to porównać do lawiny kamieni poruszonej rzutem jednego kamienia. 
Jest to bardzo wygodne np. przy obsludze klawiatury i wyzwalanych przez nacisnięcie klawiszy działaniach - we fragmencie odpowiadajacym danemu klawiszowi wpisujemy tylko wartośc, którą ma przyjąć nasza zmienna sterująca i gotowe. Dodatkową korzyścią (i często koniecznością) jest możliwość sprawdzenia czy nasz podprogram nie jest już w stanie "wyzwolenia" przez jakiś proces. Wystarczy sprawdzić zmienną sterującą i jeśli jest ona różna od zera, to nasz podprogram jest w trakcie wykonywania się.
 
Zasady dzielenia podprogramów na stany
Każdy z fragmentów podprogramu odpowiadający jednemu ze stanów, na które dzielimy nasz program powinien spełniać pewne kryteria, aby ten podział po prostu miał sens.
Po pierwsze, jeśli w podprogramie ma występować odwoływanie się do sprzętu, układów zewnętrznych (np. zewnętrznych pamięci EEPROM itp.), to kolejne stany powinny być tak uporządkowane, aby podczas każdego z nich następowało tylko jedno odwołanie do układów zewnętrznych, tak aby okres oczekiwania na dane przypadał na czas pomiędzy stanami (inicjalizacja transmisji w jednym stanie/wyskok ze stanu/odczyt danych w następnym stanie).  
Po drugie, jeśli nasz program jest dość skomplikowany, zawiera wiele instrukcji itd., itp., to należy starać się, aby zostały one podzielone na kilka fragmentów o zbliżonym czasie wykonania, tak aby uniknąć spiętrzania się obciążenia procesora w jednym ze stanów, podczas gdy w innych obciążenie to będzie bardzo małe. Generalnie należy dążyć, aby stany były dość krótkie czasowo. Umożliwi to dostęp do procesora innym podprogramom (ich stanom).   
Przy odwoływaniu się do układów zewnętrznych, przy różnego rodzaju transmisjach występują pewne sekcje krytyczne, które muszą sie wykonywać w pewnym, ściśle określonym czasie - zakłócenie tego czasu spowoduje błąd transmisji. Natomiast pomiędzy tymi sekcjami mogą występować przerwy - zazwyczaj o dowolnie długim czasie trwania, bez obaw o zakłócenie transmisji. W zależności od rodzaju protokołu przerwy te mogą występować pomiędzy pojedyńczymi bitami danych, pomiędzy bajtami lub większymi zespołami bitów. To doskonale nadaje się do wykorzystania w naszych programach dzielonych na stany - w jednym ze stanów wykonujemy sekcje krytyczną dostępu do danych (1 bit, bajt lub wiecej bitów), pomiędzy nimi wyskakujemy ze stanu i mogą się wykonywać inne podprogramy, a następnie wchodzimy w następny stan, gdzie wykonuje się następna sekcja krytyczna itd, itd. W ten sposób transmisja danych, mimo, że stosunkowo wolna w porównaniu z szybkością wykonywania instrukcji procesora, nie zwalnia działania procesora, nie ma żadnych wstrzymań na czas transmisji danych (z wyjątkiem sekcji krytycznych) - dzieje się ona jakby "w tle", "w wolnych chwilach procesora". W ten sposób można w jednym układzie połączyć np. obsługę transmisji I2C, RS232, 1-wire, obsługę przetwarzania A/C, klawiatury, wyświetlacza, beeper'a i jeszcze kilku innych urządzeń i transmisji, a mimo to nie następuje żadne zazębianie się tych programów. Z punktu widzenia obserwatora wykonują się one równocześnie.     
 
Praktyczne przykłady programów  
Oto przykłady praktyczne realizacji w/w sposobu programowania - w  języku ASM dla procesorów 8051 i PIC. Proszę zwrócić uwagę na jedną rzecz - jesli procedurę wywołujemy poleceniem CALL, to wówczas każda ze ścieżek musi się kończyc poleceniem RET (RETURN). Jeśli natomiast procedurę wywołujemy poleceniem GOTO (SJMP), to każda ze scieżek musi się kończyć również GOTO (SJMP). Procedury te możemy stosować zarówno w pętli głownej, jak i w przerwaniach - przy przerwaniach powrotem z procedury bedzie oczywiscie rozkaz RETI.
 
a) Dla mikrokontrolerów z rodziny MCS51
 
;----------------------------------------------------------------------------
TimeKeeper  ; przykładowa nazwa naszej procedury
;----------------------------------------------------------------------------
               mov dptr,#poczatek_tablicy_skokow ; ładujemy adres tablicy
               mov a,zmienna_sterujaca  ; pobieramy naszą zmienna sterującą do ACC
               clr c  ;  zerowanie Carry
               rlc a  ; mnozymy zmienna sterujaca przez 2 bo rozkazy sjmp sa dwubajtowe
               jmp @a+dptr  ; skok do adresu w tablicy wskazywanego przez zmienna sterującą
 
;----------------------------------------------------------------------------
poczatek_tablicy_skokow:  ; tablica rozkazów koków
               sjmp sciezka_1    ; rozkazy skoków do różnych ścieżek działań
               sjmp sciezka_2
               sjmp sciezka_3
               sjmp sciezka_n
 
;----------------------------------------------------------------------------
sciezka_1:      ; sciezka dzialania nr 1
               nop  ; tutaj mamy ciało procedury
               inc zmienna_sterujaca ; następna ścieżka w następnym przebiegu procedury
               ret lub sjmp ciag_dalszy
 
;----------------------------------------------------------------------------
sciezka_2:      ; sciezka dzialania nr 2
               nop  ; tutaj mamy ciało procedury
               inc zmienna_sterujaca ; następna ścieżka w następnym przebiegu procedury
               ret lub sjmp ciag_dalszy
 
;----------------------------------------------------------------------------
sciezka_3:      ; sciezka dzialania nr 3
               nop  ; tutaj mamy ciało procedury
               inc zmienna_sterujaca ; następna ścieżka w następnym przebiegu procedury
               ret lub sjmp ciag_dalszy
 
;----------------------------------------------------------------------------
sciezka_n:    ; sciezka dzialania nr n
               nop  ; tutaj mamy ciało procedury
               mov zmienna_sterujaca,#0 ; wracamy do pierwszej scieżki w następnym przebiegu procedury 
               ret lub sjmp ciag_dalszy
 
;----------------------------------------------------------------------------
ciag_dalszy:   ; częsc wspólna - jeśli scieżki mają sie znowu polączyć 
              ret, reti lub sjmp
 
 
b) Dla mikrokontrolerów z rodziny PIC  (tu PIC 16F877)
 
;----------------------------------------------------------------------------
TimeKeeper; obsluga zegara czasu rzeczywistego - Maszyna stanow  
;----------------------------------------------------------------------------
              movlw HIGH(TKZwrotn)  ; pobranie PCH adresu spod labelu TKZwrotn
              movwf PCLATH     ; zaladowanie PCLATH (=PCH) ta wartoscia
              movfw TimeState     ; pobranie zmiennej sterującej do W
              andlw B'00000111'   ; zabezp indeksu przed przekroczeniem 8
              addlw D'3'          ; dodanie przeskoku o 3 rozkazy (do poczatku tablicy)
              addwf PCL,W    ; suma PCL i rej indeksowego w W
TKZwrotn
              skpnc                    ; spr. czy nie przekroczylismy FF 
                incf PCLATH,F        ; jesli tak, zwieksz PCH (za pomoca PCLATCH) o 1
              movwf PCL          ; zaladowanie PCL suma (skok)
 
 ;--------------------------------------------------------------------- -----
              goto TK_idle                       ; State=0
;-------inicjalizacja ukladu zegara
              goto DS1307_init                 ; State=1
;-------odczyt czasu z zegara
              goto DS1307_set_adres      ; State=2
              goto DS1307_read_time     ; State=3
;-------zapis czasu do zegara
              goto DS1307_write_time    ; State=4
;---------------------------------------------------------------------------
              goto TK_idle                      ; State=5
              goto TK_idle                      ; State=6
              goto TK_idle                      ; State=7
 
;****************************************************************************
TK_idle ; stan idle procedury 
;---------------------------------------------------------------------------
              clrf TimeState  ; zerowanie zmiennej sterującej
              return
 
itd....dalsze podprogramy.
 
Zakończenie
Programy tego typu można dosyć mocno rozbudowywać, chociaż w praktyce nie zdarzyło mi się wyjść poza kilkanaście stanów zmiennej sterującej dla jednego podprogramu. Dzieląc w ten sposób kilka czy kilkanaście swoich podprogramów mamy pewność, że będą się one wykonywały niemalże równolegle, wzajemnie przeplatając swoje stany i nie nastąpi blokowanie jednego podprogramu przez drugi. Oczywiscie, nie wszystkie programy da się wykonać w opisany sposób - to już wymaga indywidualnego rozpatrzenia sytuacji.   
 
Jacek Domański (Jado)

http://www.tomaszbogusz.blox.pl/

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.