AVR-GCC - wejście i wyjście binarne
Rejestry
Tutaj sytuacja się trochę (pozornie) komplikuje. Do każdego z fizycznych portów (tzn. tych dostępnych z zewnątrz układu) przypisane są trzy rejestry. Ale dzięki temu możemy niemal dowolnie konfigurować każdą końcówkę układu związaną z portami, tzn. ustalać kierunek, podciągać wejścia do zasilania, odłączać od reszty układu elektronicznego itp.
Ustalanie kierunku - DDRx
Rejestr kierunku danych (ang. Data Direction Register X, gdzie X jest oznaczeniem znakowym portu np. DDRA jest rejestrem kierunku dla portu A). Ustalenie kierunku odbywa się wg zasady:
- ustawienie odpowiedniego bitu - wyjście
- skasowanie odpowiedniego bitu - wejście
Przykład:
DDRB = _BV(7)|_BV(6)|_BV(5)|_BV(4);
lub
DDRB = 0xF0;
Ustawia odpowiednie końcówki portu B: 0-3 na wejścia, 4-7 na wyjścia.
Rejestr zapisu danych - PORTx
Rejestr Danych Portu X (gdzie X jest oznaczeniem znakowym portu np. PORTA jest rejestrem danych dla portu A). Na przykład aby załadować do rejestru PORTB wartość 0xAA należy użyć następującego kodu:
PORTB = 0xAA;
Rejestr odczytu danych - PINx
Odczyt z portu X (gdzie X jest oznaczeniem znakowym portu np. PINA jest rejestrem odczytu portu A). Odczyt z tego portu daje nam fizyczny stan na końcówkach - oczywiście pod warunkiem wcześniejszego ustalenia kierunku poprzez wpisanie zer w odpowiednie bity rejestru kierunku.
Przykład:
DDRA = 0x00; // ustawienie kierunku na wejście res = PINA;
Czyta fizyczną wartość na porcie A i umieszcza ją w res.
Praktyczny sposób dostępu do wyprowadzeń układu
Trzy rejestry dla jednego fizycznego portu mogą być dla wielu zbyt dużą uciążliwością szczególnie w przypadku, gdy z jakiegoś powodu musimy zmienić port, do którego podłączamy jakieś urządzenie - wtedy w całym programie musieli byśmy dokonywać zmian dotyczących trzech portów - łatwo więc o pomyłkę. Można jednak wykorzystać fakt, że wszystkie opisywane tu rejestry łączy pewna zależność: w przestrzeni adresowej układu znajdują się one "koło siebie". Załóżmy że piszemy procedury obsługi np. wyświetlacza LCD, będzie on podłączony do jednego fizycznego portu np. PORTB. Wystarczy umieścić w programie odpowiednie makrodefinicje a cała operacja stanie się banalnie prosta. Przykład:
#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. r. kier. PORTx #define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. r. wej. PORTx #define LCD_PORT PORTB // używany port #define LCD_PORT_O LCD_PORT // rejestr wyjściowy #define LCD_PORT_D DDR(LCD_PORT) // rejestr kierunkowy #define LCD_PORT_I PIN(LCD_PORT) // rejestr wejściowy
Po napisaniu takich makrodefinicji w dalszej części programu już nie posługujemy się nazwami w rodzaju: PORTB, PINB, DDRB ale: LCD_PORT_O, LCD_PORT_I, LCD_PORT_D. Ma to również jeszcze jedną zaletę - jeśli będziemy chcieli zmienić w programie port do którego ma być przyłączone obsługiwane urządzenie wystarczy zmiana tylko jednej linijki np.:
#define LCD_PORT PORTB
na:
#define LCD_PORT PORTD
a dalszą zamianą zajmie się kompilator (konkretnie jego preprocesor).
Podciąganie wejścia do logicznej jedynki
Bardzo interesującą cechą układów AVR (szczególnie dla używających w swej praktyce układów MCS-51) jest możliwość "podciągania" wejść do logicznej jedynki bez użycia zewnętrznych rezystorów. Robi się to w ten sposób, że przy wpisanym zerze do bitu kierunku DDRx (ustawienie bitu portu jako wejście) należy wpisać jedynkę na ten sam bit - ale do portu PORTx. Zostanie to zilustrowane na przykładzie:
cbi(DDRB,7); // użyj linii PB7 jako wejścia sbi(PORTB,7); // "podciągnij" do logicznej 1 linię PB7
Tutaj celowo użyto dwóch instrukcji, jednak praktycznie można użyć tylko ostatniej "podciągającej" wejście gdy mamy pewność, że wcześniej nie był ustawiany dany bit ustalający kierunek w pocie (DDRx) - po starcie mikrokontrolera wszystkie bity związanie z portami fizycznymi są wyzerowane czyli porty są ustawione na odczyt danych.
Programy przykładowe
Po zapoznaniu z podstawowymi operacjami wejścia/wyjścia możemy napisać i przetestować nasz pierwszy program na mikrokontroler AVR w języku C. Na początek będzie to sterowanie diodą LED podłączoną przez rezystor 470 om między zasilanie a wyjście PD4 przy pomocy przycisku monostabilnego podłączonego między linię PD3 a masę.
// Sterowanie diodą LED podłączoną do linii PD4 mikrokontorlera // za pomocą przycisku podłączonego do linii PD3 mikrokontrolera #include <avr/io.h> // dostęp do rejestrów int main( void ) // program główny { sbi(DDRD,4); // użyj linii PD4 jako wyjścia sbi(PORTD,3); // "podciągnij" do logicznej 1 linię PD3 while(1) // pętla nieskończona { cbi(PORTD,4); // zapal diodę LED podłączoną do linii PD4 loop_until_bit_is_clear(PIND,3); // czekaj na naciśnięcie przycisku na PD3 sbi(PORTD,4); // zgaś diodę LED podłączoną do linii PD4 loop_until_bit_is_clear(PIND,3); // czekaj na naciśnięcie przycisku na PD3 } }
Dyrektywa #include
z fizycznymi portami mikrokontrolera. W efekcie linia PB7 mikrokontrolera pracuje jako wejście "podciągnięte" do logicznej jedynki - nie musimy używać rezystora zewnętrznego. Kolejna instrukcja while(1) to pętla nieskończona, w której wykonywana jest reszta programu mikrokontrolera. Każdy program na mikrokontrolery musi zawierać jakąś pętlę nieskończoną - program nie może się bowiem zakończyć. W ostateczności może być zakończony pustą pętlą nieskończoną. Tak będą realizowane niektóre proste programy przykładowe. Dalej występuje instrukcja cbi(PORTB,0) powodująca wpisanie zera do bitu 0 rejestru PORTB co w efekcie powoduje pojawienie się stanu niskiego na wyprowadzeniu PB0 mikrokontrolera i zapalenie diody LED podłączonej między linię PB0 a linię zasilania (oczywiście przez rezystor). W następnej linii instrukcja loop_until_bit_is_clear(PINB,7) powoduje zatrzymanie programu do momentu, aż na bicie 7 rejestru PINB pojawi się stan niski. W efekcie program czeka na naciśnięcie przycisku podłączonego pomiędzy linię PB7 mikrokontrolera a masę. Następnie instrukcja sbi(PORTB,0) wpisuje jedynkę do bitu 0 rejestru PORTB co w efekcie powoduje pojawienie się stanu wysokiego na wyprowadzeniu PB0. W efekcie gasi diodę LED podłączoną do linii PB0. Dalej ponownie pojawia się instrukcja loop_until_bit_is_clear(PINB,7) i pętla się zamyka - program przechodzi ponownie do wykonywania instrukcji cbi(PORTB,0) itd.
Po zaprogramowaniu układu mikrokontrolera powinna zapalić się dioda LED podłączona do linii PB0. Naciśnięcie przycisku podłączonego do linii PB7 powinno spowodować lekkie zmniejszenie jasności świecenia diody (pojawia się tam fala prostokątna o wypełnieniu 50\%). Gdy przestaniemy naciskać przycisk dioda zapali się lub zgaśnie w zależności od tego, w którym miejscu był program w momencie zwolnienia przycisku. Załóżmy jednak, że w przyszłości będziemy chcieli podłączyć diodę LED do innej końcówki układu, bo np. w ten sposób uprościmy płytkę drukowaną dla układu. Podobnie może być z przyciskiem. I co wtedy? Tutaj być może jeszcze nie będzie wielkiego problemu, ot zmienimy wpisy w paru linijkach kodu i po kłopocie!
Jednak gdy kod źródłowy rozrośnie się powiedzmy do kilkuset a nawet kilku tysięcy linii to tak napisany program może być bardzo trudny do późniejszej modyfikacji, a o pomyłkę będzie bardzo łatwo. Warto więc zawczasu nabrać kilku dobrych nawyków. Po pierwsze - korzystajmy z makrodefinicji (to nie pochłania pamięci programu mikrokontrolera!). Program źródłowy jest większy i wygląda na bardziej skomplikowany lecz wielkość programu wynikowego nie zmienia się w stosunku do tego z powyżsego listingu.
// Sterowanie diodą LED podłączoną do dowolnej linii mikrokontorlera // za pomocą przycisku podłączonego do dowolnej linii mikrokontrolera #include <avr/io.h> // dostęp do rejestrów #define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. rej. kier. PORTx #define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. rej. wej. PORTx #define LED_PORT PORTD // port diody LED #define LED_BIT 4 // bit diody LED #define LED_PORT_O LED_PORT // rejestr wyjściowy #define LED_PORT_D DDR(LED_PORT) // rejestr kierunkowy #define LED_PORT_I PIN(LED_PORT) // rejestr wejściowy #define KEY_PORT PORTD // port przycisku #define KEY_BIT 3 // bit przycisku #define KEY_PORT_O KEY_PORT // rejestr wyjściowy #define KEY_PORT_D DDR(KEY_PORT) // rejestr kierunkowy #define KEY_PORT_I PIN(KEY_PORT) // rejestr wejściowy int main( void ) // program główny { sbi(LED_PORT_D,LED_BIT); // użyj linii jako wyjścia sbi(KEY_PORT_O,KEY_BIT); // "podciągnij" linię do logicznej 1 while(1) // pętla nieskończona { cbi(LED_PORT_O,LED_BIT); // zapal diodę LED loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie przycisku sbi(LED_PORT_O,LED_BIT); // zgaś diodę LED loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie przycisku } }
Makrodefinicjami:
#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) #define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2)
wyliczają adresy rejestru kierunkowego i wejściowego dla podanego portu wyjściowego. W kolejnych liniach mamy definicję:
#define LED_PORT PORTB
gdzie symbolowi LED_PORT jest przypisywana wartość znajdująca się w linii po nim czyli PORTB. W ten sposób zmieniając napis PORTB np. na PORTD spowodujemy, że symbol LED_PORT przyjmie wartość PORTD. Teraz w dalszej części programu będziemy się posługiwać symbolem LED_PORT. W kolejnym wierszu programu znajduje się definicja:
#define LED_BIT 0
przyporządkowująca symbolowi LED_BIT wartość 0. Analogicznie jak w definicji portu możemy zmieniać tę wartość w zakresie od 0 do 7 - oczywiście, jeżeli budowa portu używanego mikrokontrolera na to pozwala. Linie programu zawierające makrodefinicje:
#define LED_PORT_O LED_PORT
- definiuje rejestr wyjściowy - w dalszej części programu będziemy używać nazwy LED_PORT_O
#define LED_PORT_D DDR(LED_PORT)
- definiuje rejestr kierunkowy - w dalszej części programu będziemy używać nazwy LED_PORT_D
#define LED_PORT_I PIN(LED_PORT)
- definiuje rejestr wejściowy - w dalszej części programu będziemy używać nazwy LED_PORT_I.
Analogiczne definicje dla klawisza (KEY_PORT, KEY_BIT) znajdziemy w kolejnych liniach programu. Dalej znajduje się główna funkcja programu int main(void) której "ciało" wygląda analogicznie do tej z listingu pierwszego z tą tylko różnicą, że nazwy portów i numery bitów zastąpiono nazwami definiowanymi przez nas. Jak widać długość listingu programu w stosunku do poprzedniego wzrosła dosyć znacznie - ale to się opłaca! - teraz zmiana przyporządkowania urządzeń do końcówek układu wymaga zmian w czterech miejscach. Już w tak krótkim programie zyskaliśmy 3 razy mniej zmian!


