Programowanie mikrokontrolerów 8051 w języku C
Transkrypt
Programowanie mikrokontrolerów 8051 w języku C
PROGRAMOWANIE
MIKROKONTROLERÓW
Wprowadzenie
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 można od
razu zobaczyć na własne oczy. Ja zwykle, gdy
zaczynam pracę z nowym mikrokontrolerem, piszę
program, który zapala diodę LED. W ten sposób
można najszybciej przekonać się o poprawnym
działaniu programu. Do mikrokontrolera należy
podłączyć 8 diod LED w sposób pokazany na rysunku
:
Wartości rezystorów należy dobrać odpowiednio do
posiadanych diod LED. Jeśli są to diody
standardowe, to rezystancja rezystorów powinna
mieć wartość ok. 330 Ohm. Natomiast gdy
dysponujemy diodami niskoprądowymi, to
rezystancja rezystorów może mieć wartość ponad 1
kOhm (należy zwrócić także uwagę na wydajność
prądową portów mikrokontrolera).
Operator przypisania.
W tym podtemacie zapoznamy 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, bądź stałej.
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// zapisanie do portu P0 liczby 0x55
P0 = 0x55;
// pusta pętla nieskończona - zatrzymanie programu
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ływać 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
standardowym 8051, to musimy dołączyć odpowiedni dla danego mikrokontrolera plik
nagłówkowy. W sytuacji, gdy używamy tylko typowych rejestrów można spokojnie
zastosować 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ą funkcji mówi kompilatorowi, że
funkcja nie zwraca wartości, albo inaczej mówiąc, że zwracana wartość jest typu void (pusty,
brak typu). Słowo void w nawiasach okrągłych po nazwie funkcji mówi kompilatorowi, że do
funkcji nie jest przekazywana żadna wartość. W zależności od tego, jaką funkcję chcemy
napisać 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) - funkcja 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 możliwych kombinacji jest bardzo dużo i zależą one od tego, jakie zadania ma
spełniać dana funkcja.
Gdy już mamy szkielet programu, to należy wpisać właściwy kod programu. W naszym
pierwszym programie w zasadzie decydujące znaczenia 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 szesnastkowym w języku C
zapisujemy właśnie w podany sposób : liczbę szesnastkową (bez znaku 'h' na końcu) należy
poprzedzić 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. Ponieważ 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 nieskończona zatrzymanie programu
while(1);
}
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// iloczyn logiczny portu P0 ze stałą
55h
P0 &= 0x55;
// pusta pętla nieskończona zatrzymanie programu
while(1);
Funkcje arytmetyczne i logiczne
Język C umożliwia stosowanie
skróconych wyrażeń, będących
połączeniem operatora przypisania z
operatorami arytmetycznymi, bądź
logicznymi. Możliwe są następujące
skrócone formy zapisu wyrażeń :
Funkcja
dodawanie
odejmowanie
mnożenie
dzielenie
iloczyn logiczny
suma lgiczna
przesunięcie w lewo
przesunięcie w prawo
alternatywa logiczna
Zapis skrócony
a += b
a -= b
a *= b
a /= b
a &= b
a |= b
a <<= b
a >>= b
a ^= b
Zapis normalny
a=a+b
a=a-b
a=a*b
a=a/b
a=a&b
a=a|b
a = a << b
a = a >> b
a=a^b
Do ustawiania linii portów służy 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ć
odpowiedni 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 :
W naszym programie pojawił się dodatkowy wiersz kodu :
P0 |= 0xF0;
Ponownie zastosowałem skrócony zapis łączący operator przypisania z operatorem sumy
logicznej. 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ęc zaraz przed ich ustawieniem je wyzerować.
Samo ustawienie wybranych linii w stan wysoki realizuje poniższy kod :
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych procesora
#include <8051.h>
// główna funkcja programu
void main(void)
{
// ustawienie wszystkich linii portu P0 w stan niski
P0 = 0;
// suma logiczna portu P0 ze stałą F0h
P0 |= 0xF0;
// pusta pętla nieskończona - zatrzymanie programu
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ęc zaraz przed ich ustawieniem je wyzerować. Samo ustawienie
wybranych linii w stan wysoki realizuje poniższy kod :
P0 |= 0xF0;
Ponownie zastosowałem skrócony zapis łączący operator przypisania z operatorem
sumy logicznej. Nie są wymagane żadne dodatkowe zmienne pośredniczące w
przesyłaniu danych z portu P3 do portu P0.
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 poniższym 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 wymusza na wyprowadzeniu
portu stan niski. Port musi zawierać wewnętrzne rezystory
podciągające linie portu do szyny zasilającej.
W przeciwnym razie będziemy 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 należy się upewnić, że port do którego
chcemy podłączyć klawiaturę posada rezystory podciągające.
Pierwszy program wykorzystujący klawiaturę jest bardzo prosty i zamieszczony poniżej :
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych 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 LEDpo naciśnięciu dowolnego
przycisku. zauważmy, że naciśniecie przycisku wywołuje wymuszenie na odpowiednim
wyprowadzeniu portu p3 stanu niskiego, a zapalenie diody LEDjest 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 żadnych
dodatkowych zabiegów. jest to zrealizowane przez instrukcję :
p0 = p3;
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 się 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 języku 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ępująco :
for(inicjalizacja zmiennej licznikowej; warunek;
modyfikacja zmiennej 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
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych 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ą kombinacje 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łem w
pierwszej części kursu, funkcja czekaj nie zwraca żadnej wartości (void) i wymaga
jednego parametru typu unsigned char (liczba 8-bitowa bez znaku). Parametrem
tym będzie żądana przez nas długość opóźnienia (mniej-więcej w milisekundach).
Przyjrzyjmy się 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żdżonych pętli for. W wyniku
tego łączny czas wykonywania tych pętli jest iloczynem powtórzeń każdej pętli.
Dokładny czas opóźnienia trudno jest określić, ponieważ ze względu na różne
techniki optymalizacji kodu przez kompilator nie jest znany dokładny czas
wykonywania jednej pętli. Można co prawda odczytać z pliku *.asm generowanego
przez kompilator jakie instrukcje zostały użyte 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, bądź 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
generowaniu przybliżonych 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 rozbudowanego bloku instrukcji if-else wygodniej jest
zastosować instrukcję switch. Ogólna postać 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 być dowolnym wyrażeniem bądź zmienną, pod warunkiem że
wartość tej zmiennej lub wyrażenia jest typu całkowitego. Nasz przykładowy
program niech realizuje następującą funkcję : po naciśnięciu przycisku zapal
przeciwną diodę LED (tzn. "od drugiej strony").
Przyjrzyjmy się kodowi:
// 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;
}
- #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 dyrektywę #define, to
zastąpi ten ciąg, ciągiem do niego przypisanym. Dzięki temu możemy zamiast
niewiele znaczących nazw portu używać 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ę instrukcja switch. Jako zmienną tej
instrukcji wykorzystamy 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 należy 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ć instrukcję 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 należy wcześniej
zdefiniować stałą o nazwie jasno mówiącej
o jej przeznaczeniu. Poza tym, 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 miejscach w których należy ją
zmienić będzie bardzo kłopotliwe. Tym
bardziej, że nie będziemy mogli użyć
mechanizmu "znajdź/zamień", ponieważ
łatwo zmienimy nie ten znak co trzeba.
Bardziej elegancka (oczywiście nie
najbardziej - ta będzie za chwile) wersja
programu jest przedstawiona obok :
// dołączenie pliku nagłówkowego
// zawierającego definicje rejestrów
// wewnętrznych 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
// zawierającego definicje rejestrów
// wewnętrznych 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
o przeznaczeniu stałej" wybrałem pozx,
gdzie x = 1..8. Jak łatwo można się domyśleć // główna funkcja programu
"poz" to skrót od "pozycja". Ponieważ stałe void main(void)
// pętla nieskończona
liczbowe odpowiadające przyciskom, jaki
while(1)
diodom LED sa identyczne nie
{
zastosowałem rozróżnienia czy chodzi o
// zgaszenie diod LED
pozycję przycisku czy diody LED. Jednak już diody = 0xFF;
// w zależnosci od wciśniętego przycisku
naprawdę elegancko będzie, gdy użyjemy
odpowiednich stałych dla diod i przycisków // zapal odpowiednią diodę LED
switch(klawiatura){
osobno
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;
}
}
}
// 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 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;
}
}
}
cd z poprzedniej strony
#define D7 191
// dołączenie pliku nagłówkowego
#define D8 127
// zawierającego definicje rejestrów
// główna funkcja programu
// wewnętrznych procesora
void main(void)
#include <8051.h>
{
// zdefiniowanie alternatywnej nazwy portu P2
// pętla nieskończona
#define klawiatura P2
while(1)
// dołączenie pliku nagłówkowego
{
// zawierającego definicje rejestrów
// zgaszenie diod LED
// wewnętrznych procesora
diody = 0xFF;
#include <8051.h>
// w zależności od wciśniętego przycisku
// zdefiniowanie alternatywnej nazwy portu P2
// zapal odpowiednią diodę LED
#define klawiatura P2
switch(klawiatura){
// zdefiniowanie alternatywnej nazwy portu P0
case S1 : diody = D8; break;
#define diody P0
case S2 : diody = D7; break;
//
case S3 : diody = D6; break;
#define S1 254
case S4 : diody = D5; break;
#define S2 253
case S5 : diody = D4; break;
#define S3 251
case S6 : diody = D3; break;
#define S4 247
case S7 : diody = D2; break;
#define S5 239
case S8 : diody = D1; break;
#define S6 223
}
#define S7 191
}
#define S8 127
}
//
Z powyższych trzech programów to dla każdego z nich byłby identyczny. Dzieje się
#define D1 254
tak, że z punktu widzenia kompilatora te trzy programy są identyczne, ponieważ w
#define D2 253
#define D3 251
"miejscach strategicznych" występują te same dane. Jest to kolejnym objawem
#define D4 247
preprocesora - kod źródłowy programu przed kompilacją został doprowadzony do
#define D5 239
postaci zrozumiałej przez kompilator (gdyby pominąć proces przetwarzania pliku
#define D6 223
przez preprocesor, to kompilator zgłosiłby mnóstwo błędów).
cd z poprzedniej strony
#define D7 191
// dołączenie pliku nagłówkowego
#define D8 127
// zawierającego definicje rejestrów
// główna funkcja programu
// wewnętrznych procesora
void main(void)
#include <8051.h>
{
// zdefiniowanie alternatywnej nazwy portu P2
// pętla nieskończona
#define klawiatura P2
while(1)
// dołączenie pliku nagłówkowego
{
// zawierającego definicje rejestrów
// zgaszenie diod LED
// wewnętrznych procesora
diody = 0xFF;
#include <8051.h>
// w zależności od wciśniętego przycisku
// zdefiniowanie alternatywnej nazwy portu P2
// zapal odpowiednią diodę LED
#define klawiatura P2
switch(klawiatura){
// zdefiniowanie alternatywnej nazwy portu P0
case S1 : diody = D8; break;
#define diody P0
case S2 : diody = D7; break;
//
case S3 : diody = D6; break;
#define S1 254
case S4 : diody = D5; break;
#define S2 253
case S5 : diody = D4; break;
#define S3 251
case S6 : diody = D3; break;
#define S4 247
case S7 : diody = D2; break;
#define S5 239
case S8 : diody = D1; break;
#define S6 223
}
#define S7 191
}
#define S8 127
}
//
Z powyższych trzech programów to dla każdego z nich byłby identyczny. Dzieje się
#define D1 254
tak, że z punktu widzenia kompilatora te trzy programy są identyczne, ponieważ w
#define D2 253
#define D3 251
"miejscach strategicznych" występują te same dane. Jest to kolejnym objawem
#define D4 247
preprocesora - kod źródłowy programu przed kompilacją został doprowadzony do
#define D5 239
postaci zrozumiałej przez kompilator (gdyby pominąć proces przetwarzania pliku
#define D6 223
przez preprocesor, to kompilator zgłosiłby mnóstwo błędó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 jakieś dane należy zastosować poniższy zapis :
int tablica[5] = {5, 2, 31, 55, 40};
#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żdżona 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 generować prostą sekwencję (przechowywaną właśnie w tablicy)
odpowiednio zapalanych diod LED, podłączonych do 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 sterujące 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 pętli:
for(a = 0; a < 10; ++a)
#include <8051.h>
// a ta 100 razy
char code tablica[4] =
for(b = 0; b < 25; ++b);
{0x55,0xAA,0x0F,0xF0};
}
// definicja funkcji opóźniającej
void main(void)
void czekaj(unsigned char x)
{
{
char i;
// deklaracja dwóch zmiennych
while(1)
pomocniczych
{
unsigned char a, b;
for(i = 0; i < 4; i++)
// potrójnie zagnieżdżona pętla for
{
// ta pętla zostanie wykonana x-razy
P0 = tablica[i];
for( ; x > 0; x--)
czekaj(250);
// ta 10 razy
}
}
}
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ępujący sposób :
typ*wskaźnik = 0;
Aby uzyskać dostęp do zmiennej wskazywanej przez wskaźnik należy użyć operatora
wyłuskania *, na przykład poniższa instrukcja :
typ * nazwa;
spowoduje zapisanie do zmiennej (a raczej do komórki pamięci) wskazywanej przez
wskaźnik 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źnikó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 pozwolą nam na jego
używanie. W tym momencie nasz wskaźnik nie przechowuje żadnego adresu, a zwłaszcza
adresu naszej tablicy. Przed jego użyciem, należy 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 przypisać adres jakiejś zmiennej należy użyć operator adresu &, tak jak
pokazano poniżej :
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 jego
numeru. Gdybyśmy użyli takiego zapisu :
wskaznik = &tablica;
często sprawiają problemy początkującym programistom.
to kompilator zgłosi błąd, gdyż zapis ten oznacza 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