Programowanie mikrokontrolerow 8051 w jezyku C


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, badz 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, badz logicznymi. Możliwe są następujące skrócone formy zapisu wyrażeń :
Zapis
Funkcja Zapis skrócony
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
a >>= b a = a >> b
prawo
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óznienia. Funkcja
generująca opóznienie (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ózniającą do generowania prostego efektu świetlnego na diodach
LED. Kod programu przedstawiony jest poniżej :
// definicja funkcji opózniają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óznienia (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ózniają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óznienia 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ądz warunku pętli, badz wartości początkowych oraz końcowych kompilator nie zastosuje innego
kodu. Tak więc generowanie opóznień za pomocą pętli jest przydatne tylko przy generowniu przyblizonych
opóznień. 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ądz 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ądz 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 zródłowego. Gdy preprocesor przegląda plik zró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 zró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ózniająca czekaj, służąca do generowania
opóznień 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ózniają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.
Wskazniki
Wskazniki są bardzo ważnym elementem języka C. Ogólnie mówiąc wskaznik jest zmienną przechowująca
adres innej zmiennej. Wskazniki deklarujemy w następujacy sposób :
typ * nazwa;
Aby uzyskac dostęp do zmiennej wskazywanej przez wskaznik 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 wskazników, skoro wskazują ona na inną zmienną, jakby nie można
było się posługiwać tylko tą zmienną. Wskazniki 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
wskazikó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 wskaznika :
char code * wskaznik;
Zgodnie z tym, co pisałem o deklarowaniu wskaznika wskazuje on na typ char umieszczony w pamięci
programu code. Jednak samo zadeklarowanie wskaznika nie pozwola nam na jego używanie. Wtym momecie
nasz wskaznik 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 wskaznikiem! Tak więc powyższy zapis
przepisuje do jednego wskaznika zawartość innego wskaznika. Aby w jawny sposób do wskaznika 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 wskaznika 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 wskaznika adresu innego wskaznika, a nie
adresu danej typu char. Wiem, że to na początku jest bardzo skomplikowane, bowiem wskazniki 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
$INCLUDE (ATMEL/REG8252.INC)
P01_01_ASM SEGMENT CODE
RSEG P01_01_ASM
CLR P0.0 ; USTAWIENIE WYPROWADZENIA P0.0 W STAN NISKI
; POWODUJE ZAÅšWIECENIE SI DIODY LED D1
SJMP $ ; PTLA NIESKOCCZONA
END
Przykład 1.1.C
#include
sbit LED1 = 0x80; // Deklaracja zmiennej o nazwie LED1
// Znajdujacej sie pod adresem 80h
// Co odpowiada pinowi 0 portu 0
void main(void)
{
LED1 = 0; // ustawienie pinu P0.0 w stan niski
while(1); // petla nieskonczona
}
Przykład 1.1.BAS
Reset P0.0
Do : Loop : End
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 wskanika 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
;
; D51 V2.6 8051 Disassembly of asm.hex
; 10/11/2003 16:58
;
org 0
;
clr p0.0
X0002: sjmp X0002
;
end
;
Przykład 1.1.C.D51
;
; D51 V2.6 8051 Disassembly of c.hex
; 10/11/2003 16:58
;
org 0
;
ljmp X0003
;
X0003: mov sp,#8
clr a
mov r0,#0ffh
X0009: mov @r0,a
djnz r0,X0009
mov p2,#0ffh
ljmp X0012
;
X0012: clr p0.0
X0014: sjmp X0014
;
end
;
Przykład 1.1.BAS.D51
;
; D51 V2.6 8051 Disassembly of bas.hex
; 10/11/2003 16:58
;
org 0
;
ljmp X002e
;
reti
;
org 0bh
;
reti
;
org 13h
;
reti
;
org 1bh
;
reti
;
org 23h
;
reti
;
org 2bh
;
reti
;
org 2eh
;
X002e: mov r0,#0ffh
clr a
X0031: mov @r0,a
djnz r0,X0031
mov sp,#21h
mov 20h,#0
clr p0.0
X003c: ljmp X003c
;
clr ea
X0041: sjmp X0041
;
end
;
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
mov r0,#0ffh
X0009:
mov @r0,a
djnz r0,X0009
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
$INCLUDE (ATMEL/REG8252.INC)
LED1 EQU P0.0
P01_02_ASM SEGMENT CODE
RSEG P01_02_ASM
CLR LED1 ; USTAWIENIE WYPROWADZENIA P0.0 W STAN NISKI
; POWODUJE ZAÅšWIECENIE SI DIODY LED D1
SJMP $ ; PTLA NIESKOCCZONA
END
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
Reset Led1
Do : Loop : End
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
$INCLUDE (ATMEL/REG8252.INC)
P01_03_ASM SEGMENT CODE
RSEG P01_03_ASM
ANL P0,#0FEh
SJMP $ ; PTLA NIESKOCCZONA
END
Przykład 1.3.C
#include
void main(void)
{
P0 &= 0xFE;
while(1);
}
Przykład 1.3.BAS
P0 = P0 And &B11111110
Do : Loop : End
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
NAME P01_03_ASM_SRC
$INCLUDE (ATMEL/REG8252.INC)
P01_03_ASM SEGMENT CODE
RSEG P01_03_ASM
ANL P0,#0AAh
SJMP $ ; PTLA NIESKOCCZONA
END
Przykład 1.4.C
#include
void main(void)
{
P0 &= 0xAA;
while(1);
}
Przykład 1.4.BAS
P0 = P0 And &B10101010
Do : Loop : End
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
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
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
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
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 BASCOMłie 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
#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
#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
#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
#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
#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
#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
#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
#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


Wyszukiwarka

Podobne podstrony:
programowanie mikrokontrolerow 8051 w jezyku c pierwsze kroki rapidshare
Konfiguracja pamięci mikrokontrolera 8051 dla programów napisanych w języku C
Układy licznikowe mikrokontrolera 8051
programowanie mikrokontrolerów
avt 515 Programator mikrokontrolerów AVR i AT89S8252
Mikrokontroler 8051
Układ przerwań mikrokontrolera 8051
Programowanie mikrokontrolerów za pomocą programatora USBasp » Majsterkowo pl
Zastosowanie mikrokontrolera 8051 do sterowania ATA ATAPI CDROM
Prosty programator mikrokontrolerów AT89Cx051 AT89C5x
Podstawy programowania mikrokontrolerów AVR8 w środowisku AVR Studio 4
Visual Studio 05 Programowanie z Windows API w jezyku C vs25pw
Programator mikrokontrolerów ATMEL AT89C2051 1051

więcej podobnych podstron