Mikrokontrolery_ARM_cz12.pdf

(394 KB) Pobierz
097-099_arm_cz12.indd
K U R S
Mikrokontrolery z rdzeniem
ARM, część 12
Porty GPIO
Budowa portów GPIO
Mikrokontrolery LPC213x posia-
dają dwa 32–bitowe porty wejścia/
wyjścia P0 i P1, przy czym port
P1 ma wyprowadzone tylko najstar-
sze 16 bitów (P1.16…P1.31). Z por-
tu P0 nie ma wyprowadzonej linii
P0.24, natomiast port P0.31 może
pełnić tylko funkcję wyjścia. Porty
P0 i P1 są dwukierunkowe i mają
maksymalną wydajność prądową
rzędu 45 mA zarówno od plusa
jak i minusa napięcia zasilającego.
W przypadku, gdy linie portu skon-
figurowane są jako wejściowe, port
P0 nie posiada rezystorów podcią-
gających, natomiast port P1 wypo-
sażony jest w rezystory podciągają-
ce o wartości 60…300 kV. Każdy
z pinów może pełnić również rolę
jednej z trzech funkcji alternatyw-
nych zapewniając podłączenie we-
wnętrznych układów peryferyjnych
na przykład wyprowadzenie prze-
twornika A/C. Podobnie jest w in-
nych mikrokontrolerach, jak AVR
czy 8051, gdzie każdy port wej-
ścia/ wyjścia może również pełnić
rolę wyprowadzenia wewnętrznego
układu peryferyjnego, jednak za-
zwyczaj jest to pojedyncza funkcja.
Domyślnie po zerowaniu mikrokon-
trolera wszystkie piny pełnią rolę
portów I/O i są skonfigurowane
w kierunku wejściowym. Za rolę,
jaką pełni dane wyprowadzenie
mikrokontrolera odpowiedzialne są
rejestry PINSELx. Port P0 posiada
dwa rejestry konfiguracyjne PIN-
W poprzednich odcinkach zajmowaliśmy się
układami peryferyjnymi mającymi bezpośredni
wpływ na pracę rdzenia mikrokontrolera.
Omówiliśmy także przykładowy plik startowy
konfigurujący powyższe układy oraz
inicjalizujący pamięć mikrokontrolera zgodnie
ze standardem ANSI C/C++.
Tematem bieżącego odcinka będą porty wejścia/wyjścia (GPIO)
mikrokontrolerów LPC213x, które umożliwiają bezpośrednie
sterowanie układami podłączonymi do wyprowadzeń mikrokontrolera.
SEL0 (0xE002 C000) oraz PINSEL1
(0xE002 C004) natomiast port P1
z uwagi że ma wyprowadzonych
tylko 16 najstarszych bitów po-
siada, jeden rejestr konfiguracyjny
PINSEL2 (0xE002 C014) . Do kon-
figuracji każdego bitu portu P0
wykorzystywane są dwa bity z re-
jestru PINSELx. Na rys. 29 przed-
stawiono budowę jednej linii portu
P0 (P0.1).
W zależności od stanu bitów
[3..2] rejestru PINSEL0 port linia
P0.1 mikrokontrolera pełni rolę
portu wejścia/wyjścia (00b), wejścia
RxD pierwszego portu szeregowe-
go (01b), wyjścia PWM (10b), lub
wejścia przerwania zewnętrznego
(11b) Zastosowanie dodatkowych
rejestrów oraz multipleksera wybo-
ru funkcji alternatywnej jest bardzo
interesującym rozwiązaniem, ponie-
waż nie musimy konfigurować por-
tów wejścia/wyjścia w odpowiednim
kierunku. Wybranie funkcji alter-
natywnej spowoduje automatyczne
ustawienie linii portu w kierunku
odpowiadającym pełnionej funkcji.
W tab. 17 przedstawiono wszystkie
funkcje, jakie mogą pełnić poszcze-
gólne linie portu P0 wraz z odpo-
wiednią kombinacją bitów rejestrów
PINSEL0 oraz PINSEL1 potrzebną
do ustawienia odpowiedniej funk-
cji.
ce amatorskiej nie będziemy wyko-
rzystywać. Za sterowanie funkcjami
alternatywnymi portu P1 odpowie-
dzialny jest rejestr PINSEL2, który
zawiera bity konfiguracyjne pokaza-
ne w tab. 18 .
Po wyzerowaniu mikrokontro-
lera badany jest stan linii P1.26
i P1.20. W przypadku, gdy linia
P1.26 podczas zerowania znajdzie
się w stanie niskim wówczas in-
terfejs DEBUG jest włączany. Na-
tomiast, gdy linia P1.20 podczas
zerowania będzie się znajdować
w stanie niskim, wówczas włączo-
ny będzie interfejs TRACE. Ponie-
waż linie portu P1 posiadają re-
zystory podciągające, pozostawienie
ich nie podłączonych spowoduje
że domyślnie po zerowaniu inter-
fejs DEBUG i TRACE będzie wy-
łączony. Linie mikrokontrolera po
zerowaniu domyślnie pracują jako
porty I/O więc gdy chcemy sko-
rzystać z wybranych funkcji alter-
natywnych portu musimy odpo-
wiednio skonfigurować go poprzez
zapis odpowiednich wartości do
rejestrów PINSELx. Na przykład,
jeżeli chcemy skorzystać z portu
szeregowego UART0, który wyko-
rzystuje linie RxD0 i TxD0, należy
ustawić odpowiednie bity w reje-
strze PINSEL0:
#define PINTXD0 0x01
#define PINRXD0 (0x01<<2)
PINSEL0 |= PINTXD0 | PINRXD0; //Linie
P0.0 I P0.1 jako TxD0 i RxD0
Porty mikrokontrolerów LPC213x
w przeciwieństwie do mikrokontro-
W przypadku portu P1 sytuacja
jest dużo prostsza, ponieważ jedy-
ną funkcją alternatywną jaką pełni
ten port, jest interfejs debugowania
i śledzenia, którego raczej w prakty-
Rys. 29.
Elektronika Praktyczna 11/2006
97
197730146.011.png 197730146.012.png
K U R S
Tab. 17. Funkcje pełnione przez poszczególne linie portu P0
spowoduje ustawienie wszystkich
linii portu P0 w stan niski.
Rejestr IO0SET ( 0xE00028004 )
oraz IO1SET ( 0xE00028014 ) umożli-
wia ustawienie wybranych linii I/O
w stan wysoki („1”) bez zmiany sta-
nu pozostałych linii. Na przykład
instrukcja IO0SET=0x80 spowoduje
ustawienie P0.7 w stan wysoki bez
zmiany stanu pozostałych linii.
Rejestr IO0CLR ( 0xE00028004 )
oraz IO1CLR ( 0xE00028014 ) umoż-
liwia ustawienie wybranych linii
I/O w stan niski („0”) bez zmiany
stanu pozostałych linii. Wpisanie
1 na wybranym bicie powoduje
wyzerowanie odpowiadająego bitu
w porcie I/O. Na przykład instrukcja
IO0CLR=0x80 spowoduje ustawie-
nie P0.7 w stan niski bez zmiany
stanu pozostałych linii.
Jak więc widzimy sterowanie
portami I/O mikrokontrolera jest
bardzo proste. Aby odczytać za-
wartość linii wejścia/wyjścia mi-
krokontrolera, wystarczy skonfigu-
rować wybraną linię jako wejścio-
wą za pomocą rejestru kierunku
IOxDIR, a następnie odczytać stan
wybranej linii z rejestru IOxPIN.
Natomiast jeżeli chcemy ustawić
wybrane linie w odpowiedni stan
wystarczy za pomocą rejestru IO-
xDIR ustawić wybrane linie jako
wyjściowe i za pośrednictwem par
rejestrów IOxSET, IOxCLR lub IO-
xPIN ustawić odpowiednie bity.
Zastosowanie rejestrów IOxSET
i IOxCLR jest bardzo wygodne po-
nieważ możemy ustawić lub ska-
sować wybrane bity portu bez
wcześniejszego ich odczytywania.
W przypadku, gdy chcemy zmie-
nić całą zawartość danego portu,
wygodniej będzie skorzystać z re-
jestru IOxPIN, który od razu usta-
wi cały port zgodnie z zawartością
rejestru. Wszystko wygląda bardzo
kolorowo, jednak jest jeden drobny
mankament charakterystyczny dla
mikrokontrolerów LPC213x. Mia-
nowicie rejestry wejścia/wyjścia są
umieszczone w obszarze rejestrów
VPB do których dostęp odbywa
się za pomocą stosunkowo wolnej
magistrali urządzeń peryferyjnych
VPB, w wyniku czego rdzeń po-
trzebuje dodatkowych cykli, aby
przesłać zawartość tego obszaru
pamięci do rejestru ogólnego prze-
znaczenia. Efektem tego jest bardzo
wolny dostęp do porów I/O mikro-
kontrolera. Dla porównania AVR
z zegarem 16 MHz potrafi szybciej
Port Rejestr PINSELx 00b 01b 10b 11b
P0.0 PINSEL0 [1:0] P0.0 TxD0 PWM1 –
P0.1 PINSEL0 [3:2] P0.1 RxD0 PWM3 EINT0
P0.2 PINSEL0 [5:4] P0.2 SCL0 CAP0.0 –
P0.3 PINSEL0 [7:6] P0.3 SDA0 MAT0.0 EINT1
P0.4 PINSEL0 [9:8] P0.4 SCK0 CAP0.1 AD0.6
P0.5 PINSEL0 [11:10] P0.5 MISO0 MAT0.1 AD0.7
P0.6 PINSEL0 [13:12] P0.6 MOSI0 CAP0.2 AD1.0
P0.7 PINSEL0 [15:14] P0.7 SSEL0 PWM2 EINT2
P0.8 PINSEL0 [17:16] P0.8 TxD1 PWM4 –
P0.9 PINSEL0 [19:18] P0.9 RxD1 PWM6 EINT3
P0.10 PINSEL0 [21:20] P0.10 RTS1 CAP1.0 AD1.2
P0.11 PINSEL0 [23:22] P0.11 CTS1 CAP1.1 SCL1
P0.12 PINSEL0 [25:24] P0.12 DSR1 MAT1.0 AD1.3
P0.13 PINSEL0 [27:26] P0.13 DTR1 MAT1.1 AD1.4
P0.14 PINSEL0 [29:28] P0.14 DCD1 EINT1 SDA1
P0.15 PINSEL0 [31:30] P0.15 RI1 EINT2 AD1.5
P0.16 PINSEL1 [1:0] P0.16 EINT0 MAT0.2 CAP0.2
P0.17 PINSEL1 [3:2] P0.17 CAP1.2 SCK MAT1.2
P0.18 PINSEL1 [5:4] P0.18 CAP1.3 MISO MAT1.3
P0.19 PINSEL1 [7:6] P0.19 MAT1.2 MOSI CAP1.2
P0.20 PINSEL1 [9:8] P0.20 MAT1.3 SSEL EINT3
P0.21 PINSEL1 [11:10] P0.21 PWM5 AD1.6 CAP1.3
P0.22 PINSEL1 [13:12] P0.22 AD1.7 CAP0.0 MAT0.0
P0.23 PINSEL1 [15:14] P0.23 – – –
P0.24 PINSEL1 [17:16] – – – –
P0.25 PINSEL1 [19:18] P0.25 AD0.4 AOUT –
P0.26 PINSEL1 [21:20] P0.26 AD0.5 – –
P0.27 PINSEL1 [23:22] P0.27 AD0.0 CAP0.1 MAT0.1
P0.28 PINSEL1 [25:24] P0.28 AD0.1 CAP0.2 MAT0.2
P0.29 PINSEL1 [27:26] P0.29 AD0.2 CAP0.3 MAT0.3
P0.30 PINSEL1 [29:28] P0.30 AD0.3 EINT3 CAP0.0
P0.31 PINSEL1 [31:30] P0.31 (Out) –
lerów rodziny 51 są w pełni dwu-
kierunkowe. Do sterowania portami
służą następujące rejestry SFR:
Rejestr kierunku IO0DIR (0xE-
00028008) (port P0), IO1DIR (0xE-
00028018) (port P1) umożliwia wy-
bór kierunku pracy wybranej linii
I/O. Ustawienie bitu w tym rejestrze
powoduje, że odpowiadająca mu
linia I/O pełni rolę wyjścia, nato-
miast jego wyzerowanie powoduje,
że wybrana linia pełni rolę wejścia.
Na przykład wykonanie operacji
IO0DIR=0x02; spowoduje ustawie-
nie linii P0.1 jako wyjściowej.
Rejestr IO0PIN (0xE0028000)
oraz IO1PIN (0xE0028010) umoż-
liwia odczytanie oraz ustawienie
stanu wybranej linii I/O. W przy-
padku, gdy wybrany pin skonfigu-
rowany jest jako wejściowy odczyt
tego rejestru jest bezpośrednim
odzwierciedleniem stanu sygna-
łów elektrycznych panujących na
tym pinie, natomiast, gdy wybra-
na linia skonfigurowana jest jako
wyjściowa, odczytanie tego reje-
stru powoduje odczytanie stanu
wewnętrznych przerzutników portu
i odzwierciedla stan w jakim znaj-
duje się wybrana linia wyjściowa.
Zapis do tego rejestru w przypad-
ku gdy wybrana linia skonfiguro-
wana jest jako wyjściowa powodu-
je wystawienie stanów logicznych
odzwierciedlających stan rejestru
na odpowiednich pinach mikro-
kontrolera. Na przykład IO0PIN=0,
98
Elektronika Praktyczna 11/2006
197730146.013.png 197730146.014.png 197730146.001.png 197730146.002.png 197730146.003.png 197730146.004.png 197730146.005.png 197730146.006.png
K U R S
Tab. 18.
Bit Nazwa
Opis
Wart. pocz.
LED0 i wylacz LED1
LEDSET = LED0;
LEDCLR = LED1;
}
[1:0] –
Zarezerwowane
0
Sprawdzanie wciśnięcia klawi-
sza odbywa się poprzez odczy-
tanie rejestru IO0PIN (KEYPIN),
natomiast włączanie i wyłączanie
diod odbywa się poprzez wpisa-
nie jedynki na wybranym bicie
w rejestrze IO1SET (LEDSET) gdy
chcemy załączyć wybraną dio-
dę (linia portu przyjmie wówczas
stan wysoki) i poprzez wpisanie
jedynki na wybranym bicie w reje-
strze IO1CLR (LEDCLR) gdy chce-
my wyłączyć wybraną diodę (linia
portu przyjmie wówczas stan ni-
ski). Sprawdzanie wciśnięcia stanu
klawisza S2 oraz załączenie diody
LED1 i wyłączenie LED0 odbywa
się w sposób analogiczny jak po-
przednio. Dla pokazania sposobu
sterowania wyjściami za pomocą
rejestru IOxPIN działanie fragmen-
tu programu odpowiedzialnego za
wykrycie wciśnięcia klawisza S3
jest trochę inne. Wykrywane jest
zbocze opadające na linii klawisza
S3 (P0.6) poprzez porównanie bie-
żącego i poprzedniego stanu klawi-
sza:
//Jezeli zbocze opadajace na S3 to
zmien stan LED2
key = KEYPIN & S3;
if (pkey && !key)
{
LEDPIN ^= LED2;
}
pkey = key;
W przypadku, gdy zostanie wy-
kryte zbocze, stan linii P1.18
(LED2) jest zmieniany na przeciw-
ny za pomocą operacji XOR na re-
jestrze LEDPIN (P1PIN).
Uruchamiając ten program mo-
żemy zauważyć że nie jest on od-
porny na drgania styków. Nie ma
to znaczenia w przypadku reakcji
na klawisz S1 i S2, ponieważ gdy
linia danego portu jest już skaso-
wana lub ustawiona, to ponowne
skasowanie lub ustawienie tej sa-
mej linii nie spowoduje żadnych
efektów. Natomiast w przypadku
wciśnięcia klawisza S3 stan linii
portu zmieniany jest na przeciw-
ny. Możemy więc zauważyć wielo-
krotne zmiany stanu diody LED2.
Modernizację programu tak, aby
był odporny na drgania zestyków
pozostawiam Czytelnikowi jako
ćwiczenie do samodzielnego wyko-
nania.
Lucjan Bryndza, EP
lucjan.bryndza@ep.com.pl
[2] GPIO/DEBUG
0 – Linie P1.26..P1.31 pracują jako porty IO
1 – Linie P1.26..P1.31 skonfigurowane są jako port
DEBUG
~P1.26
[3] GPIO/TRACE
0 – Linie P1.25..P1.16 pracują jako porty IO
1 – Linie P1.25..P1.16 skonfigurowane są jako port
DEBUG
~P1.20
[31:4] –
Zarezerwowane
zmieniać stan linii portu I/O niż
LPC213x pracujący z częstotliwo-
ścią 60 MHz. Konstruktorzy Philip-
sa szybko zauważyli ten problem
i w mikrokontrolerach LPC214x ob-
szar portów wejścia/wyjścia został
podłączony bezpośrednio do ma-
gistrali lokalnej i porty te zostały
nazwane szybkimi portami GPIO.
W mikrokontrolerach LPC214x re-
jestry te nie zostały bezpośrednio
przeniesione pod nowy obszar, tyl-
ko dodano nowy zestaw rejestrów,
a stare rejestry dla kompatybilności
wstecznej nadal znajdują się na
swoim miejscu. Sposób korzystania
z tych rejestrów zostanie przedsta-
wiony w ostatnim odcinku cyklu,
który będzie poświecony w całości
nowemu LPC214x.
co ułatwi późniejsze zmiany oraz
wpłynie na większą przejrzystość
kodu:
#define LEDDIR IO1DIR //Rejestr ki-
erunku LED
#define LEDSET IO1SET //Rejestr
ustawiający bity LED
#define LEDCLR IO1CLR //Rejestr
kasujący bity LED
#define LEDPIN IO1PIN //Rejestr
portu LED
#define KEYDIR IO0DIR //Rejestr
kierunku klawiszy
#define KEYPIN IO0PIN //Rejestr
portu klawiszy
#define LEDY (0xFF<<16) //Wszystkie
LEDY P.16..P1.24
#define LED0 (1<<16) //P1.16 – Di-
oda LED0
#define LED1 (2<<16) //P1.17
– Dioda LED1
#define LED2 (4<<16) //P1.18
– Dioda LED2
#define S1 0x10 //P0.4 – Klawisz S1
#define S2 0x20 //P0.5 – Klawisz S2
#define S3 0x40 //P0.6 – Klawisz S3
Działanie programu rozpoczyna
się w funkcji main od ustawie-
nia rejestrów kierunku. Bity portu
P1.16…P1.18 odpowiedzialne za
sterowanie diodami LED ustawia-
ne są w kierunku wyjściowym, na-
tomiast linie portu do których są
podłączone klawisze S1, S2, S3
ustawiane są jako wejściowe:
//Kierunek dla ledow wyjście
LEDDIR |= LEDY;
//Kierunek dla klawiszy wejście
KEYDIR &= ~(S1|S2|S3);
Operacja ustawienia linii portu
P0.4...P0.6 nie są niezbędne, po-
nieważ po wyzerowaniu mikrokon-
troler ustawia wszystkie linie I/O
w kierunku wejściowym. Tak samo
w programie tym nie ustawiamy
w ogóle bitów rejestru PINSELx,
ponieważ domyślnie po zerowaniu
do linii wejściowych podłączone
są porty GPIO. Po tej czynności
program wchodzi do pętli nieskoń-
czonej while (1){…}, w której
sprawdzane jest wciśnięcie klawi-
sza S1 (stan niski) i w przypadku
jego naciśnięcia włączana jest dio-
da LED0 oraz wyłączana LED1:
if (!(KEYPIN & S1))
{
//!Jezeli wcisniety S1 to zalacz
Trochę praktyki – przykładowy
program
Mając już odpowiednią dawkę
wiedzy teoretycznej zajmiemy się
teraz napisaniem prostego progra-
mu mającego na celu zapozna-
nie się z rejestrami portów GPIO.
Oczywiście i w tym przypadku bę-
dziemy korzystać z zestawu uru-
chomieniowego ZL6ARM. Działanie
programu będzie następujące: po
wciśnięciu przycisku S1 zostanie
zapalona dioda LED0 oraz wyłą-
czona dioda LED1; po wciśnięciu
przycisku S2 dioda LED0 zgaśnie,
natomiast dioda LED1 zostanie za-
palona. Po wciśnięciu klawisza S3
stan diody LED3 zostanie zmienio-
ny na przeciwny. W programie tym
wykorzystamy omawiane we wcze-
śniejszych odcinkach pliki starto-
we, dlatego nie będziemy się już
nimi tutaj zajmować. Przykładowy
program można także ściągnąć ze
strony EP ( ep5a.zip ) i zaimporto-
wać do środowiska Eclipse . Pisanie
programu rozpoczynamy od zdefi-
niowania stałych odpowiadających
bitom poszczególnych diod, przy-
cisków oraz portów do których są
podłączone diody LED i klawisze,
Elektronika Praktyczna 11/2006
99
197730146.007.png 197730146.008.png
K U R S
Program drugi – wyświetlacz
LCD
Kolejnym programem jaki na-
piszemy w ramach ćwiczeń z por-
tami GPIO będą procedury obsłu-
gi znakowego wyświetlacza LCD
(HD44180). Procedury te będziemy
intensywnie wykorzystywać w dal-
szej części kursu. W różnych cza-
sopismach o tematyce elektronicznej
obsługa znakowego wyświetlacza
LCD była poruszana wielokrotnie.
Dlatego aby nie powielać tych sa-
mych schematów tym razem biblio-
teka ta zostanie napisana w nieco
odmienny sposób za pomocą pro-
gramowania obiektowego C++. Nie
będziemy tutaj szczegółowo oma-
wiać aspektów działania wyświe-
tlacza LCD, a zainteresowanych od-
syłam do EdW 11/97. W zestawie
ZL6ARM linie D0.D7 LCD podłą-
czone są do portu P1.16…P1.23.
Linia E podłączona jest do portu
P0.30 natomiast RS do portu P0.31.
W zestawie niestety nie przewidzia-
no możliwości sterowania linią R/
W przez co niemożliwe jest odczy-
tywanie stanu wyświetlacza, dlatego
po wysłaniu każdego znaku i rozka-
zu musimy odczekać pewien okres
czasu tak aby wybrana operacja
została wykonana. Prawie wszystkie
komendy wykonywane są w czasie
do 120 ms poza rozkazem czysz-
czenia wyświetlacza który może za-
jąć maksymalnie 4,8 ms. Za obsłu-
gę LCD odpowiedzialna jest klasa
CLcdDisp , której deklaracja znajduje
się w pliku CLcdDisp.h natomiast
definicja została umieszczona wpli-
ku CLcdDisp.c . Metody (funkcje)
i obiekty (zmienne) zadeklarowane
z modyfikatorem private mogą być
używane tylko wewnątrz klasy, co
zapewnia ukrycie ich przed użyt-
kownikiem końcowym. W sekcji tej
zapisano stałe związane z wyświe-
tlaczem LCD takie jak przypisanie
bitów odpowiedzialnych za linię E
i RW wyświetlacza oraz stałe zwią-
zane z komendami kontrolera LCD.
Klasy, Obiekty, oraz programowanie zorientowane
obiektowo w skrócie
Pisząc w języku C program, który dotyczy jakiś re-
alnych obiektów na przykład regulatora temperatury,
tablicy świetlnej, sterownika akwariowego musimy
wszelkie zależności i wielkości zamienić na zestaw
luźnych liczb i funkcji operujących na danych. Na
przykład w zmiennej float temp trzymamy tem-
peraturę bieżącą, przy czym tylko my wiemy że
jest to temperatura zadana. Równie dobrze liczbę
reprezentującą temperaturę moglibyśmy podstawić
do jakiejś innej funkcji realizującą całkiem inne
zadanie a kompilator nawet nie zaprotestowałby
tylko wyliczyłby jakieś bzdury. Natomiast otaczający
nas świat nie składa się z luźnych liczb i funkcji
tylko z obiektów. Na przykład wspomniany regulator
temperatury jest obiektem, który z kolei zawiera
w sobie obiekty takie jak wyświetlacz LCD, czujnik
temperatury, czy klawiaturę. Właśnie język C++
pozwała nam działać w sposób obiektowy umożli-
wiając budowanie modeli rzeczywistych obiektów,
a nie luźnego zestawu liczb oraz funkcji. Każdy
model posiada zestaw danych (pól) oraz zachowań
(metod). Na przykład wyświetlacz LCD posiada
dane w postaci tekstu do wyświetlenia oraz zacho-
wania (metody) takie jak wyczyszczenie wyświetla-
cza, wypisanie liczby, czy przesunięcie kursora na
wskazaną pozycję. Zbierając te wszystkie dane i za-
chowania w jedną całość budujemy konkretny typ
(klasę) wyświetlacza LCD. Wymyśliliśmy więc opis
umożliwiający zbudowanie konkretnego egzemplarza
(obiektu) wyświetlacza LCD, nie jest to jeszcze ża-
den konkretny wyświetlacz. Definicja klasy wjęzyku
C++ ma następującą postać:
class budowany_typ
public : //
Specyfikator dostepu
budowany_typ();
//Konstruktor klasy
~budowany_typ();
//Destruktor klasy
metoda1();
finicji klasy zamknęliśmy wszelkie pola i meto-
dy klasy co nazywamy enkapsulacją danych.
W deklaracji klasy znajdują się także specyfika-
tory dostępu public, protected, private . Etykieta
private oznacza że pola i metody znajdujące
się pod nią dostępne są tylko z wnętrza klasy.
Etykieta protected oznacza że pola i metody
znajdujące się pod nią są dostępne dla klas
dziedziczonych od tej klasy. (O dziedziczeniu bę-
dziemy jeszcze mówić przy innej okazji) . Nato-
miast etykieta public oznacza że pola i metody
dostępne są wewnątrz jak i na zewnątrz klasy.
Zastosowanie specyfikatorów dostępu pozwala
ukryć przed użytkownikiem końcowym wszelkie
mechanizmy wewnętrzne klasy. Po prostu użyt-
kownik korzystający np. z klasy wyświetlacza
LCD nie powinien mieć dostępu do metody
przesyłającej na magistralę bajt danych, metoda
ta powinna być wywoływana tylko przez inne
metody z wnętrza klasy. W sekcji public widzimy
metodę której nazwa jest identyczna jak nazwa
klasy jest to tak zwany konstruktor klasy, który
jest specjalną metodą wywoływaną w momencie
tworzenia obiektu danej klasy. Umożliwia nam
to wykonanie pewnych czynności zanim obiekt
danej klasy powstanie. Np. tworząc obiekt klasy
wyświetlacz LCD w konstruktorze będziemy ini-
cjalizować wyświetlacz tak aby był on w stanie
wyświetlać znaki. Tworząc na przykład klasę
pojemnika na liczby w konstruktorze tej klasy
alokować będziemy pamięć do przechowywa-
nia tych liczb. W konstruktorze nie ma żadnej
magii jest to po prostu zwykła funkcja której
osobliwością jest to że jest ona wywoływana
w momencie tworzenia obiektu danej klasy. Ana-
logiczną funkcją do konstruktora, wywoływaną
w momencie niszczenie obiektu danej klasy jest
destruktor klasy wywoływany w momencie gdy
obiekt danej klasy przestaje istnieć. Destruktor
deklarujemy poprzedzając metodę o takiej samej
nazwie jak klasa znakiem ~ . Na przykład we
wspomnianym wcześniej pojemniku na liczby
destruktor będzie zawierał funkcje dealokacji
pamięci którą wcześniej przydzieliliśmy w kon-
struktorze. Definicję klasy najczęściej tworzymy
w plikach nagłówkowych *.h. Natomiast deklara-
cję poszczególnych metod możemy zawrzeć we
wnętrzu definicji ciała samej klasy np.
class mojaklasa
public : //
Specyfikator dostepu
int metoda1(int a)
{
return a*a + mx;
}
int mx;
};
Wówczas metoda ta zostanie potraktowana jako
metoda inline i zostanie rozwinięta w miejscu
wywołania. Metody zawierające więcej niż kilka
linijek kodu powinny być zadeklarowane w pli-
kach *.c. w sposób następujący.
Zwracany_typ Nazwa_klasy::NazwaMetody(ar-
gumenty)
{
//Ciało metody
}
Widzimy że nazwę metody poprzedza nazwa
klasy zakończona specyfikatorem dostępu :: co
określa że dana metoda należy do danej klasy.
Na przykład we wspomnianym wcześniej przy-
kładzie klasy mojaklasa zadeklarowanie metody
klasy w pliku *.c wygląda następująco:
int mojaklasa::metoda1(int a)
{
return a*a + mx;
}
To o czym wcześniej mówiliśmy było tylko de-
finicją klasy określającą sposób wjaki ona była
zbudowana. Sama definicja klasy nie deklaru-
je żadnych obiektów (egzemplarzy) tej klasy.
Utworzenie konkretnych obiektów danej klasy
odbywa się w taki sam sposób jak tworzenie
obiektów typów wbudowanych np. int a,b,c,d;
spowoduje utworzenie 4 obiektów typu int o na-
zwach a b c d. Tak samo napisanie mojaklasa
a,b,c,d ; spowoduje utworzenie czterech obiek-
tów o nazwach a b c d klasy mojaklasa . Należy
sobie uzmysłowić że utworzenie 4 obiektów
klasy mojaklasa spowoduje utworzenie 4 od-
dzielnych kompletów danych dla poszczególnych
obiektów danej klasy. Natomiast metody operu-
jące na tych składnikach definiowane są tylko
jednokrotnie. Każda metoda do pól danej klasy
odwołuje się za pomocą wskaźnika this , który
pokazuje na konkretny egzemplarz danej klasy.
Na przykład we wspomnianej wcześniej meto-
dzie metoda1() odwołanie do pola mx będącego
składnikiem danej klasy odbywa się za pomo-
cą wskaźnika this następująco: return a*a +
this–>mx. Wskaźnik ten jest tutaj wywoływany
niejawnie przez kompilator, ale my czasami bę-
dziemy z niego świadomie korzystać. Odwołanie
do wybranego pola danej klasy odbywa się
za pomocą znaku kropki. Na przykład wpisanie
b=a.mx spowoduje przepisanie pola mx obiektu
a do zmiennej b. Natomiast wywołanie metod
na rzecz konkretnego obiektu odbywa się po-
przez wpisanie po kropce danej metody. Na
przykład c.metoda1(4 ) spowoduje wywołanie
metoda1() działającej na danych będących skła-
dowymi obiektu b.
metoda2();
protected : //Spe-
cyfikator dostepu
metoda3();
private : //Spe-
cyfikator dostepu
int pole1;
float pole2;
};
Zdefiniowane własnej klasy nie jest trudne naj-
pierw występuje tutaj słowo kluczowe class na-
stępnie występuje nazwa klasy po czym klamra
a w niej ciało klasy. W ciele klasy deklarujemy
wszelkie metody (zachowania) i pola (dane) kla-
sy. Jest to bardzo ważny aspekt bowiem w de-
100
Elektronika Praktyczna 11/2006
197730146.009.png
K U R S
//Funkcja opozniajaca
void Delay( unsigned int del);
//Wysyla do portu
void PortSend( unsigned char
data, bool cmd= false );
//Pin E P0.30
static const unsigned int E =
0x40000000;
//Pin RW P0.31
static const unsigned int RS =
0x80000000;
//Maska danych
static const unsigned int DMASK =
0x00FF0000;
//Domyslne sprzetowe
static const unsigned int DELAY_HW
= 15;
//Opoznienie komend
static const unsigned int DELAY_CMD
= 3000;
//Opoznienie dla CLS
static const unsigned int DELAY_CLS
= 30000;
//Komendy wyswietlacza
enum {CLS_CMD=0x01,HOME_
CMD=0x02,MODE_CMD=0x04,ON_CMD=0x08,
SHIFT_CMD=0x10,FUNC_CMD=0x20,CGA_
CMD=0x40,DDA_CMD=0x80};
//Komenda MODE
enum {MODE_R=0x02,MODE_L=0,MODE_
MOVE=0x01};
//Komenda SHIFT
enum {SHIFT_DISP=0x08,SHIFT_
R=0x04,SHIFT_L=0};
//Komenda FUNC
enum {FUNC_8b=0x10,FUNC_4b=0,FUNC_
2L=0x08,
FUNC1L=0,FUNC_5x10=0x4,FUNCx7=0};
};
void CLcdDisp::Delay( unsigned int
del)
{
asm volatile
(
“dloop%=:”
“subs %[del],%[del],#1\t\n”
“bne dloop%=\t\n”
: :[del]”r”(del)
);
}
if (cmd) LCDCCLR = RS;
else LCDCSET = RS;
Metoda PortSend służy do wysy-
łania pojedynczego bajtu danych do
wyświetlacza LCD została ona zade-
klarowana następująco:
void PortSend( unsigned char data, bo-
ol cmd= false );
Jako parametr data przekazujemy
instrukcję lub daną którą chcemy
wysłać do wyświetlacza LCD. Gdy
parametr cmd przyjmie wartość fal-
se oznacza to, że liczba przekaza-
na jako data zinterpretowana będzie
jako znak do wyświetlenia, w prze-
ciwnym przypadku przesłana dana
stanowić będzie rozkaz. W języku
C++ możemy deklarować metody
i funkcję z parametrami domyślny-
mi. W przypadku gdy wywołamy
funkcję bez drugiego argumentu pa-
rametr cmd przyjmie wartość false,
natomiast gdy drugi parametr bę-
dzie określony podczas wywołania
argument domyślny będzie ignoro-
wany. Mechanizm ten został stwo-
rzony w celu zastąpienia funkcji ze
zmienną listą argumentów (…) zna-
ną z języka C, pozwala on zapew-
nić większą kontrolę nad przekazy-
wanymi argumentami. Działanie tej
metody jest następujące: Najpierw
sygnał E ustawiany jest w stan 0
w efekcie czego wyświetlacz ignoru-
je wszystkie stany pojawiające się
na liniach danych wyświetlacza.
Linie D0..D7 wyświetlacza LCD są
zerowane poprzez ustawienie bitów
16.23 w rejestrze IO1CLR. Do portu
IO1SET przesyłana jest zawartość
zmiennej data przesuniętej o 16 bi-
tów w lewo. W wyniku tych dwóch
operacji linie P1.16..P1.23 przyj-
mują wartość zgodną z zawartością
zmiennej data bez zmiany pozosta-
łych bitów portu.
//E=0
LCDCCLR = E;
//Data = 0;
LCDDCLR = DMASK;
//Wyslij dane
LCDDSET = (( unsigned int )data) <<
16;
Następnie na linii E generowany
jest dodatni impuls w wyniku które-
go następuje zapisanie danych lub
instrukcji do wyświetlacza LCD.
//Ustaw Enable
LCDCSET = E;
Delay(DELAY_HW);
//Skasuje enable
LCDCCLR = E;
Wszystkie metody zadeklarowane
jako public dostępne są dla użyt-
kownika i stanowią zewnętrzny in-
terfejs klasy. Klasa CLcdDisp zawie-
ra następujące składowe publiczne:
public :
CLcdDisp();
~CLcdDisp();
void Write( const char *str);
void Write( char zn);
void Write( unsigned int licz);
//Wyczysc wyswietlacz
void Clear( void );
//Zalacz wylacz kursor
void SetCursor( unsigned char cmd);
void GotoXY( unsigned char
x, unsigned char y);
template < class T> CLcdDisp& opera-
tor <<(T obj)
{
Write(obj);
return * this ;
}
CLcdDisp& operator <<(pos obj)
{
GotoXY(obj.mx,obj.my);
return * this ;
}
CLcdDisp jest domyślnym kon-
struktorem klasy i jest on wywo-
ływany podczas tworzenia nowego
obiektu danej klasy. W konstrukto-
rze napisano procedurę inicjaliza-
cji wyświetlacza LCD. Inicjalizacja
rozpoczyna się od ustawienia linii
RS,E i D0..D7 oraz odczekania kil-
kudziesięciu milisekund na ustabili-
zowanie napięcia zasilającego:
//Konstruktor klasy obslugi wyswie-
tlacza LCD
CLcdDisp::CLcdDisp()
{
//Linie E i RS jako wyjsciowe
LCDCDIR |= E|RS;
LCDCCLR = E|RS;
//Linia danych jako wyjsciowa
LCDDDIR |= DMASK;
Delay(100000);
Następnie trzykrotnie wysyłana
jest komenda ustawiająca wyświe-
tlacz w tryb 8 bitowy
PortSend(FUNC_CMD|FUNC_8b, true );
Delay(DELAY_CLS);
PortSend(FUNC_CMD|FUNC_8b, true );
Delay(DELAY_CMD);
PortSend(FUNC_CMD|FUNC_8b, true );
Delay(DELAY_CMD);
po czym następuje ustawie-
nie wyświetlacza tak aby praco-
wał w rozdzielczości 5x7 załączenie
wyświetlacza, wyczyszczenie oraz
ustawienie kurosa w pozycji po-
czątkowej. Kolejnymi metodami pu-
blicznymi są metody Write służące
do wypisania na wyświetlaczu po-
jedynczego znaku, łańcucha teksto-
wego, oraz liczby stałoprzecinkowej.
Umieszczono tu także dwie me-
tody: Delay , odpowiedzialną za ge-
nerowanie opóźnień, oraz PortSend
wysyłającą bajt danych do wyświe-
tlacza Lcd . Pętla opóźniająca zosta-
ła napisana w asemblerze, aby było
możliwe dokładne określenie czasu
jej wykonania. Jako argument meto-
dy podajemy liczbę która następnie
jest ładowana do któregoś z reje-
strów ogólnego przeznaczenia w któ-
rym następuje cykliczne odejmowa-
nie liczby jeden, aż do momentu
gdy rejestr ten osiągnie wartość 0.
Przeładowanie nazw funkcji i metod
Programując w języku C przyzwyczailiśmy się
że w programie może być tylko jedna funkcja
o takiej samej nazwie. W języku C++ nato-
miast może istnieć więcej niż jednak funkcja
lub metoda w obrębie klasy posiadająca taką
samą nazwę pod warunkiem że posiada ona
inną listę argumentów. Inaczej rzecz mówiąc
kompilator C++ rozpoznaje funkcje lub me-
todę nie tylko po samej nazwie ale też po
liście argumentów. Na przykład w C gdybyśmy
chcieli napisać funkcję do wyświetlania po-
szczególnych typów danych na wyświetlaczu
LCD musielibyśmy dla każdego typu zdefinio-
wać funkcję o innej nazwie: WriteInt(int w);
WriteStr(char *s); WriteChar(char c); W mo-
mencie gdy chcieliśmy wypisać konkretny typ
danej na przykład int musieliśmy wywołać
funkcję WriteInt(). W języku C++ możemy na-
tomiast zdefiniować trzy funkcje otakiej samej
nazwie Write z inną listą argumentów np. tak:
Write(int w); Write(char *s); Write(char c);
W momencie wywołania funkcji nie musimy
się zastanawiać którą wersję funkcji chce-
my wywołać po prostu piszemy Write(„Text”)
a kompilator sam na podstawie listy argumen-
tów ustali że trzeba wywołać funkcję Write-
(char *s);
Po przesłaniu danych na linię
D0..D7 następuje ustawienie linii
RS w odpowiedni stan w zależno-
ści od tego czy dane przesłane na
magistrale zinterpretowane zostaną
jako rozkaz (stan wysoki) albo znak
do wyświetlenia (stan niski)
//Skasuj lub ustaw RS
Elektronika Praktyczna 11/2006
101
197730146.010.png
Zgłoś jeśli naruszono regulamin