Programowanie - pętle, porty
Transkrypt
Programowanie - pętle, porty
Politechnika Wrocławska, Wydział Elektroniki Mikrosystemów i Fotoniki Wydziałowy Zakład Metrologii Mikro- i Nanostruktur LABORATORIUM UKŁADÓW PROGRAMOWALNYCH PROCESORY OSADZONE kod kursu: ETD 7211 SEMESTR ZIMOWY 2015/2016 Programowanie - pętle, porty Prowadzący: dr inż. Daniel Kopiec Konsultacje: PN godz. 11-13, ŚR godz. 14-16 Miejsce konsultacji: sala 015a budynek C2 Email: [email protected] Tel.: 71 320 3651 Miejsce odbywania zajęć: sala 108 budynek C2 Spis treści: 1. Programowanie – składnia, pętle, instrukcje warunkowe 2. Konfiguracja rejestrów wejścia/wyjścia linii GPIO 1 1. Programowanie – składnia, pętle, instrukcje warunkowe Większość projektów realizowanych podczas zajęć laboratoryjnych realizowana będzie w języku C, jednakże aby przedstawić architekturę układów ARM fragmenty kodu uznane za newralgiczne realizowane będą w asemblerze. Język C jest językiem dobrze znanym, a jego podstawy przedstawione był już na wielu realizowanych kursach. Dla przypomnienia niezbędnych podstaw zachęcam do zapoznania się z krótkim kompendium [1]. Kompletną listę poleceń asemblerowych oraz składnię poszczególnych rozkazów można znaleźć w odnośniku [2]. Szkielet programu – podstawowy kod Podstawowy progam powinen posiadać: odpowiednie pliki nagłówkowe, biblioteki, deklaracje funkcji, zmienne globalne, program główny int main (void){…} oraz pętlę nieskończoną wewnątrz, której umieszczony zostanie kod programu. #include "LPC23xx.h" // standardowa biblioteka definiująca rejestry, peryferia // pliki nagłówkowe, biblioteki, deklaracje funkcji, // zmienne globalne int main (void) { // konfiguracja układów, rejestrów itp.. while (1) { // pętla główna programu } Należy pamiętać, że jeżeli w programie zabraknie pętli nieskończonej wówczas program wykona się tylko raz. Niewątpliwą zaletą tworzenie projektów w C/C++ jest możliwość wzbogacenia kodu – fragmentami kodu asemblera. Kod pisany w asemblerze powinien być poprzedzony słowem kluczowym __asm a zawartość umieszczona pomiędzy nawiasami klamrowymi { } zgodnie z poniższym przykładem: __asm{ MOV R0, #1000000 MOV R1, #1 } Kod assemblera można wstawić w dowolnym miejscu programu, może to być również osobna funkcja lub wywołanie z osobnego pliku. Warto jednak pamiętać, że operandy wstawiane w instrukcjach asemblerowych nie odnoszą się do fizycznych rejestrów mikrokontrolera. Zadanie rozdzielania rejestrów zależy tylko i wyłącznie 2 od kompilatora, dlatego też operandy traktowane będą tylko i wyłącznie jako zmienne – wirtualne rejestry. Połączenie asemblera z C/C++ Przykładowy kod łączący asembler oraz C przedstawiony został poniżej. Znaczenie poszczególnych instrukcji (składnia, przykłady itp.) dostępna jest w [2]. include "LPC23xx.h" void delay(){ __asm{ MOV R0, #1000000 MOV R1, #1 dalej: CMP R0,R1 BEQ stop SUB R0, R0, R1 B dalej stop: } } int main (void) { GPIO_init(); while (1) { LED_ON(); delay(); LED_OFF(); delay(); } } /* standardowa biblioteka definiująca rejestry, peryferia */ /* funkcja opóźniająca delay (), ciało standardowej funkcji C stanowią instrukcje asemblera. MOV R0, #1000000 - wartość dziesiętna 1000000 zapisana zostanie do zmiennej R0 domyślnie typu int, CMP R0, R1 - nastąpi porównanie zawartości zmiennej R0 i R1, (dokładnie wykonane zostanie R0 – R1), wykonanie instrukcji wpływa na stan flag w rejestrze CPSR tj. N, Z, V, C BEQ stop - B (branch) – oznacza skok do etykiety, jednak w tym wypadku warunek EQ w mnemoniku informuje, że instrukcja zostanie wykonana jeżeli w poprzedniej operacji flaga Z = 1, czyli tak naprawdę R0 = R1, SUB R0, R0, R1 - odejmowanie, dokładniej od zawartości rejestru R0 zostanie odjęta zawartość R1 a wynik operacji zostanie zapisany w R0, wykonanie tej instrukcji nie powoduje uaktualnienie flag w rejestrze CPSR dalej, stop - w ten sposób definiuje się etykiety, czyli miejsca do których może nastąpić skok */ /* główna część programu pisanego w języku C, po słowach kluczowych int main (void){ umieszcza się zazwyczaj funkcje mające na celu konfigurację układów peryferyjnych, rejestrów, zmiennych itp. GPIO_init() – funkcja inicjująca piny układu jako wyjścia, w założeniu pin P1.21 jako wyjście LED_ON() – ustawienie stanu linii wyjściowej w stan 0 lub 1 w zależności od wymagań, np. dla płyty EVBmm stanem zapalającym diodę LED jest stan niski LED_OFF() - ustawienie stanu linii wyjściowej w stan 0 lub 1 w zależności od wymagań */ 3 a) Przydatne konstrukcje w języku C Pętle - w języku C do najczęściej stosowanych pętli zalicza się: - while – jest to pętla, która wykonuje instrukcje zawarte między nawiasami klamrowymi dopóki warunek jest spełniony (warunek jest spełniony jeżeli zwraca wartość =1) while (warunek){ /* instrukcje wykonywane tylko wtedy, gdy warunek jest spełniony*/ } - do … while – jest to pętla podobna do pętli while. Instrukcje wykonywane będą tak długo jak długo będzie prawdziwy warunek (wynik sprawdzenia warunku =1) z tą jednak różnicą, że pętla do … while wykona się przynajmniej jeden raz (przez jedną iterację warunek nie jest sprawdzany) do { /*blok instrukcji zostanie wykonany przynajmniej raz */ } while(warunek); - for – jest to pętla iteracyjna wykonująca ciąg instrukcji i-razy for (int i = 0; i < 100 ; i++) { /* ciąg instrukcji wykonany zostanie i-razy */ } Instrukcje warunkowe: - if – instrukcje wykonywane są jeżeli warunek jest spełniony, if (warunek) { /* blok wykonany, jeśli warunek jest prawdziwy */ } 4 - if - else – jeżeli warunek jest spełniony wykonywane są instrukcje po if, jeżeli warunek nie jest spełniony wykonywany jest ciąg instrukcji po else. if (warunek) { /* blok wykonany, jeśli warunek jest prawdziwy */ } else { /* instrukcje wykonane zostaną jeżeli warunek jest nieprawdziwy */ } - kaskadowa instrukcja if - else, której ogólną postać można zapisać następująco if (warunek) { /* blok wykonany, jeśli warunek jest prawdziwy */ } else if (warunek) {..instrukcje ..} else if (warunek) {..instrukcje ..} else instrukcje /* instrukcje wykonane zostaną jeżeli warunek jest nieprawdziwy */ Instrukcje wyboru: - jedną z popularnijszych instrukcji wyboru jest instrukcja switch - case, która jednocześnie pozwala ograniczyć ilość instrukcji if – else w realizowanym kodzie. Postać ogólą można zapisać jako: switch (x) { case wartość1: /* instrukcje, jeśli x == wartość1 */ break; case wartość2: /* instrukcje, jeśli x == wartość2 */ break; /* ... */ default: /* instrukcje, jeśli żaden z wcześniejszych warunków nie został spełniony */ break; } 5 Tego rodzaju instrukcje idealnie nadają się do obsługi wszelkiego rodzaju klawiatur, procedur warunkowych itp. b) Przydatne konstrukcje w asemblerze Większość instrukcji asemblera może być wykonywana warunkowo, uzależniając wykonywanie kodu od stanu zawartości rejestru CPSR (ang. Current Program Status Register) procesora a dokładniej flag statusowych. flagi statusowe rejestru CPSR, N – nagative, Z – zero, C – carry, V – overflow Liczba dostępnych warunków jest na tyle duża, że z powodzeniem można z podobną łatwością budować kod jak w językach wyższego poziomu. W tabeli przedstawione zostały dostępne operatory warunkowe: Operator warunkowy EQ NE Rozwinięcie Warunek Stan flagi equal not equal równy nie równy ustawiona flaga przeniesienia carry; większy lub równy wyzerowana flaga przeniesienia carry, mniejszy ujemny dodatni lub zerowy ustawiona flaga przepełnienia wyzerowana flaga przepełnienia większy (liczby bez znaku) mniejszy lub równy (liczby bez znaku) Z=1 Z=0 CS carry set CC carry clear MI PL VS VC HI minus/negative plus/positive or zero overflow set overflow clear unsigned higher unsigned lower or same signed greater than or equal signed less than signed greater than signed greater then or equal always LS GE LT GT LE AL C=1 C=0 N=1 N=0 V=1 V=0 C=1 i Z=0 C=0 lub Z=1 większy lub równy N=V mniejszy większy N≠V Z=0 i (N=V) mniejszy lub równy Z=1 lub (N≠V) zawsze bez znaczenia 6 Ogólną postać instrukcji asemblera przedstawia poniższy zapis: <MNEMONIK> {<warunek>} {S} Rd, Rz, Operand gdzie: MNEMONIK – to kod operacji w asemblerze np. ADD, SUB, MOV, LDR, warunek – warunek, pod jakim nastapi wykonanie instrukcji, S – modyfikator informujący o aktualizacji rejestru CPSR po wykonaniu instrukcji, Rd – rejestr docelowy, miejsce gdzie zapisany zostanie wynik działania instrukji, Rz – rejestr źródłowy , miejsce z którego pobrane zostaneą dane, Operand – dana lub rejest, z którgo pobranę będą dane, Przykład: ADDEQS R0, R1, R3 Podany zapis można przetłumaczyć w następujący sposób: - jeżeli flaga Z w rejestrze CPSR równa jest 1 wykonaj dodawanie zawartości rejestrów R1 + R2 a wynik operacji zapisz w R0, co jest równożnaczna z zapisem: if (Z == 1) then R0 = R1 + R3 . Dla zobrazowania jak łatwo można zapisać odpowiednik funkcji C w asemblerze posłużą poniższe przykłady: przykład w C int total; int i; total = 0; for (i = 10; i > 0; i--) { total += i; } odpowiednik w asemblerze MOV R0, #0 MOV R1, #10 ; wyzerowanie rejestru R0, ; R0 - analogia zmiennej total ADD R0, R0, R1 SUBS R1, R1, #1 BNE dalej ; zapisanie wartości 10 do R1 dalej: stop: B stop ; dekrementacja R1 – analogia do zmiennej i ; BNE - Branch if Not Equal - przeskok 7 Warto zwrócić uwagę, że w prezentowanym przykładzie zastosowano zarówno instrukcje warunkowe oraz modyfikator rejestru CPSR. Rozkazy standardowe nie zawierające modyfikatora S domyślnie nie uaktualniają flag statusu w rejestrze CPSR, wyjątek stanowią: CMP, CMN, TST, TEQ. przykład w C a = 40; b = 25; while (a != b) { if (a > b) a -= b; else b -= a; } odpowiednik w asemblerze – wersja 1 odpowiednik w asemblerze – wersja 2 MOV R0, #40 ; R0 odpowiednik a MOV R1, #25 ; R1 odpowiednik b dalej: CMP R0, R1 BEQ stop BLT mniej SUB R0, R0, R1 B dalej mniej: SUB R1, R1, R0 B dalej stop: B stop CMP - Compare, wpływa na flagi N, Z, C, V; operacja (R0 – R1) BEQ - Branch if Equal (Z=1) BLT - Branch if Less Than (N ≠ V) MOV R0, #40 ; R0 odpowiednik a MOV R1, #25 ; R1 odpowiednik b dalej: CMP R0, R1 SUBGT R0, R0, R1 SUBLT R1, R1, R0 BNE dalej stop: B stop SUBGT – Subtract if Greater Than Z=0 i (N=V) SUBLT – Subtract if Less Than N≠V 8 2. Konfiguracja rejestrów wejścia/wyjścia linii GPIO Praktycznie większość dostępnych na rynku mikrokontrolerów posiada w swojej architekturze zestaw linii wejścia/wyjścia oraz układów peryferyjnych jak timery, liczniki, kontrolery przerwań zewnętrznych, przetworniki ADC czy interfejsy komunikacyjne. Często zdarza się, że dla jednego wyprowadzenia układu przewidziano kilka funkcji jakie mogę być realizowane. Dzieje się tak za sprawą rejestrów specjalnych mikrokontrolera definiujących jego funkcjonalność. W wypadku układów LPC2xxx, rolę rejestru wyboru funkcji realizowanej przez dane wyprowadzenie pełni rejestr PINSELx. Zasada działania rejestru PINSELx schematyczne wyjaśniona została na poniższym rysunku: Układem przełączającym jest multiplekser sterowany zawartością wpisaną do rejestru PINSELx. Funkcję każdego z pinów portu definiuje się za pomocą dwóch bitów tego rejestru. Warto zwrócić uwagę, że dla każdego wyprowadzenie przewidziano dwa bity sterujące. Stan bitów w PINSELx 00 01 10 11 Funkcja domyślnie, port GPIO funkcja 1 - odbiornik linii CAN, RD1 funkcja 2 - nadajnik UART, TXD3 funkcja 3 - linia danych I2C, SDA1 W wypadku pinu P0.0 są to bity 1:0, natomiast dla pinu P0.4 – bity 9:8. 9 A zatem chcąc dla układu LPC2368, ustawić linię P0.0 jako nadajnik (TXD3) interfejsu szeregowego UART, do rejestru PINSEL0, zgodnie z dokumentacją należy zapisać wartość binarną 0x00000002. Do konfiguracji kierunku portu GPIO (wejście lub wyjście), stanu panującego na liniach portu służą cztery rejestry. Odpowiednio: IODIRx, IOPINx, IOCLRx, IOSETx, gdzie: x – oznacza numer portu IO IODIR – odpowiada za kierunek portu: 1 – wyjście, 0 – wejście, IOPIN – pozwala odczytać stan linii portu skonfigurowanego jako wejście, zapis wartości powoduję zmianę stanu portu, IOCLR – odpowiada za zerowanie stanu linii wyjściowej, IOSET – odpowiada za ustawienie wartości 1 na linii wyjściowej. Dla przykładu można zapisać: IODIR0=0x00FF0000; IOSET0=0x00F00000; IOCLR0=0x00F00000; // piny 16 – 23 portu P0 ustawiono jako wyjście // piny 20 – 23 ustawiono w stan wysoki // piny 20 – 23 ustawiono w stan niski Podobnie jak w układach AVR (Atmega), dla linii GPIO możliwe jest ustawienie rezystora podciągającego do VCC (pull-up) lub GND (pull-down). Za ustawienie odpowiada rejestr PINMODEx, a dokładniej każde jego 2 bity. Pull-up/pull-down może być ustawiony dla każdej linii niezależnie od realizowanej funkcji. Stan bitów w PINMODEx 00 01 10 11 Funkcja domyślnie, na linii portu pull-up wartość zarezerwowana brak pull-up oraz pull-down na linii portu pull-down Zgodnie z dokumentacją chcąc na linii portu P0.0 ustawić pull-down do rejestru PINMODE0 należy wpisać wartość 0x00000003. 10 Odwołanie do rejestrów w asemblerze W asemblerze sprawa nieco bardziej się komplikuje, ponieważ zamiast nazw symbolicznych rejestrów konieczne staje się użycie adresów komórek pamięci przypisanych do tych właśnie rejestrów. Dane te łatwo można odnaleźć w dokumentacji technicznej [3]. W poniższej tabeli przedstawiono kilka z nich: Rejestr IODIR0 IOPIN0 IOSET0 IOCLR0 Adres w pamięci 0xE0028008 0xE0028000 0xE0028004 0xE002800C Dla przykładu, chcąc ustawić pin P1.21 w stan wysoki zapiszemy: przykład w C przykład w asemblerze IOSET1 = (1<<21); LDR R1,=0xE002801C LDR R2, 0x00200000 STR R3, [R1] IOSET1 = (1<<21); __asm{ MOV R1, 0xE002801C MOV R3, 0x00200000 STR R3, [R1] } Dla uproszczenia zapisu asemblerowego adresy pamięci można zapisać jako definicje, których można następnie używać podobnie jak nazw symbolicznych w języku C, zdefiniowanych w pliku nagłówkowym *.h. Literatura: [1] http://users.ece.utexas.edu/~adnan/c-refcard.pdf [2] http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0473j/dom136128985050 9.html [3] http://www.nxp.com/documents/user_manual/UM10211.pdf 11