Programski jezik C je proceduralni programski jezik, nastao 1972. godine. Autor jezika je Denis Riči, a nastao je u istraživačkom centru Bell Laboratories u Nju Džersiju za potrebe operativnog sistema UNIX. Proslavivši se svojom brzinom, postao je jezik izbora za mnoge buduće projekte, poput različitih verzija Windows-a i UNIX-a, ali i u aplikativnom programiranju za mnogobrojne aplikacije tokom decenija. I danas se koristi u velikoj meri, prvenstveno za sistemsko programiranje.
Struktura C programa
Program pisan u jeziku C se sastoji od nekoliko elemenata:
- promenljive – predstavljaju podatke nad kojima se vrši obrada, a takođe mogu predstavljati i hardverske ulaze/izlaze kao i interne registre;
- funkcije – elementi koji opisuju sam tok programa, odnosno realizuju algoritam. Same funkcije, predstavljaju potprograme;
- preprocesorske direktive – komande kojima se mogu dodati novi elementi jeziku, definisati simbolička imena, isključiti pojedini delovi programskog koda iz prevođenja kao i zadati specifična uputstva prevodiocu za generisanje konačnog koda;
- komentari – proizvoljan tekst čija je jedina svrha objašnjavanje i dokunemtovanje pojedinih delova programa.
Ovde navedeni elementi, biće detaljnije opisani u nastavku.
Promenljive
U savremenim programskim jezicima, definisanje tipa promenljive je od velikog značaja. Biranje adekvatnog tipa promenljive za svaki podatak je od još većeg značaja u sistemima sa ogramičenim resursima, kao što su mikrokontrolerski sistemi. Time se obezbeđuje da se ni jedan podatak ne skladišti u memorijskom prostorui većem nego što zaista treba. Sa druge strane, prevodilac strogo vodi računa o tipu svakog podatka i može da upozori programera u slučaju da je napravio neki previd – npr. pokušao da obavi nekakvu operaciju sa podacima nekompatibilnog tipa. Kod novijih prevodilaca (uticaj jezika C++), moguće je definisati i konstantnost promenljivih određenog tipa. Veoma važni tipovi podataka tipični za jezik C su nizovi i pokazivači. Za svaki tip podataka može se izvesti odgovarajući niz ili pokazivač. Posebnu vrstu tipova promenljivih čine strukture i unije koje ćemo takođe ukratko pomenuti.
Tip promenljive u širem smislu, mogao bi da podrazumeva i vrstu memorije u kojoj se skladišti, način kreiranja promenljive kao i oblast važenja promenljive.
Tipovi promenljivih
Promenljive se u jeziku C definišu na sledeći način:
[mem/scope/const] tip ime_promenljive [= vrednost];
Delovi unutar uglastih zagrada su opcioni, dakle ne moraju se uvek navesti. Ako se navede deo na početku, pobliže se određuje neka od karakteristika tipa promenljive u širem smislu (način kreiranja, oblast važenja itd.). Promenljiva se može i inicijalizovati odmah po definisanju navođenjem opcionog dela sa desne strane.
Osnovni tipovi promenljivih
char – osnovna namena je skladištenje jednog karaktera kodiranog ASCII (mada bi mogao biti i bilo koji drugi osmobitni kod) kodom. Tipičan je po tome da je to uvek najkraći tip (po pitanju memorijskog prostora) na datom sistemu, tj. ima najmanji broj bita, a to je po pravilu 8;
int – standardni označeni celobrojni tip. Označeni znači da skladištena vrednost može biti i pozitivna i negativna, a u operacijama sa tom vrednošću vodiće se računa o znaku. Vrednost može biti samo ceo broj. Način predstavljanja broja je binarni, negativni brojevi se predstavljaju u komplementu dvojke. Najčešće se vrednost skladišti kao 16-bitna na mikrokontrolerskim sistemima, međutim to može biti i drugačije. Ako je procesor za koji se program prevodi ima registre od 32 ili više bita, tip int se skladišti sa istim tim brojem bita;
short – kratka celobrojna vrednost (označena). Po pravilu 16-bitna promenljiva. Sve ostalo što je rečeno za tip int, važi i ovde;
long – duga celobrojna vrednost (označena). Po pravilu 32-bitna promenljiva. Sve ostalo što je rečeno za tip int, važi i ovde;
unsigned – ovo je zapravo prefiks koji bilo koji od gore navedenih tipova pretvara u neoznačenu celobrojnu vrednost iste dužine.Ako se navede samo unsigned, podrazumeva se unsigned int. Primetimo, da se u računskim operacijama i tip char smatra označenom vrednošću, a sa unsigned char dobijamo neoznačenu varijantu;
float – realna promenljiva, tj. vrednost predstavljena u pokretnom zarezu. Mikrokontrolerski sistemi po pravilu nemaju hardverski implementirane operacije sa ovakvom vrstom podataka. Zbog toga, korišćenje ovog tipa obično troši znatne resurse procesora, kako u pogledu radnog i programskog skladišnog prostora, tako i u pogledu procesorskog vremena;
double – realna promenljiva, sve rečeno za tip float važi i ovde. Razlika je u broju bita koji se koristi za skladištenje promenljive. Ovaj prostor je veći u slučaju tipa double, a time se povećava i broj značajnih cifara koje se pamte za ovakvu promenljivu, ali se troši i više memorije;
bit – preomenljiva dužine jedan bit. Ovaj tip promenljive, osoben je za pevodioce za mikrokontrolerske sisteme. Obično se taj bit skladišti u bit-adresabilnoj zoni RAM memorije mikrokontrolera. Tip bit, obično ne postoji kod prevodioca za veće računarske sisteme.
Konstantne promenljive
Navođenjem ključne reči const ispred tipa promenljive, sve promenljive definisane u nastavku biće navedenog tipa, ali će imati dodatno ograničenje konstantnosti. Možemo smatrati da svaki tip (osnovni ili izvedeni) može imati običnu i konstantnu varijantu. Definisanje promenljive kao konstantne ima dve posledice:
- glavna karakteristika konstante je da njena vrednost neće biti promenjena tokom izvršavanja programa. Zbog toga, ako prevodilac u nekom delu programa primeti pokušaj dodeljivnja vrednosti konstantnoj promenljivoj, prijaviće grešku. Na ovaj način, prevodilac nam može pomoći u otkrivanju lgičkih grešaka u programu;
- budući da uvodimo ograničenje konstantnosti na promenljivu, prevodilac sa takvom promenljivom može da postupa drugačije i može automatski da uvede optimizacije i stvori kraći i efikasniji izvršni kod. Npr. konstantni niz može biti smešten u programsku mermoriju, ostavljajući više radne memorije na raspolaganju ostatku programa.
Budući da vrednost konstantne promenljive ne može naknadno biti promenjena, toj promenljivoj mora biti dodeljena vrednost na mestu gde se definiše. Npr.:
const int limit = 100;
Nizovi
Od svakog tipa, može se izvesti novi koji će predstavljati niz elemenata istog tog tipa. Način definisanja promenljive tip niz je sledeći:
<mem/scope/const> tip ime_prom[N] < = { e1, e2, … en} >;
U gornjem šablonu, opcioni deo ograničen je znacima < i >, pošto su znaci [ i ] u ovom slučaju deo sintakse. Novost je vrednost N unutar uglastih zagrada i predstavlja broj elemenata koliko niz treba da ima, od čega zavisi i količina memorija koliko će biti rezervisano za skladištenje sadržaja niza. Postoji mogućnost inicijalizacije niza pomoću notacije sa vitičastim zagradama. Samo ime promenljive ime_prom se u programu može koristiti kao pokazivač na tip promenljive od koje se niz sastoji i ima pokazuje na prvi element niza. Elementi niza, indeksirani su celim brojevima od 0 do N-1.
Mogu se definisati i višedimenzioni nizovi po obrascu koji je jasan iz sledećeg primera u kom se definiše trodimenzionalni niz tipa int:
int trodim[3][4][10];
Pokazivači
Za svaki tip, možemo deklarisati i odgovrarajući pokazivač. Promenljiva tipa pokazivač na određeni tip, predstavlja referencu na promenljivu tog određenog tipa. U implementacijama C prevodilaca, ta referenca obično predstavlja memorijsku adresu promenljive na koju pokazivač pokazuje. Definisanje promenljive pokazivača izvodi se na sledeći način:
[mem/scope/const] tip *ime_prom;
Posmatrajmo sledeći primer:
int *pbroj;
int element;
char tekst;
pbroj = &element; // ovo je u redu
pbroj = &tekst; // potencijalna greška
int element;
char tekst;
pbroj = &element; // ovo je u redu
pbroj = &tekst; // potencijalna greška
Promenljiva pbroj, definisana je kao pokazivač na tip int. Operator & vraća pokazivač na promenljivu ispred koje je stavljen. Promenljiva element je tipa int, pa je sve u redu. Međutim, kada pokušamo da iskoristimo pbroj da pokazuje na promenljivu tipa drugačijeg od int, različiti prevodioci mogu reagovati različito. Većina prevodilaca će izdati upozorenje i ipak generisati (ispravan) kod, a neki će prijaviti grešku. O ovome treba voditi računa jer ovakve situacije uvek kriju potencijalnu grešku.
Dozvoljeno je definisati pokazivač na tip void. Tom pokazivaču, dozvoljeno je dodeliti pokazivač na bilo koji tip.
Strukture i unije
Često se javlja potreba da neki objekat iz programa opišemo ne samo jendnim tipom podatka, nego grupom različitih tipova kojima se obraćamo preko istog imena. Takvu mogućost nam pruža izvedeni tip zvan struktura. Tip struktura definišemo na sledeći način:
struct primer
{
int a, b;
char ime[10];
struct primer *sledeci; // objašnjenje sledi
int a, b;
char ime[10];
struct primer *sledeci; // objašnjenje sledi
};
Ključna reč struct počinje deklaraciju novog izvedenog tipa koji će imati ime primer. Unutar vitičastih zagrada, navode se redom tipovi i imena promenljivih (baš kao da ih definišemo za program) koje će činiti strukturu. Struktura se može odmah i definisati tako što će se pre tačka-zareza koji okončava deklaraciju navesti ime promenljive (ili više njih) koje će postati promenljive tipa struktura. Druga je mogućnost da se negde u programu (gde je struktura prethodno deklarisana) navede:
struct primer strvar;
Strvar će time biti definisan kao promenljiva tipa struktura imena primer. primer je identifikator strukture (tipa), a strvar identifikator promenljive. Iz neke funkcije odakle je vidljiva definisana struktura, elementima se pristupa operatorom tačka (.). Npr.
strvar.b = 6472;
Ako je pak definisan pokazivač na promenljivu tipa struktura, uobičajeni način pristupa članovima je operator ->. Primer:
struct primer var;
struct primer *pvar;
pvar = &var;
pvar -> b = 6472; // pristup preko pokazivača
struct primer *pvar;
pvar = &var;
pvar -> b = 6472; // pristup preko pokazivača
Može se još postaviti pitanje, kako možemo u deklaraciji strukture navesti pokazivač (u našem slučaju na strukturu čije je deklarisanje upravo u toku) na nešto što još nije (do kraja) definisano? U slučaju pokazivača, postoji izuzetak. Dozvoljeno je navoditi pokazivače na još nepoznate strukture jer je u trenutku definisanja pokazivača jedino bitno da je to struktura odgovarajućeg imena. Tek u trenutku prvog pristupa elementima strukture, opis kompletne strukture mora biti poznat prevodiocu.
Unije predstavljaju način da se kontrolisano na istom memorijskom prostoru skladište različiti tipovi podataka. Deklaracija je slična kao kod struktura:
Unije predstavljaju način da se kontrolisano na istom memorijskom prostoru skladište različiti tipovi podataka. Deklaracija je slična kao kod struktura:
union multi
{
int a;
char ime[10];
};
{
int a;
char ime[10];
};
Ovim je definisana nova unija multi. Navođenjem imena promanljive pre završnog tačka-zareza ili uobičajenom definicijom:
union multi univar;
Suštinska razlika između unije i strukture je što unija nema dva odvojena elementa koji su uvek na raspolaganju odvojeno. Na potpuno istom fizičkom prostoru se skladišti i celobrojna promenljiva a i niz karaktera ime. Pristup određenim elemntima je pomoću operatora tačka (.). Ako se izvrši
univar.a=10;
pokvariće se deo niza karaktera. Ako se pak izvrši
univar.ime[2] = ‘c’;
promeniće se celobrojan vrednost u a. Očigledna uloga unije je da bi se uštedelo na memorijskom prostoru, pri čemu se taj prostor ne koristi istovremeno za obe namene. Dalje, često se koriste za definisanje strukture bafera koji se koriste za učitavane podataka različite strukture. Na taj način se ista memorija efikasno koristi za višestruke svrhe.
univar.a=10;
pokvariće se deo niza karaktera. Ako se pak izvrši
univar.ime[2] = ‘c’;
promeniće se celobrojan vrednost u a. Očigledna uloga unije je da bi se uštedelo na memorijskom prostoru, pri čemu se taj prostor ne koristi istovremeno za obe namene. Dalje, često se koriste za definisanje strukture bafera koji se koriste za učitavane podataka različite strukture. Na taj način se ista memorija efikasno koristi za višestruke svrhe.
Oblast važenja promenljivih i njihov životni vek
Sva imena (promenljivih ili funkcija), u jeziku C se nazivaju identifikatorima. Što se imena (identifikatora) promenljivih tiče, svako ima tačno određenu oblast u programu iz kog se može koristiti, odnosno oblast važenja ili vidljivosti. Oblast važenja je često u uskoj sprezi sa životnim vekom promenljive, tako da ćemo obe osobine razmotriti zajedno i to navođenjem različitih mogućnosti koje su naraspolaganju i odgovarajućeg načina definisanja promenljive. Opcioni deo scope u gornjim obrascima se odnosi na ovo.
Automatske promenljive
Svaka promenljiva definisana unutar tela neke funkcije je podrazumevano automatska. Ipak, promenljiva se može eksplicitno definisati automatskom navođenjem ključne reči auto ispred definicije promenljive. No, to je gotovo u svakoj situaciji suvišno. Npr.:
auto int g;
Automatske promenljive jedino imaju smisla unutar funkcija jer su strogo lokalne i postoje samo dok se izvršava funkcija u kojoj su definisane. Strogo lokalne znači da se promenljiva definisana kao automatska unutar neke funkcije može koristiti samo unutar te funkcije, a nije joj moguće (barem ne pod njenim imenom) pristupiti iz neke druge funkcije. Kod većine C prevodilaca, njihova definicija mora da sledi na samom početku opisa funkcije. Ove se promenljive kreiraju i po potrebi inicijalizujz na početku izvršavanja funkcije u kojoj su definisane, a nakon potpunog završetka izvršavanja iste funkcije memorijski prostor koji zauzimaju se oslobađa. Klasični pristup je da se ove promenljive skladište na procesorskom steku, međutim, posebno kod mikrokontrolerskih sistema, to često nije slučaj zbog ograničenog ili nepostojećeg steka.
Važno je primetiti, da se svakim pozivom funkcije ove promenljive ponovo kreiraju i inicijalizuju. Ovo je posebno važno ako funkcija poziva samu sebe (rekurzija). Tada će pri svakom novom pozivu funkcije nastajati novi set promenljivih. Zbog ograničenih resursa, prevodioci za mikrokontrolerske sisteme često zabranjuju rekurziju.
Lokalne statičke promenljive
Ove promenljive su što se oblasti važenja tiče iste kao i automatske, odnosno moguće im je pristupiti samo iz funkcije u kojoj su definisane. Samim tim, moraju biti definisane na istom mestu kao i automatske, unutar neke funkcije. Razlikuju se u tome što se memorijski prostor za njih rezerviše i one bivaju inicijalizovane na početku izvršavanja programa. Na taj način, pri sledećem pozivu funkcije u kojoj je statička promenljiva definisana vrednost te promenljive se zatiče onakvom kakva je bila pri poslednjem izvršavanju te funkcije (nema ponovne inicijalizacije). Ovo ima odgovarajuće posledice i na rekurziju i može izazvati greške u algoritmu ako se izgubi iz vida.
Lokalne statičke promenljive se definišu dakle, unutar neke funkcije navođenjem ključne reči static inpred definicije promenljive. Npr.:
static int brojac;
Globalne promenljive
Kao što bi se očekivalo na osnovu naziva, ovim promenljivama može se pristupati iz celog programa, tj. iz svih funkcija koje pripadaju određenom programu. Izuzetak je jedino ako neka lokalna promenljiva (automatska ili statička) istog imena “pokriva” globalnu. Npr. ako je promenljiva int x definisana i kao globalna promenljiva i kao automatska u funkciji test, unutar te funkcije moćiće se pristupiti samo automatskoj promenljivoj x.
Globalnu promenljivu treba definisati bez posebnih ključnih reči van tela bilo koje funkcije. Takva promenljiva će biti dostupna (navođenjem njenog imena tj. identifikatora) unutar tela svake fnkcije koja pripada tom programu. Npr. promenljiva xg će biti dostupna iz funkcije t1, ali neće iz funkcije t2 jer je tu pokriva lokalna promenljiva istog imena):
int xg;
void t1()
{
xg = 6; // pristupa se globalnoj prom. xg
}
void t2()
{
int xg; // lokalna automatska promenljiva
xg = 3; // globalno xg je pokrivena lokalnim xg
}
void t1()
{
xg = 6; // pristupa se globalnoj prom. xg
}
void t2()
{
int xg; // lokalna automatska promenljiva
xg = 3; // globalno xg je pokrivena lokalnim xg
}
Važna i često korišćena mogućnost jezika C je da se program može razbiti u puno malih fajlova izvornog koda – modula. Globalne promenljive mogu i često moraju biti dostupni u svim tim modulima. Ne sme svaki od tih modula da definiše istoimenu promenljivu jer će to rezultovati greškom višestruke definicije, tj. memorijski prostor za istoimenu promenljivu će biti rezervisan više puta. Sa druge strane, ako se promenljiva ne definiše, prevodiocu će ime i tip promenljive biti nepoznati i javljaće grešku zbog nepoznatog identifikatora. Rešenje je u sledećem: definicija promenljive će biti prisutna samo u jednom modulu i zbog te definicije će biti rezervisan i odgovarajući memorijski prostor za tu promenljivu. Ostali moduli će sadržati samo obaveštenje prevodiocu o tome da je ta promenljiva negde već definisana, kakvog je tipa itd. Ovakvo obaveštenje se obično naziva deklaracijom. Deklaracija treba da je potpuno ista kao i definicija, jedino što ispred nje stoji ključna reč extern. Primer:
/* modul 1, definise globalnu promenljivu ‘gl_i’ */
int gl_i; // definise globalno gl_i
void f1_mod1()
{
gl_i = 3; // koristi globalnu promenljivu
}
…
/* modul 2, koristi globalnu promenljivu ‘gl_i’ */
extern int gl_i; // deklarise globalno gl_i za modul 2
// definisano je u modulu 1
void f1_mod2()
{
gl_i = 6; // koristi globalnu promenljivu
}
int gl_i; // definise globalno gl_i
void f1_mod1()
{
gl_i = 3; // koristi globalnu promenljivu
}
…
/* modul 2, koristi globalnu promenljivu ‘gl_i’ */
extern int gl_i; // deklarise globalno gl_i za modul 2
// definisano je u modulu 1
void f1_mod2()
{
gl_i = 6; // koristi globalnu promenljivu
}
Promenljive globalne na nivou modula
Postoji mogućnost da se definiše promenljiva koja će biti globalna na nivou modula (jednog fajla izvornog koda) i samim tim dostupna iz svake funkcije opisane unutar tog modula. Iz drugih modula, promenljiva neće biti dostupna, čak ni navođenjem deklaracije uz ključnu reč extern. Kao posledica toga, svaki modul može imati globalnu promenljivu istog imena, a sve će biti različite promenljive sa stanovišta celokupnog programa koji se dobija povezivanjem (linkovanjem) pojedinačnih modula. Postoji dva načina za definisanje ovakvih promenljivih.
Jedan način je da se promenljiva definiše izvan tela bilo koje funkcije navođenjem ključne reči static ispred definicije. Drugi način je da se promenljiva definiše unutar tela bilo koje funkcije navođenjem ključne reči extern ispred definicije. Oba načina imaju isti efekat.
Memorijske klase promenljivih
Kao što smo već više puta pomenuli, mikrokontrolerski sistemi predstavljaju osobene sisteme sa ograničenim resursima. Zbog toga i C prevodioci moraju imati neke nestandardne osobine i mogućnosti da se do detalja utiče na raspoređivanje pojedinih promenljivih u različite tipove memorija. Ovde prikazane mogućnosti i ključne reči uobičajene su za prevodioce namenjene sistemima sa Intel8051, ali postoji mogućnost da kod različitih implementacija postoje određene razlike. Zato se uvek mora konsultovati dokumentacija konkretnog prevodioca po ovim pitanjima. Ovde izloženo važi za prevodioce SDCC i Keil.
Deo označen sa mem u gornjem obrascu se odnosi na ovo. Dakle definiciju promelnjive može da prethodi jedna od sledećih ključnih reči:
code – promenljiva će biti smeštena u programskoj memoriji. Kontroler ne može da upisuje u ovu memoriju, tako da će se ovakve promenljive moći samo čitati.
data – promenljiva će se skladištiti u internom RAM-u i to u direktno adresabilnom delu. (Npr. kod 8051 od adrese 0 do 127.)
idata – promenljiva će se skladištiti u internom RAM-u i to u indirektno adresabilnom delu. (Npr. kod 8052, od adrese 128 do 256, kod 8051 ova memorija ne postoji.)
bdata – promenljiva će se skladištiti u internom RAM-u i to u bit-adresabilnom delu. (Npr. kod 8051 od adrese 32 do 63, a nadlje do 256 sve adrese deljive sa 8.)
xdata – promenljiva će se skladištiti u eksternom RAM-u. Za to u mikrokontrolerski sistem mora biti dodata spoljšnja memorija, a prevodilac treba konfigurisati u skladu sa tim, tj. treba podesiti opseg implementiranih adresa u spoljašnjoj memoriji.
pdata – promenljiva će se skladištiti u eksternom RAM-u i pristupaće joj se pomoću straničnog (paged) adresiranja. Kao i kod xdata, i za ovo se prevodilac mora konfigurisati.
sfr – promenljiva će predstavljati sfr (special function register) registar mikrokontrolera. Uključivanjem header fajla odgovarajućeg korišćenom mikrokontroleru (pomoću #include), biće definisani svi postojeći sfr registri. Napomenimo da ovo nije samo ključna reč koja prethodi promenljivu, nego poseban tip promenljive!
sbit – promenljiva će predstavljati bit promenljivu unutar nekog sfr registra. Kao i kod sfr tipa, uključivanje odgovarajućeg header-a definisaće sve postojeće. Napomenimo da ovo nije samo ključna reč koja prethodi promenljivu, nego poseban tip promenljive dužine jedan bit!
code – promenljiva će biti smeštena u programskoj memoriji. Kontroler ne može da upisuje u ovu memoriju, tako da će se ovakve promenljive moći samo čitati.
data – promenljiva će se skladištiti u internom RAM-u i to u direktno adresabilnom delu. (Npr. kod 8051 od adrese 0 do 127.)
idata – promenljiva će se skladištiti u internom RAM-u i to u indirektno adresabilnom delu. (Npr. kod 8052, od adrese 128 do 256, kod 8051 ova memorija ne postoji.)
bdata – promenljiva će se skladištiti u internom RAM-u i to u bit-adresabilnom delu. (Npr. kod 8051 od adrese 32 do 63, a nadlje do 256 sve adrese deljive sa 8.)
xdata – promenljiva će se skladištiti u eksternom RAM-u. Za to u mikrokontrolerski sistem mora biti dodata spoljšnja memorija, a prevodilac treba konfigurisati u skladu sa tim, tj. treba podesiti opseg implementiranih adresa u spoljašnjoj memoriji.
pdata – promenljiva će se skladištiti u eksternom RAM-u i pristupaće joj se pomoću straničnog (paged) adresiranja. Kao i kod xdata, i za ovo se prevodilac mora konfigurisati.
sfr – promenljiva će predstavljati sfr (special function register) registar mikrokontrolera. Uključivanjem header fajla odgovarajućeg korišćenom mikrokontroleru (pomoću #include), biće definisani svi postojeći sfr registri. Napomenimo da ovo nije samo ključna reč koja prethodi promenljivu, nego poseban tip promenljive!
sbit – promenljiva će predstavljati bit promenljivu unutar nekog sfr registra. Kao i kod sfr tipa, uključivanje odgovarajućeg header-a definisaće sve postojeće. Napomenimo da ovo nije samo ključna reč koja prethodi promenljivu, nego poseban tip promenljive dužine jedan bit!
Često se promenljive ovog tipa koriste da bi se definisao pristup registrima ili oblatima memorije specifičnim za određeni sistem (specifične namene). U takvom slučaju, header fajlovi ne sadrže definisane sve potrebne adrese, nego to moramo sami uraditi. Ovo je ujedno i primer nečega što se različito izvodi u slučaju prevodioca Keil i SDCC, tako da ćemo dati objašnjenje za oba slučaja. Cilj je dakle da se prevodiocu kaže da određenu promenljivu uskladišti baš na tačno određenoj adresi. Za primer ćemo uzeti promenljivu tipa unsigned char koja treba da se skladišti na adresi 0x8010 u eksternoj memoriji (xdata). Ime promenljive biće xmem_prom. Ovo važi i za ostale tipove memorije s tim da adresa mora biti takva da je na konkretnom sistemu zaista i implementirana.
Za prevodilac Keil:
xdata unsigned char xmem_prom _at_ 0x8010;
xdata unsigned char xmem_prom _at_ 0x8010;
Za prevodilac SDCC:
xdata unsigned char __at 0x8010 xmem_prom;
xdata unsigned char __at 0x8010 xmem_prom;
Često postoji potreba da se definiše neki bit unutar sfr registra kao posebna bit promenljiva. Ovo se posebno često koristi kod ulazno/izlaznih portova mikrokontrolera. Biće u pitanju promenljiva tipa sbit, ime promenljive će biti signal, a fizički će se nalaziti na trećem bitu porta P0 predstavljenog istoimenom sfr promenljivom.
Za prevodilac Keil:
sbit signal P0^3;
sbit signal P0^3;
Za prevodilac SDCC:
sbit __at 0x83 signal;
sbit __at 0x83 signal;
Vidimo da se kod Keil-a definiše po principu sfr_registar^bit_unutar_registra, a kod SDCC-a navođenjem direktne (bit adresabilne) adrese potrebnog bita unutar sfr-a.
Funkcije
Funkcije predstavljaju potprograme. Funkcija pod imenom main ima posebnu ulogu, jer je to funkcija koja će prva biti pozvana nakon starta programa tj. kod mikrokontrolerskih sistema nakon reseta. Do trenutka pozivanja funkcije main, biće inicijalizivane sve globalne promenljive koje su definisane sa inicijalizacijom.
U programskom jeziku C, sve funkcije su istog nivoa, dakle nikakva hijerarhija nije, niti može biti definisana među njima. Svaka funkcija je globalna i vidljiva ostalim funkcijama unutar jednog istog programa. Jedino, mogu postojati ograničenja po pitanju rekurzivnog i međusobnog pozivanja funkcija iz razloga objašnjenog kod automatskih promenljivih u prethodnom odeljku. Obrazac za definiciju funkcije u jeziku C je sledeći:
tip_funkcije ime_funkcije(tip_1 ime_1, tip_2 ime_2)
{
tip_a1 ime a11, ime_a12;
tip_a2 ime a21;
…
opis algoritma:
upravljačke strukture // komentar, C++ tip
+
operatori
…
/* komentar – klasični tip */
return(promenljiva ili vrednost tipa tip_funkcije);
}
{
tip_a1 ime a11, ime_a12;
tip_a2 ime a21;
…
opis algoritma:
upravljačke strukture // komentar, C++ tip
+
operatori
…
/* komentar – klasični tip */
return(promenljiva ili vrednost tipa tip_funkcije);
}
ime_funkcije može biti bilo koji identifikator koji važi za ispravan u jeziku C. tip_funkcije je tip vrednosti koju funkcija vraća. Ideja je da se funkcija pozove sa nekim ulaznim parametrima na osnovu kojih ona nešto računa, a rezultat proračuna vraća na mesto odakle je pozvana. Pošto bilo koja vrednost ili promenljiva u jeziku C mora biti određenog tipa, tako se mora definisati i tip onoga što funkcija vraća. Ako nije predviđeno da funkcija bilo šta vrati, treba je definisati kao da vraća tip void.
Kada se izvrši komanda return, preida se sa izvršavanjem funkcije i vraća se na mesto odakle je funkcija pozvana. Vrednost iza return, biće vraćena kao povratna vrednost na mesto poziva. U novim verzijama C prevodilaca, nije obavezna upotreba zagrada oko vrednosti koju return vraća.
Funkcija može imati proizvoljan broj argumenata koji moraju biti navedeni u definiciji kao parovi tip, ime. Svi ovi argumenti će se ponašati kao automatske promenljive definisane unutar tela funkcije, s tim da će biti inicijaizovani na one vrednosti sa kojima je funkcija sa neog drugog mesta pozvana. Kao primer, u gornjem obrascu, definisano je i nekoliko automatskih promenljivih, mada se ne mora definisati ni jedna, ako za to nema potrebe.
Kao što vidimo iz gornjeg obrasca, opis funkcije jeste algoritam koji treba da se izvrši, a sastoji se od upravljačkih struktura i operatora, a naravno i komentari mogu da stoje na mestima gde je to potrebno. Deo između dve vitičaste zagrade koje ograničavaju izvršni deo funkcije, nazivamo telom funkcije.
Kako program može biti razbijen i prevođen iz više modula, postavlja se pitanje kako se u modulu iz kog se neka funkcija poziva, a u okviru nje nije definisana, zna kakav tip vraća i kakvi su joj argumenti? To se postiže deklaracijom funkcija, što je veoma važno i za otkrivanje potencijalnih grešaka u programu. Ako se bilo gde u programu, van tela neke funkcije, navede prvi red iz gornjeg obrasca završen sa tačka-zarez (;), to će se smatrati deklaracijom funkcije.
tip_funkcije ime_funkcije(tip_1 ime_1, tip_2 ime_2);
Gde god se nakon deklarisanja pomene ime_funkcije, smatraće se da je u pitanju funkcija sa argumentima i tipom povratne vrednosti navedenim u deklaraciji. Uobičajeno je da se deklaracije svih funkcija grupišu u jedan header fajl koji će se uključiti u svaki jedan modul. Prevodilac će prijavii grešku ako bilo koji poziv ili definicija deklarisane funkcije odstupa od forme zadate u deklaraciji. Na taj način pomaže u otkrivanju grešaka. Napomenimo još, da je u okviru deklaracije dovoljno navesti samo ime tipa argumenta, a da se ime argumenta može izostviti.
Upravljačke strukture
Upravljačke strukture predstavljaju elemente pomoću kojih se postiže izvršavanje pojedinih delova programa u zavisnosti od nekih uslova i ponavljanje pojedinih delova alogitma. Pošto u programskom jeziku C ne postoji logički tip promenljive, uslov je svaki izraz čija se vrednost može izračunati, a uslov se smatra ispunjenim (tačnim) ako je vrednost različita od nule. U suprotnom (vrednost je jednaka nuli), smatra se da uslov nije ispunjen, tj. netačan je.
Dakle, u pitanju su uslovna grananja i petlje, poznate i iz drugih programskih jezika. Krenimo redom.
if uslov
Obrazac za if uslov je sledeći:
if(uslov)
{
operatori i upravljačke strukture ako jeste uslov
}
else
{
operatori i upravljačke strukture ako nije uslov
}
{
operatori i upravljačke strukture ako jeste uslov
}
else
{
operatori i upravljačke strukture ako nije uslov
}
Ključne reči su if i else. Vitičaste zagrade ograničavaju delove algoritma koji će se izvršiti ako je navedeni uslov ispunjen (deo odmah posle if(uslov)), odnosno ako nije ispunjen (deo posle else). else deo je opcioni i ne mora se navesti ako za tim nema potrebe. Nema potrebe ni za vitičastim zagradama ukoliko bi se unutar njih našao samo jedan izraz (naredba) ili samo jedna upravljačka struktura.
while petlja
Obrazac za while petlju je sledeći:
while(uslov)
{
operatori i upravljačke strukture
}
{
operatori i upravljačke strukture
}
Deo između vitičastih zagrada će se izvršavati sve dok je uslov ispunjen. Ukoliko uslov nije ispunjen u trenutku nailaska na petlju, deo unutar zagrada neće biti izvršen nijednom. Nije potrebno stavljati vitičaste zagrade ukoliko bi se unutar njih našao samo jedan izraz (naredba) ili samo jedna upravljačka struktura.
do – while petlja
Obrazac za do – while petlju je sledeći:
do
{
operatori i upravljačke strukture
}
while(uslov);
{
operatori i upravljačke strukture
}
while(uslov);
Odmah primećujemo da na početku petlje nemamo nikakav uslov, ključna reč do je tu samo da označi početak petlje. Petlja će se izvršavati sve dok je uslov ispunjen. Sadržaj petlje će se uvek izvršiti barem jednom jer se uslov proverava tek na kraju. U tome je suštinska razlika u odnosu na while petlju.
for petlja
Obrazac za for petlju je sledeći:
for(inicijalizacija; uslov; inkrement_operacija)
{
operatori i upravljačke strukture
}
{
operatori i upravljačke strukture
}
Telo petlje je isto kao i kod ostalih petlji i vitičaste zagrade nisu potrebne ako bi se između njih nalazio samo jedan izraz (naredba) ili samo jedna upravljačka struktura. Unutar zagrede, posle ključne reči for se nalaze tri celine. Razmotrimo ih.
inicijalizacija predstavlja samo jedan izraz (ili više izraza odvojenih zarezima). Ovaj će se izraz izvršiti samo jednom i to prilikom nailaska na petlju. Služi da bi se inicijalizovao brojač petlje i po pravilu je oblika index_petlje=početna_vrednost.
uslov je izraz koji se izračunava u neku vrednost (kao i kod prethodnih petlji) na osnovu koje će se odlučivati da li će se telo petlje još jednom izvršiti ili će se petlja napustiti.
inicijalizacija predstavlja samo jedan izraz (ili više izraza odvojenih zarezima). Ovaj će se izraz izvršiti samo jednom i to prilikom nailaska na petlju. Služi da bi se inicijalizovao brojač petlje i po pravilu je oblika index_petlje=početna_vrednost.
uslov je izraz koji se izračunava u neku vrednost (kao i kod prethodnih petlji) na osnovu koje će se odlučivati da li će se telo petlje još jednom izvršiti ili će se petlja napustiti.
inkrement_operacija je jedan izraz (ili više izraza odvojenih zarezima) koji će se izvršiti nakon svakog prolaska kroz petlju. Obično se koristi za promenu vrednosti indeksa petlje (obično inkrement ili dekrement operacija).
Sledeći primer predstavlja for petlju u kojoj se indeks i menja od 1 do 10, dakle petlja će se izvršiti 10 puta:
Sledeći primer predstavlja for petlju u kojoj se indeks i menja od 1 do 10, dakle petlja će se izvršiti 10 puta:
for(i=1; i<= 10; i++)
{
telo petlje
}
{
telo petlje
}
switch – case struktura
Switch – case struktura predstavlja strukturu višestrukog izbora. Obrazac sledi:
switch(izraz)
{
case vrednost_1:
// nastavak programa za slučaj 1
break;
case vrednost_2:
// nastavak programa za slučaj 2
// … može biti proizvoljno mnogo case stavki
case vrendost_N:
// nastavak programa za slučaj N
default:
// nastavak programa inače
}
switch(izraz)
{
case vrednost_1:
// nastavak programa za slučaj 1
break;
case vrednost_2:
// nastavak programa za slučaj 2
// … može biti proizvoljno mnogo case stavki
case vrendost_N:
// nastavak programa za slučaj N
default:
// nastavak programa inače
}
Izraz u zagradi posle ključne reči switch se izračunava. Program se nastavlja iza one ključne reči case iza koje se nalazi vrednost koja je jednaka rezultatu izračunavanja. Ako nijedna navedena vrednost nije jednaka rezultatu, program se nastavlja iza ključne reči default. Ključna reč default je opciona, taj deo ne mora biti naveden i u tom slučaju se izvršavanje programa, ako se reultat ne poklapa ni sa jednom vrednošću, nastavlja od mesta posle cele switch – case strukture. Primetimo da se nailaskom na sledeću case stavku izvršavanje ne nastavlja iza switch – case strukture, nego se nastavlja preko ostalih case stavki. Ako to ne želimo, pre sledećeg case moramo staviti ključnu reč break, kao što je to u obrascu učinjeno posle stavke case vrednost_1.
break i continue naredbe
Uvek se može javiti potreba da prekinemo izvršavanje petlje pre nego što to dozvoli definisani uslov. Upravo toj svrsi služi naredba break. Ako unutar tela petlje izvršimo break, program se odmah nastavlja od mesta neposredno nakon tela petlje. break obično ide unutar neke if strukture unutar petlje i prekida je pod uticajem nekog uslova.
Takođe se javlja potreba da se prolazak kroz petlju ponovi pre nego što je izvršavanje tela petlje stiglo do svog kraja. Toj svrsi služi naredba continue. Kada se na nju naiđe, telo petlje se izvršava ponovo, s tim da se uslov petlje ipak proverava (ako nije ispunjen, petlja se prekida), a kod for petlje se izvršava i navedena operacija inkrementiranja.
Napomenimo još da break i continue nisu predviđene za korišćenje unutar if uslova. Dakle break i continue su legalni samo unutar petlji. break se može koristiti i unutar switch-case strukture na način opisan ranije.
Matematički i logički operatori
U jeziku C, sve što nije deklaracija ili definicija, preprocesorska direktiva ili upravljačka struktura, predstavlja operator. Čak i dodeljivanje vrednosti promenljivoj predstavlja operator, odnosno binarnu operaciju. Analizirajmo ovo:
v = 3 * g * 6;
g i v predstavljaju promenljive tipa int, a 3 je kontanta. Pretpostavmo da promenljiva g ima vrednost 8. Operatori imaju definisane prioritete i prvo će se izvršiti ni višeg prioriteta, a potom oni nižeg. Ako su istog prioriteta, mogu se izvršavati redom na levo ili na desno zavisno od operatora. Množenje (*) ima viši prioritet od dodele (=), pa će se množenje prvo izvršiti. Dalje, množenja imaju isti prioritet, pa će se izvršavati s leva na desno jer je tako definisano u jeziku C za operator množenja. Dakle, prvo će se izračunati 3 * g. Rezultat te operacije je 18, pošto je g tipa int, i proizvod će biti tipa int. Ostaje da se izračuna 18 * 6. Naredni rezultat je 108, opet tipa int. Ostaje operacija v = 108. Oba operanda su tipa int, te nema potrebe za nikakvom konverzijom, jednostavno će v preuzeti vrednost 108, a rezultat operacije će takođe biti jednak 108, međutim pošto nema više nikakvih operacija u ovom izrazu, taj rezultat neće biti upotrebljen nizašta. Primetimo tačka-zarez (;) na kraju izraza – on je obavezan i označava kraj izraza. Takođe odvaja naredni izraz od prethodnog.
Ovde možete skinuti tabelu operatora koji se mogu koristiti u izrazima. Dati su po opadajućem prioritetu, a uz svaki je dat i redosled izvršavanja unutar iste klase prioriteta.
Preprocesor
Preprocesor je poseban deo prevodioca koji obrađuje izvorni kod programa pre bilo kakvog prevođenja u izvršni kod. Na taj način, moguće je da se neki delovi koda zamene ili da se neki delovi koda uklone iz programa pre nego što se prevođenje zapravo desi. Sve instrukcije namenjene preprocesoru, tzv. preprocesorske direktive, počinju karakterom #. Navešćemo one uobičajene koje se mogu naći kod svih prevodioca.
Uključivanje celog fajla u prevođenje
Direktivom #include možemo uključiti drugi fajl u prevođenje. Efekat je isti kao da se ceo sadržaj uključenog fajla nalazio na mestu gde se direktiva nalazila. U suštini, možemo bilo koji fajl uključiti u pevođenje na ovaj način. Međutim, uobičajeno je da na ovaj način ukljulujemo tzv. header fajlove (za razliku od ekstenzije .c koju imaju fajlovi sa C izvornim kodom, ovi obični imaju ekstenziju .h). Header fjlovi uglovnom sadrže deklaracije funkcija i preomenljivih potrebne za ispravno prevođenje modula u koji se uključuju. Postoje dve varijante ove direktive:
#include <ime_sistemskog_headera.h>
Fajl koji se uključuje sadrži komponente specifične za sistem ili okruženje za koje se program piše. Prevodilac će navedeni fajl naći u za to posebno definisanom direktorijumu.
#include “ime_korisničkog_headera.h”
Ovim se uključuju fajlovi koje piše korisnik i sadrži eklaracije vezane za konkretan projekat. Npr. deklaracije funkcija programa, deklarcije globalnih promenljivih, konstanti važnih za program, stringove itd. Uključeni fajl se traži među ostalim fajlovima sa izvornim kodom konkretnog projekta.
Definisanje makroa
Direktiva #define uvodi novo ime makroa. Npr.
#define MAX_LENGTH 100
Od mesta gde se naišlo na ovu direktivu, makro MAX_LENGTH će biti prepoznato kao novo ime koje treba zameniti novim tekstom, a to je u ovom slučaju broj 100. Tako da će sledeći isečak iz nastavka izvornog koda
int receive_buffer[MAX_LENGTH];
biti pretvoren u sledeće:
int receive_buffer[100];
biti pretvoren u sledeće:
int receive_buffer[100];
Makro pod imenom MAX_LENGTH, prepoznat je i zamenjen definisanim tekstom što je u ovom slučaju 100. Ovo je uobičajeni način da se unesu konstante u program. Često je potrebno da definišemo veći broj nizova određene dužine. Ako samu dužinu definišemo kao makro, i svuda koristimo makro umesto konkretnog broja, kasnija izmena te dužine u programu svešće se samo na predefinisanje makroa, a ne na desetine izmena u programu. Na taj način se izmena pojednostavljuje i smanjuje se mogućnost greške.
Pomoću iste direktive, moguća je i definicija makroa sa argumentima. Na taj način, moguće je implementirati i jednostavne funkcije koje mogu poboljšati čitljivost programa. Primer:
#define SQR(a) a*a
Kasnije, možem u program ubaciti sledeće
sq = SQR(g);
Ovo će se nakon preprocsiranja pretvoriti u
sq = g * g;
Kasnije, možem u program ubaciti sledeće
sq = SQR(g);
Ovo će se nakon preprocsiranja pretvoriti u
sq = g * g;
Na ovaj način, dobili smo jednostavnu pseudo-funkciju SQR sa jednim argumentom koja nam izračunava kvadrat nekog broja. Prilikom definisanja makroa u zagradi možemo navesti proizvoljan broj imena koja će predstavljati argumente. U nastavku, ta imena predstavljaju argumente kojima će se makro pozivati. Kada se makro primeni (kaže se razvije) realni argumenti će se ubacivati umesto imena koja predstavljaju određeni argument.
Takođe je moguće definisati prazan makro. Time će taj makro biti definisan, ali neće imati nikakvu vrednost:
#define VERZIJA_6
ovim će makro VERZIJA_6 postati definisan, ali bez ikakve vrednosti.
#define VERZIJA_6
ovim će makro VERZIJA_6 postati definisan, ali bez ikakve vrednosti.
Bilo koji makro se može ukloniti direktiviom #undef. Time makro tog imena prestaje da postoji. Sledeća direktiva uklanja prethodno definisani makro
#undef VERZIJA_6
#undef VERZIJA_6
Uslovno prevođenje
Sledeće direktive omogućuju da se pojedini delovi izvornog koda izuzmu iz prevođenja. Osnovna varijanta je sledeća:
#if KONSTANTNI_IZRAZ
… deo izvornog koda programa
#endif
#if KONSTANTNI_IZRAZ
… deo izvornog koda programa
#endif
Deo koji se nalazi između #if i #endif će biti uključen u prevođenje samo ako je KONSTNTNI_IZRAZ tačan. KONSTANTNI_IZRAZ može sadržati operacije poređenja (==, !=, <, <=, >, >=), operator negacije (!) kao i logičke operacije (&&, ||), ali samo između konstanti i drugih makroa koji mogu imati vrednost u trenutku preprocesiranja.
Postoje i dodatne mogućnosti uslovnog prevođenja kao što su #else i #elif. Prvi predstavlja dopunu #if strukturi i iz nje se navodi deo koji treba uključiti u prevođenje ukolko uslov uz #if nije ispunjen. #elif predstavlja kombinaciju else i naredne if direktive, te je iza nje neophodno navesti novi uslov. Kompletan obrazac bi dakle bio:
#if KONSTANTNI_IZRAZ_1
… deo izvornog koda programa 1
#elif KONSTANTNI_IZRAZ_2
… deo izvornog koda programa 2
#else
… deo izvornog koda programa 3
#endif
… deo izvornog koda programa 1
#elif KONSTANTNI_IZRAZ_2
… deo izvornog koda programa 2
#else
… deo izvornog koda programa 3
#endif
Često se makroi definišu da bi odredili prisutnost nekih opcija u programu ili neke elemene koji zavise od verzije softvera itd. U tu svrhu, u sistemskim header fajlovima, mogu se definisati se prazni makroi, kao na primer VER_6. Makro je izmišljen samo za potrebe ovog primera i znači da se radi o verziji 6 sistemskog softvera.
#ifdef VER_6
var = getinput(i_ch);
#else
var = getvar(i_ch);
#endif
var = getinput(i_ch);
#else
var = getvar(i_ch);
#endif
Na ovaj način, ako se radi o verziji 6 sistemskog softvera, uključiće se prva varijanta u prevođenje, u suprotnom druga. U ovakve svrhe, može se iskoristiti i direktiva defined IME_MAKROA, koja se može iskoristiti u konstantnim izrazima uz direktivu #if. Izračunava se u tačan izraz ako je IME_MAKROA definisano, u suprotnom rezulatat je netačan izraz. Primetimo da direktivi defined ne prethodi # karakter. Na taj način #if defined VER_6 je ekvivalentan #ifdef VER_6. No prva varijanta omogućuje pravljenje složenijih izraza uključivanjem dodatnih uslova osim definsanosti Npr.
#if defined VER_6 && IN_CH > 3
Uslov će biti tačan ako je makro VER_6 definisan i makro IN_CH ima vrednost veću od 3.
Komentari
Komentari u samom programskom tesktu su veoma korisni i neophodni za kasnije održavanje i dopunjavanje programa, kao i za timski rad kada i drugi prorameri treba da se upoznaju sa delovima koda koje su napisali drugi.
Savremeni C prevodioci podržavaju dve vrste komentara. Klasični tip komentara je bilo koji tekst koji se nalazi između znakova /* i */. U pitanju je simetrična oznaka pomoću koje su jasno označeni početak i kraj komentara. Upravo zbog toga se ova vrsta komentara koristi za komentare i uputstva koji se prostiru u dužini od više redova. Takođe se često koristi da bi se pojedini delovi koda privremeno isključili iz prevođenja, najčešće iz razloga testiranja. Primer:
/* Sistemski softver za
upravljanje pogonom lifta
*** GLAVNI MODUL ***
2006, FTN Novi Sad */
void main()
{
/* inicijalizacija sistema
odmah nakon reseta */
init();
main_loop() // pokretanje glavne petlje
}
upravljanje pogonom lifta
*** GLAVNI MODUL ***
2006, FTN Novi Sad */
void main()
{
/* inicijalizacija sistema
odmah nakon reseta */
init();
main_loop() // pokretanje glavne petlje
}
Dati primer predstavlja tipičan glavni modul nekog projekta. Na samom početku dato je informativno zaglavlje koje opisuje modul. Takođe, jasno je kako se ubacuje i klasični tip komentara u programski kod.
Novi (C++) tip komentara počinje sa dva uzastopna karaktera /, bez razmaka. Komentar počinje odmah iza ova dva znaka i traje do kraja reda. Zgodan je za kratke komentare, a često se koristi i kod automatski generisanih odsečaka programa koje možemo sresti kod nekih savremenih razvojnih okruženja. Primer za ovu vrstu komentara takođe možemo videti u prehodnom primeru.
U komentaru može stajati bilo šta, prevodilac će tekst iz komentara u potpunosti zanemariti. Međutim, komentar se ne sme zloupotrebljavati jer kada se pređe određena granica, umesto da poboljšava čitljivost koda, otežava njegovo čitanje. Zato je pametno nakon završetka razvoja određenog modula revidirati komentare i ukloniti zastarele i nesuštinske komentare. Nekakva konceptualna objašnjenja je najbolje izvući na početak modula ili ga grupisati u jedan veći, čisti komentar (bez programskog koda) ispred dela od interesa.
Literatura:
- Mikrokontroler 8051/8052, Praktikum za laboratorijske vežbe – Miloš Slankamenac , Kalman Babković , Ivan Mezei
Fakultet tehničkih nauka, Katedra za elektroniku