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