Programowanie mikrokontrolerów 8051 w jezyku C - część 1
Gdy już skompletujemy nasz warsztat programistyczny i sprzętowy, pora na napisanie pierwszego programu w języku C. Najbardziej efektowne są programy, których działanie mozna odrazu zobaczyc na własne oczy. Ja zwykle, gdy zaczynam pracę z nowym mikrokontrolerm, piszę program, który zapala diode LED. W ten sposób można najszybciej przekonać się o poprawnym działaniu programu. Do mikrokontrolera należy podłączyc 8 diod LED w sposób pokazany na rysunku :
Wartości rezystorów należy dobrać odpwiednio do posiadanych diod LED. Jesli są to diody standardowe, to rezystancja rezystorów powinna mieć wartość ok. 330 Ohm. Natomiast gdy dysponujemy diodami niskopądowymi, to rezystancja rezystorów może mieć wartośc ponad 1 kOhm (należy zwrócić także uwagę na wydajność prądową portów mikrokontrolera).
Operator przypisania.
W tym podtemacie zapozamy się z najczęściej używanym operatorem - operatorem przypisania. Służy on, jak jego nazwa wskazuje, do przypisania do danej zmiennej wartości innej zmiennej, badź stałej.
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// zapisanie do portu P0 liczby 0x55
P0 = 0x55;
// pusta pętla nieskonczona - zatrzymanie porgramu
while(1);
}
Na początku musimy dołączyć plik nagłówkowy z definicjami rejestrów procesora. Gdybyśmy tego nie zrobili, to nie moglibyśmy odwoływac się do rejestrów procesora za pomocą nazw symbolicznych, tylko przez podawanie adresu danego rejestru. Takie rozwiązanie byłoby bardzo niewygodne. Ja w naszym przykładzie dołączyłem plik 8051.h - "bezpieczny" dla większości mikrokontrolerów z rodziny 8051. W przypadku, gdybyśmy korzystali z rejestrów specyficznych dla danego mikrokontrolera, nie występujących w standartowym 8051, to musimy dołaczyć odpowiedni dla danego mikrokontrolera plik nagłówkowy. W sytuacji, gdy używamy tylko typowych rejestrów można spokojnie zastosowac plik 8051.h.
Każdy program pisany w języku C musi się składać z głównej funkcji main. Funkcja ta nie zwraca żadnej wartości ani do funkcji nie jest przekazywana żadna wartość, więc funkcję tą deklarujemy jako void main(void). Słowo void przed nazwą fukcji mówi kompilatorowi, że funkcja nie zwraca wartości, albo inaczej mówiąc, że zwracana wartośc jest typu void (pusty, brak typu). Słowo void w nawiasach okrągłych po nazwie fukcji mówi kompilatorowi, że do funkcji nie jest przekazywana żadna wartość. W zależności od tego, jaką funkcę chcemy napisac i jakie parametry ma ona przyjmować oraz jaką wartość może zwracać, deklaracja funkcji może przyjmować najróżniejsze postaci :
void funkcja(char x) - funkcja przyjmująca jeden parametr typu char (znak), niezwracająca wartości
void funkcja(char x, char y) - funkca przyjmująca dwa parametry typu char, niezwracająca wartości
char funkcja(void) - funkcja nieprzyjmująca żadnych parametrów, zwracająca wartość typu char
Oczywiście mozliwych kombinacji jest bardzo dużo i zależą one od tego, jakie zadania ma spełniać dana funkcja.
Gdy już mamy szkielet programu, to nalezy wpisac właściwy kod programu. W naszym pierwszym programie w zasadzie decydujące dznaczenia ma jeden wiersz programu : P0 = 0x55;. Jest to instrukcja przypisująca do portu P0 wartość 55h. Objawi się to zapaleniem co drugiej diody LED podłączonej do portu P0. Liczby w systemie szesnastowym w języku C zapisujemy właśnie w podany sposób : liczbę szesnastkową (bez znaku 'h' na końcu) nalezy poprzedzic ciągiem znaków '0x'.
Po zapisaniu do portu właściwej wartości należy zatrzymać wykonywanie programu. Najłatwiej dokonać tego wykorzystując pętlę while. Pętla ta jest wykonywana tak długo, aż jej warunek jest prawdziwy. Poniewaz w naszym programie warunek pętli jest wartością stałą, reprezentującą prawdę logiczną, to nasza pętla będzie się wykonywała bez końca.
Funkcje logiczne.
Wyzerowanie odpowiednich linii portu możemy zrealizować także w inny sposób, przez wykonanie iloczynu logicznego portu ze stałą. Przedstawia to tabela prawdy funkcji AND :
Jeśli potraktujemy zmienną A jako nasz port, a zmienną B jako maskę określającą które bity należy wyzerować, to będą nas interesować dwa ostatnie wiersze tej tabeli. Jak wynika z tabeli odpowieni bit rejestru zostanie wyzerowany, gdy odpowiadający mu bit maski będzie miał wartość 0. W przeciwnym przypadku stan bitu rejestru sie nie zmieni. Rzeczą ważną jest aby pamietać, że odpowiednie bity rejestru, na których chcemy przerowadzić iloczyn musza być w stanie 1. Kod programu realizującego zerowanie linii portu P0 za pomocą iloczynu logicznego jest przedstawiony poniżej :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// iloczyn logiczny portu P0 ze stałą 55h
P0 &= 0x55;
// pusta pętla nieskonczona - zatrzymanie porgramu
while(1);
}
Wiekszość kodu jest taka sama jak w programie poprzednim. Wyjaśnienia wymaga jeden wiersz kodu :
P0 &= 0x55;
Jest to skrócony zapis nastepującego wyrażenia :
P0 = P0 & 0x55;
Język C umozliwia stosowanie skróconych wyrażeń, będących połączeniem operatora przypisania z operatorami arytmetycznymi, badź logicznymi. Możliwe są następujące skrócone formy zapisu wyrażeń :
Funkcja |
Zapis skrócony |
Zapis normalny |
dodawanie |
a += b |
a = a + b |
odejmowanie |
a -= b |
a = a - b |
mnożenie |
a *= b |
a = a * b |
dzielenie |
a /= b |
a = a / b |
iloczyn logiczny |
a &= b |
a = a & b |
suma lgiczna |
a |= b |
a = a | b |
przesunięcie w lewo |
a <<= b |
a = a << b |
przesunięcie w prawo |
a >>= b |
a = a >> b |
alternatywa logiczna |
a ^= b |
a = a ^ b |
Po zerowaniu linii portu nadszedł czas na ich ustawianie. Służy do tego funkcja logiczna OR. Tabela prawdy funkcji OR jest przedstawiona poniżej :
Tym razem interesują nasz dwa pierwsze wiersze tabeli. Wynika z nich, że aby ustawić odpowniedni bit rejestru, to odpowiadający mu bit maski musi mieć wartość 1 no i oczywiście bity do ustawienia muszą mieć wartość 0. Program przedstawiony jest poniżej :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// ustawienie wszystkoch linii portu P0 w stan niski
P0 = 0;
// suma logiczna portu P0 ze stałą F0h
P0 |= 0xF0;
// pusta pętla nieskończona - zatrzymanie porgramu
while(1);
}
W naszym programie pojawił się dodatkowy wiersz kodu :
P0 = 0;
Ustawia on wszystkie linie portu P0 w stan niski. Ponieważ chcemy linie portu P0 ustawić więc muszą być w stanie niskim. Jednak zaraz po zresetowaniu procesora wszystkie pory są ustawiane w stan niski. Musimy więczaraz przed ich ustawieniem je wyzerować. Samo ustawnienie wybranuch linii w stan wysoki realizuje poniższy kod :
P0 |= 0xF0;
Ponownie zastosowałem skrócony zapis łaczący operator przypisania z operatorem sumy logicznej.
Programowanie mikrokontrolerów 8051 w języku C - część 2
W drugiej części kursu programowania zapoznamy się z obsługą klawiatury.W tym celu musimy podłączyć do układu z poprzedniej części kursu, do portu P3, 8 przycisków w sposób pokazany na ponizszym rysunku :
Klawiatura ta działa w następujący sposób : po naciśnięciu przycisku wyprowadzenie portu, do którego jest podłączony przycisk, jest zwierane do masy, co wymysza na wyprowadzeniu portu stan niski. Port musi zawierać wewnętrzne rezystory podciągajęce linie portu do szyny zasilającej. W przeciwnym razie będziumy musieli dołączyć zewnętrzne rezystory podciągające. W typowych mikrokontrolerach rodziny 8051 rezystory te są wbudowane w każdy port, za wyjątkiem portu P0 a także w bardzo popularnym AT89C2051 za wyjątkiem linii P1.0 i P1.1. Tak wiec przed podłączeniem klawiatury nalezy się upewnić, że port do którego chcemy podłaczyc klawiaturę posada rezystory podciągające.
Pierwszy program wykorzystujący klawiaturę jest bardzo prosty i zamieszczony poniżej :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// pętla nieskończona
while(1)
// przepisanie do portu P0 stanu linii portu P3
P0 = P3;
}
Cały program opiera się w zasadzie na nieustannym przepisywaniu stanu linii portu P3 do portu P0. Efektem tego jest zapalanie odpowiedniej diody LED po naciśnięciu dowolnego przycisku. Zauważmy, że naciśniecie przycisku wywołuje wymuszenie na odpowiednim wyprowadzeniu portu P3 stanu niskiego, a zapalenie diody LED jest spowodowane ustawieniem na odpowiednim wyprowadzeniu portu P0 również stanu niskiego. Dzięki temu możemy po prostu przepisać stan linii portu P3 do portu P0, bez zadnych dodatkowych zabiegów. Jest to zrealizowane przez instrukcję :
P0 = P3;
Nie są wymagane żadne dodatkowe zmienne pośredniczące w przesyłaniu danych z portu P3 do portu P0.
Instrukcja warunkowa if
Teraz poznamy bardzo ważną instrukcję warunkową if. Służy ona do warunkowego wykonania fragmentu programu. Najprostsza wersja instrukcji if wygląda następująco :
if(warunek)
instrukcja;
Jesli warunek ma wartość true (prawda) to wykonywana jest instrukcja, w przeciwnym razie instrukcja nie będzie wykonana.
Przykład zastosowania instrukcji warunkowe if jest pokazany poniżej :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// Pętla nieskończona
while(1)
{
// jeśli P3.0 jest w stanie niskim
if(P3_0 == 0)
// to ustaw na P0.0 stan niski
P0_0 = 0;
// jesli P3.1 jest w stanie niskim
if(P3_1 == 0)
// to ustaw na P0.0 stan wysoki
P0_0 = 1;
}
}
Zapis P3_0 odpowiada zapisowi P3.0, czyli określa pin 0 portu P3. Przyjrzyjmy sie teraz dokładniej instrukcji if : if(P3_0 == 0)
W nawiasach okrągłych po słowie if umieszczono warunek. Warunkiem musi być wyrażenie zwracające wartość logiczną, czyli prawda lub fałsz. W naszym przykładzie dokonujemy sprawdzenia, czy wyprowadzenie P3.0 jest w stanie niskim. Jeśli tak, to oznacza to, że naciśnięty został przycisk podłączony do tego wyprowadzenia. Należy podjąć wtedy odpowiednie działanie, czyli ustawić wyprowadzenie P0.0 w stan niski. Początkujących adeptów programowania w jezyku C może zadziwić znak "==", czyli operator porównania. Jest on inny niż w językach Basic lub Pascal i z początku bardzo łatwo się myli z operatorem przypisania "=". Instrukcja if może także wyglądać następująco:
if(warunek)
instrukcja1;
else
instrukcja2;
Słowo else (w przeciwnym przypadku) umieszczone przed instrukcja2 mówi, że instrukcja2 zostanie wykonana tylko wtedy, gdy warunek instrukcji if będzie niespełniony, czyli będzie wartości false. W sytuacji, gdy w przypadku spełnienia danego warunku wykonanych ma być kilka instrukcji, to należy blok tych instrukcji ująć w nawiasy klamrowe {}:
if(warunek)
{
instrukcja1;
instrukcja2;
instrukcja3;
}
Instrucka iteracyjna for
Zapoznamy się teraz z kolejną niezwykle użyteczna instrukcją - z instrukcją for. Służy ona do realizowania wszelkiego rodzaju pętli. Ogólna postać instrukcji for wygląda następujaco :
for(inicjalizacja zmiennej licznikowej; warunek; modyfikacja zminnej licznikowej)
inicjalizacja zmiennej licznikowej - jest to przypisanie do zmiennej licznikowej jej wartości początkowej
warunek - warunek, który określa kiedy pętla ma być wykonywana
modyfikacja zmiennej licznikowej - odpowiednie zmodyfikowanie zmiennej licznikowej (inkrementacja, dekrementacja lub cokolwiek innego)
W przykładowym programie wykorzystamy pętlę for do wygenerowania pewnego opóźnienia. Funkcja generująca opóźnienie (a raczej przerwę w wykonywaniu programu) jest bardzo przydatna przy współpracy mikrokontrolera z wolniejszymi układami peryferyjnymi, gdzie trzeba czekać np. na zakończenie pomiaru, itp. W naszym programie wykorzystamy funkcję opóźniającą do generowania prostego efektu świetlnego na diodach LED. Kod programu przedstawiony jest poniżej :
// definicja funkcji opóźniającej
void czekaj(unsigned char x)
{
// deklaracja dwóch zmiennych pomocniczych
unsigned char a, b;
// potrójnie zagnieżdzona pętla for
// ta pętla zostanie wykonana x-razy
for( ; x > 0; x--)
// ta 10 razy
for(a = 0; a < 10; ++a)
// a ta 100 razy
for(b = 0; b < 25; ++b);
}
// główna funkcja programu
void main(void)
{
// pętla nieskończona
while(1)
{
// zapal odpowiednią kombinace diod LED
P0 = 0x55;
// odczekaj pewien okres czasu
czekaj(250);
// zapal inną kombinację diod LED
P0 = 0xAA;
// odczekaj pewien okres czasu
czekaj(250);
}
}
Po raz pierwszy stworzyliśmy własną funkcję. Zgodnie z tym, co napisałęm w pierwszej części kursu, funkcja czekaj nie zwraca zadnej wartości (void) i wymaga jednego parametru typu unsigned char (liczba 8-bitowa bez znaku). Parametrem tym będzie żadana przez nas długość opóźnienia (mniej-więcej w milisekundach). Przyjrzyjmy sie pierwszej pętli for :
for( ; x > 0; x--)
Brakuje tutaj części inicjalizacji zmiennej licznikowej, ponieważ tą zmienną jest parametr przekazywany do funkcji. Gdybyśmy w tym miejscy zainicjalizowali zmienną x, to przekazywany parametr zostałby zamazany. W pętli for może brakować dowolnego elementu - może nawet pętla for wyglądać następująco :
for( ; ; ; )
W takim przypadku pętla ta będzie wykonywana bez końca.
Nasza funkcja opóźniająca składa się z trzech zagnieżdzonych pętli for. Wwyniku tego łączny czas wykonywania tych pętli jest iloczynem powtórzeń każdej pętli. Dokłądny czas opóźnienia trudno jest określić, ponieważ ze wzgledu na rózne techniki optymalizacji kodu przez kompilator nie jest znany dokłądny czas wykonywania jednej pętli. Można co prawda odczytać z pliku *.asm generowanego przez kompilator jakie instrukcje zostały uzyte do realizacji tych pętli i określić dokładny czas ich wykonania, ale nie mamy gwarancji, że po zmianie bądź warunku pętli, badź wartości początkowych oraz końcowych kompilator nie zastosuje innego kodu. Tak więc generowanie opóźnień za pomocą pętli jest przydatne tylko przy generowniu przyblizonych opóźnień. Do odmierzania dokładnych odcinków czasu należy zastosować wewnętrzne timery mikrokontrolera.
Programowanie mikrokontrolerów 8051 w jezyku C - część 3
Instrukcja switch i preprocesor.
W sytuacji, gdy chcemy sprawdzić jedną zmienną na okoliczność różnych jej wartości, zamiast użycia rozbudwanego bloku instrukcji if-else wygodniej jest zastosować instrukcję switch. Ogólna postac instrukcji switch wygląda następująco :
switch(zmienna){
case jakasWartosc1: instrukcja; break;
case jakasWartosc2: instrukcja; break;
.
.
.
case jakasWartoscn: instrukcja; break;
default : instrukcja; break;
}
zmienna może byc dowolnym wyrażeniem bądź zmienną, pod warukiem że wartość tej zmiennej lub wyrażenia jest typu całkowitego. Nasz przykładowy program niech realizuje następującą funkcję : po nacisnięciu przycisku zapal przeciwną diodę LED (tzn "od drugiej strony"). Przyjrzyjmy sie kodowi naszego programu :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// zdefiniowanie alternatywnej nazwy portu P2
#define klawiatura P2
// zdefiniowanie alternatywnej nazwy portu P0
#define diody P0
// główna funkcja programu
void main(void)
{
// pętla nieskończona
while(1)
{
// zgaszenie diod LED
diody = 0xFF;
// w zależnosci od wciśniętego przycisku
// zapal odpowiednią diodę LED
switch(klawiatura){
case 254 : diody = 127; break;
case 253 : diody = 191; break;
case 251 : diody = 223; break;
Jak zapewne zauważyłeś, pojawiła się nowa dyrektywa preprocesora - #define - jak nazwa wskazuje służy ona do definiowania. W rzeczywistości ma ona dwojakie zastosowanie. Po pierwsze służy do prostego przypisania do ciągu znaków bądź stałęj wartości liczbowej lub, jak w naszym przypadku, do określenia "wygodniejszej" nazwy jakiejś zmiennej. Po drugie do definiowania symboli kompilacji warunkowej. Z drugim zagadnieniem spotkamy się w dalszej części kursu, więc teraz nie zaprzątajmy nim sobie uwagi. Tak więc za pomocą dyrektywy #define przypisaliśmy do napisu klawiatura napis P2. W tym miejscu należy wyjaśnić co to takiego jest preprocesor. Tak więc słowo "preproesor" jest połączeniem słów "pre" - przed oraz "procesor" - w tym przypadku kompilator. Tak więc preprocesor jest programem uruchamianym przed uruchomieniem właściwego kompilatora. Preprocesor służy do wstępnej obróbki pliku źródłowego. Gdy preprocesor przegląda plik źródłowy i natrafi na ciąg znaków zdefiniowany przez dyrektywe #define, to zastąpi ten ciąg, ciągiem do niego przypisanym. Dzięki temu mozemy zamiest niewiele znaczących nazw portu uzywać w programie jasnych i jednoznacznie mówiących o ich przeznaczeniu nazw zmiennych i stałych. Po nadaniu portom naszego mikrokontrolera wygodnych i przejrzystych nazw nadchodzi czas na właściwy program. I znowu będzie on wykonywany w pętli nieskończonej while(1). Na początku tej pętli przypiszemy do portu diody liczbę 0xFF czyli 255. Spowoduje to wygaszenie diod po zwolnieniu przycisku, a także w sytuacji gdy naciśniemy więcej niż jeden przycisk. Następnie pojawia się instrucka switch. Jako zmienną tej instrukcji wykorzysatmy naszą klawiaturę. Teraz należy sprawdzić przypadek naciśnięcia każdego z przycisków osobno. Ponieważ naciśnięcie przycisku jest sygnalizowane wymuszeniem na linii, do której jest podłączony stanu niskiego, to po naciśnięciu przycisku S1 klawiatura przyjmie wartość 11111110 binarnie, czyli 254 dziesiętnie. Jeżeli naciśnięcie tego przycisku zostanie stwierdzone, to naley zapalić diodę D8 - przez przypisanie do portu diody liczby 01111111 dwójkowo, czyli 127 dziesiętnie. Po wykonaniu założonego zadania należy opuścić isntrukcję switch za pomocą słowa kluczowego break. W podobny sposób sprawdzamy pozostałe siedem przypadków.
W powyższym przykładzie niezbyt elegancko wygląda zarówno sprawdzanie który klawisz został naciśnięty, jak i zapalanie odpowiedniej diody LED. Podczas pisania programu nie należy podawać stałych liczbowych (ani żadnych innych) bezpośrednio, tylko nalezy wcześniej zdefioniować stałą o nazwie jasno mówiącej o jej przeznaczeniu. Pozatym, w sytuacji gdy będziemy musieli zmienić stałą (oczywiście na etapie pisania programu, a nie w czasie jego działania) to w bardziej rozbudowanych programach zmienienie tej liczby w miescach w których nalezy ją zmienić będzie bardzo kłopotiwe. Tym bardziej, że nie będziemy mogli użyć machanizmu "znajdż/zamień", ponieważ łatwo zmienimy nie ten znak co trzeba. Bardziej elekgancka (oczywiście nie najbardziej - ta będzie za chwile) wersja programu jest przedstawiona poniżej :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// zdefiniowanie alternatywnej nazwy portu P2
#define klawiatura P2
// zdefiniowanie alternatywnej nazwy portu P0
#define diody P0
//
#define poz1 254
#define poz2 253
#define poz3 251
#define poz4 247
#define poz5 239
#define poz6 223
#define poz7 191
#define poz8 127
// główna funkcja programu
void main(void)
{
// pętla nieskończona
while(1)
{
// zgaszenie diod LED
diody = 0xFF;
// w zależnosci od wciśniętego przycisku
// zapal odpowiednią diodę LED
switch(klawiatura){
case poz1 : diody = poz8; break;
case poz2 : diody = poz7; break;
case poz3 : diody = poz6; break;
case poz4 : diody = poz5; break;
case poz5 : diody = poz4; break;
case poz6 : diody = poz3; break;
case poz7 : diody = poz2; break;
case poz8 : diody = poz1; break;
}}}
Jako "nazwy jasno mówiące o przenaczeniu stałej" wybrałem pozx, gdzie x = 1..8. Jak łatwo można sie domysleć "poz" to skrót od "pozycja". Ponieważ stałe liczbowe odpowiadające przyciskom, jaki diodm LED sa identyczne nie zastosowałem rozróżnienia czy chodzi o pozycję przycisku czy diody LED. Jednak już naprawde elegancko będzie, gdy użyjemy odpowiednich stałych dla diod i przycisków osobno.
#include <8051.h>
// zdefiniowanie alternatywnej nazwy portu P2
#define klawiatura P2
// zdefiniowanie alternatywnej nazwy portu P0
#define diody P0
//
#define S1 254
#define S2 253
#define S3 251
#define S4 247
#define S5 239
#define S6 223
#define S7 191
#define S8 127
//
#define D1 254
#define D2 253
#define D3 251
#define D4 247
#define D5 239
#define D6 223
#define D7 191
#define D8 127
// główna funkcja programu
void main(void)
{
// pętla nieskończona
while(1)
{
// zgaszenie diod LED
diody = 0xFF;
// w zależnosci od wciśniętego przycisku
// zapal odpowiednią diodę LED
switch(klawiatura){
case S1 : diody = D8; break;
case S2 : diody = D7; break;
case S3 : diody = D6; break;
case S4 : diody = D5; break;
case S5 : diody = D4; break;
case S6 : diody = D3; break;
case S7 : diody = D2; break;
case S8 : diody = D1; break;
}}}
Teraz nasz program wygląda juz bardzo przejrzyście. Ja wybrałem nazwy identyczne z numerami elementów na mojej płytce uruchomieniowej, Ty możesz je dowolnie zmienić. Gdyby porównać kod wynikowy powyższych trzech programów to dla każdego z nich byłby identyczny. Dzieje się tak, że z punktu widzenia kompilatora te trzy programy są identyczne, ponieważ w "miejscach strategiczych" występują te same dane. Jest to kolejnym objawem preprocesora - kod źródłowy programu przed kompilacją został doprowadzony do postaci zrozumiałej przez kompilator (gdyby pominąć proces przetwarzania pliku przez preprocesor, to kompilator zgłosiłby mnóstwo błedów).
Programowanie mikrokontrolerów 8051 w języku C - część 4
Tablice danych.
Tablica jest miejscem przechowywania danych o tym samym typie. Tablicę deklarujemy podając typ elementów w niej przechowywanych, jej nazwę oraz rozmiar. Rozmiar podaje się w nawiasach kwadratowych []. Elementy tablicy są przechowywane w kolejno następujących po sobie komórkach pamięci. Przykładowa deklaracja tablicy może wyglądać następująco :
int tablica[5];
Powyższy zapis deklaruje pięcioelementową tablicę danych typu int. Dostęp do poszczególnych elementów tablicy uzyskujemy przez podanie nazwy tablicy, oraz numeru elementu tablicy, do którego dostęp chcemy uzyskać. Przykładowo, zapisanie do drugiego elementu tablicy jakiejś wartości może wyglądac nastepująco :
tablica[1] = 0;
W tym miejscu ważna uwaga : elementy tablicy sa numerowane od 0 a nie od 1. Tak więc pierwszy element ma numer 0, drugi 1 itd. Aby w miejscu deklaracji tablicy od razu umieścić w niej jakies dane należy zastosować poniższy zapis :
int tablica[5] = {5, 2, 31, 55, 40};
Gdy już mamy jako takie pojęcie na temat tablic przyjrzyjmy się pierwszemu przykładowemu programowi w tej części kursu:
}
void main(void)
{
while(1)
{
P0 = tablica[0];
czekaj(250);
P0 = tablica[1];
czekaj(250);
P0 = tablica[2];
czekaj(250);
P0 = tablica[3];
czekaj(250);
}
}
Program ten ma za zadanie genereować prostą sekwencję (przechowywaną właśnie w tablicy) odpowiednio zapalanych diod LED, podłączonychdo portu P0. Ponieważ poszczególne elementy tej tablicy nie będą się nigdy zmieniać (są to dane stałe), możemy ją umieścić w pamięci programu. Określa to słowo code przed nazwą tablicy. Po deklaracji tablicy pojawia się znajoma już nam funkcja opóźniająca czekaj, służąca do generowania opóźnień w wykonaniu programu. Program główny opiera się na wysyłaniu na port, do którego podłączone sa diody LED, kolejnych elementów tablicy zawierającej dane sterujace diodami. Gdy tablica składa się z niewielu elementów, to powyższy program jeszcze może zostać uznany za poprawny, ale w sytuacji gdy tablica będzie się składać z kilkunastu, lub nawet kilkudziesięciu elementów, to przepisywanie elementów z tablicy do portu należy już zrealizować w pętli. Przykładowa realizacja z użyciem pętli for przedstawiona jest poniżej :
#include <8051.h>
char code tablica[4] = {0x55,0xAA,0x0F,0xF0};
// definicja funkcji opóźniającej
void czekaj(unsigned char x)
{
// deklaracja dwóch zmiennych pomocniczych
unsigned char a, b;
// potrójnie zagnieżdzona pętla for
// ta pętla zostanie wykonana x-razy
for( ; x > 0; x--)
// ta 10 razy
for(a = 0; a < 10; ++a)
// a ta 100 razy
for(b = 0; b < 25; ++b);
}
void main(void)
{
char i;
while(1)
{
for(i = 0; i < 4; i++)
{
P0 = tablica[i];
czekaj(250);
}}}
Licznik pętli for jest jednocześnie indeksem elementu w tablicy.
Wskaźniki
Wskaźniki są bardzo ważnym elementem języka C. Ogólnie mówiąc wskaźnik jest zmienną przechowująca adres innej zmiennej. Wskaźniki deklarujemy w następujacy sposób :
typ * nazwa;
Aby uzyskac dostęp do zmiennej wskazywanej przez wskaźnik nalezy użyć operatora wyłuskania *, na przykład poniższa instrikcja :
*wskaznik = 0;
spowoduje zapisanie do zmiennej (a racezj do komórki pamieci) wskazywanej przez wskaznik liczby 0. Można sobie zadać pytanie jaki jest cel stosowania wskaźników, skoro wskazują ona na inną zmienną, jakby nie można było się posługiwać tylko tą zmienną. Wskaźniki odgrywają dużą rolę w przekazywaniu do funkcji parametrów, a ściślej mówiąc pozwalają na modyfikowanie parametru przekazanego do funkcji. Jednak na razie użyjemy wskaźików do innego celu, a mianowicie do dostępu do poznanej wcześniej tablicy z danymi. Przyjrzyjmy się poniższemu programowi :
}
void main(void)
{
while(1)
{
wskaznik = tablica;
P0 = *wskaznik++;
czekaj(250);
P0 = *wskaznik++;
czekaj(250);
P0 = *wskaznik++;
czekaj(250);
P0 = *wskaznik++;
czekaj(250);
}}
Realizuje on dokładnie taką samą funkcję jak pierwszy program z tej części kursu. Pierwszą zmianą w stosunku do poprzedniego programu jest deklaracja wskaźnika :
char code * wskaznik;
Zgodnie z tym, co pisałem o deklarowaniu wskaźnika wskazuje on na typ char umieszczony w pamięci programu code. Jednak samo zadeklarowanie wskaźnika nie pozwola nam na jego używanie. Wtym momecie nasz wskaźnik nie przechowuje żadnego adresu, a zwłaszcza adresu naszej tablicy. Przed jego użyciem, nalezy zapisać do niego adres tablicy. Dokonujemy tego w poniższy sposób :
wskaznik = tablica;
I tu powstanie małe zamieszanie, ponieważ tablica również jest wskaźnikiem! Tak więc powyższy zapis przepisuje do jednego wskaźnika zawartość innego wskaźnika. Aby w jawny sposób do wskaźnika przypisac adres jakiejś zmiennej należy użyć operator adresu &, tak jak pokazano poniżej :
wskaznik = &tablica[0];
I tu znów małe zamieszanie, ponieważ chcemy do wskaźnika zapisać adres pierwszego elementu tablicy, do którego dostęp uzyskamy przez podanie w nawiasach kwadratowych jedo numeru. Gdybyśmy użyli takiego zapisu :
wskaznik = &tablica;
to kompilator zgłosi błąd, gdyż zapis ten ozancza przypisanie do wskaźnika adresu innego wskaźnika, a nie adresu danej typu char. Wiem, że to na początku jest bardzo skomplikowane, bowiem wskaźniki należą do najtrudniejszych zagadnień w języku C i bardzo często sprawiają problemy początkujacym programistom.
Część 1 - Sterowanie diodami LED
W przykładzie tym poznamy sposób sterowania diodami LED. Zalecam użycie mikrokontrolera AT89S8252, lub innego w obudowie 40-końcówkowej. W tej sytuacji dostępnych jest 32 linie I/O, co pozwoli nam na swobodne poznawanie mikrokontrolera, bez martwienia się o brakujące porty.
Aby przeprowadzić ćwiczenie należy do portu P0 naszego mikrokontrolera podłączyć 8 diod LED w sposób pokazany na rysunku. Aby włączyć dowolną diodę LED należy na odpowiednim wyprowadzeniu portu ustawić stan niski.
Przykład 1.1.ASM
NAME P01_01_ASM_SRC |
Przykład 1.1.C
#include <ATMEL/REG8252.H> |
Przykład 1.1.BAS
Reset P0.0 |
Omówienie:
W przykładzie 1.1 włączenie diody LED D1 następuje przez ustawienie na wyprowadzeniu P0.0 stanu niskiego.
- W języku asemblera jest to zrealizowane przez użycie instrukcji "CLR P0.0". Instrukcja CLR (ang. Clear) służy wyłącznie do zerowania wskaĄnika przeniesienia lub dowolnego bitu adresowanego bezpośrednio, tzn. bitu w obszarze pamięci RAM adresowanym bitowo lub przestrzeni rejestrów SFR dostępnych bitowo (o adresie podzielnym przez 8).
- W języku C najpierw trzeba zadeklarować zmienną o wybranej nazwie (w podanym przykładzie "LED1"). Następnie za pomocą zwykłej instrukcji przypisania do zmiennej odpowiadającej danemu bitowi przypisujemy "0".
- W języku BASIC do zerowania bitu portu służy instrukcja "RESET".
W każdym z języków do zrealizowania danego celu (ustawienie wyprowadzenia P0.0 w stan niski) użyto jednej instrukcji tego języka. Zobaczymy teraz na ile instrukcji tłumaczone jest to polecenie i jak dużo miejsca w pamięci zajmuje każdy program. Pliki wynikowe programu (*.HEX) zostały zdiasemblowane przy użyciu programu D51.
Przykład 1.1.ASM.D51
; |
Przykład 1.1.C.D51
; |
Przykład 1.1.BAS.D51
; |
Prrogram napisany w asemblerze po zdiasemblowaniu składa się z dokładnie tych samych instrukcji, jakie zostały użyte w kodzie Ąródłowym.
Nieco gorzej jest z programem napisanym w C. Pierwszą instrukcją jest instrukcja "LJMP X0003", która w naszym przykładzie powoduje "przeskok" do następnej instrukcji z kolei, czyli ta instrukcja jest zbędna! Wynika z tego fakt, że kompilator nie sprawdza zakresu skoku i wstawia tą instrukcję zupełnie niepotrzebnie. Jeżeli już musi być ona użyta to lepszym rozwiązaniem byłoby użycie dwubajtowej "AJMP" lub "SJMP" zamiast trzybajtowej "LJMP". Z kolejnych siedmiu instrukcji składa się procedura inicjalizacji mikrokontrolera. Instrukcja "MOV SP, #8" ustawia wskaˇnik stosu na 8. Cztery kolejne instrukcje:
clr a |
służą do wyzerowania wewnętrznej pamięci RAM mikrokontrolera, której zawartość po włączeniu zasilania może być nieokreślona. Kolejna instrukcja "MOV P2,#0FFh" powoduje ustawienie wszystkich wyprowadzeń portu P2 w stan wysoki. Listing programu inicjującego mikrokontroler znajduje się w katalogiu kompilatora i bardziej dociekliwi czytelnicy tego kursu mogą się z nim zapoznać. Następnie występuje instrukcja "LJMP X0012". I tutaj znowu powoduje ona przejście do następnej z kolei instrukcji, więc jest niepotrzebna. Petla nieskończona jest zrealizowana przy użyciu dwubajtowej instrukcji "SJMP X0014", czyli najodpowiedniejszej do tego celu.
Program napisany w Bascomie już wygląda dużo gorzej. Na początku występuje instrukcja skoku do procedury inicjalizującej procesor. Po tej instrukcji następuje kilka instrukcji powrotu z procedur obsługi przerwań. Jeżeli w naszym programie nie wykorzystujemy przerwań, to te instrukcje spokojnie można by było pominąć. Zaoszczędzilibyśmy kilkadziesiąt bajtów pamięci programu, co w niektórych przypadkach może być zbawienne. Procedura inicjalizująca procesor jest podobna do tej z C. Po procedurze inicjalizującej jest w końcu nasza instrukcja powodująca włączenie diody LED. Pętla nieskończona jest w tym przypadku zrealizowana przy użyciu trzybajtowej instrukcji "LJMP", chociaż dwubajtowa "SJMP" w zupełności by wystarczyła. Znowu kompilator nie sprawdza zakresu skoku. Następnie występuje instrukcja blokująca przerwania, pomimo niewykorzystywania układu przerwań w programie. Ta ostatnia isntrukcja trochę mnie dziwi, ponieważ nie zostanie ona nigdy wykonana! Program zapętli się na poprzedniej instrukcji realizującej pętlę nieskończoną , więc dwie ostatnie instrukcje w kodzie są zupełnie zbędne. Znowu marnotrawienie pamięci w przypadku BASCOMA.
Jak widać z powyższego porównania, najwięcej niepotrzebnych instrukcji generuje Bascom. W C jest o wiele lepiej niż w Bascomie, ale i tak dużo gorzej niż w asemblerze. Tylko ten jeden przykład jest w stanie uzmysłowić nam, jaka jest zaleta programowania w asemblerze - całkowita kontrola rozmiaru kodu wynikowego.
Ponieważ w pakiecie RIDE 51 występuje bardzo pożyteczna moim zdaniem opcja kompilowania programu poprzez program asemblera ( i generowania jednocześnie pliku *.SRC do każdego pliku *.C) w następnych przykładach wykorzystamy tą funkcję zamiast diasemblowania pliku *HEX (chyba, że zajdzie taka potrzeba).
Przykład 1.2
W tym przykładzie nauczymy się wykorzystywać aliasy nazw (tzn. będziemy danemu pinowi portu przypisywać inną nazwę i w programie odwoływać się do tego pinu poprzez tą nazwę).
Przykład 1.2.ASM
NAME P01_02_ASM_SRC |
Przykład 1.2.C
Ponieważ w C alias nadaliśmy już w pierwszym przykładzie nie ma wersji przykładu 1.2 w języku C.
Przykład 1.2.BAS
Led1 Alias P0.0 |
Ze względu na to, że przykład 1.2 nie różni się pod względem kodu wynikowego od przykładu 1.1 nie przeprowadzono diasemblacji i analizy kodu wynikowego. Przykałd 1.2 miał na celu przedstawienie wygodniejszego posługiwania się pinami. Polecam ten sposób odwoływania się do pinów ze względu na łatwiejsze wprowadzanie zmian do programu. Wyobraˇmy sobie sytację, gdy w programie liczącym kilkadziesiąt, lub nawet kilkaset instrukcji odwoływaliśmy się do pinów bezpośrednio przez ich nazwę i nagle musimy zmienić pin pełniący daną funkcję. Zamiast męczyć sie z tymi kilkuset instrukcjami i przeoczyć połowę z nich lub popełnić wiele innych błedów, zmianiamy tylko deklarację aliasu. Jest to rozwiązanie duzo prostsze, a pozatym daje ono bardziej czytelne programy. Bardziej wymowne jest stosowanie nazw typu: "LED1" czy "KLAWISZ1" zamiast P0.0 itp.
Przykład 1.3
Ten przykład prezentuje metodę ustawiania stanu niskiego na wyprowadzeniu za pomocą instrukcji iloczynu logicznego ANL.
Przyjrzyjmy się tabeli prawdy dla funkcji AND.
A |
B |
Y |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
Jak widać, wynikiem funkcji AND jest logiczna jedynka tylko wtedy, gdy obydwa parametry funkcji są logicznymi jedynkami. W pozostałych przypadkach wynikiem jest "0". Możemy to wykorzystać do włączenia diody, a nawet kilku diod za pomocą jednej instrukcji asemblera.
Przykład 1.3.ASM
NAME P01_03_ASM_SRC |
Przykład 1.3.C
#include <ATMEL/REG8252.H> |
Przykład 1.3.BAS
P0 = P0 And &B11111110 |
Ponieważ iloczyn logiczny jest wykonywany na całym porcie, a nie na pojedynczym bicie, możemy to wykorzystać do zmiany stanu kilku bitów portu jednocześnie. W ten sposób zaoszczędzimy miejsce w pamięci programu, bo zamiast użycia np. czterech dwubajtowych instrukcji "CLR bit" użyjemy jedną trzybajtową "ANL drect, #data", co przedstawia przykład 1.4.
Przykład 1.4
Przykład 1.4.ASM |
Przykład 1.4.C
#include <ATMEL/REG8252.H> |
Przykład 1.4.BAS
P0 = P0 And &B10101010 |
Jak widać, za pomocą tylko jednej instrukcji włączyliśmy cztery diody jednocześnie. Takie rozwiązanie jest bardzo estetyczne i użyteczne, chociaż z drugiej strony bardziej czytelne jest użycie pojedynczych instrukcji z poszczególnymi nazwami ustawianych pinów.
Cześć 2 - obsługa klawiatury
W ćwiczeniu tym zapoznamy się z obsługą prostej klawiatury. Aby przeprowadzić to ćwiczenie musimy do portu P2 podłączyć przełączniki zwierające wyprowadzenia portu do masy w momencie naciśnięcia przycisku, tak jak pokazano na rysunku . Podłączenie diod LED jest identyczne jak w przykłądzie poprzednim. Nie nadaje się do tego celu port P0 ze względu na brak wewnętrznych rezystorów podciągających. Jeżeli chcemy do portu P0 podłączyć klawiaturę to musimy "podciągnąć" piny porto P0 do plusa zasilania poprzez dolączenie pomiędzy wyprowadzenia portu a szynę zasilającą rezystorów o wartości kilku- do kilkudziesięciu kiloomów.
Przykład 2.1
Program przedstawiony w tym przykładzie kopiuje stan linii portu P2 do portu P0, czyli naciśnięcie przycisku będzie sygnalizowane zaświeceniem się odpowiedniej diody LED. Dioda będzie się świeciła tylko czasie przyciśnięcia przycisku. Po jego puszczeniu diada zgaśnie.
Przykład 2.1.ASM
NAME P02_01_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P02_01_ASM SEGMENT CODE
RSEG P02_01_ASM
PETLA:
MOV P0,P2
SJMP PETLA
END
Przykład 2.1.C
#include <ATMEL/REG8252.h>
void main(void)
{
while(1)
P0 = P2;
}
Przykład 2.1.BAS
Do
P0 = P2
Loop
End
Omówienie
W powyższym przykładzie stan linii portu P2 jest kopiowany do portu P0.
W języku asemblera zostało to zrealizowane przy pomocy instrukcji "MOV direct1, direct2" , kopiującej zawartośćˇ komórki wewnętrznej pamięci RAM z obszaru adresowanego bezpośrednio lub z obszaru rejestrów SFR do komórki pamięci, lub rejestru SFR w tym samym obszarze. W celu nieskończonego powtarzania tej instrukcji zastosowano instrukcję skoku krótkiego "SJMP adr" wykonującą skok do etykiety "PETLA"
W języku C zadanie to zostało zrealizowane poprzez przypisanie do portu P0 wartości portu P2. Pętla nieskończona została zrealizowana poprzez użycie instrukcji "while(1)"
W języku Basic instrukcja przypisania wartości portu P2 do portu P0 została ujęta pomiędzy instrukcje "DO" i "LOOP", które realizują pętlę nieskończoną.
Przeprowadˇmy teraz analizę kodu wynikowego powyższego programu wygenerowanego poprzez każdy z użytych kompilatorów.
Przykład 2.1.ASM.D51
;
; D51 V2.6 8051 Disassembly of asm.hex
; 10/28/2003 19:06
;
org 0
;
X0000: mov p0,p2
sjmp X0000
;
end
;
Przykład 2.1.C.D51
main:
?WHILE1:
; SOURCE LINE # 6
;$@$C_SOURCE_LINE(6)
MOV P0,P2
SJMP ?WHILE1
; END OF main
Przykład 2.1.BAS.D51
;
; D51 V2.6 8051 Disassembly of bas.hex
; 10/28/2003 19:06
;
. ; Pominięte zostały instrukcje powrotu
. ; z podprogramów obsługi przerwań
.
X002e: mov r0,#0ffh
clr a
X0031: mov @r0,a
djnz r0,X0031
mov sp,#21h
mov 20h,#0
; Tutaj rozpoczyna się właściwy program
X003a: mov p0,p2
ljmp X003a
;
clr ea
X0042: sjmp X0042
;
end
Kompilator każdego z języków użył do zrealizowania podanego zadania jednej instrukcji "MOV direct1, direct2", a więc zrealizował to zadanie optymalnie. Jedyną wątpliwość budzi użycie przez Bascoma trzybajtowej instrukcji "LJMP" do zrealizowania pętli nieskończonej. Ponieważ skok następuje do poprzedniej instrukcji wystarczyło użyć dwubajtową instrukcję "SJMP" , ewentualnie także dwubajtową instrukcję "AJMP". Świadczy to o tym, że Bascom nie sprawdza zakresu skoku i zawsze wstawia instrukcję obejmującą największy obszar pamięci i niepotrzebnie zajmującą jeden bajt pamięci prgramu więcej. Natomiast kompilator jezyka C użył do zrealizowania pętli najbardziej optymalnej instrukcji "SJMP".
Przykład 2.2
Program przedstawiony w tym przykładzie po naciśnięciu przycisku S1 zapala diodę D1, a po naciśnięciu przycisku S8 gasi tę diodę.
Przykład 2.2.ASM
NAME P02_02_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P02_01_ASM SEGMENT CODE
RSEG P02_01_ASM
S1 EQU P2.0
S8 EQU P2.7
LED EQU P0.0
START:
JB S1, DALEJ
CLR LED
DALEJ:
JB S8, START
SETB LED
SJMP START
END
Przykład 2.2.C
#include <ATMEL\REG8252.H>
sbit S1 = P2^0;
sbit S2 = P2^7;
sbit LED1 = P0^0;
void main(void)
{
while(1)
{
if (S1 == 0) LED1 = 0;
if (S2 == 0) LED1 = 1;
}
}
Przykład 2.2.BAS
S1 Alias P2.0
S2 Alias P2.7
LED Alias P0.0
Do
If S1 = 0 Then Reset LED
If S2 = 0 Then Set LED
Loop
End
W tym przykładzie doskonale widać, który z języków oferuje najlepszą czytelność kodu. Asembler oczywiście nie zapewnia tej jakże ważnej cechy. Moim zdaniem najbardziej czytelny jest program napisany w języku C, choć zwolennicy Bascoma zapewne nie przyznają mi racji.
Przykład 2.3
Program w tym przykładzie po naciśnięciu przycisku S1 zapala diodę D1 a po ponownym naciśnięciu przycisku S1 gasi tą diodę.
Przykład 2.3.ASM
NAME P02_02_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P02_01_ASM SEGMENT CODE
RSEG P02_01_ASM
S1 EQU P2.0
LED EQU P0.0
START:
JB S1, START
CPL LED
SJMP START
END
Przykład 2.3.C
#include <ATMEL\REG8252.H>
sbit S1 = P2^0;
sbit LED1 = P0^0;
void main(void)
{
while(1)
{
if (S1 == 0)
LED1 = !LED1;
}
}
Przykład 2.3.BAS
S1 Alias P2.0
Led1 Alias P0.0
Do
If S1 = 0 Then Led1 = Not Led1
Loop
End
Opis:
Nasz program nie zachowuje się tak, jak powinien. Po naciśnięciu przycisku dioda się zapala w sposób przypadkowy. Gdy przycisk przytrzymamy wciśnięty trochę dłużej, to jasność świecenia diody spada, co świadczy o tym, że dioda jest na przemian zapalana i gaszona. Przyczyną tego jest ogromna szybkość wykonywania programu przez mikrokontroler, wynosząca ok. 1 milion instrukcji na sekundę. Aby temu zapobiec należy wstawić pętlę opóˇniającą.
Przykład 2.4.ASM
NAME P02_04_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P02_04_ASM SEGMENT CODE
RSEG P02_04_ASM
S1 EQU P2.0
LED EQU P0.0
START:
JB S1, START
CPL LED
ACALL CZEKAJ
SJMP START
CZEKAJ:
MOV R1,#255
L2:
MOV R0,#255
L1:
DJNZ R0,L1
DJNZ R1,L2
RET
END
Przykład 2.4.C
#include <ATMEL\REG8252.H>
sbit S1 = P2^0;
sbit LED = P0^0;
void czekaj(void)
{
char x,y;
for (x = 255; x > 0; --x)
for (y = 255; y > 0; --y);
}
void main(void)
{
while(1)
{
if (S1 == 0)
{
LED = !LED;
czekaj();
}
}
}
Przykład 2.4.BAS
S1 Alias P2.0
Led1 Alias P0.0
Do
If S1 = 0 Then Led1 = Not Led1
waitms 200
Loop
End
Opis:
Przyjrzyjmy się procedurze opóˇniającej napisanej w języku asemblera:
CZEKAJ:
MOV R1,#255
L2:
MOV R0,#255
L1:
DJNZ R0,L1
DJNZ R1,L2
RET
Instrukcja "MOV R1, #255" służy do ustawienia licznika pętli L2. Instrukcja "MOV R0, #255" służy do ustawienia licznika pętli L1. Użycie dwóch pętli zagnieżdżonych podyktowane jest małym zakresem powtórzeń realizowanych przez instrukcję "DJNZ" (maksymalnie 256 razy, gdy licznik pętli ustawimy na "0"). Do zrealizowania pętli służy instrukcja "DJNZ Rn, rel". W naszym przykładzie w pierwszej kolejności jest realizowana pętla L1 (255 powtórzeń, co daje ok. 510 us, przy zegarze 12 MHz). Pętla ta jest z kolei powtarzana 255 razy w pętli L2. W sumie daje to opóˇnienie ok. 130 ms.
W języku C procedura opóˇniająca wygląda następująco:
void czekaj(void)
{
char x,y;
for (x = 255; x > 0; --x)
for (y = 255; y > 0; --y);
}
W pierwszym wierszu procedury deklarujemy dwie zmienne, które posłużą nam jako liczniki pętli. Instrukcja "for(x = 255; x >0; --x)" realizuje pętlę, w której jest zagnieżdżona druga pętla, podobna do pierwszej. Skonstruowanie tych pętli, jako liczących "w dół" narzucone zostało przez architekturę mikrokontrolera i występowanie w języku asemblera instrukcji pętli wykorzystujących dekrementację licznika pętli. Daje to bardziej zwięzły kod wynikowy niż z realizująca tą samą liczbę powtórzeń pętla "for(x = 0; x < 255, ++x)".
W języku BASCOM do opóˇnienia wykorzystana została specjalnie do tego celu przeznaczona instrukcja "WAITMS x". Jest ona najprostsza w użyciu, bowiem podajemy liczbowo czas opóˇnienia w milisekundach nie musimy mozolić się z obliczaniem ilości powtórzeń i czasu realizowania pętli, jak w przypadku C i asemblera.
Porównajmy teraz kod wynikowy tych trzech programów. Już samo porównanie rozmiaru plików *.BIN daje nam jasny obraz efektywności każdego z trzech języków:
- plik generowany przez BASCOM - 101 B
- plik generowany przez RC51 - 37 B
- plik generowany przez RA51 - 18 B
Program napisany w BASCOMie jest ponad 5,6 razy większy od programu napisanego w asemblerze spełniającego tą samą funkcję. Dokładnie tyle samo te programy zajmują pamięci w mikrokontrolerze.
Przykład 2.4.BAS.D51
;
; D51 V2.6 8051 Disassembly of bas.hex
; 2/16/2004 13:07
;
. ; Pominięte zostały instrukcje powrotu
. ; z podprogramów obsługi przerwań
.
X002e: mov r0,a
X002f: mov a,#0adh
X0031: inc a
nop ; data truncated
;
org 3ah
;
jnz X0031
nop
djnz r0,X002f
ret
;
X0040: mov r0,#0ffh
clr a
X0043: mov @r0,a
djnz r0,X0043
mov sp,#21h
mov 20h,#0
X004c: mov c,p2.0
mov 20h.4,c
jnc X0055
ljmp X005a
;
X0055: mov c,p0.0
cpl c
mov p0.0,c
X005a: mov a,#0c8h
acall X002e
ljmp X004c
;
clr ea
X0063: sjmp X0063
;
end
;
Jak widać kod wynikowy programu napisanego w Bascomie jest zagmatwany i mało czytelny.
Część 3 - wyświetlacz siedmiosegmentowy.
W celu wykonania ćwiczenia należy wykonać połączenia jak na rysunku. Na rysunku nie zaznaczono rezystorów ograniczających prąd, ale nie wolno o nich zapomnieć w układzie.
Przykład 3.1.ASM
NAME P03_01_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P03_01_ASM SEGMENT CODE
RSEG P03_01_ASM
WYSW EQU P0
W1 EQU P2.0
W2 EQU P2.1
W3 EQU P2.2
W4 EQU P2.3
CLR W1
MOV WYSW, #0
SJMP $
END
Przykład 3.1.C
#include <ATMEL\REG8252.H>
#define WYSW P0
sbit W1 = P2^0;
sbit W2 = P2^1;
sbit W3 = P2^2;
sbit W4 = P2^3;
void main(void)
{
W1 = 0;
WYSW = 0;
while(1);
}
Przykład 3.1.BAS
WYSW alias p0
w1 alias p2.0
w2 alias p2.1
w3 alias p2.2
w4 alias p2.3
w1 = 0
wysw = 0
do
loop
end
Omówienie:
Powyższy program powoduje zaświecenie wszystkich segmentów wyświetlacza W1. Ustawienie stanu niskiego na linii P2.0 (W1) wprowadza tranzystor w stan nasycenia i na anodę wyświetlacza W1 podane jest napięcie zasilania. Następnie wszystkie linie portu P0 są ustawiane w stan niski. Powoduje to włączenie wszystkich segmentów wyświetlacza W1.
Przykład 3.2
W tym przykładzie zajmiemy się wyświetleniem na wyświetlaczu kolejno cyfr kodu szesnastkowego (0..F). Sposób połączenia poszczególnych segmentów wyświetlacza z wyprowadzeniami mikrokontrolera przedstawia rysunek.
Tabela kodów sterujących wyświetlaczem siedmiosegmentowym:
Tabela 3.1
Wyświetlana cyfra |
Liczba dziesiętnie |
Liczba szesnastkowo |
Liczba dwójkowo |
0 |
192 |
C0 |
11000000 |
1 |
249 |
F9 |
11111001 |
2 |
164 |
A4 |
10100100 |
3 |
176 |
B0 |
10110000 |
4 |
153 |
99 |
10011001 |
5 |
146 |
92 |
10010010 |
6 |
130 |
82 |
10000010 |
7 |
248 |
F8 |
11111000 |
8 |
128 |
80 |
10000000 |
9 |
144 |
90 |
10010000 |
A |
136 |
88 |
10001000 |
B |
131 |
83 |
10000011 |
C |
198 |
C6 |
11000110 |
D |
161 |
A1 |
10100001 |
E |
134 |
86 |
10000110 |
F |
142 |
8E |
10001110 |
Przykład 3.2.ASM
NAME P03_02_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P03_02_ASM SEGMENT CODE
RSEG P03_02_ASM
_0 EQU 0C0h
_1 EQU 0F9h
_2 EQU 0A4h
_3 EQU 0B0h
_4 EQU 099h
_5 EQU 092h
_6 EQU 082h
_7 EQU 0F8h
_8 EQU 080h
_9 EQU 090h
_A EQU 088h
_B EQU 083h
_C EQU 0C6h
_D EQU 0A1h
_E EQU 086h
_F EQU 08Eh
WYSW EQU P0
W1 EQU P2.0
W2 EQU P2.1
W3 EQU P2.2
W4 EQU P2.3
CLR W1
START:
MOV WYSW,#_0
ACALL CZEKAJ
MOV WYSW,#_1
ACALL CZEKAJ
MOV WYSW,#_2
ACALL CZEKAJ
MOV WYSW,#_3
ACALL CZEKAJ
MOV WYSW,#_4
ACALL CZEKAJ
MOV WYSW,#_5
ACALL CZEKAJ
MOV WYSW,#_6
ACALL CZEKAJ
MOV WYSW,#_7
ACALL CZEKAJ
MOV WYSW,#_8
ACALL CZEKAJ
MOV WYSW,#_9
ACALL CZEKAJ
MOV WYSW,#_A
ACALL CZEKAJ
MOV WYSW,#_B
ACALL CZEKAJ
MOV WYSW,#_C
ACALL CZEKAJ
MOV WYSW,#_D
ACALL CZEKAJ
MOV WYSW,#_E
ACALL CZEKAJ
MOV WYSW,#_F
ACALL CZEKAJ
SJMP START
CZEKAJ:
MOV R1,#255
L2:
MOV R0,#255
L1:
DJNZ R0,L1
DJNZ R1,L2
RET
END
Przykład 3.2.C
#include <ATMEL\REG8252.H>
#define WYSW P0
#define _0 192
#define _1 249
#define _2 164
#define _3 176
#define _4 153
#define _5 146
#define _6 130
#define _7 248
#define _8 128
#define _9 144
#define _A 136
#define _B 131
#define _C 198
#define _D 161
#define _E 134
#define _F 142
sbit W1 = P2^0;
sbit W2 = P2^1;
sbit W3 = P2^2;
sbit W4 = P2^3;
void czekaj(void)
{
char x,y;
for (x = 255; x > 0; --x)
for (y = 255; y > 0; --y);
}
void main(void)
{
W1 = 0;
while(1)
{
WYSW = _0;
czekaj();
WYSW = _1;
czekaj();
WYSW = _2;
czekaj();
WYSW = _3;
czekaj();
WYSW = _4;
czekaj();
WYSW = _5;
czekaj();
WYSW = _6;
czekaj();
WYSW = _7;
czekaj();
WYSW = _8;
czekaj();
WYSW = _9;
czekaj();
WYSW = _A;
czekaj();
WYSW = _B;
czekaj();
WYSW = _C;
czekaj();
WYSW = _D;
czekaj();
WYSW = _E;
czekaj();
WYSW = _F;
czekaj();
}
}
Przykład 3.2.BAS
Wysw Alias P0
W1 Alias P2.0
_0 Alias 192
_1 Alias 249
_2 Alias 164
_3 Alias 176
_4 Alias 153
_5 Alias 146
_6 Alias 130
_7 Alias 248
_8 Alias 128
_9 Alias 144
_a Alias 136
_b Alias 131
_c Alias 198
_d Alias 161
_e Alias 134
_f Alias 142
Reset W1
Do
Wysw = _0
Waitms 250
Wysw = _1
Waitms 250
Wysw = _2
Waitms 250
Wysw = _3
Waitms 250
Wysw = _4
Waitms 250
Wysw = _5
Waitms 250
Wysw = _6
Waitms 250
Wysw = _7
Waitms 250
Wysw = _8
Waitms 250
Wysw = _9
Waitms 250
Wysw = _a
Waitms 250
Wysw = _b
Waitms 250
Wysw = _c
Waitms 250
Wysw = _d
Waitms 250
Wysw = _e
Waitms 250
Wysw = _f
Waitms 250
Loop
End
Każdy program spełnia swoje zadanie, jednak napisany jest bardzo rozwlekle i nieestetycznie. To samo zadanie można zrealizować znacznie bardziej estetycznie. Porównajmy jednak wcześniej rozmiary kodu wynikowego tych programów:
- BASCOM - 197 bajtów
- C - 127 bajtów
- asembler - 93 bajty
Tym razem program w Bascomie zajmuje nieco ponad 2 razy więcej niż jego odpowiednik w asemblerze.
Przykład 3.3.ASM
NAME P03_03_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P03_03_ASM SEGMENT CODE
RSEG P03_03_ASM
WYSW EQU P0
W1 EQU P2.0
CLR W1
START:
MOV DPTR,#ZNAKI
PETLA:
CLR A
MOVC A,@A+DPTR
JZ START
MOV WYSW,A
INC DPTR
ACALL CZEKAJ
SJMP PETLA
CZEKAJ:
MOV R1,#255
L2:
MOV R0,#255
L1:
DJNZ R0,L1
DJNZ R1,L2
RET
ZNAKI:
DB 192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0
END
Przykład 3.3.C
#include <ATMEL\REG8252.H>
#define WYSW P0
char code znaki[17] = {192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0};
sbit W1 = P2^0;
void czekaj(void)
{
char x,y;
for (x = 255; x > 0; --x)
for (y = 255; y > 0; --y);
}
void main(void)
{
W1 = 0;
while(1)
{
code char * pznaki = &znaki;
while(*pznaki)
{
WYSW = *pznaki;
pznaki++;
czekaj();
}
}
}
Przykład 3.3.BAS
Wysw Alias P0
W1 Alias P2.0
Dim I As Byte
Reset W1
Do
For I = 0 To 15
Wysw = Lookup(i , Znaki)
Waitms 250
Next I
Loop
Znaki:
Data 192 , 249 , 164 , 176 , 153 , 146 , 130 , 248 , 128 , 144 , 136 , 131 , 198 , 161 , 134 , 142
End
Powyższy program realizuje dokładnie to samo zadanie, jak program poprzedni, jednak zrealizowany został z użyciem pętli i umieszczeniem kodów cyfr w tablicy. Zapis programu stał się przez to znacznie krótszy i bardziej estetyczny. Najbardziej czytelny i łatwy do zrozumienia jest program napisany w Bascomie. Do pobrania elementu tablicy służy tylko jedno polecenie - "LOOKUP". Natomiast w języku C konieczne było zastosowanie łącznie trzech poleceń. W pierwszej kolejności zadeklarowano zmienną wskaˇnikową "pznaki" i przypisano do niej przy użyciu operatora adresu "&" adres tablicy "znaki". Następnie pobrano element tablicy przy użyciu instrukcji "WYSW = *pznaki". Na końcu konieczne było zinkrementowanie wskaˇnika, aby wskazywał na kolejny element tablicy. Jak widać pobranie elementu z tablicy w języku C jest dosyć skomplikowane w porównaniu z Bascomem. Pyzatym, wskaˇniki są jednym z trudniejszych do zrozumienia elementów języka C dla początkujących. Najbardziej skomplikowana sprawa jest w asemblerze. Do zrealizowania rozpatrywanego przez nas zadania zastosowano aż osiem instrukcji:
START:
MOV DPTR,#ZNAKI
PETLA:
CLR A
MOVC A,@A+DPTR
JZ START
MOV WYSW,A
INC DPTR
ACALL CZEKAJ
SJMP PETLA
Instrukcja "MOV DPTR,#ZNAKI" odpowiada przypisaniu do wskaˇnika adresu pierwszego elementu mikrokontrolerze tablicy. W mikrokontrolerze 8051 jedynym rejestrem, który można zastosować jako 16-bitowy wskaˇnik jest rejestr DPTR. W nowszych wersjach mikrokontrolera 8051, w tym w wersji 89S8252, występują dwa wskaˇniki DPTR. Jednakże ze względu na konieczność zachowania 100% kompatybilności kodu z 8051 możliwe jest w danej chwili użycie tylko jednego wskaˇnika DPTR. W zależności od rodzaju mikrokontrolera przełączanie aktywnego danej chwili wskaˇnika jest realizowane w różny sposób. Aby nasze przykłady można było zastosować w dowolnym mikrokontrolerze nie będziemy korzystać z tego udogodnienia.
Instrukcja "CLR A" jest konieczna, ponieważ adres komórki pamięci programu pobieranej za pomocą instrukcji "MOVC A, @A+DPTR" jest sumą zawartości akumulatora i wskaˇnika DPTR. W przypadku niewyzerowania akumulatora przy powtarzaniu pętli adres byłby obliczony błędnie. Instrukcja "JZ START" sprawdza, czy osiągnięty został koniec tablicy. Jeśli tak, to następuje opuszczenie pętli. Następnie pobrany element tablicy zostaje wysłany do portu P0. Przed powtórzeniem pętli musimy jeszcze zwiększyć adres następnego elementu tablicy do pobrania o 1.
Wiemy już, jak się przedstawia stopień komplikacji programu w zależności od użytego języka. Sprawdˇmy teraz ilość miejsca zajmowanego w pamięci przez każdy program.
- Bascom - 185 bajtów (197 bajtów bez wykorzystania pętli i tablicy)
- C - 82 bajty (127 bajtów)
- Asembler - 42 bajty (93 bajty)
Zgodnie z oczekiwaniami asembler wypada najlepiej. Także w porównaniu do poprzedniego przykładu bez pętli i tablicy w asemblerze zyskujemy największą oszczędność pamięci programu.
Przejdˇmy teraz do ciekawszego zagadnienia, mianowicie do multipleksowanego sterowania wyświetlaczem. Aby zaoszczędzić porty mikrokontrolera, bardzo często stosowane jest sterowanie multipleksowane. Oznacza to, że katody odpowiednich segmentów są połączone razem do wyprowadzeń sterujących, a anody są sterowane osobno. Zapalając odpowiednio szybko poszczególne segmenty otrzymujemy wrażenie świecenia się wszystkich wyświetlaczy.
Przykład 3.4.ASM
NAME P03_04_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P03_04_ASM SEGMENT CODE
RSEG P03_04_ASM
WYSW EQU P0
W1 EQU P2.0
W2 EQU P2.1
W3 EQU P2.2
W4 EQU P2.3
MOV DPTR,#ZNAKI
PETLA:
CLR W1
MOV A,#9
MOVC A,@A+DPTR
MOV WYSW,A
ACALL CZEKAJ
SETB W1
CLR W2
MOV A,#8
MOVC A,@A+DPTR
MOV WYSW,A
ACALL CZEKAJ
SETB W2
CLR W3
MOV A,#7
MOVC A,@A+DPTR
MOV WYSW,A
ACALL CZEKAJ
SETB W3
CLR W4
MOV A,#6
MOVC A,@A+DPTR
MOV WYSW,A
ACALL CZEKAJ
SETB W4
SJMP PETLA
CZEKAJ:
MOV R1,#255
L2:
MOV R0,#255
L1:
DJNZ R0,L1
DJNZ R1,L2
RET
ZNAKI:
DB 192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0
END
Przykład 3.4.C
#include <ATMEL\REG8252.H>
#define WYSW P0
char code znaki[17] = {192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0};
sbit W1 = P2^0;
sbit W2 = P2^1;
sbit W3 = P2^2;
sbit W4 = P2^3;
void czekaj(void)
{
char x,y;
for (x = 255; x > 0; --x)
for (y = 255; y > 0; --y);
}
void main(void)
{
code char * pznaki;
pznaki = &znaki;
while(1)
{
W1 = 0;
WYSW = (*(znaki + 4));
czekaj();
W1 = 1;
W2 = 0;
WYSW = (*(znaki + 3));
czekaj();
W2 = 1;
W3 = 0;
WYSW = (*(znaki + 2));
czekaj();
W3 = 1;
W4 = 0;
WYSW = (*(znaki + 1));
czekaj();
W4 = 1;
}
}
Przykład 3.4.BAS
Wysw Alias P0
W1 Alias P2.0
W2 Alias P2.1
W3 Alias P2.2
W4 Alias P2.3
Do
Reset W1
Wysw = Lookup(0 , Znaki)
Waitms 100
Set W1
Reset W2
Wysw = Lookup(1 , Znaki)
Waitms 100
Set W2
Reset W3
Wysw = Lookup(2 , Znaki)
Waitms 100
Set W3
Reset W4
Wysw = Lookup(3 , Znaki)
Waitms 100
Set W4
Loop
Znaki:
Data 192 , 249 , 164 , 176 , 153 , 146 , 130 , 248 , 128 , 144 , 136 , 131 , 198 , 161 , 134 , 142
End
Efektem działania powyższego programu jest wyświetlanie cyfr na kolejnych wyświetlaczach od prawej do lewej strony. Celowo zastosowano duże opóˇnienie w przełączaniu wyświetlaczy, aby lepiej uzmysłowić zasadę sterowania multipleksowanego. Przy tak dużym opóˇnieniu dokładnie widać kolejno zapalane segmenty. Nas7tępny przykład zawiera już normalne opóˇnienie.
Przykład 3.5.ASM
NAME P03_05_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P03_05_ASM SEGMENT CODE
RSEG P03_05_ASM
WYSW EQU P0
W1 EQU P2.0
W2 EQU P2.1
W3 EQU P2.2
W4 EQU P2.3
MOV DPTR,#ZNAKI
PETLA:
CLR W1
MOV A,#9
MOVC A,@A+DPTR
MOV WYSW,A
MOV A,#5
ACALL CZEKAJ
SETB W1
CLR W2
MOV A,#8
MOVC A,@A+DPTR
MOV WYSW,A
MOV A,#5
ACALL CZEKAJ
SETB W2
CLR W3
MOV A,#7
MOVC A,@A+DPTR
MOV WYSW,A
MOV A,#5
ACALL CZEKAJ
SETB W3
CLR W4
MOV A,#6
MOVC A,@A+DPTR
MOV WYSW,A
MOV A,#5
ACALL CZEKAJ
SETB W4
SJMP PETLA
CZEKAJ:
MOV R0,A
L20:
MOV R6,#248
L22:
DJNZ R6,L22
MOV R6,#248
L21:
DJNZ R6,L21
DJNZ R0,L20
RET
ZNAKI:
DB 192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0
END
Przykład 3.5.C
#include <ATMEL\REG8252.H>
#define WYSW P0
char code znaki[17] = {192,249,164,176,153,146,130,248,128,144,136,131,198,161,134,142,0};
sbit W1 = P2^0;
sbit W2 = P2^1;
sbit W3 = P2^2;
sbit W4 = P2^3;
void czekaj(char k)
{
char x,y,z;
for (z = k; z > 0; --z)
for (x = 10; x > 0; --x)
for (y = 47; y > 0; --y);
}
void main(void)
{
code char * pznaki;
pznaki = &znaki;
while(1)
{
W1 = 0;
WYSW = (*(znaki + 4));
czekaj(5);
W1 = 1;
W2 = 0;
WYSW = (*(znaki + 3));
czekaj(5);
W2 = 1;
W3 = 0;
WYSW = (*(znaki + 2));
czekaj(5);
W3 = 1;
W4 = 0;
WYSW = (*(znaki + 1));
czekaj(5);
W4 = 1;
}
}
Przykład 3.5.BAS
Wysw Alias P0
W1 Alias P2.0
W2 Alias P2.1
W3 Alias P2.2
W4 Alias P2.3
Do
Reset W1
Wysw = Lookup(0 , Znaki)
Waitms 5
Set W1
Reset W2
Wysw = Lookup(1 , Znaki)
Waitms 5
Set W2
Reset W3
Wysw = Lookup(2 , Znaki)
Waitms 5
Set W3
Reset W4
Wysw = Lookup(3 , Znaki)
Waitms 5
Set W4
Loop
Znaki:
Data 192 , 249 , 164 , 176 , 153 , 146 , 130 , 248 , 128 , 144 , 136 , 131 , 198 , 161 , 134 , 142
End
Efektem działania tego programu jest już stabilny napis. Stało się tak dzięki bezwładności ludzkiego oka. Jednak wadą sterowania multipleksowanego jest, co zapewne widać, mniejsza jasność świecenia wyświetlaczy.
W bieżącym przykładzie zmodyfikowana została procedura opóˇniająca. Liczbę powtórzeń pętli opóˇniającej możemy teraz wybrać z zakresu od 1 do 256, poprzez umieszczenie w akumulatorze przed wywołaniem procedury liczby określającej ilość powtórzeń. Jedno powtórzenie procedury trwa ok. 1ms przy zegarze 12MHz. Tak więc procedura zapewnia opóˇnienie od 1 do 256ms.
Część 4 - Magistrala I2C
W tym ćwiczeniu wykorzystamy dwa dodatkowe układy scalone typu PCF8574A produkcji firmy Philips. Układ ten jest 8-bitowym ekspanderem magistrali I2C. Linie SDA i SCL należy podłączyć odpowiednio do pinów P2.0 i P2.1 mikrokontrolera. Do wyjść pierwszego układu podłączamy diody LED, a do drugiego ukłądu naszą klawiaturę.
Ze względu na złożoność procedur obsługi magistrali I2C przygotowano dla języka asemblera i C oddzielne pliki zwierające podstawowe procedury obsługi tej magistrali. Ma to na celu uniknięcie ciągłego przepisywania stosunkowo dużego kodu zawierającego niezbędne procedury. Zamieszczone poniżej pliki zawierają procedury zapewniające:
- wygenerowanie sekwencji "START"
- wygenerowanie sekwencji "STOP"
- wysłanie na magistralę bajtu danych
- odczytanie z magistrali bajtu danych
Procedury te pozwalają na komunikację z większością typowych układów scalonych zawierających interfejs I2C pracujący w trybie "MASTER-SLAVE".
Plik I2C.ASM
;
; Procedura generująca sekwencję START
;
I2C_START:
SETB SDA
SETB SCL
ACALL DELAY
CLR SDA
ACALL DELAY
CLR SCL
RET
;
; Procedura generująca sekwencję STOP.
; Stan linii SDA zostaje zapamiętany po zakończeniu
; transmisji we wskaˇniku przeniesienia C.
;
I2C_STOP:
CLR SDA
ACALL DELAY
SETB SCL
ACALL DELAY
SETB SDA
ACALL DELAY
MOV C,SDA
RET
;
; Procedura odczytująca bajt z magistrali I2C.
; Wartość bitu ACK należy przekazać do procedury poprzez
; wskaˇnik przeniesienia C.
; Odczytany bajt jest umieszczany w akumulatorze.
;
I2C_READ:
MOV ACK,C
SETB SDA
MOV R0,#8
L0: ACALL DELAY
SETB SCL
ACALL DELAY
MOV C,SDA
RLC A
CLR SCL
DJNZ R0,L0
MOV C,ACK
MOV SDA,C
ACALL DELAY
CLR SCL
RET
;
; Procedura wysyłająca bajt na magistralę I2C.
; Stan bitu ACK jest zwracany poprzez wskaˇnik przeniesienia C.
; Bajt do wysłania należy umieścić w akumulatorze.
;
I2C_WRITE:
MOV R0,#9
SETB C
L1: CLR SCL
RLC A
MOV SDA,C
ACALL DELAY
SETB SCL
ACALL DELAY
DJNZ R0,L1
MOV C,SDA
CLR SCL
RET
DELAY:
NOP
NOP
RET
Plik I2C.C
void delay(void)
{
asm{0x00};
asm{0x00};
}
void I2C_START(void)
{
SDA = SCL = 1;
delay();
SDA = 0;
delay();
SCL = 0;
}
bit I2C_STOP(void)
{
SDA = 0;
delay();
SCL = 1;
delay();
SDA = 1;
delay();
return (~SDA);
}
unsigned char I2C_READ(bit ACK)
{
unsigned char bitCount = 8, temp;
SDA = 1;
do
{
delay();
SCL = 1;
delay();
temp <<= 1;
if (SDA) temp++;
SCL = 0;
} while (--bitCount);
SDA = ACK;
delay();
SCL = 1;
delay();
SCL = 0;
return (temp);
}
bit I2C_WRITE(unsigned char byte)
{
unsigned char bitCount = 9;
bit temp;
do
{
SCL = 0;
SDA = byte & 0x80;
byte = (byte << 1) + 1;
delay();
SCL = 1;
delay();
}while(--bitCount);
temp = SDA;
SCL = 0;
return(temp);
}
Przykład 4.1
Przykład 4.1.ASM
NAME P04_01_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P04_01_ASM SEGMENT CODE
RSEG P04_01_ASM
SDA EQU P2.0
SCL EQU P2.1
ACK EQU 00h
ACALL I2C_START
MOV A, #70h
ACALL I2C_WRITE
MOV A, #254
ACALL I2C_WRITE
ACALL I2C_STOP
SJMP $
$INCLUDE (I2C.ASM)
END
Przykład 4.1.C
#include <ATMEL/REG8252.H>
#include "I2C.H"
void main(void)
{
I2C_START();
I2C_WRITE(0x70);
I2C_WRITE(0xFE);
I2C_STOP();
while(1);
}
Przykład 4.1.BAS
Config Sda = P2.0
Config Scl = P2.1
I2cstart
I2cwbyte &H70
I2cwbyte &HFE
I2cstop
Do
Loop
End
Efektem działania tego programu jest włączenie diody LED D1. Trochę uwagi należy poświęcić programowi w asemblerze i języku C.
W asemblerze dołączenie wspomnianego wcześniej pliku z podstawowymi procedurami nastąpiło przy użyciu dyrektywy "$INCLUDE". Dyrektywa ta powoduje wstawianiu w jej miejsce przez kompilator asemblera zawartości pliku wskazanego tą dyrektywą. Tak więc dwa fizyczne pliki zawierające kod ˇródłowy w czasie kompilacji są łączone w jeden. Konsekwencją tego jest fakt, że cokolwiek zadeklarujemy w pliku pierwszym będzie to obowiązywało również w pliku drugim. Dlatego pomimo pozornego niewykorzystywania bitów SDA i SCL przez program w pliku P04_01_ASM_SRC.A51 umieszczona tam deklaracja jest konieczna do poprawnego działania procedur zawartych w pliku I2C.ASM.
Trochę inaczej ta sprawa przedstawia się w języku C. Plik I2C.C został dodany do projektu za pomocą polecenia "Add node Source / Application" z menu RIDE IDE. Pliku ˇródłowym naszego programu umieszczamy dyrektywę "#include "I2C.H"".Jest to dyrektywa preprocesora włączająca plik nagłówkowy do pliku ˇródłowego. ˇródłowego pliku nagłówkowym umieszczone są prototypy procedur zawartych w pliku I2C.C.
W Bascomie sprawa przedstawia się zupełnie prosto, ponieważ obsługa magistrali I2C jest wbudowana w kompilator i nie ma potrzeby zawracania sobie głowy dołączaniem jakichkolwiek dodatkowych plików a przede wszystkim samodzielnego oprogramowywania transmisji. W tym momencie przewaga Bascoma nad asemblerem i C jest najbardziej widoczna. Aby korzystać z magistrali I2C nie potrzebujemy znać dokładnie protokołu transmisji, wystarczy tylko podłączyć do procesora odpowiednio linie SDA i SCL i możemy w prosty sposób sterować układami przez I2C.
Przykład 4.2.ASM
NAME P04_02_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P04_02_ASM SEGMENT CODE
RSEG P04_02_ASM
SDA EQU P2.0
SCL EQU P2.1
ACK EQU 00h
START:
ACALL I2C_START
MOV A, #72h
ACALL I2C_WRITE
MOV A, #0FFh
ACALL I2C_WRITE
ACALL I2C_STOP
ACALL I2C_START
MOV A,#73h
ACALL I2C_WRITE
MOV A,#0
ACALL I2C_READ
ACALL I2C_STOP
MOV R4, A
ACALL I2C_START
MOV A,#70h
ACALL I2C_WRITE
MOV A,R4
ACALL I2C_WRITE
ACALL I2C_STOP
SJMP START
$INCLUDE (I2C.ASM)
END
Przykład 4.2.C
#include <ATMEL/REG8252.H>
#include "I2C.H"
void main(void)
{
unsigned char temp;
while(1)
{
I2C_START();
I2C_WRITE(0x72);
I2C_WRITE(0xFF);
I2C_STOP();
I2C_START();
I2C_WRITE(0x73);
temp = I2C_READ(1);
I2C_STOP();
I2C_START();
I2C_WRITE(0x70);
I2C_WRITE(temp);
I2C_STOP();
}
}
Przykład 4.2.BAS
Config Sda = P2.0
Config Scl = P2.1
Dim Temp As Byte
Do
I2cstart
I2cwbyte &H72
I2cwbyte &HFF
I2cstop
I2cstart
I2cwbyte &H73
I2crbyte Temp , 9
I2cstop
I2cstart
I2cwbyte &H70
I2cwbyte Temp
I2cstop
Loop
End
Powyższy program odczytuje stan klawiatury i zapala diodę LED o numerze odpowiadającym naciśniętemu przyciskowi klawiatury, czyli odczytuje stan wejść pierwszego układu PCF8574 i przesyła go na wyjścia drugiego układu PCF8574
Część 6 - Wyświetlacz LCD
Przykład 6.1.ASM
NAME P06_01_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P06_01_ASM SEGMENT CODE
RSEG P06_01_ASM
PORTLCD EQU P2
RS EQU PORTLCD.2
E EQU PORTLCD.3
D7 EQU PORTLCD.7
D6 EQU PORTLCD.6
D5 EQU PORTLCD.5
D4 EQU PORTLCD.4
CLEAR EQU 01H
HOME EQU 02H
INCREMENT EQU 04H
DECREMENT EQU 06H
SHIFTRIGHT EQU 07H
SHIFTLEFT EQU 05H
BLINK EQU 09H
NOBLINK EQU 08H
CURON EQU 0AH
CUROFF EQU 08H
DISPON EQU 0CH
DISPOFF EQU 08H
SHIFT_CURSOR_LEFT EQU 10H
SHIFT_CURSOR_RIGHT EQU 14H
SHIFT_DISPLAY_LEFT EQU 18H
SHIFT_DISPLAY_RIGHT EQU 1CH
START:
ACALL LCD_INIT
MOV DPTR,#NAPIS1
ACALL WRITE_TEXT
SJMP $
WRITE_TO_LCD:
PUSH ACC ;zapamiętaj zawartość akumulatora na stosie
SETB E ;ustaw na linii E stan wysoki
ORL PORTLCD,#0F0H ;ustaw 4 starsze bity portu P1
ORL A,#0FH ;ustaw 4 młodsze bity akumulatora
ANL PORTLCD,A ;iloczyn logiczny portu P1 i akumulatora
;powoduje zmianę tylko 4 starszych bitów portu 4 młodsze bez zmian
CLR E;opadające zbocze na E zapisuje do rejestru wyświetlacza
POP ACC ;przywróć początkową zawartość akumulatora
SWAP A ;zamień miejscami połówki akumulatora
SETB E ;ustaw na linii E stan wysoki
ORL PORTLCD,#0F0H ;ustaw 4 starsze bity portu P1
ORL A,#0FH ;ustaw 4 młodsze bity akumulatora
ANL PORTLCD,A ;iloczyn logiczny portu P1 i akumulatora
;powoduje zmianę tylko 4 starszych bitów portu 4 młodsze bez zmian
CLR E ;opadające zbocze na E zapisuje do rejestru wyświetlacza
MOV A,#1 ;opóˇnienie ok. 1ms
LCALL DELAY
RET
WRITE_COMMAND:
CLR RS ;0 na RS -> zapis do rejestru rozkazów
LCALL WRITE_TO_LCD ;zapis bajtu do rejestru wyświetlacza
RET
WRITE_CHAR:
SETB RS ;1 na RS -> zapis do pamięci obrazu
LCALL WRITE_TO_LCD ; zapis bajtu do rejestru wyświetlacza
RET
WRITE_TEXT:
MOV R7,#0 ;wyzeruj rejestr r7
M0:
MOV A,R7 ;przenieś do akumulatora rejestr r7
MOVC A,@A+DPTR ;prześlij bajt z pamięci kodu do akumulatora
JZ M1 ;jeśli ten bajt jest zerem to skocz do końca procedury
LCALL WRITE_CHAR ;wyświetl znak na wyświetlaczu
INC R7 ;zwiększ adres znaku o 1
AJMP M0 ;skocz na początek pętli
M1:
RET
DEFINE_CHARACTERS:
MOV A,#40H ;ustaw tryb zapisu znaków do pamięci generatora znaków
LCALL WRITE_COMMAND
MOV DPTR,#CHAR_PL ;do DPTR wpisz adres tablicy z definicjami znaków
LCALL WRITE_TEXT ;i zapisz te znaki do pamięci generatora znaków
MOV A,#80H ;powróć do trybu zapisu znaków do pamięci obrazu
LCALL WRITE_COMMAND
RET
LCD_CLS:
MOV A,#CLEAR ;do akumulatora zapisz kod rozkazu czyszczenia ekranu
LCALL WRITE_COMMAND
RET
LCD_LOWERLINE:
MOV A,#0A8H
LCALL WRITE_COMMAND
RET
LCD_SHIFT_LEFT:
MOV A,#SHIFT_DISPLAY_LEFT
LCALL WRITE_COMMAND
RET
LCD_SHIFT_RIGHT:
MOV A,#SHIFT_DISPLAY_RIGHT
LCALL WRITE_COMMAND
RET
CURSOR_SHIFT_LEFT:
MOV A,#SHIFT_CURSOR_LEFT
LCALL WRITE_COMMAND
RET
CURSOR_SHIFT_RIGHT:
MOV A,#SHIFT_CURSOR_RIGHT
LCALL WRITE_COMMAND
RET
LCD_INIT:
MOV A,#0FH
LCALL DELAY
CLR E
CLR RS
MOV R0,#3
LL:
SETB E
ANL PORTLCD, #3FH
CLR E ;zapis do wyświetlacza
MOV A,#5
LCALL DELAY ;czekaj ok. 5 ms
DJNZ R0,LL ;powtórz inicjalizację 3 razy
SETB E
ANL PORTLCD, #2FH
CLR E
MOV A,#01H
LCALL DELAY
MOV A,#28H
LCALL WRITE_COMMAND
MOV A,#08H
LCALL WRITE_COMMAND
MOV A,#01H
LCALL WRITE_COMMAND
MOV A,#06H
LCALL WRITE_COMMAND
MOV A,#0CH
LCALL WRITE_COMMAND
;LCALL DEFINE_CHARACTERS
RET
DELAY:
MOV R3,A
L1:
MOV R2,#10
L2:
MOV R1,#47
L3:
DJNZ R1,L3
DJNZ R2,L2
DJNZ R3,L1
RET
CHAR_PL:
DB 32, 32, 14, 1, 15, 17, 15 , 2, ;ą (8)
DB 2, 4, 14, 16, 16, 17, 14 , 32, ;ć (9)
DB 32, 32, 14, 17,31, 16, 14 , 4, ;ę (10)
DB 12, 4, 6, 12, 4, 4, 14, 32, ;ł (11)
DB 2, 4, 22, 25, 17, 17, 17, 32, ;ń (12)
DB 2, 4, 15, 16, 14, 1, 30, 32, ;ś (13)
DB 32, 4, 31, 2, 4, 8, 31, 32, ;ż (14)
DB 2, 4, 31, 2, 4, 8, 31, 32, ;ˇ (15)
DB 0
NAPIS1:
DB "ABCDEFGHIJKLMNO",0
END
Przykład 6.1.C
#include <ATMEL/REG8252.H>
#define PORT P2
sbit RS = PORT^2;
sbit E = PORT^3;
sbit D4 = PORT^4;
sbit D5 = PORT^5;
sbit D6 = PORT^6;
sbit D7 = PORT^7;
void Delay(char k)
{
char x,y;
while(k)
{
k--;
for (x = 248; x > 0 ; x--);
for (y = 248; y > 0 ; y--);
}
}
void WriteToLcd(char X)
{
E = 1;
PORT |= 0xF0;
PORT &= (X | 0x0F);
E = 0;
E = 1;
X <<= 4;
PORT |= 0xF0;
PORT &= (X | 0x0F);
E = 0;
Delay(1);
}
void WriteCommand(char X)
{
RS = 0;
WriteToLcd(X);
}
void WriteChar(char X)
{
RS = 1;
WriteToLcd(X);
}
void WriteText(char *S)
{
while(*S)
{
WriteChar(*S);
S++;
}
}
void DefineCharacters(char *znaki)
{
WriteCommand(0x40);
WriteText(znaki);
WriteCommand(0x80);
}
void LcdInit(void)
{
char i;
Delay(15);
PORT = 0x0F;
for (i = 0; i<3; i++)
{
E = 1;
PORT &= 0x3F;
E = 0;
Delay(5);
}
E = 1;
PORT &= 0x2F;
E = 0;
Delay(1);
WriteCommand(0x28);
WriteCommand(0x08);
WriteCommand(0x01);
WriteCommand(0x06);
WriteCommand(0x0C);
}
void main(void)
{
LcdInit();
WriteText("ABCDEFGHIJKLMNO");
while(1);
}
Przykład 6.1.BAS
Config Lcd = 16 * 2
Config Lcdpin = Pin , Db4 = P2.4 , Db5 = P2.5 , Db6 = P2.6 , Db7 = P2.7 , E = P2.3 , Rs = P2.2
Cursor Off
Cls
Lcd "ABCDEFGHIJKLMNO"
Do
Loop
End