Asistent

Osnove
mikroprocesorskih
sistemov
  Laboratorijske vaje
    Samostojni zagon
Inicializacija
mikrokrmilnika

Časovnik
Zunanje prekinitve
Prekinitve
Ura realnega časa in
zapis v pomnilnik flash

A/D in D/A pretvorba
Asinhrona zaporedna komunikacija
Vodilo I2C

Računalniško
načrtovanje vezij II


Mikroprocesorji
v elektroniki

Vodilo I2C

Besedilo vaje: Izdelajte program, ki bo omogočal pošiljanje sporočil med večimi mikrokrmilniškimi sistemi preko vodila I2C. Znaki sprejeti preko asinhronih zaporednih vrat uart1 naj bodo prikazani v prvi vrstici prikazovalnika LCD. V drugi vrstici naj se izpišejo znaki sprejeti preko vodila I2C. Pritisk tipke T0 naj sproži oddajo prve vrstice preko vodila I2C izbranemu mikrokrmilniškemu sistemu, pritisk tipke T2 pa sprejem vrstice od izbranega sistema. S tipko T1 izberemo mikrokrmilniški sistem kateremu oddajamo, s tipko T3 pa sistem od katerega zahtevamo njegovo vrstico. Hkrati mora biti naš sistem ves čas pripravljen na podobne zahteve ostalih sistemov na vodilu I2C. Torej na sprejem vrstice drugega sistema oziroma oddajo svoje vrstice.

Razlaga: Program napišete v razvojnem okolju, ki teče na osebnem računalniku. Pripravljen delovni prostor (angl. workspace) z vsemi pripadajočimi datotekami najdete tukaj. Opis datotek, ki se nahajajo v paketu, najdete v uvodu razlage prve vaje (samostojni zagon), datoteka vic.c je opisana v tretji vaji (časovnik), datoteki uart.c in uart.h pa v osmi vaji (asinhrona zaporedna komunikacija). Dodani sta datoteki i2c.c in i2c.h, kjer je se nahaja koda za delo z obema vmesnikoma do vodila I2C. Lastno kodo, ki izvaja to vajo, dodate v prazno definicijo funkcije main() v datoteki main.c. Najprej morate mikrokrmilnik inicializirati, kar naredite s klicem funkcije init() v funkciji start_up() v datoteki startup.c. Za njeno uporabo morate vključiti datoteko init.h. Opis funkcije init() prav tako najdete v razlagi prve vaje (samostojni zagon).

Dva ali več mikrokrmilniških sistemov bomo povezali preko vodila I2C, kot prikazuje slika spodaj. Vsak izmed sistemov je preko asinhronih zaporednih vrat uart1 povezan s terminalskim programom, ki teče na osebnem računalniku. Sprejete znake bo mikrokrmilniški sistem prikazoval v prvi vrstici prikazovalnika LCD. Vmesnik mikrokrmilnika i2c0 priključimo na povezavi sda in scl vodila I2C. Obe povezavi potrebujeta tako imenovana 'pull-up' upora do napajalne napetosti. Vsem sistemom je potrebno zagotoviti tudi skupno maso.

Mikrokrmilnik ves čas sprejema podatke, ki prispejo preko asinhronih zaporednih vrat uart1, in jih prikaže v prvi vrstici prikazovalnika LCD. Sprejem naredimo s prekinitvijo, ki jo ob vsakem prispelem znaku zahteva uart1. Pred uporabo asinhrona zaporedna vrata uart1 inicializiramo s klicem funkcije uart1_init(), zaradi česar moramo vključiti datoteko uart.h (glej osmo vajo - asinhrona zaporedna vrata). Po inicializaciji ne pozabite omogočiti oddaje. Funkcijo uart1_init() kličemo iz funkcije start_up(), kjer so zbrane vse inicializacije. Algoritem prekinitve uart1 je zelo podoben tistemu iz osme vaje (asinhrona zaporedna vrata) in je prikazan na sliki spodaj.

Delovanje programa ponazarja naslednja slika. Na prvem terminalu uporabnik odtipka tekst 'xxx', ki se prikaže v prvi vrstici prikazovalnika LCD prvega sistema. Podobno je na drugem terminalu odtipkan tekst 'yyy' in na tretjem 'zzz'. Številka na prvem mestu v prvi vrstici pove, kateremu sistemu bo prikazan tekst ob pritisku na tipko T0 poslan. Spreminjamo jo s pritiski na tipko T1. Številka na prvem mestu druge vrstice pa pove od katerega sistema bo tekst ob pritisku tipke T2 sprejet. Spreminjamo jo s pritiski na tipko T3.

V tretjem koraku je ob pritisku na tipko T0 sistema 1 tekst 'xxx' poslan sistemu 3. Sistem 1 je v tem primeru zahteval oddajo podatkov sistemu 3. Pri prenosu deluje sistem 1 v načinu master transmit, sistem 3 pa v načinu slave receive. V četrtem koraku je situacija spremenjena. Pritisk tipke T2 sistema 2 povzroči prenos teksta 'zzz' s sistema 3. Sistem 2 zahteva sprejem podatkov s sistema 3. Zato sedaj sistem 2 deluje v načinu master receive, sistem 3 pa v načinu slave transmit. Kot je bilo nakazano, lahko naprava na vodilu I2C deluje na štiri različne načine, in sicer: master transmit, master receive, slave transmit in slave receive. Vsi štirje načini delovanja so s kodo podprti v datoteki i2c.c.

Preden začnete vmesnik i2c0 uporabljati, ga je potrebno inicializirati. To naredite s funkcijo i2c0_init(), ki se prav tako nahaja v datoteki i2c.c. Funkcijo i2c0_init() kličemo iz funkcije start_up(), kjer so zbrane vse inicializacije. Za njeno uporabo morate vključiti datoteko i2c.h. Deklaracija funkcije i2c0_init() je naslednja:

  void i2c0_init(int address, int general, int duty, char *tx_buf);

Argumenti funkcije i2c0_init() podajajo nastavitve vmesnika i2c0 do vodila I2C:

   - address ... naslov naprave na vodilu I2C (možne vrednosti so med 0x00 in 0x7f),
- general ... naprava se odzove na splošni naslov (0x00), če je general različen od nič, v nasprotnem primeru se naprava na splošni naslov ne odzove; poseben primer nastopi, ko sta address = 0x00 in general = 0; takrat se naprava nikdar ne odzove in lahko deluje le v načinu master transmit ali master receive,
- duty ... argument sestavljata dve samostojni 16-bitni številki, ki določata hitrost prenosa: fI2C = fVPB / (sclh + scll), pri čemer sclh podaja trajanje visokega stanja, scll pa trajanje nizkega stanja; sclh je zapisan v zgornjih 16 bitih argumenta duty, scll pa v spodnjih 16 bitih; hitrost prenosa ne sme biti večja od fI2C < 400kHz, veljati mora tudi sclh > 3 in scll > 3 in
- tx_buf ... kazalec na niz znakov, ki se prične oddajati na zahtevo, ko je naprava naslovljena (naprava deluje v načinu slave transmit).

Po inicializaciji bo vmesnik i2c0 pripravljen na delovanje, ko ga omogočimo. To storimo s postavitvijo bita i2en v registru I2C0CONSET. Vmesnik uporablja prekinitev i2c0. Ob prekinitvah i2c0 naj se izvrši funkcija handle_i2c0_state(), ki je že napisana v datoteki i2c.c. Ostala programska oprema komunicira z vmesnikom i2c0 preko globalnih spremenljivk. Odvisno od načina delovanja se uporabljajo različne globalne spremenljivke, kar podaja tabela spodaj.

način globalne spremenljivke komentar
master transmit in
master receive
int i2c0_status
int i2c0_num_of_bytes
int i2c0_address_rw
char *i2c0_buf
Zahtevo po oddaji ali sprejemu lahko podamo le, če je prejšnja zahteva končana, oziroma ko je i2c0_status = i2c_idle. Takrat postavimo:
i2c0_status = i2c_busy,
i2c0_num_of_bytes = število bajtov, ki jih želimo oddati ali sprejeti,
i2c0_address_rw = 2 * naslov + read za sprejem, ali
i2c0_address_rw = 2 * naslov + write za oddajo in
i2c0_buf = kazalec na začetek znakovnega niza, ki ga želimo oddati, oziroma na začetek prostora, kamor želimo znake sprejeti.
Oddajo, oziroma sprejem sprožimo s postavitvijo bita sta v registru I2C0CONSET.
Ko je prenos končan, je spremenljivka i2c0_status = i2c_idle. V spremenljivki i2c0_num_of_bytes je število bajtov, ki še čakajo na oddajo, oziroma na katere še čakamo, da jih sprejmemo (navadno je i2c0_num_of_bytes = 0).
Če pride na vodilu I2C do napake, je i2c0_status = i2c_error. Spremenljivka i2c0_num_of_bytes v tem primeru nima pomena.
slave transmit char *i2c0_txslv Kazalec i2c0_txslv kaže na niz znakov, ki se prične oddajati na zahtevo. Kazalec se nastavi že ob inicializaciji. Po potrebi ga lahko spreminjamo tudi kasneje.
slave receive char i2c0_rxslv[bufsize]
int i2c0_rxslv_begin
int i2c0_rxslv_end
Znaki, ki jih vmesnik i2c0 sprejme na zahtevo, se vpisujejo v ciklični FIFO medpomnilnik i2c0_rxslv dolžine bufsize. Kazalec i2c0_rxslv_begin kaže na prvi prispeli znak, kazalec i2c0_rxslv_end pa na prvo prosto mesto v medpomnilniku. Ko je medpomnilnik poln, se na novo prispeli znaki ignorirajo. Za praznjenje mora skrbeti programska oprema.

Glavni program te vaje nadzira dogodke na tipkah. S tipko T1 nastavljamo naslov sistema, kateremu bomo sporočilce poslali, s tipko T3 pa naslov sistema, od katerega bomo sporočilce sprejeli. Tipki T0 in T2 sprožita oddajo, oziroma sprejem. V prvem primeru deluje vmesnik i2c0 v master transmit načinu, drugem primeru v master receive načinu. Algoritem glavnega programa podaja spodnja slika. Inicializacije (mikrokrmilnik, asinhrona zaporedna vrata uart1, vmesnik i2c0 do vodila I2C ...) so izpuščene.

Glavni program v neskončni zanki preverja stanje tipk, ter ustrezno reagira. Vsak pritisk tipke povzroči le en dogodek, ne glede na to, kako dolgo je tipka pritisnjena. Takšno obnašanje zagotavlja zgradba algoritma, ki po vsakem pritisku v zanki čaka na konec pritiska, preden prične z novim preverjanjem tipk. Po tej plati je algoritem le malo spremenjena različica algoritma iz druge vaje (inicializacija mikrokrmilnika), ki si je pomagal s spremenljivko ready.

Pri izpisu znakov na prikazovalnik LCD si pomagamo z gonilnikom lcd_driver_1(), katerega izvorna koda je priložena v datoteki main.c. Gonilnik ob vsakem klicu na prikazovalnik LCD izpiše vsebino niza, na katerega kaže globalni kazalec lcd_string (glej prvo vajo - samostojni zagon). Vendar v našem primeru nikdar ne spreminjamo celotnega napisa na prikazovalniku LCD. Spreminjamo le prvi znak v prvi ali drugi vrstici, ali spreminjamo napis v le eni izmed vrstic ... V nizu lcd_string zamenjamo le nekaj znakov. Da postane sprememba vidna na prikazovalniku LCD, moramo poklicati gonilnik lcd_driver_1(), ki bi ga potemtakem klicali po vsaki zamenjavi znakov v nizu lcd_string. Gonilnik lcd_driver_1() bi bil zato klican tako iz glavnega programa, kot iz prekinitve uart1. Prekinitev uart1 se seveda lahko zgodi tudi med osveževanjem prikazovalnika LCD iz glavnega programa. Sledi osveževanje prikazovalnika LCD znotraj prekinitve uart1, česar gonilnik lcd_driver_1() ne dopušča (funkcija lcd_driver_1() ni napisana kot funkcija z dovoljenim ponovnim vstopom - angl. reentrant). To pomeni, da se prekinitev uart1 med osveževanjem prikazovalnika LCD iz glavnega programa ne bi smela zgoditi.

To pa je težje zagotoviti, zato se odločimo za drugačno rešitev. Gonilnika lcd_driver_1() naj neposredno ne kliče niti glavni program, niti prekinitev uart1. Oba naj spreminjata le niz lcd_string. Sam gonilnik kličemo s pomočjo prekinitve timer1, in sicer dovolj pogosto, da so vse spremembe vidne dovolj hitro. Prekinitev timer1 tako skrbi za redno in časovno enakomerno osveževanje vsebine prikazovalnika LCD. Da je temu tako poskrbimo s primerno inicializacijo časovnika timer1. Uporabimo funkcijo timer1_init() (glej tretjo vajo - časovnik). Po inicializaciji ne pozabite pričeti s štetjem.

Poleg osveževanja prikazovalnika LCD naj ima prekinitev timer1 še dodatno vlogo. Ko kateri izmed preostalih sistemov našemu sistemu odda svoje sporočilo, se to sporočilo sprejme v medpomnilnik i2c0_rxslv, katerega je seveda potrebno sproti prazniti, sprejeto vsebino pa prikazati v drugi vrstici prikazovalnika LCD. Algoritem prekinitve timer1 je dokaj preprost in je prikazan na sliki spodaj. Prekinitev timer1 vsakič prebere morebitne sprejete znake in osveži prikazovalnik LCD. Naj ob tej priliki omenimo še, da sistem na zahtevo drugega sistema po sprejemu odda niz, na katerega kaže kazalec i2c0_txslv. Oddati moramo prvo vrstico prikazovalnika LCD. Zato niz i2c0_txslv že ob inicializaciji vmesnika i2c0 postavimo tako, da kaže na pravi del celotnega niza lcd_string.

Iz vsega povedanega sledi, da imamo tri vrste prekinitev, in sicer uart1, i2c0 in timer1. Za prvo in zadnjo moramo napisati svoji funkciji, za kateri smo v razlagi te vaje že podali algoritma. Za prekinitev i2c0 poskrbi funkcija handle_i2c0_state(), ki se nahaja v datoteki i2c.c. Vsemu povedanemu primerno moramo inicializirati vektorski nadzornik prekinitev, kar naredimo s klicem funkcije vic_init() (glej tretjo vajo - časovnik). Za njeno uporabo moramo vključiti datoteko vic.h.