AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 5.pdf
(
447 KB
)
Pobierz
ep_07_089-091_avr_gcc_cz5.indd
K U R S
AVR–GCC: kompilator C
mikrokontrolerów AVR,
część 5
W tej części kursu skupiamy się na omówieniu zakresu zmiennych, budowie
i funkcjach plików nagłówkowych, przybliżając w ten sposób kolejne tajniki
kapryśnego – jak głosi nośna opinia – kompilatora.
Zakres zmiennych
W zależności od miejsca oraz
sposobu zadeklarowania zmiennych
mogą mieć one w naszym projekcie
różny zasięg – tzn. możemy z nich
korzystać w jednym pliku źródło-
wym (module), w wielu plikach albo
tylko wewnątrz kodu funkcji. Mówi-
my w takim przypadku o zmiennych
globalnych oraz lokalnych. Podział
ten nie ma wpływu na typ zmien-
nej ale jest istotny w trakcie pisa-
nia programu, inny jest też sposób
obsługiwania zmiennych lokalnych
przez kompilator.
Do tej pory ograniczaliśmy się
do zmiennych globalnych (zasięg
globalny jest domyślny) deklarowa-
nych i używanych w pojedynczym
pliku (module) źródłowym projek-
tu. Utwórzmy teraz następny przy-
kładowy projekt zawierający kilka
modułów:
main.c
,
funkcje.c
oraz
dane.h
– zapiszmy go w subfolde-
rze
\Projects\Kurs\Przyklad–03\
jako
Test03
. Dodawanie plików do pro-
jektu jest w AvrSide bardzo proste:
wykonujemy komendę menu
Projek-
t>Dodaj pustą stronę
(dostępna tak-
że w menu kontekstowym projektu
wywoływanym skrótem
CTRL
+
.
)
i zapisujemy nową zakładkę
NoNa-
me
jako odpowiedni typ pliku (c,
s, h) z wybraną nazwą (typ pli-
ku źródłowego wybieramy z listy
– rozszerzenie będzie dodane auto-
matycznie więc nie musimy go do-
pisywać). Jednak najpierw musimy
wpisać do modułu jakiś kod (może
to byc na wstępie sam komentarz)
gdyż AvrSide blokuje zapis pliku
pustego. W pliku
main.c
wstawimy
jak zwykle szablon modułu główne-
go natomiast w pliku
dane.h
– sza-
blon “nagłówek danych projektu”
(
headdat
).
Szablon danych został przygo-
towany tak aby bez wielokrotne-
go przepisywania deklaracji moż-
na było używać w całym projekcie
wspólnych globalnych zmiennych,
funkcji oraz definicji:
// plik nagłówkowy globalnych danych
projektu
#ifndef _PROJ_DAT_H_
#define _PROJ_DAT_H_
// #include:
// #define:
// definicje typów typedef
// dane globalne
#ifdef _MAIN_MOD_
// definicje danych – tylko w module
main()
// char x;
int test = 10;
#else
// deklaracje danych jako importowanych
– w każdym innym module
// extern char x;
extern int test;
#endif
// deklaracje funkcji
// extern char Myfunc(int,char);
extern int Myfunc(char x,char y);
#endif
Wstawiamy tutaj wspólne dla
wszystkich modułów projektu pliki
nagłówkowe (np.
#include <avr/
io.h>
), definicje konfiguracji i pod-
łączeń sprzętowych (np.
#define
LED PB2
), własne definicje typów
(np.
typedef unsigned char uchar
).
Po dołączeniu naszego nagłówka do
dowolnego modułu (
#include „dane.
h”
) mamy od razu w module dostęp
do wszystkich tych ustawień.
Trochę więcej komplikacji jest
z globalnymi zmiennymi. Zwy-
kłe ich zadeklarowanie spowodu-
je wprawdzie, że będą widoczne
w projekcie i nie zostanie zgłoszony
błąd na etapie kompilacji poszcze-
gólnych modułów ale nie da sobie
z tym rady konsolidator sygnalizu-
jąc błąd wielokrotnej definicji. Mo-
żemy to od razu sprawdzić dopi-
sując
int test=10;
w obu naszych
plikach źródłowych c (
main
i
funk-
cje
): kompilacja (
CTRL+F9
) prze-
biegnie sprawnie ale projektu nie
da się zakończyć (
F9
– błąd linke-
ra –
“multiple definition of test”
).
Z pomocą przychodzi kompila-
cja warunkowa: w pliku głównym
ze zdefiniowanym makrem _MAIN_
MOD_ preprocesor wstawi peł-
ną definicję
int test=10;
natomiast
w pozostałych plikach tylko infor-
mację dla kompilatora, że zmienna
test
już gdzieś w projekcie istnieje
(
extern
) i można z niej bezpiecznie
korzystać.
Nowsze wersje
avr–gcc
pozwa-
lają na pominięcie tego sposobu
w przypadku zmiennych automa-
tycznie zerowanych (sekcja
bss
)
– taka zmienna (np.
int test;
) jest
samoczynnie bez dodatkowych za-
biegów traktowana jako pojedyncza
pomimo wielokrotnego zdefiniowa-
nia i zostaje jej przydzielony jeden
wspólny obszar w SRAM.
W przypadku funkcji można bez
błędu użyć we wszystkich modu-
łach deklaracji
extern
– w ten spo-
sób funkcja (którą dokładnie zdefi-
niujemy tylko w jednym dowolnie
wybranym module) będzie widocz-
na i możliwa do użycia w całym
projekcie. Zróbmy to zaraz definiu-
jąc w pliku
funkcje.c
funkcję zade-
klarowaną w
dane.h
jako
extern int
Myfunc (char x, char y);
(funkcja
o dwóch argumentach typu
char
,
zwracająca rezultat typu
int
) (nie
zapomnijmy oczywiście o dołącze-
niu do obu źródeł nagłowka z da-
nymi:
#include „dane.h”
):
int Myfunc(char x,char y)
{
char a,b;
a=2*x + y;
b=x + 2*y;
return (a+b);
}
Teraz w pliku głównym
main.c
możemy już bez problemu posłużyć
się tą funkcją:
test = Myfunc(10,5);
W funkcji celowo wprowadzi-
łem zmienne lokalne
a
,
b
(chociaż
nie są dla wykonania obliczeń ko-
nieczne) aby przedstawić sposób
ich obsługi przez kompilator. Takie
zmienne – definiowane wewnątrz
ciała funkcji (zwane też zmien-
nymi automatycznymi) są dostęp-
ne i możliwe do wykorzystywania
tylko i wyłącznie w obrębie tego
ciała funcji. Próba odwołania do
nich spoza funkcji powoduje błąd.
Elektronika Praktyczna 7/2005
89
K U R S
Rys. 14. Podgląd zmiennych lokalnych na stosie
odrębniania zmien-
nych lokalnych. Jest
to bardzo pozytyw-
ny rezultat jednak
dla potrzeb naszego
testu wyłączmy na
chwilę optymalizację
(odpowiada to opcji
–O0
kompilatora).
Teraz widzimy (pa-
miętajmy o użyciu
komendy
Build
a nie
Make
po zmianie
opcji), że zmienne
a
oraz
b
są z chwilą
wejścia programu do funcji tradycyj-
nie tworzone tymczasowo na stosie
(w moim przykładzie pod adresami
0x045A
i
0x045B
) i niszczone po za-
kończeniu funkcji. Jednak od razu
zauważymy też znaczący przyrost
objętości kodu. Możemy przy okazji
porównać generowane kody assem-
blera i obejrzeć ile pożytecznej pracy
wykonuje optymalizator. Nic dziw-
nego, że często symulacja w AvrStu-
dio „nie zgadza się” z naszym zapi-
sem źródłowym: nie wykorzystywane
zmienne mogą byc usunięte, niektóre
linie kodu są eliminowane itd. In-
gerencja optymalizatora może być
na tyle duża, że ten sam program
ze zmienionym poziomem optyma-
lizacji czasem zaczyna zachowywać
się nieco inaczej. Dlatego chwilowe
przełączanie poziomów optymalizacji
tylko po to aby lepiej obejrzeć wy-
nik w symulatorze (tak jak to przed
chwilą zrobiliśmy w celach edukacyj-
nych) jest generalnie kiepskim po-
mysłem (nie ma niestety możliwości
selektywnego ustawiania różnych po-
ziomów optymalizacji dla poszczegól-
nych fragmentów kodu).
W praktyce zamiast rezygno-
wać z zalet optymalizacji lepiej
jest kontrolować istotne dla nas
zmienne przy pomocy używanego
już słowa kluczowego
volatile
. In-
formuje ono kompilator, żeby tak
opisanej zmiennej nie poddawać
jakimkolwiek działaniom optymali-
zującym i upraszczającym i wyko-
nywać na niej wszystkie operacje
przewidziane w kodzie (chociaż
z punktu widzenia optymalizatora
mogą one wyglądać na zbędne).
Główne zastosowanie tego mecha-
nizmu to zabezpieczanie zmien-
nych używanych w przerwaniach
(to wynika bezpośrednio z nazwy:
volatile
– czyli ulotny, nietrwały
– oznacza, że wartość zmiennej
może być w każdej chwili uaktu-
alniona przez czynnik zewnętrz-
ny – przerwanie – i nie można
w związku z tym pominąć żadnej
związanej z nią operacji w głównej
pętli programu), jednak często jest
pomocny także w różnych innych
sytuacjach. Sprawdźmy zaraz, że
zmiana deklaracji na
volatile char
a,b;
(przy ponownym włączeniu
maksymalnej optymalizacji) daje
ten sam efekt: zmienne wędrują
z obszaru rejestrów na stos. Jest to
pokazane na
rys. 14
.
Zobaczmy jeszcze, że takie
same nazwy zmiennych mogą być
z powodzeniem użyte w innej funk-
cji – w tym celu definiujemy sobie
dodatkowo:
int Myfunc1(char x,char y)
{
volatile char a,b;
Zmienne te istnieją tylko w czasie
wykonywania funkcji – po wywoła-
niu funcji, w prologu, są tworzone
albo na stosie albo (jeśli optyma-
lizator stwierdzi, że ma chwilowo
do dyspozycji odpowiednią liczbę
rejestrów) w obszarze rejestrów ro-
boczych. Po zakończeniu działania
funkcji po prostu przestają istnieć
– pamięć dla nich przydzielona
zostaje przeznaczona na inne bie-
żące cele.
Zobaczmy, jak przedstawi nam to
w działaniu AvrStudio. Po omawia-
nym już wstępnym skonfigurowaniu
sesji AvrStudio wstawmy do okienka
podglądu zmiennych wszystkie użyte
zmienne:
test
,
a
,
b
.
Test
po zerowaniu przyjmuje war-
tość 10, natomiast
a
i
b
są określone
jako
„not in scope”
(poza zakrese-
m),czyli wszystko zgodnie z oczeki-
waniami. Przejdźmy teraz krokami
(
F11
) do wnętrza funkcji, spotka nas
niestety niespodzianka: zmienne
a
i
b
nadal nie są obsługiwane (
„location
not valid”
– AvrStudio ma kłopot
z ich umiejscowieniem w pamięci).
Przyczyną jest wspomniane powyżej
skuteczne działanie optymalizatora.
W kodzie asemblera znajdujemy:
int Myfunc(char x,char y)
{
5c: 28 2f movr18, r24
5e: 86 2f movr24, r22
char a,b;
a=x + y;
b=x – y;
return (a*b);
}
i oglądamy jak traktowane są
zmienne
a
oraz
b
przy wywoła-
niach kolejno
Myfunc
oraz
Myfun-
c1
(dobrze jest w tym celu dodat-
kowo włączyć w AvrStudio okienko
podglądu pamięci danych jak na
rys. 14). Przekonamy się, że war-
tości chwilowe
a
i
b
zmieniają się
w zależności od tego, która funkcja
aktualnie z nich korzysta.
Może nas w pierwszej chwili
zdziwić fakt, że w momencie wej-
ścia do funkcji
Myfunc1 a
oraz
b
zachowały wartości przypisane we-
wnątrz poprzedniej funcji (
Myfunc
)
– przecież miały stracić ważność.
Przyczyną jest prostota naszego
przykładu. Kompilator nie niszczy
zmiennych lokalnych (np. przez
wyzerowanie) ale po prostu prze-
staje się nimi „przejmować”. Gdy-
by pomiędzy wywołaniami
Myfunc
i
Myfunc1
pojawiły się jakieś ope-
racje wykorzystujące stos –
a
i
b
zostałyby nadpisane. Ponieważ jed-
nak nic takiego nie zachodzi war-
tości wstawione pod adresy 0x45a
i 0x45b pozostały nie zmienione.
Możliwość użycia takich samych
nazw zmiennych lub funkcji jest
też czasem korzystna w odniesie-
niu do poszczególnych modułów
kodu źródłowego. W C uzyskuje-
my to poprzez ograniczenie zakre-
su ważności zmiennej (funkcji) do
pojedynczego modułu – sprawia to
słowo kluczowe
static
.
Zadeklarujmy sobie takie lokalne
symbole: w module
main.c
dopisze-
my na przykład:
// deklaracja zmiennej lokalnej dla
a=2*x + y;
60: 92 2f movr25, r18
62: 99 0f addr25, r25
64: 96 0f addr25, r22
b=x + 2*y;
66: 88 0f addr24, r24
68: 82 0f addr24, r18
return (a+b);
6a: 29 2f movr18, r25
6c: 33 27 eorr19, r19
6e: 27 fd sbrc r18, 7
70: 30 95 comr19
72: 99 27 eorr25, r25
74: 87 fd sbrc r24, 7
76: 90 95 comr25
78: 82 0f addr24, r18
7a: 93 1f adcr25, r19
7c: 08 95 ret
}
Optymalizator wykonał wszystkie
potrzebne działania w obszarze reje-
strów w sposób na tyle zwięzły, że
nie zaszła potrzeba wyraźnego wy-
90
Elektronika Praktyczna 7/2005
K U R S
Rys. 15. Tablica symboli pokazuje
tylko symbole globalne
modułu main
static char k=1;
// funkcje:
static char LocFunc(char Value);
// deklaracja funkcji lokalnej dla mo-
dułu main
// oraz definicja tej funkcji
char LocFunc(char Value)
{
return Value + 2;
}
a w module
funkcje.c
:
// deklaracja zmiennej lokalnej dla mo-
dułu funkcje
static char k=2;
static char LocFunc(char Value);
// deklaracja funkcji lokalnej dla mo-
dułu funkcje
// oraz definicja tej funkcji
char LocFunc(char Value)
{
return Value + 10 +k;
}
Rys. 17. Zmienne lokalne funkcji w wersji inicjalizowanej
Przy kompilacji stwierdzamy, że
w tym przypadku nie występuje
błąd wielokrotnej definicji. Wiąże się
z tym również ukrycie powyższych
lokalnych nazw w oknie podglądu
symboli konsolidatora (
rys. 15
), wy-
szczególnione są tylko symbole glo-
balne (okno podglądu symboli wy-
wołujemy klawiszem F8).
Oczywiście pomimo tego ukry-
cia zmienne
k
są fizycznie uloko-
wane w pamięci SRAM (pod adre-
sami 0x60 oraz 0x63 na
rys. 16
),
znajdziemy je też przeglądając plik
symboli
Test03.smb
. Użycie poszcze-
gólnych adresów zależy od modułu,
z którego się do naszej zmiennej
k
odwołujemy (kod modułu
main.c
korzysta z adresu 0x63, natomiast
moduł
funkcje.c
używa 0x60). Jeśli
zechcemy to prześledzić w Avr-
Studio zauważymy, że po wstawie-
niu do okienka podglądu zmiennej
k
będzie ona opisana wartością i
adresem zależnym od modułu, do
którego wchodzimy pracą krokową.
Podobnie jest z funkcjami – każ-
dy moduł odwołuje się do swojej
własnej lokalnej definicji
LocFunc
. Ję-
zyk C daje nam jeszcze jedną możli-
wość łączącą właściwości powyższych
przypadków. Jeśli mianowicie użyje-
my kwalifikatora
static
do zmiennej
lokalnej deklarowanej wewnątrz cia-
ła funkcji (automatycznej) uzyskamy
następujacy efekt: zakres używania
zmiennej pozostanie nadal ograni-
czony do ciała funkcji ale zarazem
zmiennej zostaje przydzielona na
stałe przestrzeń w obszarze danych
SRAM. Po wyjściu z funkcji zmienna
taka nie jest zatem - jak poprzednio
- narażona na zniszczenie (nadpisa-
nie) ale przechowuje ostatnio przy-
pisaną wartość – aż do ponownego
wywołania używającej ją funkcji. Wy-
próbujmy to zaraz przepisując nieco
nasze poprzednie definicje:
int Myfunc(char x,char y)
{
static char a,b;
a=2*x + y;
b=x + 2*y;
return(a+b);
}
int Myfunc1(char x,char y)
{
static char a,b;
a=x + y;
b=x - y;
return(a*b);
}
tor
const
informuje kompilator, że jest
to szablon tylko do odczytu):
int Myfunc(char x,char y)
{
const char Cyfry[] =”0123456789”;
static char a,b;
a=2*x + y;
b=x + 2*y;
return(a+b+ Cyfry[1]);
}
Wydawałoby się, że w trakcie
tworzenia ramki stosu dla funkcji
podczas jej wywołania powinna być
powtórzona procedura taka sama jak
dla zmiennych inicjalizowanych
data
(przepisanie wartości z końca obsza-
ru kodu bezpośrednio na stos). Nie-
stety w tym przypadku avr-gcc nie
postępuje optymalnie. Sprawdźmy to
w AvrStudio –
rys. 17.
Okazuje się, że
string
Cyfry[]
jest
już w trakcie ogólnej inicjalizacji
również przepisywany na stałe do
obszaru
data
SRAM (podobnie jak
wszystkie “zwykłe” zmienne inicja-
lizowane) gdzie spokojnie czeka na
wywołanie funkcji. Wtedy dopiero
spod adresu w sekcji
data
jest prze-
pisywany do ramki stosu.
Zamiast spodziewanych korzy-
ści mamy więc w efekcie wydłuże-
nie kodu wykonywalnego i żadnej
oszczędności RAM w porównaniu z
przypadkiem użycia tego stringa jako
zwykłej zmiennej globalnej (ewentual-
nie lokalnej ale dla całego modułu).
Widać więc, że takiej konstrukcji na-
leży raczej unikać (chyba, że czytel-
ność kodu postawimy na absolutnie
priorytetowym miejscu).
Jerzy Szczesiul, EP
jerzy.szczesiul@ep.com.pl
Prowadząc krokowy debugging
jak na rys. 14 zobaczymy teraz jak
zmieniła się lokalizacja zmiennych
a
i
b
: mają one przydzielony obszar w
sekcji
bss
. Opis
a
oraz
b
w okien-
ku podglądu zmienia się w trakcie
wchodzenia i opuszczania kolejnych
funkcji. Zauważmy, że biorąc pod
uwagę przydział pamięci zmienne te
nie różnią się obecnie od zwykłych
lokalnych czy nawet globalnych. Na-
tomiast znacznie poprawia się czytel-
ność kodu oraz jest redukowana moż-
liwość błędów wynikających z powtó-
rzenia nazw.
Zobaczmy jeszcze jak zachowają
się zmienne automatyczne inicjalizo-
wane. Jako przykład niech posłuży
łańcuch (string) z cyframi (kwalifika-
Rys. 16. Przydział pamięci dla zmien-
nych lokalnych
UWAGA!
Środowisko IDE dla AVR-GCC opracowane
przez autora artykułu można pobrać ze
strony http://avrside.ep.com.pl.
Elektronika Praktyczna 7/2005
91
Plik z chomika:
roman61
Inne pliki z tego folderu:
AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 13.pdf
(305 KB)
AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 11.pdf
(356 KB)
AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 8.pdf
(617 KB)
AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 2.pdf
(295 KB)
AVR-GCC kompilator C dla mikrokontroler├│w AVR, cz─Ö┼Ť─ç 3.pdf
(372 KB)
Inne foldery tego chomika:
• Katalog półprzewodników VRT DVD2009 +crack
• Katalogi - Układy scalone
• Katalogi - Układy scalone(1)
• Katalogi - Wielka baza układów scalonych
• Katalogi - Wielka baza układów scalonych(1)
Zgłoś jeśli
naruszono regulamin