37
Programowanie
Elektronika dla Wszystkich
W poprzedniej czêci zapoznalimy siê
z funkcj¹ printf. Pokaza³em ideê dzia³añ na
strumieniach i korzystalimy ze strumieni
domylnych. Dzi kontynuujemy poznawanie
mo¿liwoci biblioteki stdio.
Niestandardowe
wejcie / wyjcie
Do tej pory korzystalimy ze standardowego
wejciawyjcia. W przypadku
WinAVR wykorzystanie strumie-
ni innych ni¿ standardowe nie jest
du¿o trudniejsze, a mo¿e ju¿ dzi
daæ nam spore korzyci.
Zajrzyj do ramki ABC... C
Powi¹zanie strumienia z pli-
kiem. Zawarte tam informacje
oraz zdobyta do tej pory wiedza
powinny byæ wystarczaj¹ce do
zrozumienia niewielkich zmian
wprowadzonych do programów
z zesz³ego miesi¹ca. Oba progra-
my przerobione zosta³y zgodnie
z listingami 59 oraz 60. Jeli
porównasz je z programami, które
uzyskalimy w zesz³ym miesi¹cu, stwierdzisz
zapewne, ¿e ró¿nica jest niewielka i ³atwa do
zrozumienia. Tym sposobem zaczêlimy
pos³ugiwaæ siê strumieniami innymi ni¿
domylne.
G³êbiej w stdio
Obieca³em Ci, ¿e zaoszczêdzimy na pamiêci.
Jeli skompilowa³e nowe wersje programów,
przyjrzyj siê wynikowi kompilacji. U mnie
pierwszy zaj¹³ co prawda kilka bajtów mniej,
drugi z kolei zaj¹³ kilka wiêcej. Stanowczo
nie to mia³em na myli, pisz¹c o
oszczêdzaniu pamiêci. Gdzie tkwi
przyczyna tego, ¿e pozornie pros-
ty program z listingu 59 wci¹¿
zajmuje ponad 1kB pamiêci?
Sposobem, aby siê o tym przeko-
naæ jest kolejne usuwanie wywo³ywanych
funkcji. Umieszczenie znaku komentarza
przed funkcj¹ fputs powoduje zaoszczêdzenie
jedynie trochê ponad 100B. Dopiero zazna-
czenie komentarzem funkcji fdevopen zdra-
dza nam sekret. Wywo³anie tylko funkcji
pisz¹cej na wyjcie spowodowa³o spadek
zajêcia pamiêci do 340B! Okazuje siê wiêc,
¿e du¿y udzia³ w zajêtoci pamiêci programu
ma pozornie prosta funkcja tworz¹ca powi¹-
zanie. Problem przybli¿y trochê znajduj¹ca
siê na tej stronie ramka Jak to robi GCC.
Uwaga #include i nazwy plików
Podczas rozmowy jeden z twórców biblioteki
z której dzi korzystamy, zaleci³ mi stosowanie
znaków slash (/) zamiast backslash (\) w nazwach
plików. Podobno zapis taki jest kompatybilny
z wiêksz¹ iloci¹ systemów. Nasz kompilator
prze³knie obie konwencje. Od dzisiejszego od-
cinka bêdê konsekwentnie stosowa³ nowy zapis.
PP
PP
rr
rr
oo
oo
gg
gg
rr
rr
aa
aa
m
m
m
m
oo
oo
w
w
w
w
aa
aa
nn
nn
ii
ii
ee
ee
pp
pp
rr
rr
oo
oo
cc
cc
ee
ee
ss
ss
oo
oo
rr
rr
óó
óó
w
w
w
w
w
w
w
w
jj
jj
êê
êê
zz
zz
yy
yy
kk
kk
uu
uu
CC
CC
czêæ 8
Listing 59 - wykorzystanie funkcji fputs
#include <avr/io.h>
#include <stdio.h>
#include <avr/pgmspace.h>
#include rs.h
#include harddef.h
iinntt
main((
vvooiidd
))
{{
FILE** fRS;;
////////////////////////////////////////////
// Inicjacja portu szeregowego
RS_SET_BAUD((DEF_BAUD));;
UCSR0C ==
1
<<<<URSEL0 ||
1
<<<<UCSZ01 ||
1
<<<<UCSZ00;;
UCSR0B ==
1
<<<<RXEN0 ||
1
<<<<TXEN0;;
UCSR0A ==
0
;;
// Koniec inicjacji
////////////////////////////////////////////
// Inicjacja we/wy
fRS == fdevopen((rs_put,, rs_get,,
0
));;
fputs_P((PSTR((
Hello world!\r\n
)),, fRS));;
}}
ABC... C
Powi¹zanie strumienia z plikiem
Zmienna FILE
W pierwotnej wersji operacje z biblioteki stdio mia³y
na celu dzia³anie na plikach. Urz¹dzenia wejciawyj-
cia by³y traktowane, od strony programowej, jako
plik o pewnych specyficznych w³aciwociach. St¹d
te¿ wziê³a siê nazwa prawdopodobnie najwa¿niejszej
dla naszej struktury biblioteki. Struktura FILE prze-
chowuje wszystkie informacje na temat strumienia,
umo¿liwiaj¹ce dzia³anie funkcji do niego pisz¹cych
lub z niego czytaj¹cych. Ze struktur¹ t¹ zwi¹zana jest
pewna nieprzyjemnoæ. Standard w ogóle nie defi-
niuje jej zawartoci. Jest to zmienna wewnêtrzna bib-
lioteki stdio i s³u¿y do przekazywania informacji tylko
miêdzy jej funkcjami. Co wa¿niejsze, twórcy bibliote-
ki do³¹czonej do WinAVR informuj¹, ¿e jej zawartoæ
mo¿e zmieniaæ siê w przysz³ych wersjach. Na razie
nie wnikaj¹c w sprawê g³êboko, mo¿emy za³o¿yæ, ¿e
znajd¹ siê tam przede wszystkim wskaniki na nasze
funkcje put oraz get, które inicjowane s¹ w chwili
wywo³ania fdevopen. Funkcja fdevopen nie jest stan-
dardow¹ funkcj¹ ANSI C. Jest prób¹ umieszczenia
biblioteki stdio tam, gdzie nie wystêpuj¹ rzeczywiste
pliki. Wspomniana funkcja wykonuje dla nas dwa
dzia³ania. Po pierwsze, na podstawie podanych
danych tworzy strukturê FILE, oraz zwraca
wskanik do niej powiemy, ¿e dokonuje
powi¹zania miêdzy strumieniem a plikiem.
Po drugie, jeli odpowiedni strumieñ
domylny (stdin lub/i stdout podajemy
get lub/i put) nie jest jeszcze zainicjowany,
wpisuje do niego wskanik na aktualnie
utworzon¹ strukturê.
Gdy powi¹¿emy ju¿ urz¹dzenie z pli-
kiem, mamy mo¿liwoæ operowania po-
rednio na zwi¹zanym z nim strumieniu.
Do tej pory zajmowalimy siê tylko funkc-
jami operuj¹cymi na strumieniach domyl-
nych. Niewielka zmiana w programie
umo¿liwia nam dzia³anie na podanym stru-
mieniu. Wystarczy zapamiêtaæ wskanik
zwrócony przez fdevopen. Praktycznie
wszystkie funkcje stdio posiadaj¹ swoje
odpowiedniki z przedrostkiem f. Funkcje zaczynaj¹ce
siê od tej litery przyjmuj¹ jako jeden z parametrów
zapamiêtany przez nas wskanik, a swoje dzia³anie
kieruj¹ do strumienia po³¹czonego z podan¹ struktur¹
FILE
.
ABC... C
Zamieszanie z puts i gets
W praktyce wiêkszoæ funkcji posiada swoje wersje
z przedrostkiem f, mog¹ce dzia³aæ na wybranym
strumieniu. Trochê zamieszania wprowadzaj¹ w tym
przypadku jedynie funkcje fputs oraz fgets. Otó¿
funkcja puts po wys³aniu napisu do³¹cza na jego
koniec znak nowej linii (przypominam: \n). Funk-
cja fputs tego nie robi. Podobnie, funkcja gets wczy-
tuje napis do znaku koñca linii, ale nie wpisuje go do
podanego bufora. Funkcja fgets wpisuje wystêpuj¹cy
znak nowej linii.
Nie jest to b³¹d wprowadzony w WinAVR, dzia-
³anie takie okrela standard ANSI-C. Nie pytajcie
mnie dlaczego.
Znamy ju¿ przyczynê problemu. Jak go
rozwi¹zaæ? W posiadanej przez nas wersji
biblioteki stdio nie ma innej ni¿ fdevopen
funkcji umo¿liwiaj¹cej powi¹zanie strumienia
ze zmienn¹ FILE. Rozwi¹zaniem by³oby
samodzielne utworzenie zmiennej typu FILE
w normalny, poznany przez nas do tej pory
sposób bez dynamicznego zajmowania
pamiêci. Nastêpnie musielibymy samodziel-
nie wpisaæ do niej odpowiednie wartoci. Nie
jest to eleganckie rozwi¹zanie... jest ponadto
(pozornie) niemo¿liwe.
Zmienna typu FILE jest definiowana w ra-
mach omawianej biblioteki jako struktura.
Zapoznaj siê z ramk¹ opisuj¹c¹ korzystanie
z tych u¿ytecznych tworów. Trochê przydat-
nych informacji o tym, jak u¿ywaæ struktury
jako nowego typu danych, znajdziesz w ram-
ce ABC... C typedef nazywanie typu. Zaj-
rzyj do niej. Nied³ugo bêdziemy korzystaæ
z opisanej tam mo¿liwoci.
Pocz¹tek naszej idei pozbycia siê proble-
mu pokazuje listing 61, bêd¹cy przeróbk¹
programu z listingu 59. Chwilowo nie wpisu-
jemy nic do struktury (jeszcze nie wiemy,
jakie posiada sk³adowe). Wa¿ne jest jedynie
sprawdzenie, jak zareaguje kompilator. Oka¿e
siê, ¿e ju¿ na tym poziomie pojawi¹ siê prob-
lemy. Spróbuj skompilowaæ taki program.
Jeli wszystko zrobi³e prawid³owo, to kom-
pilacja pójdzie... le. Otrzymasz komunikat
o b³êdzie polegaj¹cym na tym, ¿e nie jest
znany rozmiar struktury g_fRS. Có¿ to za
dziwna struktura, której rozmiar jest nieokre-
38
Programowanie
Elektronika dla Wszystkich
Listing 60 - wykorzystanie funkcji fprintf
iinntt
main((
vvooiidd
))
{{
FILE** fRS;;
iinntt
a ==
1234
;;
iinntt
b ==
0xff
;;
//////////////////////////////
// Inicjacja portu szeregowego
((......))
// Koniec inicjacji
//////////////////////////////
// Inicjacja we/wy
fRS == fdevopen((rs_put,, rs_get,,
0
));;
fprintf_P((fRS,, PSTR((
a(%%d)=%d\r\n
a(%%x)=%x\r\n
a(%%X)=%X\r\n
b(%%#x)=%#x\r\n
b(%%o)=%o\r\n
)),,
a,, a,, a,, b,, b));;
}}
ABC... C
struktura
Tworzenie, przeznaczenie
Wspomnia³em o tym, ¿e zmienna typu FILE to struk-
tura. Struktura to bardzo wygodny obiekt, w którym
mo¿emy pogrupowaæ wszystkie potrzebne nam
zmienne jakiego typu. Wyobramy sobie, ¿e chcemy
utworzyæ strukturê zawieraj¹c¹ informacjê o punkcie:
ssttrruucctt
[[punkt]]
{
iinntt
x;;
iinntt
y;;
iinntt
z;;
} [[punktA,, punktB,, x]];;
Pola umieszczone w nawiasach kwadratowych s¹
opcjonalne. Zaraz za s³owem kluczowym struct
wystêpuje etykieta. Jeli umiecimy j¹ tutaj, bêdziemy
mogli wykorzystaæ j¹ zamiast wszystkiego, co zawar-
limy w nawiasach klamrowych. Przyk³adowo, chce-
my utworzyæ kolejny punkt:
ssttrruucctt
punktKolejny;;
Z kolei wszystko, co zostanie umieszczone za
nawiasem klamrowym, powoduje utworzenie odpo-
wiednich zmiennych. W praktyce sk³adnia:
ssttrruucctt
{......} zmienna;;
Oznacza to samo co:
iinntt
zmienna;;
Z tym ¿e w pierwszym przypadku tworzymy
obiekt typu podanej struktury, w drugim tworzymy
obiekt typu int.
W nawiasach klamrowych umieszczamy infor-
macje o sk³adowych struktury. Po prostu wymieniamy
kolejno wystêpuj¹ce w niej zmienne oraz nadajemy im
nazwy. Mo¿emy korzystaæ tutaj z wszelkich dostêp-
nych typów zmiennych. Wczeniej zdeklarowane
struktury tak¿e mog¹ byæ wykorzystane.
Nale¿y pilnowaæ jedynie, aby nazwy sk³adowych
nie pokrywa³y siê miêdzy sob¹. Jednak nazwa struktu-
ry mo¿e przyj¹æ postaæ identyczn¹ jak na przyk³ad
etykieta czy nazwa zmiennej globalnej nie ma
obawy o b³êdy w programie.
Dostêp do odpowiedniej zmiennej w strukturze
jest rozró¿niany poprzez kontekst.
Dostêp do sk³adowych,
wykorzystanie w programie
Do sk³adowych struktury mo¿emy siê dostaæ na dwa
sposoby. Pierwszy z nich dotyczy przypadku, gdy dys-
ponujemy odpowiedni¹ zmienn¹. W naszym przypad-
ku niech bêdzie to punktA. Do poszczególnych pól
struktury dostaniemy siê za pomoc¹ operatora kropki:
punktA..x ==
0
;;
punktA..y ==
1
;;
punktA..z ==
2
;;
Mylê, ¿e nie budzi to w¹tpliwoci. Jeli teraz
wydaje siê nienaturalne, stanie siê prostsze podczas
pisania przyk³adów.
Drugi sposób dotyczy momentu, gdy mamy dostêp
do wskanika na strukturê. Mo¿na wyobraziæ sobie
nastêpuj¹cy przyk³ad:
vvooiidd
Zeruj((
ssttrruucctt
punkt **pPt))
{
pPt-->>x ==
0
;;
pPt-->>y ==
0
;;
pPt-->>z ==
0
;;
}
Sytuacja bardzo zbli¿ona do poprzed-
niej, korzystamy jedynie z innego operatora.
Co prawda mo¿liwe jest dzia³anie bardziej zakrêco-
ne:
(*pPt)..x ==
0
;;
Przypomnê, ¿e za pomoc¹ operatora gwiazdki
dostajemy siê do obiektu wskazywanego przez wska-
nik. Mo¿emy korzystaæ z tego jak powy¿ej, ale jest to
mniej eleganckie i mniej czytelne ni¿ podany wcze-
niej sposób.
Przedstawion¹ funkcjê wywo³amy jak ni¿ej:
Zeruj((&&punktA))
Przyk³ad mo¿e nie jest zaawansowany, ale poka-
zuje ju¿ si³ê, jaka drzemie w strukturach. Zauwa¿,
¿e do funkcji Zeruj przekazujemy tak naprawdê tylko
jeden argument 16-bitowy adres a w jej wnêtrzu
mamy dostêp do wszystkich pól naszej struktury.
Nadanie wartoci pocz¹tkowych
Nadanie wartoci pocz¹tkowych strukturze odbywa
siê podobnie jak mia³o to miejsce w przypadku tablic.
W nawiasach klamrowych musimy podaæ wartoci
ka¿dej sk³adowej. Pamiêtaj, ¿e takie dzia³anie dozwo-
lone jest jedynie w chwili tworzenia zmiennej:
ssttrruucctt
punktZ == {
0
,,
1
,,
2
};;
Operowanie ca³ymi strukturami
Jeli stworzysz w³asn¹ strukturê, mo¿esz przekonaæ
siê, ¿e nie dzia³aj¹ na nich standardowe operatory.
Zapis jak poni¿ej jest nieprawid³owy:
puntA = punktB;
Strukturê nale¿y traktowaæ jako pewien ci¹g
danych w pamiêci (obrazek w ramce). Sprawa jest
zbli¿ona do dzia³ania tablic. Mamy dwie mo¿liwoci
zast¹pienia powy¿szego przepisania. Jedna polega na
przepisywaniu wszystkich pól, dla których powy¿sze
dzia³anie jest dozwolone:
punktA..x == punktB..x;;
punktA..y == punktB..y;;
punktA..z == punktB..z;;
Pola x, y, z s¹ typu int. U¿ycie operatora przypisa-
nia na zmiennych typu int jest mo¿liwe.
Druga, wygodniejsza i czêsto bardziej optymalna
metoda polega na przekopiowaniu bloku pamiêci:
Funkcja memcpy skopiuje dane z obszaru wskazy-
wanego przez drugi argument do obszaru wskazywa-
nego przez pierwszy argument. Trzeci argument poda-
je w bajtach iloæ danych do skopiowania. Operator
sizeof zostanie w czasie kompilacji zamieniony na
odpowiedni¹ wartoæ.
Jak to robi GCC
Dlaczego fdevopen jest tak
pamiêcioch³onna?
Dokumentacja funkcji fdevopen informuje nas,
¿e funkcja ta tworzy now¹ strukturê FILE, aloku-
j¹c w tym celu dynamicznie pamiêæ. Daje nam to
doæ ciekaw¹ mo¿liwoæ: gdy zaczynamy u¿ywaæ
na przyk³ad interfejsu RS232, dokonuj¹c powi¹za-
nia poprzez funkcjê fdevopen, zajmujemy pamiêæ.
Gdy teraz zamkniemy po³¹czenie za pomoc¹
funkcji fclose pamiêæ zostanie zwolniona. Jest
jedno ale...
Dynamiczna alokacja pamiêci danych wymaga
umieszczenia doæ skomplikowanych procedur
w pamiêci programu. Czy to siê op³aca? Zauwa¿y-
³e mo¿e, ¿e do tej pory nie zapozna³em Ciê
z funkcj¹ fclose? Prawda jest taka, ¿e w mikro-
kontrolerze bardzo rzadko mamy do czynienia z
sytuacj¹, gdy chcemy zamkn¹æ otwarty strumieñ.
W ogromnej wiêkszoci przypadków to po prostu
nie ma sensu. Zwykle dokonamy powi¹zania
wszystkich strumieni w chwili startu programu i
nie bêdziemy ich nigdy zwalniaæ. Wykorzystanie
dynamicznego przydzielania pamiêci jest wiêc dla
nas przerostem formy nad treci¹.
memcpy((&&punktA,, &&punktB,,
ssiizzeeooff
((punktA))));;
lony? Aby siê tego dowiedzieæ, poszukujemy
miejsca, gdzie zosta³a ona zdeklarowana.
Znajdziesz j¹ w pliku C:\WinAVR\avr\inclu-
de\stdio.h. Interesuj¹cy nas fragment przed-
stawi³em na listingu 62. Zauwa¿, ¿e FILE jest
zdefiniowane tutaj jako sta³a symboliczna,
a nie nowy typ. Informacja o takiej mo¿liwo-
ci znajduje siê w ramce ABC... C typedef
nazywanie typu. Ciekawa rzecz dzieje siê
wy¿ej. W pierwszej z przedstawionych linii.
Aby zrozumieæ, z czym mamy do czynienia,
zajrzyj do ramki ABC... C deklaracja nie-
kompletna.
Okazuje siê, ¿e z naszego punktu widzenia
zmienna typu FILE zosta³a utworzona w taki
sposób, aby by³a w³anie typu niekompletne-
go. Od twórców WinAVR dostalimy do rêki
jedynie narzêdzie umo¿liwiaj¹ce nam pos³u-
giwanie siê uchwytami do podanej struktury.
Zabezpieczyli oni siê w ten sposób przed pró-
bami jej rêcznego modyfikowania (co w C
formalnie jest zabronione). Wydawa³oby siê,
¿e tajemnica FILE zosta³a przed nami dok³ad-
nie schowana. Jednak¿e... mo¿e nie?
Mylê, ¿e przynajmniej czêæ Czytelników
w tej chwili zaprotestuje. Jeli nie umknê³a Ci
sprawa, o której zaraz napiszê bardzo siê
cieszê.
Nie przestrasz siê tego, jak g³êboko w na-
sze narzêdzia chcê Ciê teraz zaprowadziæ.
Przekonasz siê zaraz, ¿e poszukiwanie potrze-
bnych nam informacji w kodzie wcale nie jest
takie trudne jakby siê wydawa³o. Poza tym,
w tej chwili, niejako przeprowadzê Ciê za rê-
kê, pokazuj¹c krok po kroku sposób odnale-
zienia oraz wykorzystania potrzebnych infor-
macji.
Korzystamy z mo¿liwoci,
jakie daje Open Source:
Jeli masz teraz tak¹ mo¿liwoæ, wejd na
stronê projektu: winavr.sourcefoge.net. Spójrz
na rysunek 43. Nie piesz siê z przechodze-
niem przez proponowane strony. Gdyby
wa¿na by³a jedynie strona koñcowa, móg³by
przecie¿ wpisaæ w okno przegl¹darki bezpo-
rednio: www.nongnu.org/avr-libc. Na pierw-
szej stronie znajdziesz pasek z ze-
spo³em linków. W tej chwili intere-
suje nas link [Package]. Znajdziesz
pod nim informacje o elementach
wchodz¹cych w sk³ad WinAVR.
Nie jest to jednolity pakiet tworzo-
ny tylko przez jedn¹ grupê ludzi.
Najbardziej chyba interesuj¹ce s¹
trzy pierwsze pozycje. Na pozycji
drugiej znajduje siê kompilator
GCC, generuje on kod, który mo¿e
byæ przekszta³cony na kod maszy-
nowy za pomoc¹ narzêdzi widocz-
nych na pozycji pierwszej znajdu-
je siê tam, przede wszystkim, linker oraz
kompilator asemblera.
Wystêpuj¹ce w ANSI C biblioteki standar-
dowe, stworzone specjalnie z myl¹ o proce-
sorach AVR, znajduj¹ siê pod nazw¹ avr-libc.
Po tej nazwie mo¿esz znaleæ sporo informac-
ji za pomoc¹ wyszukiwarki internetowej.
WinAVR jest programem na licencji Open Sour-
ce. Niczego nie da siê ukryæ w takim przypadku.
Jest to potê¿na przewaga w stosunku do programów
komercyjnych.
39
Programowanie
Elektronika dla Wszystkich
ABC... C
typedef - nazywanie typu
C udostêpnia mechanizm umo¿liwiaj¹cy nadawanie
nowych nazw typom danych. S³u¿y do tego s³owo klu-
czowe typedef. Mechanizmu tego u¿ywalimy, nie-
wiadomie ju¿ w drugiej czêci kursu, korzystaj¹c z
pliku <inttypes.h>. W ten sposób do³¹czalimy do
naszego programu deklaracje jak ni¿ej:
ttyyppeeddeeff ssiiggnneedd cchhaarr
int8_t;;
ttyyppeeddeeff uunnssiiggnneedd cchhaarr
uint8_t;;
ttyyppeeddeeff lloonngg
int16_t;;
Jest to przyk³ad prostego nadania nazwy typom.
Przyporz¹dkowujemy nowe nazwy typom podstawo-
wym. Zapis taki jak wy¿ej jest praktycznie równowa¿-
ny z:
#define int8_t signed char
......
Jednak we wprowadzonym do C nazywaniu typów
tkwi znacznie wiêksza si³a. Mo¿emy nazwaæ tak¿e
bardziej rozbudowany typ. Zamiast tworzyæ strukturê
opisuj¹c¹ punkt, za ka¿dym razem korzystaj¹c ze
s³owa kluczowego struct, utwórzmy nowy typ:
ttyyppeeddeeff ssttrruucctt
[[punkt]]
{
iinntt
x;;
iinntt
y;;
iinntt
z;;
} PUNKT;;
W ten sposób tworzymy nowy typ zmiennej o na-
zwie PUNKT. Opcjonalne pole [punkt] ma dok³adnie
takie samo znaczenie jak mia³o w przypadku struktu-
ry. Zgodnie z moimi próbami umieszczona w tym
miejscu etykieta mo¿e byæ identyczna z nazw¹ nowe-
go typu, a kompilator bêdzie wiedzia³ zawsze, co
mamy na myli poprzez kontekst. Nie uda³o mi siê
jednak znaleæ oficjalnego potwierdzenia tego faktu.
Uwaga. Samo umieszczenie s³owa typedef przed
s³owem struct ogromnie zmienia znaczenie zapisu. W
poprzedniej ramce, gdy tylko po zamkniêciu klamry
umiecilimy jak¹ nazwê,
tworzona by³a nowa zmienna,
w pamiêci rezerwowane by³o na ni¹ miejsce. W dru-
gim przypadku tworzony jest jedynie nowy typ danej.
Nie bêdziemy mieli z tego ¿adnej korzyci, dopóki
z niego nie skorzystamy.
Teraz ca³¹ strukturê typu PUNKT mo¿emy utwo-
rzyæ w bardzo intuicyjny sposób:
PUNKT punktC == {
1
,,
2
,,
3
};;
Mo¿na pokazaæ przyk³ad, gdzie wykorzystanie
s³owa #define do nadania nazwy typu by³oby znacznie
utrudnione. Pamiêtasz nasz¹ rozmowê o wskanikach
na funkcjê? Mo¿esz zajrzeæ w tym celu do czêci 4
kursu. Mamy mo¿liwoæ nadania nazwy typowi bêd¹-
cemu wskanikiem na funkcjê (specjalnie oznaczy³em
nazwê nowego typu na czerwono):
ttyyppeeddeeff vvooiidd
((**
MyFuncPtr
))((uint16_t));;
Nastêpnie utworzenia zmiennej bêd¹cej wskani-
kiem na funkcjê:
MyFuncPtr
moja_f;;
Albo wykorzystania nowej nazwy typu do wygod-
nego zapisania jednego z parametrów:
vvooiidd
ObslozPrzycisk((uint8_t maska,,
MyFuncPtr
proc))
......
Listing 62
deklaracja zmiennej typu FILE w pliku <stdio.h>
ssttrruucctt
__file;;
((......))
#define FILE struct __file
Listing 61 - próba statycznego utworzenia struktury FILE
FILE g_fRS;
iinntt
main((
vvooiidd
))
{{
//////////////////////////////////////////
// Inicjacja portu szeregowego
((......))
// Koniec inicjacji
//////////////////////////////////////////
fputs_P((PSTR((
Hello world!\r\n
)),, &&g_fRS));;
}}
ABC... C
deklaracja niekompletna
Gdy spotkamy siê z zapisem jak poni¿ej:
ssttrruucctt
Etykieta
Przyk³adowo:
ssttrruucctt
Nazwa** pS1;;
Funkcja((
ssttrruucctt
Nazwa** pS2))
I jeli etykieta Nazwa nie by³a poprzednio
zdeklarowana, mamy do czynienia z typem nie-
kompletnym. W takim przypadku kompilator wie
jedynie, ¿e pS1 jest wskanikiem na jak¹ strukturê.
Nie wie o niej nic ponadto. Nie zna jej sk³adowych
czy rozmiaru. Wa¿ne jest to, ¿e z typem niekomplet-
nym mo¿emy bardzo niewiele zrobiæ. W praktyce
mo¿emy korzystaæ jedynie ze wskanika do niego.
Jest to mo¿liwe, poniewa¿ wskanik na ka¿d¹ struk-
turê bêdzie wygl¹da³ tak samo. W naszym przypad-
ku bêdzie to zawsze 16-bitowy adres. Uwaga: oczy-
wicie, na takim wskaniku operator -> jest
zabroniony. Przy powy¿szych za³o¿eniach, zapis jak
ni¿ej:
ssttrruucctt
Nazwa S1;;
jest nieprawid³owy dostaniemy informacjê o b³ê-
dzie, poniewa¿ kompilator nie wie, ile miejsca w pa-
miêci zaj¹æ na podan¹ strukturê.
Specjalne znaczenie ma w tym przypadku zapis:
ssttrruucctt
Nazwa;;
Zauwa¿ rednik! Taka deklaracja sprawia, ¿e ety-
kieta Nazwa staje siê etykiet¹ nowej struktury, jed-
nak bêdzie to struktura typu niekompletnego. Przypi-
sanie takie odbêdzie siê tak¿e, jeli podana etykieta
by³a wczeniej powi¹zana z jak¹ inn¹ struktur¹.
Ta regu³a umo¿liwia nam zdeklarowanie nie-
kompletnej struktury, nawet jeli gdzie indziej (na
przyk³ad w innym pliku) u¿ylimy ju¿ podanego
identyfikatora. Dokonana zmiana bêdzie mia³a dzia-
³anie jedynie lokalne (dla pliku kodu ród³owego, we
wnêtrzu funkcji, we wnêtrzu instrukcji z³o¿onej...
zale¿nie od miejsca, gdzie powy¿sza deklaracja siê
pojawi).
Wybieraj¹c odnonik avr-libc, zostaniemy
przeniesieni do strony zawieraj¹cej podsumo-
wanie informacji na temat projektu. Mo¿li-
woæ pobrania nowych wersji bibliotek, doku-
mentacji oraz kodów ród³owych uzyskamy
po przejciu na stronê domow¹ (odnonik
Homepage w ramce Public Areas). Mniej -
wiêcej w rodku strony domowej znajdziemy
linki do pobrania kodów ród³owych oraz
dokumentacji. Po jego wybraniu bêdziesz
mia³ mo¿liwoæ dostêpu do ka¿dej wersji,
jaka zosta³a utworzona.
Pobierane kody ród³owe s¹ spakowane
w pewien szczególny sposób. S¹ to archiwa
TAR, z którymi radzi sobie dobrze na przy-
k³ad popularny WinRAR. Po otwarciu archi-
wum oka¿e siê jednak, ¿e wewn¹trz znajduje
siê plik o nazwie avr-libc-1.2.3.tar[1]. Plik
ten nale¿y wypakowaæ na przyk³ad na pulpit,
a nastêpnie usun¹æ z jego rozszerzenia cyfrê 1
razem z nawiasami kwadratowymi. Gdy roz-
pakujemy i to archiwum, uzyskamy dostêp do
wszystkich róde³ biblioteki standardowej.
Znajdziesz tutaj siedem folderów. Aktual-
nie znaczenie dla nas maj¹ dwa z nich: inclu-
de oraz libc. Pierwszy zawiera pliki nag³ów-
kowe, które mo¿emy znaleæ w pakiecie
WinAVR w folderze C:\WinAVR\avr\inclu-
de\. Pliki te ju¿ znamy. W folderze libc
umieszczono dalsze podfoldery grupuj¹ce
funkcje ró¿nych bibliotek. Znajdziemy tutaj
upragniony folder stdio.
Po otwarciu folderu libc\stdio\ odkryjesz
du¿¹ liczbê plików. Okazuje siê, ¿e praktycz-
nie ka¿da funkcja zosta³a napisana w taki spo-
sób, ¿e znajduje siê w oddzielnym pliku. Dla
wygody nazwy plików s¹ zgodne ze znajduj¹-
cymi siê w nich funkcjami. Wyjanienie, sk¹d
wzi¹³ siê taki podzia³ oraz czemu on s³u¿y,
znajdziesz w ramce Jak to robi GCC linker
³¹czenie z bibliotekami.
Teraz, gdy ju¿ wiesz, gdzie znaleæ kody
ród³owe bibliotek, po przeczytaniu tego aka-
pitu zajrzyj do ramki Nieczyste zagranie
zastêpowanie fdevopen. Przedstawiam tam
opis, jak korzystaj¹c z udostêpnionych kodów
ród³owych, mo¿emy obejæ wprowadzone
ograniczenia. Staram siê opisaæ w tym miejs-
cu sposób, w jaki dochodzi³em do pewnych
wniosków. Jednak si³¹ rzeczy opis jest skróto-
wy. Tak naprawdê utworzenie ostatecznej
wersji nak³adki na stdio by³o poprzedzone
kilkoma godzinami spêdzonymi na forach
(niestety, g³ównie angielskojêzycznych) oraz
studiowaniu podstawowej dokumentacji z
katalogu C:\WinAVR\doc\avr-libc. Pamiêtaj o
tym, jeli opisany tok mylenia bêdzie czasa-
mi zaskakiwa³. Opisane tutaj dzia³anie jest
wysoce ryzykowne. Ryzykujemy, ¿e utworzo-
ny kod bêdzie niekompatybilny z przysz³¹
wersj¹ bibliotek avr-libc. Jednak jak siê za
chwilê przekonamy, korzyci s¹ na tyle du¿e,
¿e czêsto warto podj¹æ to ryzyko. Jest to
szczególnie odczuwalne, jeli zaczynamy
mieæ problemy ze zmieszczeniem siê z pro-
gramem w pamiêci FLASH procesora.
Ramka opisuj¹ca pomys³ pozbycia siê
pamiêcioch³onnego wywo³ania fdevopen
mo¿e w jakim miejscu wydawaæ siê niejasna.
Wiem, ¿e mo¿e byæ to trudne dla osób przy-
zwyczajonych do BASCOM-a, gdzie takie
dzia³ania absolutnie nie by³y konieczne (nie
by³y te¿ mo¿liwe). Nie przejmuj siê tym w tej
chwili. Do wspomnianej ramki mo¿esz
zawsze powróciæ zaraz po przyk³adach.
Dalej bêdziemy prowadziæ eksperyment
z programem z listingu 59. Dodamy do niego
nowy plik nag³ówkowy o nazwie
fcheat_stdio.h. Pamiêtaj, ¿e dodanie nowego
pliku nag³ówka nie wymaga jakichkolwiek
zmian w pliku makefile. Dla w³asnej wygody
mo¿emy dodaæ go do projektu Programmers
Notepada, co sprawi, ¿e bêdzie on dostêpny
z poziomu panelu Projects.
Do nowo utworzonego pliku wprowad
kod widoczny na listingu 65. Plik main.c
naszego programu zmieniamy zgodnie
z listingiem 67. Skompiluj now¹ wersjê
programu. Podczas kompilacji pojawi siê
jedno ostrze¿enie. Jest to zwi¹zane z rzutowa-
Uwa¿aj. W WinAVR 20050214 zaimplementowa-
no biblioteki avr-libc 1.2.3. Nie ma sensu pobiera-
nie w tej chwili nowszych róde³ wprowadzi to za
du¿o zamieszania.
40
Programowanie
Elektronika dla Wszystkich
Jak to robi GCC
linker ³¹czenie z bibliotekami
Pamiêtasz moment, gdy zaczynalimy tworzyæ pro-
gramy z wiêkszej liczby plików? Zrobilimy to
w czêci 6 kursu. T³umaczy³em wtedy, czym jest lin-
ker. Jeli masz tak¹ mo¿liwoæ, zerknij na rysunek
27 znajduj¹cy siê we wspomnianej czêci. Jeli
w programie u¿yjemy teraz funkcji z jakiej bibliote-
ki, jej kod zostanie dodany w procesie linkowania
programu. Istniej¹ linkery bêd¹ce w stanie zrobiæ to
w bardzo inteligentny sposób. Analizuj¹ one mo¿li-
we, wewnêtrzne odwo³ania i usuwaj¹ wszelki kod,
który nie jest u¿ywany. AVR-GCC nie posiada, nie-
stety, takiej mo¿liwoci. Jeli wykryte zostanie odwo-
³anie do jakiejkolwiek funkcji znajduj¹cej siê
w danym pliku relokowalnym (przypominam: pliki
typu *.o), ca³y plik zostanie dodany do kodu wyjcio-
wego. Nawet jeli sporód 20 znajduj¹cych siê w takim
pliku funkcji, wykorzystamy tylko jedn¹, w pamiêci
mikrokontrolera znajd¹ siê wszystkie. Nie jest to wiel-
kim problemem, jeli chodzi o pliki programu tutaj
bardzo rzadko piszemy funkcje, z których nie korzysta-
my. Bardziej uci¹¿liwy fakt ten staje siê w chwili two-
rzenia oraz korzystania z uniwersalnych bibliotek.
Aby zminimalizowaæ ten efekt, biblioteki zaleca
siê dzieliæ na ma³e kawa³ki. W ten sposób w³anie
stworzona zosta³a biblioteka avr-libc. Praktycznie
ka¿da funkcja umieszczona jest w oddzielnym pliku.
To, ¿e fakt ten u³atwia nam znalezienie tego, czego
szukamy, jest jedynie mi³ym skutkiem ubocznym
osi¹gniêcia znacznie wa¿niejszego celu.
Listing 67 - nowa wersja programu przywitania
#include <avr/io.h>
#include <stdio.h>
#include rs.h
#include harddef.h
#include stdio_fcheat.h
fcheat_file fileRS ==
FCHEAT_STATIC_FDEVOPENWR((rs_put,, rs_get));;
FILE** fRS == ((FILE**))&&fileRS;;
iinntt
main((
vvooiidd
))
{
/////////////////////////////////////////
// Inicjacja portu szeregowego
((......))
// Koniec inicjacji
/////////////////////////////////////////
fputs((
Hello world!\r\n
,, fRS));;
}
Rys. 43 Poszukiwanie róde³ bibliotek
standardowych.
niem naszego wskanika na wskanik na
strukturê typu niekompletnego. Poniewa¿
kompilator nie wie, co siê pod ni¹ kryje, infor-
muje nas, ¿e dzia³anie takie mo¿e byæ niepra-
wid³owe. W tym przypadku ostrze¿enie to
mo¿emy zignorowaæ. Mo¿emy tak¿e pozbyæ
siê jego wypisywania poprzez operacjê jak na
listingu 68. Wyjaniê znaczenie tego dzia³a-
nia, gdy tylko powiemy sobie na czym polega
rzutowanie. Tak czy inaczej, program bêdzie
dzia³a³ prawid³owo i zajmie nieca³e 360B
pamiêci programu.
Nieczyste zagranie
Nieczyste zagranie
Zastêpowanie fdevopen
Umieszczone w ramce kody s¹ fragmentami kodów
ród³owych biblioteki avr-lic-1.2.3. Pochodz¹ one ze
strony www.nongnu.org/avr-libc. Autor pokazanych
fragmentów zgodzi³ siê na ich publikacjê.
Poszukiwania prawdziwej deklaracji struktury
FILE zaprowadz¹ nas do pliku \libc\stdio\stdio_priva-
te.h. Interesuj¹cy nas fragment pokaza³em na listingu
63. Wiemy ju¿, jakie pola zawiera tajemnicza struktu-
ra FILE. Jednak teraz mo¿emy jedynie domylaæ, siê
co powinnimy wpisaæ w ka¿de z nich, aby funkcje
biblioteki stdio dzia³a³y prawid³owo. Zajrzyjmy wiêc,
co robi funkcja fdevopen. Jak ³atwo siê domyliæ, jej
kod znajdziemy w pliku \libc\stdio\fdevopen.c. Przed-
stawiam go tak¿e na listingu 64, gdzie zaznaczy³em
¿ó³tym t³em miejsce, gdzie rezerwowana jest dyna-
micznie pamiêæ. Warto wiedzieæ, ¿e zgodnie ze stan-
dardem ANSI C funkcja calloc wype³nia zarezerwo-
wan¹ pamiêæ zerami.
Przyjrzyjmy siê, co robi kod, do którego uda³o
nam siê dostaæ. Wbrew pozorom nie dzieje siê tutaj
wiele. Rezerwowana jest pamiêæ na strukturê FILE.
Domylnie wszystkie pola nowej struktury s¹ ustawia-
ne na zera. Podczas tworzenia powi¹zania ze strumie-
niem ustawiane s¹ wskaniki na nasze funkcje zapisy-
wania i odczytu znaku oraz w polu flags ustawiane s¹
bity wskazuj¹ce, ¿e strumieñ nadaje siê do odczytu
i/lub zapisu.
Rozwi¹zanie: fcheat_stdio.h
Poniewa¿ w pisanym przez nas programie nie mo¿emy
utworzyæ zmiennej typu FILE, stworzymy nowy typ
zmiennej, bêd¹cy struktur¹, która bêdzie identyczna
z orygina³em. Dziêki temu uzyskamy mo¿liwoæ
odpowiedniego wype³nienia obszaru pamiêci (tak
mo¿na potraktowaæ strukturê, porównaj z odpowied-
ni¹ ramk¹ ABC... C). Jeli teraz podstawimy pod
wskanik do zmiennej FILE wskanik na
nasz¹ strukturê wszystko bêdzie dzia³a³o
zgodnie z oczekiwaniami.
Nowej zmiennej postanowi³em nadaæ
nazwê fcheat_stdio (cheat oszust). Propo-
zycjê zawartoci pliku fcheat_stdio.h przed-
stawiam na listingu 65. Utworzon¹ struktu-
rê moglibymy inicjowaæ, wype³niaj¹c
odpowiednie pola po jej utworzeniu. Jednak
najwygodniej w naszym przypadku bêdzie
wykorzystaæ mo¿liwoæ inicjacji nowo two-
rzonej zmiennej podan¹ wartoci¹. Widocz-
ne na listingu 65 makra u³atwiaj¹ intuicyjne
stworzenie potrzebnych danych. Dziêki
temu utworzenie nowej struktury, zawiera-
j¹cej odpowiednio ustawione wszystkie
pola mo¿e przebiegaæ jak na listingu 66.
Dlaczego nie mo¿emy
u¿yæ strumieni domylnych
Wydawa³oby siê, ¿e wskaniki do utworzo-
nych struktur mo¿emy przypisaæ tak¿e stru-
mieniom domylnym (zobacz jak jest to
robione na listingu 64 zielone oznacze-
nia). Nie jest to do koñca prawda. Jeli
przyjrzysz siê plikowi zawieraj¹cemu kod
fdevopen, odkryjesz, ¿e zmienna zawieraj¹-
ca dane strumieni domylnych, bêd¹ca tak
na prawdê tablic¹ wskaników na strukturê
__file, zosta³a zdeklarowana w³anie tutaj.
Zgodnie wiêc z ramk¹ o dzia³aniu linkera,
jakiekolwiek odwo³anie do strumieni
domylnych spowoduje dodanie tak¿e kodu
fdevopen do pliku kodu wyjciowego. Tego
przecie¿ ca³y czas staramy siê unikn¹æ.
fclose - uwaga
Stosuj¹c opisan¹ sztuczkê, utworzymy
nowe powi¹zanie, którego nie wolno nam
zamykaæ! Funkcja fclose uznaje, ¿e struktura FILE
by³a utworzona dynamicznie i zwalnia pamiêæ. Próba
zwolnienia pamiêci zajmowanej przez zmienn¹, która
nie zosta³a stworzona w odpowiedni sposób, mo¿e
spowodowaæ b³¹d programu.
Od technicznej strony dynamicznie zajêta pamiêæ
znajduje siê w odpowiednim obszarze RAM-u pro-
cesora. Funkcje dostaj¹ siê do niej tak samo jak do
ka¿dej innej zmiennej, tak wiêc mo¿esz mi wierzyæ,
¿e funkcjom, które chcemy nabraæ, nie zrobi ró¿-
nicy czy zmienna FILE bêdzie utworzona dynamicz-
nie, czy bêdzie to zmienna lokalna lub globalna.
45
Programowanie
Elektronika dla Wszystkich
Listing 63 - tajemnica struktury FILE, fragment pliku stdio_private.h
ssttrruucctt
__file
{
cchhaarr
**
buf;;
/* buffer pointer */
uunnssiiggnneedd cchhaarr
unget;; /* ungetc() buffer */
uint8_t flags;;
/* flags, see below */
#define __SRD 0x0001
/* OK to read */
#define __SWR 0x0002
/* OK to write */
#define __SSTR 0x0004
/* this is an sprintf/snprintf string */
#define __SPGM 0x0008
/* fmt string is in progmem */
#define __SERR 0x0010
/* found error */
#define __SEOF 0x0020
/* found EOF */
#define __SUNGET 0x040
/* ungetc() happened */
#if 0
/* possible future extensions, will require uint16_t flags */
#define __SRW 0x0080
/* open for reading & writing */
#define __SLBF 0x0100
/* line buffered */
#define __SNBF 0x0200
/* unbuffered */
#define __SMBF 0x0400
/* buf is from malloc */
#endif
iinntt
size;;
/* size of buffer */
iinntt
len;;
/* characters read or written so far */
iinntt
((**put))((
cchhaarr
));;
/* function to write one char to device */
iinntt
((**get))((
vvooiidd
));;
/* function to read one char from device */
};;
Listing 64 - Kod funkcji fdevopen
FILE *
fdevopen((
iinntt
((**put))((
cchhaarr
)),,
iinntt
((**get))((
vvooiidd
)),,
iinntt
opts __attribute__((((unused))))))
{
FILE **s;;
iiff
((put ====
0
&&&& get ====
0
))
rreettuurrnn
0
;;
iiff
((((s == calloc((
1
,,
ssiizzeeooff
((FILE)))))) ====
0
))
rreettuurrnn
0
;;
iiff
((get !!==
0
)) {
s-->>get == get;;
s-->>flags == __SRD;;
iiff
((stdin ====
0
))
stdin == s;;
}
iiff
((put !!==
0
)) {
s-->>put == put;;
s-->>flags ||== __SWR;;
iiff
((stdout ====
0
)) {
stdout == s;;
iiff
((stderr ====
0
))
stderr == s;;
}
}
rreettuurrnn
s;;
}
Listing 65 - rozwi¹zanie: plik fcheat_stdio.h
#ifndef FCHEAT_STDIO_INCLUDED
#define FCHEAT_STDIO_INCLUDED
// Struktura odpowiadaj¹ca FILE
ttyyppeeddeeff ssttrruucctt
fcheat_file
{
cchhaarr
**buf;;
uunnssiiggnneedd cchhaarr
unget;;
uint8_t flags;;
#define FCHEAT_SRD 0x0001
#define FCHEAT_SWR 0x0002
iinntt
size;;
iinntt
len;;
iinntt
((**put))((
cchhaarr
));;
iinntt
((**get))((
vvooiidd
));;
}fcheat_file;;
#define FCHEAT_STATIC_FDEVOPENR(get) \
{0,0,FCHEAT_SRD,0,0,0,get}
#define FCHEAT_STATIC_FDEVOPENW(put) \
{0,0,FCHEAT_SWR,0,0,put,0}
#define FCHEAT_STATIC_FDEVOPENWR(put, get) \
{0,0,FCHEAT_SWR|FCHEAT_SRD,0,0,put,get}
#endif
//FCHEAT_STDIO _INCLUDED
Listing 66 - tworzenie oraz inicjacja fcheat_stdio
fcheat_file fileRS ==
FCHEAT_STATIC_FDEVOPENWR((rs_put,, rs_get));;
Podsumowanie rozwi¹zania
Mo¿esz uznaæ teraz, ¿e C jest straszne. Jeli
taka myl pojawia siê w Twojej g³owie, spró-
buj zerkn¹æ na dzisiejsz¹ sztuczkê z innej per-
spektywy. Du¿a liczba stron nie zosta³a
powiêcona dzisiaj jedynie opisowi tego, jak
obejæ ograniczenia WinAVR. Powiêci³em
dzi wiele stron, pokazuj¹c zasadê dzia³ania
biblioteki stdio od rodka. Znakomicie by³o-
by, gdyby sam pobra³ na swój komputer jej
kod oraz przejrza³ go zgodnie z przedstawio-
nym opisem. Jednoczenie przedstawi³em
pojêcie struktury, delaracji niepe³nej, dzia³a-
nie s³owa typedef i wreszcie sam¹ zmienn¹
FILE.
Jednak to, ¿e w WinAVR mamy mo¿li-
woæ podejrzenia kodów ród³owych ka¿dego
dostêpnego modu³u, nie zmusza nas wcale do
robienia tego. Jest to dodatkowa mo¿liwoæ.
Napisany dzisiaj kod mo¿esz traktowaæ nawet
jak czarn¹ skrzynkê. Napisany plik
fcheat_stdio.h po prostu dzia³a
pamiêtaj jednak, ¿e dotyczy to jedy-
nie aktualnej wersji WinAVR. Jeli w
bibliotece stdio dokonana zostanie
zmiana struktury FILE, konieczne bêdzie
wprowadzenie identycznej modyfikacji w jej
46
Programowanie
Elektronika dla Wszystkich
ABC... C
Rzutowanie
konwersja zakresu wartoci zmiennej
Podczas wykonywania obliczeñ pewne przekszta³ce-
nia odbywaj¹ siê automatycznie. Zgodnie ze standar-
dem ANSI C nie wykonuje siê dzia³añ na liczbach
o rozmiarze mniejszym ni¿ int. Oznacza to w prakty-
ce, ¿e jeli dodajemy do siebie dwie liczby typu
uint8_t, przed wykonaniem dodawania zostan¹ one
zmienione na unsigned int (u nas: 16 bitów, bez
znaku). Nie protestuj, sprawdzaj¹c wygenerowany
kod. Prawdopodobnie znajdziesz tam w³anie proste
dodawanie omiobitowe. Jest to wynik odpowiedniej
optymalizacji, a to, o czym w³anie napisa³em, ma
wa¿ne konsekwencje, dotycz¹ce samych wyników
obliczeñ, o czym przekonamy siê za moment.
Jeli wykonamy dzia³anie arytmetyczne na dwóch
argumentach ró¿nego typu, ten, którego typ posiada
mniejszy zakres wartoci, zostanie zamieniony na typ
drugi o wiêkszym zakresie. To proste. Wykonujemy
dodawanie dwóch zmiennych. Jedna jest zmienn¹
32-bitow¹, druga 16-bitow¹. Niezale¿nie od tego ilu
bitowa jest zmienna, do której wpisujemy wynik,
zmienna 16-bitowa zostanie potraktowana jako posia-
daj¹ca 32 bity. Wydaje siê to oczywiste i m¹dre w
pamiêci nie przechowujemy procedur na ka¿d¹ ewen-
tualnoæ, jak mno¿enie 8x8, 16x8, 16x16, 32x8,
32x16... Znajd¹ siê tam tylko dzia³ania 16x16 oraz
32x32 (w WinAVR mo¿e pojawiæ siê tak¿e 64x64, ale
specyfikacja ANSI C tego nie normuje).
Na pocz¹tku nie musisz znaæ wszystkich regu³.
Zapamiêtaj dwie proste zasady:
1. Zmienna o mniejszym zakresie wartoci zmie-
niana jest na typ o zakresie wiêkszym.
2. Nie wykonujemy dzia³añ na typie mniejszym
ni¿ int.
Typ wartoci wykonywanego dzia³ania jest taki
sam jak argumenty (po przekszta³ceniu). Oznacza to,
¿e dzia³anie 16x16 da wynik 16-bitowy. Tymczasem,
aby pomieciæ wszystkie mo¿liwe wyniki, powinien
on mieæ raczej 32 bity!
Tutaj z pomoc¹ przychodzi nam mo¿liwoæ rzuto-
wania. W nawiasie okr¹g³ym przed argumentem, któ-
rego zakres chcemy zmieniæ, umieszczamy nazwê
typu. Kompilator zinterpretuje to tak, ¿e znajduj¹c¹ siê
z nim wartoæ ma traktowaæ, jakby by³a typu podane-
go w nawiasie. Dokona wiêc albo zwiêkszenia liczby
bitów, albo jego obciêcia. Zwiêkszanie liczby bitów
odbywa siê zawsze w taki sposób, ¿e argument nie
zmienia swojej wartoci (jeli tylko to mo¿liwe; wynik
przekszta³cenia liczby ujemnej na typ bez znaku nie
jest okrelony przez standard).
Najlepiej wyjani zasadê obiecany przyk³ad.
Spójrz na listing 69. To kompletny kod, jaki nale¿y
wpisaæ do pliku main.c ostatniej wersji naszego pro-
gramu. Rysunek 44 pokazuje dane odebrane przez
komputer. Na czerwono oznaczy³em nieprawid³owe
wyniki obliczeñ. Przyjrzyjmy siê, jak zostan¹ wyko-
nane kolejne dzia³ania:
Mno¿one s¹ dwie zmienne omiobitowe. Wynik
mno¿enia zosta³ dobrany tak, ¿e wykracza poza
dostêpny zakres (prawid³owy wynik to 20 000). Jed-
nak zgodnie z zasad¹ 2 obliczenia s¹ wykonywane na
typach int, tak wiêc 16-bitowych. Dopiero po podzie-
leniu przez sta³¹ liczbê (100) wynik jest zamieniany na
postaæ omiobitow¹.
Mno¿ymy dwie zmienne 16-bitowe. Prawid³owy
wynik mno¿enia to 120 000, co nie daje siê zapisaæ na 16
bitach. W tym momencie powstaje b³¹d. Nic nie pomaga
ju¿ podzielenie wyniku przez 10, co sprowadza go do
wartoci 12 000, która mieci siê na szesnastu bitach.
Sytuacja praktycznie identyczna jak w . Rzu-
towanie umieszczone w z³ym miejscu. Najpierw
wykonane zostanie dzia³anie typu 16x16 z wynikiem
16-bitowym, dopiero uzyskany wynik zostanie rozsze-
rzony do 32 bitów. Wynik obliczenia bêdzie niepra-
wid³owy.
Prawid³owe podejcie do problemu. Ka¿emy
kompilatorowi wykonaæ mno¿enie typu 32=32x32.
32-bitowy wynik dzielimy przez 10. Dopiero teraz
wynik jest przycinany do 16 bitów. Wynik prawid³o-
wy. Dobrym zwyczajem jest tak¿e ujêcie ca³ego obli-
czenia w nawiasy oraz kolejne rzutowanie na typ
odpowiadaj¹cy zmiennej, do której zapisujemy wynik.
Mniej elegancki sposób uzyskania prawid³owe-
go wyniku. Dziêki zasadzie 1 z tej ramki dzia³anie
32x16 zostanie potraktowane jako 32x32.
Mo¿na powiedzieæ, ¿e to typowy przyk³ad, na
którym niejeden siê ju¿ wy³o¿y³. Z naszego punktu
widzenia mo¿e wszystko wygl¹da prawid³owo, ale
mimo tego, ¿e wynik mno¿enia 16x16 chcemy zapisaæ
do zmiennej 32-bitowej, rozszerzenie wyniku jest
wykonywane dopiero po skoñczeniu mno¿enia. Efekt?
Nieprawid³owy wynik.
Zrzutowanie jednej ze zmiennych na typ 32
bitowy. Ca³e dzia³anie zostanie wykonane jako
32x32=32. Wynik jest zgodny z oczekiwanym.
Specjalnie wybra³em przyk³ady bardzo charakte-
rystyczne. Nie wyczerpuj¹ one oczywicie wszystkich
mo¿liwoci. Masz teraz przed sob¹ program, który
pozwoli Ci eksperymentowaæ. Spróbuj zmieniæ zapi-
sane w nim dzia³ania tak, aby przetestowaæ tak¿e jak
zachowa sie przy przepe³nieniu dodawanie. Sprawd,
jak WinAVR radzi sobie z rzutowaniem liczby ujem-
nej na typ bez znaku.
Aby prawid³owo operowaæ rzutowaniem w pro-
gramie, potrzeba praktyki. Konieczna jest zdolnoæ
przewidywania, jakie wartoci mog¹ przyj¹æ argumen-
ty w konkretnej aplikacji.
Rys. 44 Wynik dzia³ania programu
z listingu 69.
Listing 68 - sposób na pozbycie siê zbêdnego ostrze¿enia
fcheat_file fileRS ==
FCHEAT_STATIC_FDEVOPENWR((rs_put,, rs_get));;
FILE** fRS == ((
vvooiidd
**))&&fileRS;;
ABC... C
Kilka dodatkowych s³ów o printf
Jak pisaæ/czytaæ ³añcuch formatowania
W poprzedniej czêci pokaza³em Ci, z jakich elemen-
tów sk³ada siê ³añcuch formatowania. Obrazek w tej
ramce pokazuje, jak nale¿y rozumieæ teksty widoczne
na listingu 69. Specjalnie wybra³em bardziej rozbu-
dowan¹ formê, gdzie korzystamy ze zmiennej long
int zamiast zwyklej int.
Kolejne zastosowanie rzutowania
odpowiedni typ argumentów
Zauwa¿ ponadto, ¿e w przyk³adzie widocznym na lis-
tingu 69 rzutowanie wykorzystane zosta³o nie tylko
do obliczeñ. Wykonujemy je tak¿e podczas podawa-
nia argumentów dla funkcji fprintf. O ile w przypad-
ku zwyk³ej funkcji nie ma to znaczenia zmienna
zostanie automatycznie przekszta³cona do takiej,
jakiej wymaga funkcja, o tyle w przypadku funkcji
z otwart¹ list¹ argumentów kompilator nie wie, jakie-
go typu argumentów funkcja siê spodziewa. Gdyby-
my wprowadzili w tym miejscu zmienn¹ omiobito-
w¹, zostanie ona jako taka umieszczona na stosie.
Umieszczenie przed ka¿d¹ ze zmiennych operatora
rzutowania zapewnia nas, ¿e nawet jeli typ zmiennej
jest nieprawid³owy, zostanie on odpowiednio prze-
kszta³cony przed wys³aniem na stos. Dobrym zwycza-
jem jest stosowanie takiego rzutowania, nawet jeli
jako argument podajemy zmienn¹ typu uint16_t albo
int16_t. My wiemy, ¿e jest on równowa¿ny z int. Jed-
nak jeli przesi¹dziemy siê na kompilator dla innego
mikrokontrolera, ta zasada nie musi byæ spe³niona.
Pamiêtaj, ¿e zgodnie z ANSI C, typ int mo¿e mieæ 16
albo 32 bity jest to zale¿ne od docelowej maszyny.
kopii fcheat_file. Jest to dzia³anie bardzo
proste. Mam jednoczenie znakomit¹ infor-
macjê dla wszystkich chc¹cych unikn¹æ
koniecznoci stosowania takich sztuczek
w przysz³oci.
Mimo wszystko opisane dzia³anie by³o
znakomitym pretekstem do pokazania kodów
ród³owych naszego rodowiska oraz g³êb-
szego zapoznania siê z wewnêtrzn¹ zasad¹
dzia³ania strumieni.
Rzutowanie
Poruszylimy dzi trudny temat. Na zakoñ-
czenie zajmiemy siê czym, co przy na-
szych aktualnych mo¿liwociach po-
winno byæ proste i przyjemne. Znaj¹c ju¿
funkcjê printf (oraz jej odpowiednik:
fprintf), uzyskalimy narzêdzie umo¿li-
wiaj¹ce nam wykonanie eksperymentów
dotycz¹cych rzutowania. Mamy teraz ca-
³¹ komputerow¹ konsolê do wywietlania
wyników ;).
R a m k i :
ABC... C Rzu-
towanie kon-
wersja zakresu wartoci
zmiennej oraz ABC... C
Rzutowanie wskanika,
przedstawiaj¹ obiecane
od pewnego ju¿ czasu
informacjê o tym, jak do-
k³adniej dzia³a rzutowa-
nie i do czego s³u¿y.
Poeksperymentuj z ko-
dami z listingów 69
oraz 70 obrazuj¹cych
wykorzystanie obu ty-
pów rzutowania.
Jeli wci¹¿ masz
w¹tpliwoci, jak dzia³a
³añcuch formatowania
w funkcji printf, ram-
ka Kilka dodatko-
wych s³ów o printf byæ mo¿e rozwieje Two-
je w¹tpliwoci. Spróbuj wróciæ do poprzed-
niej czêci, gdzie pojawi³ siê dok³adny opis
tworzenia odpowiedniego opisu do³¹czanych
do printfa argumentów.
Na koniec, w ostatniej ju¿ ramce, przedsta-
wiam dodatkowe informacje o wskaniku na
typ pusty. Nazwa brzmi byæ mo¿e powa¿niej,
ni¿ sprawa naprawdê wygl¹da. Ramka wy-
jania miêdzy innymi sens kodu z listingu 68.
Problem, o którym pisalimy, zauwa¿y³o wiêcej
u¿ytkowników WinAVR. Nieca³y tydzieñ przed
oddaniem tego tekstu do sk³adu nowa wersja jest ju¿
dostêpna i problem, z którym dzi walczylimy,
zosta³ tam rozwi¹zany. Rozwi¹zanie to jest doæ
podobne do zaproponowanego tutaj, tak wiêc zdo-
byta wiedza u³atwi zrozumienie wprowadzonych
zmian.
47
Programowanie
Elektronika dla Wszystkich
Listing 69 Wp³yw ograniczonego zakresu zmiennych
na przebieg obliczeñ oraz rozwi¹zanie za pomoc¹ rzutowania.
#include <avr/io.h>
#include <stdio.h>
#include <avr/pgmspace.h>
#include rs.h
#include harddef.h
#include fcheat_stdio.h
fcheat_file fileRS == FCHEAT_STATIC_FDEVOPENWR((rs_put,, rs_get));;
FILE** fRS == ((
vvooiidd
**))&&fileRS;;
iinntt
main((
vvooiidd
))
{
uint8_t u8a,, u8b;;
uint16_t u16a,, u16b,, u16c;;
uint32_t u32a;;
///////////////////////////////////////////////
// Inicjacja portu szeregowego
((......))
// Koniec inicjacji
///////////////////////////////////////////////
fputs_P((PSTR((
START\r\n
)),, fRS));;
u8a ==
100
;;
u8b ==
200
;;
u8a == ((u8b ** u8a)) //
100
;;
fprintf_P((fRS,, PSTR((
8x8 Wynik: %u\r\n
)),,
((
uunnssiiggnneedd iinntt
))u8a));;
u16a ==
200
;;
u16b ==
600
;;
u16c == ((u16a ** u16b)) //
10
;;
fprintf_P((fRS,, PSTR((
16x16 Wynik: %u\r\n
)),,
((
uunnssiiggnneedd iinntt
))u16c));;
u16c == ((uint32_t))((u16a ** u16b)) //
10
;;
fprintf_P((fRS,, PSTR((
(32)(16x16) Wynik: %u\r\n
)),,
((
uunnssiiggnneedd iinntt
))u16c));;
u16c == ((((uint32_t))u16a ** ((uint32_t))u16b)) //
10
;;
fprintf_P((fRS,, PSTR((
(32)16x(32)16 Wynik: %u\r\n
)),,
((
uunnssiiggnneedd iinntt
))u16c));;
u16c == ((((uint32_t))u16a ** u16b)) //
10
;;
fprintf_P((fRS,, PSTR((
(32)16x16 Wynik: %u\r\n
)),,
((
uunnssiiggnneedd iinntt
))u16c));;
u32a == u16a ** u16b;;
fprintf_P((fRS,, PSTR((
32=16x16 Wynik: %lu\r\n
)),,
((
uunnssiiggnneedd lloonngg iinntt
))u32a));;
u32a == ((uint32_t))u16a ** u16b;;
fprintf_P((fRS,, PSTR((
32=(32)16x16 Wynik: %lu\r\n
)),,
((
uunnssiiggnneedd lloonngg iinntt
))u32a));;
fputs_P((PSTR((
KONIEC\r\n
)),, fRS));;
}
ABC... C
Rzutowanie wskanika
Rzutowanie wskanika powinno byæ prostsze do zro-
zumienia ni¿ przedstawione poprzednio rzutowanie
zakresu zmiennej. Pamiêtaæ musimy, ¿e dzia³anie
takie ma sens jedynie miêdzy wskanikami na obiek-
ty które maj¹ miêdzy sob¹ co wspólnego. Spójrz na
listing 70. Jest to kolejna modyfikacja ostatniego pro-
gramu, która umo¿liwia wypróbowanie, jak dzia³a
rzutowanie wskaników. Wskanik traktowany jest
jako adres do pewnego obszaru pamiêci. Zauwa¿, ¿e
w nowo tworzonej strukturze, któr¹ nazwa³em nor-
malna, sk³adowe umieszczone s¹ w pamiêci w taki
sposób, ¿e sk³adowa o nazwie a pojawia siê jako
pierwsza, a sk³adowa o nazwie d jako ostatnia.
Struktura odwrotna odwraca tê kolejnoæ oraz posia-
da o jedno pole mniej. Spójrz na rysunek 45 wynik
dzia³ania naszego programu. Zauwa¿, jaki efekt daje
dostêp za pomoc¹ wskanika typu odwrotna do
obszaru pamiêci zajmowanego przez strukturê n1
typu normalna. £atwo zauwa¿yæ, ¿e do sk³adowej d
zawartej w n1 nie jestemy w stanie w ogóle dostaæ
sie poprzez wskanik po1. Sk³adowe struktury
umieszczane bêd¹ w pamiêci w kolejnoci, na jakie
wskazuje kolejnoæ ich deklarowania. Nie ma to nic
wspólnego z ich nazw¹.
Uwaga. Nawet gdyby obie struktury z listingu
70 by³y identyczne, dla kompilatora i tak s¹ to dwa
ró¿ne typy danych. Oznacza to, ¿e przy kopiowa-
niu ich wskaników niezbêdne jest rzutowanie albo
na typ docelowy (jak na listingu 67), albo poprzez
wskanik typu void (jak na listingu 68).
Listing 70 Rzutowanie wskaników.
ttyyppeeddeeff ssttrruucctt
normalna
{
int8_t a,, b,, c,, d;;
} normalna;;
ttyyppeeddeeff ssttrruucctt
odwrotna
{
int8_t c,, b,, a;;
} odwrotna;;
iinntt
main((
vvooiidd
))
{
normalna n1;;
odwrotna** po1 == ((odwrotna**))&&n1;;
///////////////////////////////
// Inicjacja portu szeregowego
((......))
// Koniec inicjacji
///////////////////////////////
fputs_P((PSTR((
START\r\n
)),, fRS));;
n1..a ==
1
;;
n1..b ==
2
;;
n1..c ==
3
;;
n1..d ==
4
;;
fprintf_P((fRS,,
PSTR((
odwrotna:\r\na=%d\r\nb=%d\r\nc=%d\r\n
)),,
((
iinntt
))po1-->>a,, ((
iinntt
))po1-->>b,, ((
iinntt
))po1-->>c));;
fputs_P((PSTR((
KONIEC\r\n
)),, fRS));;
}
Rys. 45 Wynik
dzia³ania progra-
mu z listingu 70.
Podsumowanie
Nie zniechêcaj siê, jeli czego nie zrozumia³e.
Wszystkie kody prezentowanych programów
bêd¹ jak zwykle dostêpne na stronach Elporta-
lu. Siêgaj do nich mia³o, zmieniaj je, ekspery-
mentuj. To chyba najlepszy sposób na zdobycie
nowej wiedzy. Jeli jeszcze tego nie zrobi³e
postaraj siê zdobyæ kody ród³owe biblioteki
stdio, zgodnie z opisem w tym odcinku.
W nadchodz¹cym odcinku zobaczymy
zmiany, jakie nast¹pi³y w nowym WinAVR.
Nowe rodowisko jest ju¿ dostêpne, jednak nie
instaluj go jeszcze teraz, jeli nie czujesz siê w C
pewnie. Niektóre z wprowadzonych zmian spo-
woduj¹ niestety, ¿e czêæ z napisanych przez nas
programów nie bêdzie dzia³aæ prawid³owo.
Czêæ 9 bêdzie trochê spokojniejsza.
Poznalimy du¿o nowych elementów C i teraz
bêdziemy zajmowaæ siê przyk³adami ich
zastosowania.
Rados³aw Koppel
radoslaw.koppel@elportal.pl
"&
Programowanie
Elektronika dla Wszystkich
ABC... C
Wskanik na typ pusty
Pozna³e ju¿ typ zmiennej o nazwie void. Powie-
dzia³em Ci wtedy, ¿e jest to specyficzny typ, który nie
istnieje. Nie ma sensu tworzenie zmiennej typu void
i taka próba zostanie zg³oszona przez kompilator jako
b³¹d. Jedynym przeznaczeniem tego dziwnego tworu
jest oznaczenie w pewnych miejscach, ¿e ¿adnych
zmiennych nie ma. Ze s³owa kluczowego void korzy-
stalimy na razie przy tworzeniu funkcji. Funkcja
mo¿e pobieraæ albo zwracaæ wartoæ typu pustego, co
oznacza, ¿e... nic nie pobiera albo nic nie
zwraca.
Zupe³nie inne znaczenie ma jednak wskanik na
obiekt typu pustego. Utworzenie takiego wskanika
jest jak najbardziej prawid³owym dzia³aniem:
vvooiidd
** pv;;
Wskanik taki ma pewne szczególne w³asnoci.
Przede wszystkim nie wolno na nim wykonywaæ
jakichkolwiek dzia³añ, o jakich mowa by³a w przy-
padku wskaników. Nie mo¿na go inkrementowaæ czy
dekrementowaæ, dodawaæ do niego wartoci, nie
mo¿na odj¹æ od siebie dwóch wskaników tego typu...
Teraz trochê o tym, co nam wolno. Wskanik typu
void mo¿e byæ bez problemu zrzutowany na dowolny
inny wskanik. Wiêcej nawet. O ile normalnie nie
wolno, bez rzutowania, kopiowaæ do wskanika dane-
go typu zawartoci wskanika innego typu (patrz lis-
ting 70 w drugiej linii funkcji g³ównej odbywa siê
takie rzutowanie), o tyle wskanik typu void jest jedy-
nym elementem, dla którego jest to mo¿liwe. Do
wskanika typu void mo¿emy wpisaæ zawartoæ inne-
go typu wskanika bez koniecznoci rzutowania...
oraz w drug¹ stronê mo¿emy zapisaæ go do dowol-
nego wskanika bez koniecznoci rzutowania.
W³aciwoæ ta u³atwia czasami zapis tam, gdzie
typ wskazywanego obiektu nie ma znaczenia. Zobacz-
my na przyk³ad deklaracjê funkcji memcpy, któr¹
wstêpnie poznalimy w ramce omawiaj¹cej struktury:
Gdyby nie specyficzne w³asnoci wskanika do
typu pustego, jej wywo³anie musia³oby przebiegaæ
nastêpuj¹co:
memcpy((((
vvooiidd
**))&&s1,, ((
vvooiidd
**))&&s2,,
ssiizzeeooff
((s1))));;
Zamiast tego jednak, piszemy znacznie krócej:
memcpy((&&s1,, &&s2,,
ssiizzeeooff
((s1))));;
Ponadto, jak pokaza³ nam listing 68, wskanik
typu void pozwala nam czasami pozbyæ siê niektó-
rych wyrzucanych przez kompilator ostrze¿eñ. Ele-
ment ten jest po prostu mniej pilnowany i kompilator
musi za³o¿yæ, ¿e stosuj¹c go, sami wiemy, co robimy.
vvooiidd
**memcpy((
vvooiidd
**,,
ccoonnsstt vvooiidd
**,, size_t));;
R
E
K
L
A
M
A
R E K L A M A