2Porty_IO.pdf

(145 KB) Pobierz
Opis ćwiczeń nr 2
Tytuł:
Obsługa portów mikrokontrolera AVR, budowa i działanie pamięci, wstęp do obsługi
przerwań.
Streszczenie:
W kolejnym opisie ćwiczeń zostanie przedstawiona obsługa portów wejść/wyjść ogólnego przeznaczenia
wbudowanych w mikrokontroler AVR ATMEGA 16/32. Następnie przybliżona zostanie problematyka zarządzania
pamięcią programu i pamięcią danych. Pod koniec zaś delikatnie nakreślimy pojęcie przerwania w mikrokontrolerach
AVR.
Opis:
I. Porty
Jednym z podstawowych peryferii mikrokontrolera AVR ATMEGA jest port wejść/wyjść. Dzięki niemu możemy
sterować innymi urządzeniami, elementami mocy, sygnalizacyjnymi (jak. np. diody LED), a także odbierać sygnały
logiczne z zewnątrz. Mikrokontroler AVR ATMEGA 16/32 posiada 4-y pełne porty, czyli każdy ma po
8 wyprowadzeń. Każdy z pinów dowolnego portu możemy indywidualnie skonfigurować jako wejście lub jako wyjście.
Jeżeli chcemy by nasze wyprowadzenie działało jako wyjście, należy bezwzględnie pamiętać,
że pojedyncze wyjście danego portu nie może być obciążone prądem większym niż 20mA. Logiczna 1'dynka zgodnie
ze standardem TTL przy napięciu zasilania 5V wynosi minimum 4.2V a logiczne '0' maksymalnie 0,7V. Wykorzystując
zaś wyprowadzenie portu jako wejście, możemy podpiąć pod nie wewnętrzny rezystor pull-up (połączonego z VCC).
Aby móc odwołać się do któregoś z portów, korzystamy z 3 podstawowych rejestrów sterujących:
a) DDRx – rejestr konfiguracji kierunku
b) PORTx – rejestr ustalający stan portu
c) PINx – rejestr odczytu stanu portu (tylko do odczytu)
gdzie x – litera identyfikująca port np.: A, B, C, D
Korzystając z rejestrów DDR i PORT możemy wymusić następujące stany pracy portu:
DDRx PORTx
Tryb
Opis:
0
0
Wejście
Wejście bez pull-up
0
1
Wejście
Wejście z rezystorem pull-up
1
0
Wyjście
Wyjście, ustawione w stan niski
1
1
Wyjście
Wyjście ustawione w stan wysoki
Zastosowanie w praktyce powyższej wiedzy zostało już zaprezentowane po części 1. ćwiczeniu w przykładzie
„HelloWorld”.
Twórcy AVR-GCC w celu ułatwienia poruszania się po rejestrach i korzystania z ich zawartości zdefiniowali ich nazwy
zgodnie z dokumentacją firmy Atmel. Tak więc w celu odwołania się do któregoś z rejestru kierunku portu piszemy
DDRA, DDRB, DDRC lub DDRD. Analogicznie z PORTA, PINA, PORTB, PINB itd. Tak samo również jest przy
odwołaniu się do konkretnych bitów jednego spośród tych 8-bitowych rejestrów, jak np.:
dla rejestru kierunku DDR: DDA5, DDC0
dla rejestru PORT: PB4, PD7, PA3
dla rejestru PIN: PINA2, PIND5
jeżeli zajrzymy do pliku iom16.h w katalogu winavr/avr/include/avr/ zauważymy, że np. DDA4, PA4 i PINA4 tak
naprawdę definują tą samą cyfrę „4”. A więc jeżeli chcieli byśmy ustawić pin 4ty portu A mogli byśmy napisać:
PORTA |= _BV(PA4);
PORTA |= _BV(DDA4);
PORTA |= _BV(PINA4);
czy nawet: PORTA |= _BV(DDC4);
i w każdym przypadku pin 4ty portu A został by ustawiony w stan wysoki, jednak zapis pokroju
PORTA |= _BV(DDC4); wprowadzał by niepotrzebne zamieszanie, a kod stracił by na przejrzystości.
990421728.019.png 990421728.020.png 990421728.021.png 990421728.022.png 990421728.001.png 990421728.002.png 990421728.003.png 990421728.004.png 990421728.005.png 990421728.006.png
 
II. Zarządzanie pamięcią.
Pisząc programy na „duże” komputery, nie było potrzeby zwracania uwagi, gdzie dana zmienna czy stała była
przechowywana. Wynikało to przede wszystkim z przyjętej architektury Von Neumann'a zapewniającą współdzielność
pamięci danych i programu. Programując AVRy trzeba jednak pamiętać, że są one stworzone w architekturze
Harvardzkiej, a co za tym idzie pamięć programu jest oddzielona od pamięci danych, a ich przestrzenie adresowe
są od siebie odseparowane. W celu dostosowania GNU C do potrzeb mikrokontrolera AVR pamięci podzielone zostały
na segmenty. Pisząc program musimy zdecydować czy nasza zmienna/stała trafi do pamięci FLASH, RAM czy może
EEPROM.
FLASH
W pamięci FLASH wraz programem wykonywalnym przechowywane są wartości początkowe zmiennych, które
takową posiadają. Zasadniczo poszczególnych komórek pamięci Flash nie możemy modyfikować w czasie działania
programu, a więc jest to pamięć, w której najlepiej przechowywać stałe np. napisy, większe tablice ze stałą wartością,
struktury danych itp. Warto jednak się zastanowić czy jest sens wprowadzania pojedynczych stałych np. jedno
bajtowych, które równie dobrze mogli byśmy przypisać bezpośrednio w kodzie, lub użyć wygodnego makra #define.
Utrudnieniem w korzystaniu z owej pamięci jest fakt, że nie możemy się się odwoływać bezpośrednio do stałych w niej
zapisanych, jak to się dzieje w przypadku pamięci RAM. Z pomocą przychodzi wbudowana w WinAVR biblioteka
pgmspace.h znajdująca się w katalogu avr/include/avr/. Zawarte w niej funkcje i makra umożliwiają odczyt komórek
pamięci Flash w niej zapisanych.
W celu zadeklarowania stałej w pamięci musimy za jej nazwą użyć atrybutu __progmem__ :
const uint8_t tLiczb[3] __attribute__((__progmem__)) = {48,25,150};
przestawiona deklaracja nie wygląda zbyt zachęcająco, ale całe szczęście po to powstały definicje przez równie dobrze
możemy napisać tak:
const uint8_t tLiczb[3] PROGMEM = {48,25,150};
oczywiście możemy skorzystać z gotowego typu zdefiniowanego w pgmspace.h:
const prog_ uint8_t tLiczb[3] = {48,25,150};
praktycznie każdy standardowy typ zmiennej ma swojego bliźniaka z przedrostkiem prog_.
W kursie będziemy używać tej najwygodniejszej czyli z przedrostkiem prog_.
Przedrostek "const" nie jest wymagany, jednak jego obecność daje kompilatorowi wiadomość, że dana zmienna nie
może być modyfikowana i w przypadku takiej próby wystąpi błąd kompilacji.
Jeżeli chcemy zapisać we Flashu napis możemy go zapisać w tablicy np.:
prog_char napis[5] = {'W','i','t','a','j'};
lub wykorzystać łańcuch znaków:
prog_char lancuch[] = „Witaj”;
oczywiście należy pamiętać, że na końcu łańcucha dodatkowo znajduje się wartość 0 kończąca go.
Ważnym makro związanym z łańcuchami jest PSTR(arg) tworzy ono w pamięci programu łańcuch znaków i zwraca
wskaźnik do niego.
By móc odczytać stałą zapisaną w pamięci korzystamy z jednej z poniższych funkcji:
pgm_read_byte ( address); - funkcja zwraca wartość stałej 8-bitowej
pgm_read_word ( address); - funkcja zwraca wartość stałej 16-bitowej
pgm_read_dword ( address); - funkcja zwraca wartość stałej 32-bitowej
pgm_read_float ( address); - funkcja zwraca wartość stałej typu float.
A więc, jeżeli chcieli byśmy wczytać liczbę z naszej tablicy tLiczb do zmiennej to napisali byśmy:
uint8_t liczba = pgm_read_byte(&tLiczb[2]);
jeżeli zaś mamy do wczytania element łańcuchu możemy wczytać go podobnie jak wyżej:
char literka = pgm_read_byte(&lancuch[1]);
lub
char literka = pgm_read_byte(lancuch+1);
lancuch przechowuje adres pierwszego elementu łańcucha, a więc zostanie wczytana 2ga litera..
Bardzo wygodne i przydatne np. podczas tworzenia menu, może okazać się stworzenie tablicy łańcuchów znakowych.
W tym celu należy stworzyć elementy tablicy a następnie przypisać je do wskaźnika, który by na nie wskazywał.
Możemy to zrobić w następujący sposób:
prog_char element1[] = "Pierwszy";
prog_char element2[] = "Drugi";
prog_char element3[] = "Trzeci";
const char* WskaznikNaElementy[] PROGMEM= {element1, element2, element3};
990421728.007.png 990421728.008.png 990421728.009.png
 
nasuwa się pytanie czemu elementy tablicy zadeklarowaliśmy jako prog_char a wskaźnik jako char* i dodaliśmy
PROGMEM. Otóż trzeba pamiętać że wskaźnik to co innego jak typ zmienny- on tylko wskazuje na dany typ,
a definicji wskaźnika char* nie ma w pgmspace.h, a więc musimy zapisać go wraz z atrybutem PROGMEM.
W celu odwołania się do danego elementu tablicy piszemy:
char literka = pgm_read_byte(pgm_read_word(&WskaznikNaElementy[0])+1);
powyższy kod odwoła się do 0 elementu czyli do łańcucha „Pierwszy” i pobierze literkę „i”.
Warto zaznaczyć, że na każdy taki łańcuch tracimy dodatkowo 2 bajty.
W celu operowania na łańcuchach zapisanych w pamięci Flash, twórcy biblioteki stworzyli zestaw przydatnych
instrukcji. Ich nagłówki wraz z opisem możemy znaleźć w pliku pgmspace.h.
RAM
Kolejną pamięcią jest RAM, to do niego wpisywane są domyślnie zadeklarowane zmienne. Możemy w tej pamięci
dowolnie zapisywać i odczytywać zmienne, mogą mieć one wcześniej przypisaną wartość, ale nie muszą. Warto jednak
zwrócić uwagę, że RAM jest zazwyczaj wielokrotnie mniejszy od pamięci programu (w ATMEGA 16 – 2KB),
co zmusza programistę do racjonalnego korzystania z tego obszaru pamięci.
Przykłady deklaracji zmiennej w pamięci RAM:
uint8_t liczba;
float temperatura = 22.8;
uint8_t tablica[] = {1,50,100,25};
EEPROM
Podobnie jak z pamięcią Flash do EEPROM-u odnosimy się poprzez funkcje zadeklarowane w bibliotece AVR-Libc,
tym razem avr/eeprom.h. W przeciwieństwie do pamięci Flash zmienne zapisane w EEPROM-ie mogą być
odczytywane jak i zapisywane. E 2 PROM to pamięć typu nieulotnego, a więc po zaniku napięcia dane w niej zapisane
nie znikną. Ważne jest jednak by pamiętać o tym, że ze względu na specyfikację swojej budowy pamięci te mają
ograniczoną liczbę cykli zapisu i w przypadku tych wbudowanych w mikrokontrolery AVR producent gwarantuje
100 tysięcy poprawnie przeprowadzonych zapisów. Pamięć tego typu stosujemy głównie do zapisania parametrów
nastaw urządzenia, lub np. rzadko aktualizowanych, lub przeznaczonych przede wszystkim na odczyt zmiennych.
Mikrokontrolery AVR ATMEGA16 mają wbudowaną pamięć EEPROM o rozmiarze 1024 bajtów. Nie jest to dużo,
jednak w praktyce rzadko kiedy potrzebna jest większa pojemność. Przypisując zmienną do tego typu pamięci musimy
przypisać atrybut przydzielający zmienną do odpowiedniej sekcji pamięci. W tym celu piszemy:
uint8_t zmienna __attribute__((section(".eeprom")));
lub korzystając z gotowej definicji:
uint8_t zmienna EEMEM;
oczywiście nic nie stoi na przeszkodzie by nadać wartość początkową zmiennej:
uint8_t zmienna EEMEM = 128;
By skorzystać z pamięci EEPROM używamy takich funkcji jak:
eeprom_write_byte ( *adr, val) - zapisuje wartość val pod adres adr.
eeprom_read_byte ( *adr ) - czyta zawartość pamięci ulokowanej pod adresem adr.
eeprom_read_word ( *adr ) - czyta 16 bitową zawartość pamięci ulokowanej pod adresem adr.
eeprom_read_block ( *buf, *adr, n) - czyta n bajtów od adresu adr i zapisuje do pamięci SRAM w miejscu
wskazywanym przez argument *buf.
eeprom_is_ready () - zwraca 1 jeśli pamięć jest wolna lub 0 jeśli na niej jest wykonywana jakaś operacja (zapis
do pamięci EEPROM nie jest zbyt szybki).
A więc odczyt pamięci EEPROM wyglądał by np. tak:
uint8_t wartosc = eeprom_read_byte(&zmienna);
a zapis:
eeprom_write_byte(&zmienna, wartosc);
III.Wstęp do obsługi przerwań.
Podstawową zaletą mikrokontrolerów a więc i AVR ATMEGA16/32 jest możliwość natychmiastowego zareagowania
na zdarzenie wygenerowane przez jedno z wewnętrznych peryferii lub pośrednio zdarzeń z zewnątrz. Może to być akcja
zaistniała wewnątrz mikrokontrolera jak np.: przepełnienie jednego z liczników, zakończenie przetwarzania wartości
analogowej przez ADC, wysyłanie przez USART danych zostało zakończone itp. Prócz przerwań z wewnątrz
mikrokontroler może również reagować na zdarzenia pochodzące z zewnątrz jak np.: nadejście zbocza narastającego
na pin przerwania zewnętrznego INT0, napięcia na wejściach komparatora są sobie równe, przyszły dane szeregowe
do USART. Dzięki przerwaniom nie musimy się obawiać, że pominiemy zdarzenie, którego czas nadejścia nie jest
określony.
W celu możliwości korzystania z przerwań pisząc kod w C, należy dodać do programu nagłówek biblioteki służącej
do obsługi przerwań: #include < avr/interrupt.h > . Po dodaniu owej biblioteki do programu otrzymamy dostęp
do funkcji i definicji niezbędnych do obsługi przerwań.
Pisanie funkcji obsługi wybranego przerwania różni się trochę od pisania zwykłej funkcji. Jej konstrukcja wygląda
następująco:
ISR (Wektor_przerwania)
{
// kod użytkownika
}
Wektor_przerwania to nazwa przerwania, które chcemy obsłużyć. Nazwy wektorów są zgodne z dokumentację firmy
Atmel z tą różnicą że kończą się przyrostkie „_vector”. A więc zaglądając do dokumentacji ATMEGA16 i odnajdując
podrozdział „Interrupt Vectors in Atmega16” możemy przejrzeć dostępne dla tego mikrokontrolera przerwania i ich
krótkie opisy. Jeżeli chcemy na przykład napisać obsługę przerwania zewnętrznego INT0 to musimy napisać:
ISR (INT0_vector)
{
// kod przerwania INT0
}
a dla przerwania odebrania znaku przez USART:
ISR (USART_RXC_vector)
{
// kod przerwania, gdy odebrany znak
}
gdzie w dokumentacji występują przecinki i spacje to w kodzie zastępujemy je twardą spacją. Jeżeli jednak nie jesteśmy
pewni nazwy przerwania, wystarczy wejść w plik nagłówkowy opisujący wybrany mikrokontroler (np.:
winavr/avr/include/avr/iom16.h) i odnaleźć część opisującą wektory: /* Interrupt vectors */.
Należy pamiętać, że wywołanie domyślnie funkcji przerwania ISR wyłącza globalnie przerwania. Oznacza to, że jeżeli
w momencie wykonywania jednego przerwania nadejdzie drugie to nie zostanie ono obsłużone. Często jednak zależy
nam by te przerwania był obsługiwane ze względu na ich wysoki priorytet dla programu. W tym celu pisząc funkcję dla
przerwania o niskim priorytecie zaznaczamy w nagłówku funkcji by inne przerwania mogły również być wykonywalne.
ISR (INT0_vector, ISR_NOBLOCK)
{
// kod przerwania INT0, nieblokujący inne przerwania
}
Które przerwania są ważniejsze od innych? Na pewno te, które wykonywane są częściej i cyklicznie. Przerwania takie
powinny być możliwie jak najkrótsze i by nie dopuszczały do wykonywania w czasie ich działania innych przerwań.
Mogą to być np. przerwania od liczników o dużej częstotliwości wyzwalania. Przerwania o niskim priorytecie to takie,
które występują rzadko, nie okresowo, lub o znacznie dłuższym cyklu występowania. Ich kod może być dłuższy, gdyż
nie ma obawy, że przerwanie samo na siebie się nałoży, a wystąpienie innego przerwania w tym czasie nie spowoduje
katastrofalnego dla działania programu błędu.
Możemy oczywiście w dowolnym momencie programu wyłączyć globalnie przerwania lub je włączyć za pomocą
poniższych makroinstrukcji:
sei(); - włączenie globalne przerwań (ustawienie bitu I w rejestrze SREG)
cli(); - wyłączenie globalne przerwań (wyzerowanie bitu I w rejestrze SREG)
warto je stosować w newralgicznych miejscach programu, gdzie wystąpienie przerwania może zmienić np. wynik
operacji.
Jeżeli chcemy np. by różne przerwania obsługiwały tą samą funkcję musimy to również zaznaczyć w atrybutach
przerwania:
ISR(INT0_vect)
{
// wspolny kod przerwania
}
ISR(INT1_vect, ISR_ALIASOF(INT0_vect));
Każde przerwanie ma bit włączający/wyłączający przerwanie w przeznaczonym do jego obsługi rejestrze jak i flagę,
która w zależności czy takie przerwanie wystąpiło podnosi się lub jest przykładowo. wyzerowane. Czasami są również
przeznaczone dodatkowe bity konfigurujące przerwanie, np. w jakich okolicznościach ma wystąpić.
Jako, że jest to wstęp do przerwań przybliżona zostanie jedynie obsługa zewnętrznych przerwań INT0(pin PD2),
INT1(pin PD3) i INT2(PB2).
Do ich obsługi stosuje się bity znajdujące się w 4ech rejestrach:
GICR – General Interrupt Control Register – Ogólny rejestr kontroli przerwań
W tym rejestrze włączamy/wyłączamy zezwolenie przerwania z danego wejścia.
Znajdują się w nim bity INT0, INT1 i INT2. Więc jeżeli chcemy włączyć np. przerwanie od INT0 piszemy:
GICR |= _BV(INT0); //Włączenie przerwania od INT0
Nic nie stoi na przeszkodzie oczywiście by w dowolnym momencie, takie konkretne przerwanie wyłączyć:
GICR &= ~_BV(INT0); //Wyłączenie przerwania od INT0
GIFR – General Interrupt Flag Register – Ogólny rejestr znaczników przerwań
Rejestr ten przechowuje ustawiane sprzętowo flagi przerwań. Zazwyczaj programista z nich bezpośrednio nie korzysta,
ale nic nie stoi na przeszkodzie by je swobodnie odczytywać lub „sztucznie” ustawiać w celu wymuszenia wywołania
przerwania.
Znajdują się w tym rejestrze takie flagi jak: INTF0 , INTF1 , INTF2 .
Gdy nadejdzie zdarzenie przerwania flaga ustawia się na „1” a po obsłużeniu przerwania flaga opada na „0”.
MCUCR – MCU Control Register – Rejestr sterujący pracą mikrokontolera
Tutaj konfigurujemy na jakie zdarzenie ma zareagować dane przerwanie
MCUCSR – MCU Control and Status Register – rozszerzenie MCUCR, tutaj konfigurujemy reakcję dla INT2
Do konfiguracji reakcji konkretnego przerwania na wybranego zdarzenie służą pary: ISC00 i ISC01 , ISC10 i ISC11 a
także dla INT2: ISC2
ISCX1 ISCX0 Działanie:
0
0
Przerwanie, gdy poziom logiczny na pinie - niski
0
1
Przerwanie, gdy poziom logiczny na pinie – ulegnie zmianie
1
0
Przerwanie, gdy zbocze na pinie - opadające
1
1 Przerwanie, gdy zbocze na pinie - narastające
Bity ISCXX znajdują się w rejestrze MCUCR.
Dla INT2 można ustawić tylko reagowanie na zbocze narastające: ICS2 =1 lub opadające, gdy ICS2 =0. ICS2 znajduje
się w rejestrze MCUCSR.
Problem może wydawać się złożony, ale w rzeczywistości obsługa przerwań jest bardzo prosta. W celu rozjaśnienia
problemu przedstawiam poniżej przykład obsługi przerwań INT0 i INT1:
#include <avr/io.h>
#include <avr/interrupt.h>
ISR(INT0_vect)
{
PORTA |= _BV(PA0); // zapal diodę LED
}
ISR(INT1_vect)
{
PORTA &= ~_BV(PA0); // zgaś diodę LED
}
int main() // program główny
{
DDRA |= _BV(DDRA0); // pina 0 portu A jako wyjście
GIMSK |= _BV(INT0)|_BV(INT1); // włączenie obsługi przerwań INT0 i INT1
MCUCR |= _BV(ISC11)|_BV(ISC10) | _BV(ISC01)|_BV(ISC00);
// włącz generowanie przerwań przez narastające zbocze na INT0 i INT1
sei(); // włącz globalną obsługę przerwań
while(1); // pętla nieskończona
return 0;
}
990421728.010.png 990421728.011.png 990421728.012.png 990421728.013.png 990421728.014.png 990421728.015.png 990421728.016.png 990421728.017.png 990421728.018.png
 
Zgłoś jeśli naruszono regulamin