Programowanie mikrokontrolerów PIC
Dzisiaj zajmiemy się programem, którego celem będzie wyświetlanie linijki LED na wyjściach PORTA. Wiesz chyba co to jest linijka LED? Chodzi o to, że co pewien okres czasu zapala się kolejna dioda podłączona do wyjścia PORTA, a gaśnie poprzednia. Cykl powtarza się, aż do dojścia do ostatniej diody i rozpoczyna powrót dokładnie w ten sam sposób.
Ze względów omówionych później do linijki nie wykorzystałem w swoim programie wyjścia PORT,4, dlatego moja linijka skład się jedynie z czterech diod o kolorze żółtym. Program jest napisany tak by 1 na wyjściu włączała diodę. Oznacza to, że dla uzyskania tego samego efektu dioda powinna być włączona z wyjścia portu do masy. Schemat znajdziesz na końcu artykułu. Oczywiście nic nie stoi na przeszkodzie, aby umieścić diody drugą stroną. To znaczy od plusa do wejścia portu. W takim przypadku diody zapalałyby się, kiedy na wyjściu byłoby zero logiczne, a rotacja 1 z opisywanego programu wyglądałaby tak, że poruszałaby się jak gdyby jedna zgaszona dioda zamiast jednej zapalonej jak w przypadku opisywanej linijki.
Oczywiście Ty jeżeli masz trochę chęci możesz zmienić podany program i podłączyć diody LED pod wyjście PORTB. Będziesz miał wtedy aż 8 diod do testów. Pamiętaj tylko, że linia programatora RB7 nie może być włączona podczas działania programu. Jeżeli pozostawisz ją włączoną nic się nie stanie programatorowi, ani procesorowi. Nie będzie po prostu się zapalać ostatnia dioda z linijki. Pisałem już o tym w jednej z poprzednich części.
Poznaliśmy już ostatnimi czasy kilka pożytecznych komend teraz czas na kolejne. Przy realizacji programu przyda nam się komenda, która przesuwa bity rejestru w określona stronę. W asemblerze pod PICa mamy dwie takie komendy. Mianowicie
RRF rejestr,d
Gdzie rejestr to nazwa rejestru, którego bit chcemy przesuwać, a d to miejsce do którego ma zostać przeniesiona liczba powstała po przesunięciu. Dla przykładu w rejestrze PORTA mamy liczbę 00100 i chcemy ta jedynkę przesunąć o jedno miejsce. Możemy oczywiście w najnormalniejszy sposób wpisać do Porta liczbę 00010, ale my tego nie robimy, ponieważ chcemy poznać działanie nowej komendy.
RRF PORTA,f
Po wykonaniu tego działania liczba zapisana w PORTA będzie miała postać 00010, czyli taką jaką oczekiwaliśmy. To f na końcu oznacza, że przesuniętą wartość wpisujemy do PORTA. Gdyby komenda miała postać:
RRF PORTA,w
Liczba, czy mówiąc ściślej wynik nie zostałby zapisany do PORTA tylko do rejestru w. Nasz PORTA miałby wtedy wygląd taki jak na początku czyli 00100.
Hasło z tytułu jest chyba najważniejszą rzeczą w tym projekcie. To jest chyba jedyne miejsce, w którym można i popełnia się błąd. Próba przesunięcie zawartości rejestru, w którym znajduję się liczba 00000 spełźnie na niczym. Zapamiętaj, żeby przesunąć jakakolwiek jedynkę w lewo, czy w prawo najpierw musimy ta jedynkę tam wpisać, chyba że już tam jest ;) . W każdym bądź razie rejestr, na którym chcemy dokonać przesunięcia musi zawierać choćby jedną jedynkę.
Po kilku wstępnych uwagach bierzemy się za program. Oczywiście na samym początku ustawiamy fusebity, deklarujemy procesor itp. Kolejny krok to ustawienie portów. Ja w swoim programie jako port wyjściowy dla LED przyjąłem PORTA, ale równie dobrze mógłby to być PORTB. Nawet byłoby lepiej, ponieważ do niego możemy podłączyć większą ilość LED-ów.
Kolejną czynnością do wykonania dla programisty, czyli w tym przypadku Ciebie, jest wpisanie tej jedynki do aktualnie czystego, wyzerowanego rejestru. Jak już wspominałem musimy mieć co przesuwać. Osobiście na wszelki wypadek, gdyby coś w rejestrze przez przypadek znalazło się po zaprogramowaniu, zeruje cały rejestr przed głównym programem komendą:
CLRF PORTA
Wpisanie wspomnianej jedynki może wyglądać tak:
movlw 01h ;wpisanie pierwszej jedynki
movwf PORTA ;wpisanie do PORTA 1 do najmłodszego bitu
Teraz wystarczy już wpisać komendę RLF i wszystko będzie działać. STOP. Zapomnielibyśmy o przymusie dodania opóźnienia pomiędzy włączanie i wyłączanie kolejnych LED-ów.
Podprogram opóźnienia był już ostatnio opisany, więc nie ma co marnować na to objętości kolejnego. Tym razem dla uzyskania odpowiednio długiego opóźnienia użyłem aż trzech zmiennych. Ich inicjalizacja wygląda u mnie tak:
;***ZMIENNE***
cblock 0x0c ;blok zmiennych
liczba1 ;zmienna do opóźnienia
liczba2 ;zmienna do opóźnienia
liczba3 ;zmienna do opóźnienia
endc ;koniec bloku zmiennych
Natomiast sam podprogram opóźnienia tak:
opoznienie ;podprogram opóźnienia (długie)
movlw .4 ;czterokrotne powtórzenie najdłuższej pętli petla1
movwf liczba3
petla1
movlw .200 ;200 razy powtarza petla2
movwf liczba2
petla2
movlw .200 ;200 razy powtarza petle3
movwf liczba1
petla3
decfsz liczba1
goto petla3
decfsz liczba2
goto petla2
decfsz liczba3
goto petla1
return ;powrót do programu
Po wpisaniu podprogramu opóźnienia możemy już dokonać przesunięcia. Piszemy więc w ciele głównym programu np.
Program
Call opóźnienie
Rlf PORTA,f
Goto program
Kompilujemy odpalamy. Okazuje się, że program działa zgodnie z zamierzeniami. Przemiata jedynką po kolei po wszystkich wyjściach aż do samego końca. My jednak chcemy, żeby po osiągnięciu ostatniego wyjścia program zaczął wracać kolejno do poprzedniego i tak aż do pierwszego, a następnie znowu do drugiego aż do końca i tak dalej.
Naszym zadaniem jest teraz wyłapać moment, w którym włączone jest ostatnie wyjście i wymusić rotację w drugą stronę. W moim przypadku dla ostatniego wyświetlanego położenia jedynki w PORTA znajduję się liczba 01000 (PORTA.4 nie biorę pod uwagę ze względu na występowanie tam taimera). Jak widac musimy znaleźć moment, w którym bit PORTA,3 jest ustawiony. Dokonujemy tego znaną już procedurą :
Btfss PORTA,3
Jeżeli PORTA,3 będzie ustawiony to program przeskoczy o dwa miejsca na przykład do goto prawo, a jeżeli będzie wyzerowany powróci do programu przesuwania w lewo.
Btfss PORTA,3
Goto lewo
Goto prawo
Nasz program powinien składać się z dwóch części. Pierwsza to podprogram prawo, a druga to podprogram lewo. W podprogramie prawo odczekujemy opóźnienie, przesuwamy w prawo, sprawdzamy czy osiągnęliśmy stan końcowy. Jeżeli osiągnęliśmy to idziemy do podprogramu prawo, a jeżeli nie wykonujemy kolejne kroki w podprogramie lewo. Podprogram prawo wygląda identycznie tyle, że przesuwamy w prawo, sprawdzenia dokonujemy dla ustawienie PORTA,0, a jeżeli będzie on ustawiony to idziemy do podprogramu lewo.
PS. W tym przypadku nie chodzi mi o podprogramy wywoływane komendą CALL, tylko zwykłe etykiety prawo, lewo, do których dostajemy się przez komendę GOTO.
Ważne jest, żeby procedura opóźnienia była na samym początku podprogramu. Mam nadzieję, że powód wydaję Ci się oczywisty. Po prostu dzięki niej możemy zobaczyć jak gdyby liczbę wpisaną do PORTA. Jako, że chcemy oglądać kolejne stany 00001, 00010, 00100, 01000, 00100 itd. Opóźnienie musi wystąpić już na samym początku gdy mamy wpisaną do rejestru PORTA jedynkę do najmłodszego bitu. Jeżeli najpierw dokonalibyśmy przesunięcia, a następnie dodali opóźnienie ta jedynka na najmłodszym bicie trwałaby tak krótko, że zamiast niej zobaczylibyśmy od razu stan 00010. Tak samo, gdy wykryjemy stan skrajny. W moim przypadku jest to PORTA,3 = 1 lub PORTA,0 = 1, musimy poczekać na wyświetlenie. Z tego względu opóźnienie umieszczamy przed, a nie po rotacji. Pewnie było to dla Ciebie oczywiste, ale zawsze warto się upewniać ;) .
Dobrze by było gdybyś sam powalczył z napisaniem programu linijki LED. Najważniejsze fragment masz tutaj wklejone, ale wcale nie musisz z nich korzystać. W razie problemów dodaje program, który wykonałem. Następna lekcja to będzie omówienie przycisków. Zajmiemy się eliminacją drgań styków i dostosowaniem microswitchy do swoich potrzeb. Możliwe, że zdążymy również wzbogacić zaprogramowaną linijkę LED o obsługę przycisków. Do zobaczenia. Pamiętaj o komentarzu ;) .
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
Program
BSF PORTA,3 ;włączamy diodę
;Tutaj wklejamy kod opóźnienia czyli 5 linijek z poprzedniego programu
MOVLW .200
MOVWF liczba1
Petla1
DECFSZ liczba1,f
GOTO Petla1
BCF PORTA,3 ;wyłaczamy diodę
;I kolejne dodatkowe linijki zapewniające opóźnienie po wyłączeniu diody
MOVLW .200
MOVWF liczba1
Petla1
DECFSZ liczba1,f
GOTO Petla1
GOTO Program
END
Taki program rozwleka nam się na wiele linijek przez co jest nieczytelny i dodatkowo zwiększ wagę pliku wynikowego. Zamiast takich chałupniczych metod stosujemy po prostu podprogramy na części kodu, które się powtarzają.
Program
BSF PORTA,3 ;włączamy diodę
CALL opóźnienie ;idziemy do podprogramu opóźnienie
BCF PORTA,3 ;Wyłączamy diodę
CALL opóźnienie ;znowu idziemy do podprogramu opóźnienie
GOTO Program
;***PODPROGRAMY***
opóźnienie:
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.
END
W ten sposób otrzymujemy ładny schludny i lżejszy program.
Mniemam, że z napisaniem programu mrugania diody poradzisz sobie sam. Do ostatniego programu wystarczy dopisać konfigurację i ustawienie portów. Jeszcze słowo o inicjacji zmiennych. Wpisujemy je do pamięci SRAM, czyli do adresów od 0C do 4F oczywiście obie liczby podane heksanie. Inicjujemy je tak jak przypisujemy nazwę adresowi rejestru czyli za pomocą EQU. Możemy zrobić to na dwa sposoby. Pojedynczo
Liczba1 equ 0x0c
Liczba2 equ 0x0d
lub jako bok zmiennych
cblock 0x0d ;początek bloku zmiennych
liczba1
liczba2
endc ;koniec bloku zmiennych
Możesz poeksperymentować z różnymi czasami opóźnień, zmieniać wypełnienie (dużej włączać krócej wyłączać), mrugać dwoma, trzema lub dowolną ilością diod na przemian lub równocześnie. Takie eksperymenty są potrzebne, żeby przyswoić sobie podstawy programowania. Spróbuj np. wykonać opóźnienie przy pomocy trzech zmiennych.
Na końcu tego artykułu znajdziesz plik w asemblerze z moją propozycją programu do migania diody. Oczywiście Twój może być inny. Ważne, żeby spełnione były założenia i program był możliwie zoptymalizowany. Pamiętaj również o treściwych komentarzach. Tymi, które ja tutaj piszę nie sugeruj się. Są one zdecydowanie zbyt rozwleczone, ze względu na to, że wyjaśniam w nich działanie komendy. W programie pisze np.
DECFSZ liczba1,f ;zmniejsza liczba 1
Pamiętaj, że komentarze mają pomóc Tobie lub ewentualnie osobie, z która piszesz program. Dlatego pisz je w sposób odpowiedni dla Ciebie.
Po tej małej dygresji podsumujmy co powinniśmy umieć po tej lekcji:
- wprowadzać podprogramy’
- inicjować zmienne
- dodawać opóźnienie do programu
- zmniejszać zmienne (przy okazji jeżeli komenda ma tylko zmniejszać, a nie sprawdzać czy jest zerem piszemy DECF)
W następnym artykule poznamy kolejne ciekawe komendy, zrobimy linijkę LED i wprowadzenie do obsługi wyświetlacza LED. Jeżeli dotrwałeś do tego momentu, musze poprosić Cię o pozostawienie komentarza na temat zrozumiałości tekstu. Dziękuje ;) .
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
MOVLW 00h ;wpisujemy zero do rejestru w
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:
;***USTAWIENIE PORTÓW I/O***
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:
Program
Twój kod wpisujący 8 do PORTA
GOTO Program ;idź do program i kolejny raz wpis 8 do PORTA
END ;kompilator tego wymaga
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.
Po tym jak złożymy już cały programator, musimy zmierzyć się z kolejnym etapem jakim jest odpowiednie połączenie dwóch wtyczek. Potrzebne nam będą dwie wtyczki w obudowie i jedno złącze do montażu na płytce. Mamy tutaj pewną dowolność. Jako złącze do montażu na płytce możemy użyć wtyku męskiego lub żeńskiego, oczywiście wtyk, który będziemy wpinać w to złącze musi być analogicznie żeńskie lub męskie. Chodzi generalnie o to, żeby jakoś to wyglądało i zapewniło komunikacje z komputerem.
Numer pinu, który znajduję się na schemacie odpowiada danemu pinowi w porcie komputera. Do komputera możemy wpiąć jedynie wtyczkę męską. Na tym złączu męskim mamy wygraberowanie małe numerki pinów. Nasze zadanie polega na podłączeniu pinu nr.2 ze schematu z pinem nr.2 w tej właśnie wtyczce męskiej, nr.3 z nr.3 itd. Piszę o tym, ponieważ numeracja pinów pomiędzy złączem na płytce, a wtykiem do niej pasującym może być niezgodna. Nie przejmujemy się tym po prostu numer 3 ma być numer 3 w kończącej (tej podłączanej do komputera) wtyczce męskiej.Wszystkie piny we wtyczce, które mają być podłączone do masy mostkujemy i przesyłamy jednym przewodem. Akurat są to kolejne piny, więc sprawa jest zdecydowanie ułatwiona.
Jak już zarobimy wszystkie wtyczki możemy wstępnie miernikiem sprawdzić przejścia między odpowiednim pinem ostatniej wtyczki, a odpowiednimi padami na płytce. Jeżeli wszystko jest gotowe przechodzimy do etapu testowania.
Nie ma tu o czym pisać. Podłączamy programator do zasilania. Na razie nie przejmujemy się kablem LPT i zostawiamy go w spokoju. Bierzemy miernik i sprawdzamy napięcia w dwóch punktach układu. Na nóżce LM7812, gdzie powinno być ok. 13,2V w zależności od egzemplarzu użytych diod oraz na nóżce LM7805, gdzie musi panować napięcie równe 5V. Jeżeli nie mamy napięcia 13,2V, które nawiasem mówiąc jest bardzo ważne, to sprawdzamy napięcie na jego nóżce o nazwie GND. Powinno tam być napięcie równe dwóm spadkom napięć na przewodzących diodach. Jeżeli jest inaczej sprawdzamy dokładnie kierunek wlutowania. Diody powinny być wlutowane czarnym paskiem w stronę masy, czyli mówiąc fachowo w kierunku przewodzenia. W przypadku, gdy diody są poprawnie wlutowane, a napięcia nie ma musimy sprawdzić czy dobre są diody, ewentualnie układ LM7812. Przypominam o przymusie podania napięcia ok. 17V. Ze zbyt małym napięciem monolityczne stabilizatory wykorzystane w układzie mogą nie działać.
Analogicznie sprawdzamy czy jest obecne 5V. Dla spokoju sumienia czy nasz scalak z wyjściami OC jest poprawnie zasilony (5V na VCC) i możemy przejść do podłączenia kabla.
Jak już wspomniałem programator, który najprawdopodobniej właśnie zrobiłeś, posiada bardzo dobre, lekkie i darmowe oprogramowanie Oshon Parallere Port PIC Programing, który można poprać ze strony producenta. Oprócz tych niewątpliwych zalet posiada dodatkowo możliwość testu naszego programatora. W celu wykonania takiego sprawdzenia otwieramy program, wchodzimy w zakładkę Hardwere i dalej Check. Pojawia nam się okno z pięcioma możliwościami do zaznaczenia.
-Enable VDD line, po włączeniu powinna zaświecić się dioda LED2
-Enable VPP line, po włączeniu powinna zaświecić się dioda LED1
-Enable CLOCK line, musimy miernikiem sprawdzić czy na linii CLOCK (RB7) pojawia się jedynka logiczna (5V) po włączeniu
-Enable DATA OUT line, analogicznie jak w poprzednim
Jeżeli wszystko działa poprawnie możemy już przejść do uruchomienia pierwszego programu testowego. To jednak zostanie opisane w kolejnym artykule. W przypadku, gdy włączenie któreś funkcji nie daję żadnego rezultatu lub nie działa poprawnie mechanizm postępowania jest następujący. Sprawdzamy czy na odpowiednik pinie na wejściu LPT pojawia się jedynka logiczna. Jeżeli nie to znaczy, że popełniliśmy jakiś błąd podczas zarabiania kabla. Następnie sprawdzamy czy sygnał pojawia się na wyjściu inwerterów. Jego brak może być spowodowany nie podłączanym zasilaniem do układu, nie zwarciem nie wykorzystanych wejść do masy czy do plusa zasilania lub zniszczonym układem scalonym. w Przypadku, gdy na wyjściu układu pojawiają się odpowiednie stany logiczne, a programator nie działa tak jak powinien sprawdzenie zaczynamy od napięć zasilania w poszczególnych punktach układu. Na wyjściach stabilizatorów, na emiterach tranzystorów i na rezystorach podciągających na wyjściach bramek.
Mam nadzieje, że ten opis chociażby w małym stopniu pomoże niektórym uruchomić pierwszy programator i zaprogramować pierwszy mikrokontroler, ponieważ jest to naprawdę świetna zabawa. Niestety wymaga ona poświęcenia trochę czasu i nerwów na pierwsze uruchomienie ;). Podsumowując, jeżeli nie działa nie poddajemy się tylko szukamy błędu w układzie, próbujemy wymyślanych rozwiązań i programujemy.