Kurs programowania PIC
· Lekcja 3, programujemy linijke LED on 9 listopada, 2011
· Lekcja 2, tworzymy pętle opóźniające on 5 listopada, 2011
· Lekcja1, piszemy pierwszy program on 30 października, 2011
30 października, 2011
Ostatnio udało nam się uruchomić programator, przetestować jego działanie i w końcu zaprogramować PIC-a. Teraz nadszedł czas na poznanie tajników działania tego złożonego tworu jakim jest mikroprocesor. Zacznijmy jednak od opisu działania podstawowych komend asemblera użytych w poprzednim programie testowy.
Należy wspomnieć, że rodzina PIC-ów, którą jak mniemam postanowiłeś się zająć posiada jedynie 35 komend w języku asembler. Znakomicie ułatwia to rozpoczęcie programowania. Nie musimy uczyć się wielu komend, których zadaniem jest wykonanie pojedynczego działania mikroprocesora. Mamy za to 35 komend, które zostały uznane za najpraktyczniejsze. Część z nich powoduje wykonanie jednej operacji, a część jest złożeniem kilku. Tak małą ilość komend zawdzięczamy architekturze RISC w jakiej został wykonany mikroprocesor. Charakteryzuje się ona właśnie zniwelowaniem ilość komend. Zostało to osiągnięte poprzez połączenie kilku podstawowych komend w jedną.
Teorią dotyczącą działania mikroprocesora i subtelnościami PIC-ów zajmiemy się możliwie w najbliższym czasie. Przy nauce programowania w asemblerze i tak siłą rzeczy poznamy jego działanie, a czytanie suchej niezrozumiałej teorii na pewno dla nikogo nie jest przyjemnością.
Nazwałem tak część programu, która musi wystąpić przy każdym tworzonym projekcie. Składa się na nią:
– Zdeklarowanie używanego mikroprocesora. Realizuje tą funkcję komenda, która jest wysyłana tylko do kompilatora. Ma to na celu zapoznanie kompilator z procesorem, który programujemy. Dzięki temu wie on niejako jakie adresy mogą wystąpić w danym programie. Oznacza to tyle, że dzięki temu kompilator wyrzuci nam błąd, kiedy odwołamy się do rejestru, który nie istnieje w programowanym mikrokontrolerze.
Procesor 16f84 ;to jest właśnie ta komenda
– Powiązanie pliku nagłówkowego nagłówkowego z pisanym programem. Jak już doszliśmy do wniosku w poprzednim artykule, (a raczej ja napisałem) plik nagłówkowy znakomicie ułatwia pisanie programu i skraca i tak już długi czas poświęcony na programowanie. Taki plik nagłówkowy, dany nam przez twórców kompilatora asemblera, składa się praktycznie w całości z bloku procedur mających następujący wygląd, zasadę działania:
nazwa rejestru equ adres rejestru np.
PORTA equ xxx
Tłumacząc, rejestr o adresie xxx nazwij PORTA. Dzięki temu zamiast przy każdym kolejnym odwołaniu do adresu rejestru PORTA, zamiast pisać niezrozumiały i mylący adres piszemy po prostu PORTA.
W pliku nagłówkowym znajdują się również adresy procedur bitów konfiguracyjnych. Chodzi mi o bity odpowiedzialne za wyłączenie/włączenie watchdoga, typ kwarcu z jakim pracujemy itp.
– Ustawienie bitów konfiguracyjnych, fusebitów. Wspominałem już wcześniej o swoim problemie z watchdogiem. Takich problemów unikniemy świadomie ustawiając bity konfiguracyjne. Najważniejsze z nich to właśnie watchdog, oscylator z jakim pracujemy i kilka innych, które na moim etapie nauki po prostu zostawiam w spokoju.
_config – mówimy procesorowi o tym, że zaraz nastąpi przesłanie danych dotyczących ustawienia bitów konfiguracyjnych.
_XT_OSC – oscylatorem jest kwarc o normalnej prędkości (do 4 MHz)
_WTD_OFF – wyłączamy watchdoga
Wszystkie procedury konfiguracji łączymy ze sobą spójnikiem &.
Mamy już za sobą konfigurację. Jeszcze raz zwracam uwagę na to, że są to bardzo ważne ustawienia. Często rozpoczynając programowanie chcemy jak najszybciej skompilować program i przetestować czy wszystkie komponenty działają. Jeżeli mamy trochę szczęścia to pierwszy prosty program zadziała (np. ze względu na to, że użyjemy oscylatora RC, który jest automatycznie ustawiany). Niestety po wymianie przytoczonego oscylatora na kwarc wszystko zacznie się sypać, my stracimy chęci i skończymy z nauką uważając, że jest to jakieś magiczne. W końcu skoro nie działa wpisanie 1 na wyjście RB.0, które wcześniej zostało zainicjowane jako wyjście, to albo coś jest nie tak ze sprzętem, albo z człowiekiem. Zamiast skazywać się na takie rozmyślania sumiennie przestudiujmy działanie bitów konfiguracyjnych i dobierzmy je wedle swoich potrzeb.
PIC, którym się zajmujemy czyli pic16f84 posiada dwa porty wyjściowe. PORTA o adresie 05h i PORTB o adresie 06h. PORTA posiada cztery wyjścia RA.0 (nóżka 17), RA.1 (nóżka 18), RA.2 (nóżka 1), RA.3 (nóżka 2) i RA.4 (nóżka3). Dla nas oznacza to możliwość zaadresowania takiego portu przy użyciu 5 bitów (kod dwójkowy). Tłumacząc to prościej wpisanie liczby 00001 do PORTA spowoduje pojawienie się jedynki na wyjściu RA.0. Wpisanie 11111 spowoduje pojawienie się jedynek na wszystkich wyjściach tego portu. Oczywiście wcześniej musimy ustalić czy mają to być wejścia czy wyjścia, ale o tym za chwilę. PORTB ustawiamy analogicznie jak PORTA. Mamy tutaj do dyspozycji aż 8 wyprowadzeń. Należy wziąć pod uwagę to, że dwa z nich wykorzystujemy do programowania (RB.6 i RB.7). Nie oznacza to, że nie możemy ich używać, ale wpisanie jedynki na RB.7 w momencie gdy mamy tam podłączoną linie programatora nie spowoduje pojawienia się tam 5V. Dopiero po odłączeniu tej linii mikroprocesor zachowa się tak jak chcieliśmy.
Podsumowując: PORTA umożliwia obsługę 5 wyjść, PORTB natomiast 8. W tym drugim uważamy na wyjścia RB.7 i RB.6. Programujemy je normalnie, ale żeby działały tak jak chcemy odłączamy po prostu linie programatora. Na razie nie zajmujemy się specyfiką wyjść tych portów. W niedalekim czasie zajmiemy się tym, a na razie nie zaprzątaj sobie głowy tym co jest tam w środku i co oznacza TOCKI przy RA.4.
Zajmijmy się teraz opisem poszczególnych procedur użytych w programie. Na pierwszy ogień idzie ustawianie portów jako wejściowe i wyjściowe. Wejściowe oznacza, że będą one odczytywały sygnał zewnętrzny, natomiast wyjściowe będą przekazywać sygnał na zewnątrz.
Jak już, jako wielbiciel teorii się dowiedziałeś, bohater tego cyklu (pic16f84) ma pamięć podzieloną na dwa banki. Czemu i w jakim celu spróbujemy dojść później. Sygnały na podawane na porty, czy procedury odczytywania znajdują się w BANK0, a bity odpowiedzialne za ustawienie portów jako wyjścia i wejścia w BANK1. Dla programisty, czyli nas oznacza to przymus przełączenia się między tymi bankami w celu ustawienia portów.
Za to, który bank jest aktualnie włączony, odpowiada bit piąty rejestru STATUS, który znajduję się pod adresem 03h. Jeżeli bit ten jest wyzerowany to jesteśmy w obszarze BANK0 jeżeli ustawiony (1) to w obszarze BANK1. Zerowanie czyli ustawienie bitu w stan zera logicznego wykonywany jest za pomocą funkcji bcf rejestr,numer bitu .
BCF czyli bit clear, wyczyść bit.
Natomiast jedynkę logiczną ustawiamy komendą bsf rejestr,numer bitu.
BSF czyli bit set, ustaw bit.
Przechodzimy więc do przykładu. Jako, że na początku programu zawsze znajdujemy się w BANK0, przejdziemy teraz do BANK1. Dokonamy tego przy pomocy następującej procedury:
BCF STATUS,5 ;bit 5 rejestru STATUS jest teraz zerem, co oznacza, że jesteśmy jesteśmy obszarze BANK1.
Kiedy już znajdujemy się w pożądanym banku, musimy dokonać ustawienia portów. Wpisując do portu jedynkę ustawiamy go jako wyjście, natomiast wpisując zero jako wejście. W celu wpisania jakiejkolwiek liczby do rejestru musimy wykonać dwie czynności. Wpisać żądaną liczbę do rejestru w, a następnie przepisać zawartość rejestru w do docelowego rejestru (w naszym przypadku TRISA lub TRISB). W to główny rejestr mikroprocesora PIC, w którym przechowywane są dane liczbowe, które mogą być potem przesuwane do innych rejestrów, dodawane itp. Generalizując w to rejestr zapewniający przestrzeń do wykonywania działań na liczbach.
W celu wpisania czegokolwiek do rejestru w wykonujemy komendę movlw liczba. Gdzie wpisywana liczba musi mieć wyraźnie zaznaczony kod np.
MOVLW d’10’ – wpisuje do rejestru w liczbę 10 dziesięteni
MOVLW 10h – wpisuje do rejestru w liczbę 10 heksalnie
MOVLW b’1010’ – wpisuje do rejestru w liczbę 10 binarnie
Jest to ważne, bo w przypadku nie określenia kodu w jakim zapisana jest dana liczba kompilator wyrzuci błąd.
Kolejny krok to wpisanie liczby z rejestru w do innego rejestru. Jest to zadanie komendy movwf adres rejestru (przesuń z rejestru w do f). Jak widać dla znających język angielski chociażby w najmniejszym stopniu komendy asemblera są same w sobie zrozumiałe i czytelne. Jedyne co jest tu do zapamiętania to składnia jaka występuję po komendzie.
Podajmy dalszy przykład dla naszej liczby dziesięć. Załóżmy, że jesteśmy w BANK0 i chcemy tą dziesiątkę wpisać do PORTA, który wcześniej został zdefiniowany jako wyjściowy.
MOVLW 10h ;wpisujemy dziesięć do rejestru ogólnego przeznaczenia w
MOVWF PORTA ;przepisujemy zawartość z rejestru w do PORTA
Po wykonaniu tych dwóch komend na wyjściach PORTA panują następujące stany 01010. Oczywiście jest to włożona do rejestru w liczba dziesięć tyle, że zapisana dwójkowo.
Wiemy już jak ustawiać i zerować poszczególne bit. Oczywiście nie odnosi się to nie tylko do rejestru STATUS i bitu odpowiedzialnego za wybór banku. Poznanymi poleceniami jesteśmy w stanie wyzerować i ustawić każdy bit dowolnego rejestru. Jak wyzerować wszystkie bity danego rejestru? Mamy kilka możliwości. Mianowicie możemy zerować każdy bit po kolei poznaną już komendą, ale zużyjemy tym samym masę pamięci na powtarzanie tej samej komendy. Możemy wpisać zero do rejestru w, a następnie do zerowanego rejestru (przypuśćmy, że zerujemy PORTA).
MOVLW 00h ;wpisujemy zero do rejestru w
MOVWF PORTA ;wpisujemy zawartość w (czyli 0) do rejestru PORTA
Jak widać tracimy na to, tylko dwie komendy. My jednak znając wszystkie dostępne komendy skorzystamy z procedur clrf adres rejestru, która w jednej komendzie wyczyści nam całą zawartość wskazanego rejestru.
CLRF PORTA ;zerujemy cały PORTA
Na początek obrazek ze schematem układu, pod który napisany był program i pod którym na pewno on zadziała:
Mamy już wystarczająco dużo wiadomości by napisać program, który zapali nam diodę dajmy na to na wyjściu RA.3 , a pozostawi zgaszoną na wyjściu RA.4.
Zaczynamy oczywiście od najważniejszego, czyli procedur konfiguracyjnych:
;***PLIKI KONFIGURACYJNE***
processor 16f84
;wpisujemy typ naszego PIC-a
__config _XT_OSC & _PWRTE_ON & _CP_OFF & _WDT_OFF
;konfigurujemy oscylator kwarcowy i wyłączamy watchdoga
include <p16f84a.inc>
;dołączamy plik p16f84a.inc , w którym znajdują się nazwy wszystkich rejestrów PIC-a z ;przypisanymi adresami
Dodam, że po znaku ; umieszczamy komentarze, które później naprawdę się przydają. Nawet te najprostsze operacje warto obarczyć krótkim opisem. Najlepiej krótkim i zrozumiałym dla każdego, a najbardziej dla nas samych.
Procedura konfiguracyjna została już omówiona wcześniej, więc przejdźmy już do głównego programu. Naszym celem jest podanie 01000 na PORTA. Czwarty bit ma być jedynka co znaczy, że spowoduje włączenie odpowiednio dołączonej diody. 01000 binarnie to liczba 8 heksalnie. Tyle wprowadzenie teraz zajmijmy się programem. Cały PORTA ma tutaj pracować jako wyjścia danych czyli musimy go ustawić jako wyjście. Na ustawianie portów mamy dwa sposobu. Możemy to zrobić tak jak już wcześniej zaczęliśmy czyli:
;***USTAWIENIE PORTÓW I/O***
BSF STATUS,5 ;idziemy do BANK1
MOVWF TRISA ;przepisujemy zawartość w do rejestru TRISA odpowiedzialnego za ;ustawienie portów (PORTA)
BCF STATUS,5 ;powracamy do BANK0, gdzie wykonuje się większość operacji
Drugi sposób jest krótszy i zachęcam Ci do jego stosowania:
MOVLW 00h ;wpisujemy zero do w
TRIS PORTA ;procedura wpisująca zawartość w do rejestrów PORTA odpowiedzialnych za ;ustawienie portów, czyli do TRISA
Polecenie TRIS wykonuje trzy czynności. Najpierw zmienia obszar na BANK1. Następnie wpisuje zawartość w do rejestru TRIS podanego portu, a na końcu znowu wraca do BANK0. Znakomite ułatwienie, nie sądzisz?
Główna część programu jest w tym przypadku najkrótsza. Oczywiście musimy wpisać tą wspomnianą wcześniej ósemkę heksanie lub 01000 binarnie do PORTA. Mam nadzieję, że sam poradzisz sobie z tym zadaniem. Ja wspomnę tylko o przymusie zapętlenia programu. Procesor jeżeli jest włączony to ciągle pracują (na razie pomijamy różne tryby wstrzymania czy watchdogi), dlatego musimy go czymś zająć w większości przypadków wystarczy zakończyć program komendą end, a kompilator się wszystkim zajmie. My jednak lubimy być zabezpieczeni przed nie lubianymi wpadkami i sami zadbamy oto by procesor miał co robić. Skorzystamy tutaj z funkcji goto, czyli idź do.
Twój kod wpisujący 8 do PORTA
Program
GOTO Program ;idź do procedury Program
END ;kompilator tego wymaga
Procesor natrafia na procedurę (podprogram, jak zwał tak zwał) o nazwie Program, przechodzi dalej i trafia na komendę wysyłającą go do wcześniej zdefiniowanej procedury Program. Procesor posłusznie wykonuje rozkaz i jest zapętlony w nieskończoność. Oczywiście często zdarzy się, że będziemy musieli wyprowadzać program z takich pętli. Tym jednak zajmiemy się później. Twój program równie dobrze mógłby wyglądać tak:
GOTO Program ;idź do program i kolejny raz wpis 8 do PORTA
Jak widać programy wykonujące te samą funkcję możemy napisać na kilka różnych sposobów. Najczęściej jest tak, że wybieramy ten najoptymalniejszy lub ten, który najbardziej nam odpowiada. W przyszłości jeszcze nie raz przekonasz się o wielorakich możliwościach rozwiązania tego samego problemu.
Co już umiesz, powinieneś umieć?
Zapoznaliśmy się ze sposobem ustawiania konfiguracji nieodłącznym dla każdego programu. Umiemy wpisywać liczby do dowolnych rejestrów, a także zerować i ustawiać zarówno całe rejestry jaki i ich pojedyncze bity. Potrafimy zapętlić program i tworzyć własne procedury. Wiedza ta doprowadziła Cię mam nadzieję do stworzenia pierwszego programu. Nic wielkiego ledwie potrafisz zapalić diodę. Ona nawet nie mruga. Na większe projekty przyjdzie jednak czas. Te pierwsze programy, które wykonujemy samodzielnie są bardzo ważne i wymagają dużo cierpliwości, dlatego gratuluję Ci pierwszego własnoręcznie napisanego programu i do zobaczenia w kolejnym wpisie. Jeżeli masz chwilę czasu zostaw tutaj jakiś komentarz odnośnie jakości tekstu, problemów, czy chociażby pochwal się pierwszym programem.
5 listopada, 2011
W pierwszym ćwiczeniu nauczyliśmy się obsługiwać porty wejścia i wyjścia mikrokontrolera. Znamy również kilka przydatnych instrukcji asemblera pod PIC16. Dzisiaj spróbujemy napisać program, którego celem będzie mruganie diodą podłączoną załóżmy do portu RA.3 (pin 2).
Standardowo jak to bywa w programowaniu możemy to wykonać na kilka zgoła różnych sposobów, które dadzą jednak ten sam efekt. Teoretycznie moglibyśmy po prostu dodać do poprzedniego programu taką komendę:
Program ;główny program
BSF PORTA,3 ;ustaw RA.3 (RA.3 = 1)
BCF PORTA,3 ;zeruj RA.3 (RA.3 = 0)
GOTO Program ;powrót do programu głównego
Jak myślisz co się stanie jeżeli skompilujemy taki plik i wyślemy do mikrokontrolera? Dioda będzie mrugać, ale z tak dużą prędkością, że my z naszym niedoskonałym okiem będziemy widzieć ciągłe świecenie. Wynika to oczywiście z tego, że mikrokontroler na wykonanie jednej operacji (np. BSF, czy BCF) potrzebuje bardzo mało czasu. W przypadku pic16f84, którego używamy czas ten wynosi jak podaje katalog 200ns. Dla ścisłości te 200ns to czas pojedynczej instrukcji wykonywanej przez mikroprocesor. Składa się na niego kilka stałych czynności. Dla przykładu polecenie BSF będzie wykonywane mniej więcej tak:
– zaadresuje pamięć
– odczyta rozkaz BSF
– zwiększy licznik programu, o którym później, 0 jeden
– wykona rozkaz, w tym przypadku ustawi dany bit
Dla nas praktycznie nie ma to większego znaczenia. Dioda mruga nam z częstotliwością zależną od użytego kwarcu i nie satysfakcjonuje nas ten stan rzeczy. Musimy obniżyć częstotliwość mrugania, czyli przedłużyć czas wyłączenia diody i jej włączenia. Zrobimy to za pomocą programowego opóźnienia. W innych językach programowania mielibyśmy pewnie gotowe procedury odmierzające czas, ale niestety w asemblerze wszystko musimy robić samodzielnie.
Pętle opóźniającą wykonamy w następujący sposób. Zainicjujemy w mikrokontrolerze dwie zmienne np. liczba1 i liczba2. Następnie wpiszemy do nich odpowiednie wartości. Od tych wartości będzie zależała częstotliwość mrugania diody. Pierwszą zmienną z wpisaną wartością będziemy w każdym cyklu zmniejszać, a kiedy osiągnie ona zero przejdziemy do zmniejszania drugiej zmiennej. Po pojedynczym zmniejszeniu tej drugiej program znowu zacznie zmniejszać pierwszą i tak, aż wartość drugiej nie będzie się równać zeru. Jeżeli tak się stanie program opuści pętle opóźniającą i przejdzie do wykonywania dalszych komend.
Opóźnienie: ;tu wskakujemy po przejściu do opóźnienia
MOVLW .200 ;wpisujemy do rejestru w 200 dziesiętnie
MOVWF liczba2 ;wpisujemy 200 do naszej drugiej zmiennej
Petla 1 ;tu wskakujemy, gdy liczba1 = 0 i liczba2 została zmniejszona o jeden
MOVWF liczba1 ;wpisujemy 200 do naszej pierwszej liczby
Petla2 ;tu wskakujemy po każdym zmniejszeniu zmkiennej liczba1
DECFSZ liczba1,f ;zmniejszamy liczba1 o jeden i jeżeli liczba1 = 0 to przeskakuje jedną ;komendę
GOTO Petla2 ;tu wskoczy jeżeli liczba1 nie będzie zerem
DECFSZ liczba2,f ;tu wskoczy, gdy liczba1 będzie zerem i dodatkowo zmniejszy o jeden ;liczba2 i podobnie jak wcześniej, jeżeli jest zerem przeskoczy komendę jeżeli nie przejdzie ;do następnej
GOTO Petla1 ;jeżeli nie jest zerem idzie do Petla1, gdzie wpisuje 200 do licza1 i proces się ;powtarza
RETURN ;powrót do miejsca, z którego został wywołany podprogram opóźnienie, w momencie gdy liczba2 = 0.
Może to się wydać trochę skomplikowane, ale po krótkiej analizie działania dojdziesz do wniosku, że wcale tak nie jest. Wyjaśnijmy teraz działanie nowych komend. Pojawiła się tutaj komenda DECFSZ f,d ,gdzie f to nazwa zmiennej, która jest poddawana operacji, a d to miejsce gdzie zapisywany jest wynik operacji. Dla d = 0 (lub gdy dodaliśmy plik nagłówkowy po prostu w) wynik jest zapisywany w rejestrze w. Natomiast gdy d = 1 (lub f w programie z plikiem nagłówkowym) wynik jest zapisywany w rejestrze f.
DECFSZ to komenda, która jest złożeniem dwóch operacji. Mianowicie zmniejszenia zmiennej o jeden, a następnie sprawdzeniu czy zmienna równa się zeru. W naszym przypadku zmniejszaniu ulegały zmienne liczba1 i liczba2, a ich wartości zmniejszone wpisywane były jak gdyby do ich samych dzięki wstawieniu po zmiennej f (zapis wyniku do rejestru f). Po tej komendzie program ma możliwość wyboru. Jeżeli zmienna nie równa się zeru program wykonuje się dalej normalnie komenda po komendzie. W przypadku, gdy zmienna równa się zeru program przeskakuje, pomija jedną komendę i idzie do następnej
DECFSZ zmienna,f
Wykona komendę wpisaną tutaj, gdy zmienna nie jest równa zeru
Przeskoczy tutaj nie wykonując komendy poprzedniej, gdy zmienna = 0
W celu zrozumienia skąd PIC wie gdzie i o ile ma przeskakiwać należy wprowadzić pojęcie PC (program counter) czyli licznika programu. Przy przejściu do każdej następnej komendy licznik ten jest zwiększany o jeden. Z wartości tego licznika PIC wie, którą komendę ma aktualnie wykonać
1 Opoznienie:
2 MOVLW .200
3 MOVWF liczba1
4 Petla1
5 DECFSZ liczba1,f
6 GOTO Petla1
7 RETURN
W przypadku natrafienia na komendę DECFSZ w PC znajduję się liczba 5. Gdy zmienna liczba1 = 0, do PC jest dodawana liczba 2 i w następnym cyklu instrukcji przechodzi on do lini 7 (5+2=7). Jeżeli liczba1 jest różna od zera do PC jest dodawana standardowo jak przy każdej „zwykłej komendzie” liczba jeden. W tym przypadku PC = 6 i program wykonuje komendę GOTO Petla1.
Trafiła nam się kolejna zagadka. Skąd PIC wie, gdzie jest procedura Petla1, do której ma iść. Znowu jest tu wykorzystany licznik programu. Po natrafieniu na jakąś procedurę (np. Petla1) mikroprocesor zapisuje sobie informacje o tym przy jakim stanie PC (w tym przypadku PC = 4) została ona wywołana i dzięki temu przy instrukcji GOTO ta informacja jest wydobywana i program powraca tam, gdzie ma wrócić.
Praktycznie nie obchodzi nas zupełnie jaką wartość ma PC i jakie liczby są do niego wypisywane. Warto jednak wiedzieć, że takie cudo istnieje i to dzięki niemu PIC wie gdzie w aktualnym momencie ma skoczyć i jaką komendę wykonać.
Przy okazji skróciłem trochę program opóźnienia. Jednak najprawdopodobniej taki krótki będzie wytwarzał za małe opóźnienie i dioda będzie świeciła ciągle. Zależy to od tego jakiego rezonatora używasz.
Ten krótki programie da nam 200 cyklów opóźnienia, natomiast ten pierwszy dłuższy 200*200, czyli 40000. Mam nadzieję, że wiesz z czego wynikają te liczby. W pierwszym programie liczba1 jest zmniejszana w każdym cyklu o jeden, aż nie osiągnie wartości 0. Gdy osiągnie tą wartość liczba2 jest zmniejszana o jeden, program wraca do procedury Petla1, wpisuje do liczba1 kolejne 200 i żeby zmniejszyć liczba2 o kolejną jedynkę liczba1 znowu musi zostać zmniejszona do zera.
W powyższych przykładach komendy były pisanie w podprogramie. Podprogram to taka część kodu, która może zostać wywołana z dowolnego miejsca w programie poprzez instrukcję CALL podprogram. Działa to podobnie jak z instrukcją GOTO. Do PC jest wpisywana aktualna wartość, procesor skacze do podprogramu, wykonuje go i gdy natrafi na RETURN dodaje do poprzednio zapisanej wartości PC jeden przez co wraca do komendy bezpośrednio po CALL.
Dzięki podprogramom zmniejszamy wagę programu wynikowego. Zamiast przed i po zapaleniu diody dawać dwie kolumny opóźnień dodajemy tam jedynie CALL opóźnienie.
Zilustrujmy to na przykładzie
BSF PORTA,3 ;włączamy diodę
;Tutaj wklejamy kod opóźnienia czyli 5 linijek z poprzedniego programu
MOVLW .200
MOVWF liczba1
Petla1
...
tebulaj