Autor: Deucalion
Redakcja: Dondu
Kurs ARM: Spis treści
Jeśli pin ma ustawioną funkcję standardowego wejścia/wyjścia, do sterowania tym pinem służy zestaw rejestrów w module GPIO.
Każdy port ma własny zestaw rejestrów, które służą do konfiguracji i sterowania pinami z danego portu. Do rejestrów portów odwołujemy się poprzez wskaźniki na struktury, które reprezentują cały zestaw rejestrów.
Nazwy wskaźników są podobne dla wszystkich portów - LPC_GPIOx, należy tylko pod x podstawić numer portu od 0 do 3 np.: LPC_GPIO1.
typedef struct { union { __IO uint32_t MASKED_ACCESS[4096]; // 0x0000 to 0x3FFC Port data Register // for pins PIOn_0 to PIOn_11 (R/W) struct { uint32_t RESERVED0[4095]; __IO uint32_t DATA; // 0x3FFC Port data Register (R/W) }; }; uint32_t RESERVED1[4096]; __IO uint32_t DIR; // 0x8000 Data direction Register (R/W) __IO uint32_t IS; // 0x8004 Interrupt sense Register (R/W) __IO uint32_t IBE; // 0x8008 Interrupt both edges Register (R/W) __IO uint32_t IEV; // 0x800C Interrupt event Register (R/W) __IO uint32_t IE; // 0x8010 Interrupt mask Register (R/W) __IO uint32_t RIS; // 0x8014 Raw interrupt status Register (R) __IO uint32_t MIS; // 0x8018 Masked interrupt status Register (R) __IO uint32_t IC; // 0x801C Interrupt clear Register (R/W) } LPC_GPIO_TypeDef; #define LPC_GPIO0_BASE (LPC_AHB_BASE + 0x00000) #define LPC_GPIO1_BASE (LPC_AHB_BASE + 0x10000) #define LPC_GPIO2_BASE (LPC_AHB_BASE + 0x20000) #define LPC_GPIO3_BASE (LPC_AHB_BASE + 0x30000) #define LPC_GPIO0 ((LPC_GPIO_TypeDef *) LPC_GPIO0_BASE ) #define LPC_GPIO1 ((LPC_GPIO_TypeDef *) LPC_GPIO1_BASE ) #define LPC_GPIO2 ((LPC_GPIO_TypeDef *) LPC_GPIO2_BASE ) #define LPC_GPIO3 ((LPC_GPIO_TypeDef *) LPC_GPIO3_BASE )
Rejestry GPIO:
DIR
Rejestr kierunku portu. Za pomocą tego rejestru ustawiamy, który pin z danego portu ma być wejściem, a który wyjściem. Po resecie większość pinów ustawiona jest jako wejścia z podciąganiem. Ustawienie bitu odpowiadającego konkretnemu pinowi ustawia pin jako wyjście, wyzerowanie bitu ustawia pin jako wejście.Przykład:
LPC_GPIO1->DIR = 0xff0; // 1111 1111 0000Instrukcja ta ustawia piny P1.0 – P1.3 jako wejścia, a P1.4 – P1.11 jako wyjścia.
IS
Rejestr ustawiający tryb generacji przerwania. Od niego zależy, czy pin reaguje na zbocze czy na poziom na pinie. Jeśli bit jest wyzerowany, przerwanie zostanie wyzwolone od zbocza, jeśli bit jest ustawiony przerwanie będzie wyzwalane od poziomu. To, który poziom lub które zbocze ma generować przerwanie ustalamy w innych rejestrach.Przykład:
LPC_GPIO1->IS = 0x0ff; // 0000 1111 1111Instrukcja konfiguruje piny P1.0 – P1.7 do wyzwalania przerwania od poziomu, a piny P1.8 – P1.11 do wyzwalania przerwania od zbocza. Rejestr służy tylko do konfiguracji przerwania, do zezwolenia na wyzwolenie przerwania służy rejestr IE.
IBE
Rejestr ustawiający tryb generacji przerwania. Jeśli dla danego pinu w rejestrze IS jest ustawione wyzwalanie przerwania od zbocza, to odpowiadającymi bitami w tym rejestrze konfigurujemy wyzwalanie przerwań od obydwu zboczy, narastającego i opadającego.Przykład:
LPC_GPIO1->IS = 0x00f; // Piny P1.0 – P1.3 reagują na zbocza LPC_GPIO1->IBE = 0x003; // Piny P1.0 – P1.1 reagują na opadające i narastające zboczePiny P1.2 –P1.3 reagują tylko na jedno zbocze ustalone rejestrem IEV
IEV
Rejestr konfigurujący tryb generacji przerwania. Od niego zależy, które zbocze lub który poziom generuje przerwanie. Jeśli odpowiadający pinowi bit jest wyzerowany przerwanie generowane jest od niskiego poziomu lub opadającego zbocza. Jeśli bit jest ustawiony przerwanie generuje wysoki poziom lub narastające zbocze.Przykład:
LPC_GPIO1->IEV = 0x003; // 0000 0000 0011Piny P1.0 – P1.1 generują przerwanie od narastającego zbocza
lub od wysokiego poziomu
Piny P1.2 – P1.11 generują przerwanie od opadającego zbocza
lub od niskiego poziomu
IE
Rejestr zezwalający na generacje przerwań od zdarzeń na pinach. Ustawiony bit odpowiadający danemu pinowi skutkuje zezwoleniem na generację przerwań od tego pinu.Przykład:
LPC_GPIO1->IE = 0x00f; // 0000 0000 1111Zdarzenia na pinach P1.0 – P1.3 będą generować przerwanie
Każdy port ma oddzielny wektor przerwań, ale już piny z danego portu korzystają z tego samego wektora. Jeśli zezwolenie na generację przerwań ma więcej niż jeden pin z tego samego portu, wtedy funkcja obsługi przerwania musi za pomocą rejestrów RIS lub MIS sprawdzić od którego pinu pochodziło zdarzenie.
RIS
Rejestr tylko do odczytu. Jeśli zostanie wygenerowane zdarzenie na pinie, odpowiadający temu pinowi bit zostanie w tym rejestrze ustawiony niezależnie od tego, czy w rejestrze IE przerwania dla tego pinu są włączone czy też nie. Kasowanie tego rejestru odbywa się za pomocą rejestru IC.Przykład:
LPC_GPIO1->RIS == 0x01f // 0000 0001 1111Na pinach P1.0 – P1.4 zostało zarejestrowane zdarzenie generujące przerwanie
MIS
Rejestr tylko do odczytu. Zachowanie tego rejestru jest podobne do rejestru RIS z tą tylko różnicą, że ustawiane są tylko bity od tych pinów, na których jest włączona generacja przerwań w rejestrze IE. Jeśli w tym rejestrze są ustawione jakieś bity i nie są one kasowane za pomocą rejestru IC, np. w procedurze obsługi przerwania, to po wyjściu z tej procedury. przerwanie zostanie ponownie wywołane.IC
Rejestr tylko do zapisu. Ustawienie bitu odpowiadającego określonemu pinowi, który generuje przerwania od zbocza, powoduje wykasowanie bitów w rejestrach MIS i RIS. Skutkiem tej operacji jest gotowość na rejestrację kolejnego zdarzenia (reakcji na zbocze) na danym pinie. W przypadku pinów, które mają ustawioną generację przerwania od poziomu, skasowanie odpowiadającym im bitów nie odniesie żadnego skutku i przerwanie będzie cały czas wywoływane, aż do momentu zmiany poziomu na pinie na nieaktywny.Przykład:
LPC_GPIO1->IC = LPC_GPIO1->RIS;Wszystkie zarejestrowane zdarzenia zostaną skasowane
DATA
Rejestr, który nie jednej osobie przysporzył wielu kłopotów. Najpierw spójrzmy jak jest zadeklarowany:union { __IO uint32_t MASKED_ACCESS[4096]; struct { uint32_t RESERVED0[4095]; __IO uint32_t DATA; }; };Jak widać rejestr zajmuje aż 16kB przestrzeni adresowej procesora!!! Gdyby jeden bit reprezentował jeden pin to wyglądałoby na to, że mamy do dyspozycji aż 131072 piny! Na szczęście możemy mieć tylko do 12 pinów na każdym porcie.
I o co tu chodzi? Chodzi o to, aby dostarczyć mechanizm jednoczesnego atomowego dostępu do poszczególnych grup pinów, tzn. że możemy zmieniać kilka pinów na raz nie generując dostępu do innych. Np. bez problemu można w jednej operacji zmienić piny P1.0, P1.2, P1.5 i P1.11. Bez odpowiednich mechanizmów taka operacja nie byłaby możliwa i mogłaby powodować błędne działanie.
Przykład standardowego podejścia na przykładzie mikrokontrolera AVR. Chcemy wyzerować piny P1 i P3 a ustawić piny P0 i P2:
PORTA = ( PORTA & ~((1 << P1) | (1 << P3))) | ((1 << P0) | (1 << P2));Niezbyt przejrzyście to wygląda i w dodatku trochę procesorowi operacji zajmuje:
1. Odczyt portu do kopii roboczej
2. Wyzerowanie na kopii bitów 1 i 3
3. Ustawienie na kopii bitów 0 i 2
4. Zapis kopii do portu.
Tak wygląda sytuacja np. w zwykłych AVRach, a tak w tym procesorze:
LPC_GPIO1->MASKED_ACCESS[(1<<0) | (1<<1) | (1<<2) | (1<<3)] = (1<<0) | (1<<2);Też to wygląda na skomplikowane, z tym, że zajmuje procesorowi nie 4, tylko jedną operację - ZAPIS do portu. Oczywiście taki zapis można uprościć deklarując wcześniej odpowiednio piny:
#define P0 (1<<0) #define P1 (1<<1) #define P2 (1<<2) #define P3 (1<<3)Wtedy powyższa instrukcja będzie bardziej przejrzysta i przy okazji prostsza do wytłumaczenia zasady działania:
LPC_GPIO1->MASKED_ACCESS[ P0 | P1 | P2 | P3 ] = P0 | P2;W nawiasie kwadratowym podajemy, które piny portu chcemy modyfikować, a po znaku przypisania podajemy tylko te piny, które mają zostać ustawione na 1, te nie podane zostaną ustawione na 0. Gdybyśmy chcieli ustawić wszystkie wymienione w nawiasie kwadratowym piny na 0, to wtedy po znaku przypisania wystarczy wpisać 0:
LPC_GPIO1->MASKED_ACCESS[ P0 | P1 | P2 | P3 ] = 0;
Istniej możliwość dostępu dokładnie takiego samego jak w przytoczonym wyżej przykładzie dla AVRa, wtedy musimy odwołać się przez rejestr DATA.
LPC_GPIO1->DATA = (LPC_GPIO1->DATA & ~( P1 | P2 )) | ( P0 | P2 );Jak widać wygląda to tak samo jak dla AVRa, ale i też zajmuje dokładnie tyle samo operacji.
Trik z MASKED_ACCESS[ ] polega na tym, że podając odpowiedni offset w nawiasie kwadratowym powodujemy wygenerowanie odpowiedniego adresu z przestrzeni tych 16kB. Bity tego adresu wykorzystywane są do ustalenia maski, dzięki której przysłaniane są bity, które mają pozostać niezmienione. Rejestr DATA to nic innego jak maska z ustawionymi wszystkimi bitami:
LPC_GPIO1->DATA <=> LPC_GPIO1->MASKED_ACCESS[ 0xfff ]Używanie MASKED_ACCESS ma jedną wadę, jest wolniejsze niż używania DATA. Spowodowane to jest tym, że offset do rejestru bazowego obliczany jest w trakcie działania programu i to zajmuje dodatkowe cykle. Jeśli potrzebny jest szybszy dostęp i maskowanie, wtedy już na etapie kompilacji trzeba ustalić adres, pod który ma nastąpić zapis.
MASKED_ACCESS można również używać do odczytu. Jako offset podajemy te bity, które mają zostać odczytane. W trakcie odczytu zamaskowane bity będą miały wartość 0;
x = LPC_GPIO1->MASKED_ACCESS[ P1 ];W zmiennej x znajdzie się tylko wartość pinu P1, reszta będzie równa 0
Obsługa przerwań od GPIO
Obsługa przerwań w tych procesorach będzie opisana w jednej z następnych części kursu, teraz przedstawię tylko przykładowy kod funkcji obsługującej przerwanie od pinu P1.0 i P1.5.void PIOINT1_IRQHandler( void ) { if( LPC_GPIO1->MIS & 0x001 ) { } if( LPC_GPIO1->MIS & 0x020 ) { } LPC_GPIO1->IC = LPC_GPIO1->RIS; // Skasowanie wszystkich zarejestrowanych zdarzeń }Tak nazywająca się funkcja oraz ustawione odpowiednie bity w rejestrze LPC_GPIO1->IE wystarczą do obsługi przerwań.
Brak komentarzy:
Prześlij komentarz