Przeskocz nawigację.

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 dołącza do programu pliki z definicjami portów, rejestrów i funkcji dostępu do nich. Najlepiej, podczas pisania programu mieć zawsze otwarty gdzieś "na boku" ten plik i inne, które on dodatkowo włącza - głównie plik z definicjami dla mikrokontrolera, na który będzie skompilowany nasz program np. dla AT90S8515 będzie to avr/io8515.h. Dalej znajduje się główna funkcja programu int main(void), od której zawsze rozpoczyna się działanie programu napisanego w języku C. Instrukcja sbi(DDRB,0) powoduje wpisanie jedynki do bitu 0 rejestru DDRB. Jest to rejestr ustalający kierunek przepływu danych w porcie B. W efekcie możemy używać linię PB0 mikrokontrolera jako wyjścia. Kolejna instrukcja: sbi(PORTB,7) wpisuje jedynkę do bitu 7 rejestru PORTB, w tym samym czasie bit 7 portu DDRB jest wyzerowany, gdyż mikrokontroler po starcie ma wpisane zera do wszystkich rejestrów związanych
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!