Programowanie mikrokontrolerów 8051 w jezyku C
Transkrypt
Programowanie mikrokontrolerów 8051 w jezyku C
PROGRAMOWANIE MIKROKONTROLERÓW 8051
W JĘZYKU C
KŁ
ZSP4 2012
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);
}
// 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;
}// dołączenie pliku nagłówkowego
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;
}
zapisowi P3.0, czyli określa pin 0 portu P3. Przyjrzyjmy sie teraz dokładniej instrukcji if :
if(P3_0 == 0)
P0_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;
}
Instrukcja 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 :
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// 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 na
// 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;
case 247 : diody = 239; break;
case 239 : diody = 247; break;
case 223 : diody = 251; break;
case 191 : diody = 253; break;
case 127 : diody = 254; break;
}
szego programu : 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
// 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;
}
}
}
e 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
// 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 S1 254
#define S2 253
#define S3 251no.
// 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 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
#define
#define
#define
#define
D4
D5
D6
D7
D8
247
239
223
191
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;
}
}
}
// dołączenie pliku nagłówkowego
// zawierajacego definicje rejestrow
// wewnetrznych procesora
#include <8051.h>
// zdefiniowanie alternatywnej nazwy portu P2
#define klawiatura P2
// 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 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;
}
}
}
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:
#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)
{
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 :
#include <8051.h>
char code tablica[] = {0x55,0xAA,0x0F,0xF0};
char code * wskaznik;
// 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)
{
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.